1 /**
2 * \file m4afile.cpp
3 * Handling of MPEG-4 audio files.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 25 Oct 2007
8 *
9 * Copyright (C) 2007-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "m4afile.h"
28 #include "mp4v2config.h"
29
30 #include <QFile>
31 #include <QDir>
32 #include <QByteArray>
33 #include <stdio.h>
34 #ifdef HAVE_MP4V2_MP4V2_H
35 #include <mp4v2/mp4v2.h>
36 #else
37 #include <mp4.h>
38 #endif
39 #include <cstdlib>
40 #include <cstring>
41 #include "genres.h"
42 #include "pictureframe.h"
43
44 /** MPEG4IP version as 16-bit hex number with major and minor version. */
45 #if defined MP4V2_PROJECT_version_major && defined MP4V2_PROJECT_version_minor
46 #define MPEG4IP_MAJOR_MINOR_VERSION ((MP4V2_PROJECT_version_major << 8) | \
47 MP4V2_PROJECT_version_minor)
48 #elif defined MPEG4IP_MAJOR_VERSION && defined MPEG4IP_MINOR_VERSION
49 #define MPEG4IP_MAJOR_MINOR_VERSION ((MPEG4IP_MAJOR_VERSION << 8) | \
50 MPEG4IP_MINOR_VERSION)
51 #else
52 #define MPEG4IP_MAJOR_MINOR_VERSION 0x0009
53 #endif
54
55 #if MPEG4IP_MAJOR_MINOR_VERSION < 0x0200
56 /** Set content ID. */
57 #define MP4TagsSetContentID MP4TagsSetCNID
58 /** Set artist ID. */
59 #define MP4TagsSetArtistID MP4TagsSetATID
60 /** Set playlist ID. */
61 #define MP4TagsSetPlaylistID MP4TagsSetPLID
62 /** Set genre ID. */
63 #define MP4TagsSetGenreID MP4TagsSetGEID
64 #endif
65
66 namespace {
67
68 /** Mapping between frame types and field names. */
69 const struct {
70 const char* name;
71 Frame::Type type;
72 } nameTypes[] = {
73 { "\251nam", Frame::FT_Title },
74 { "\251ART", Frame::FT_Artist },
75 { "\251wrt", Frame::FT_Composer },
76 { "\251alb", Frame::FT_Album },
77 { "\251day", Frame::FT_Date },
78 { "\251enc", Frame::FT_EncodedBy },
79 { "\251cmt", Frame::FT_Comment },
80 { "\251gen", Frame::FT_Genre },
81 { "trkn", Frame::FT_Track },
82 { "disk", Frame::FT_Disc },
83 { "gnre", Frame::FT_Genre },
84 { "cpil", Frame::FT_Compilation },
85 { "tmpo", Frame::FT_Bpm },
86 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105
87 { "\251grp", Frame::FT_Grouping },
88 #endif
89 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
90 { "aART", Frame::FT_AlbumArtist },
91 { "pgap", Frame::FT_Other },
92 #endif
93 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
94 { "cprt", Frame::FT_Copyright },
95 { "\251lyr", Frame::FT_Lyrics },
96 { "tvsh", Frame::FT_Other },
97 { "tvnn", Frame::FT_Other },
98 { "tven", Frame::FT_Other },
99 { "tvsn", Frame::FT_Other },
100 { "tves", Frame::FT_Other },
101 { "desc", Frame::FT_Description },
102 { "ldes", Frame::FT_Other },
103 { "sonm", Frame::FT_SortName },
104 { "soar", Frame::FT_SortArtist },
105 { "soaa", Frame::FT_SortAlbumArtist },
106 { "soal", Frame::FT_SortAlbum },
107 { "soco", Frame::FT_SortComposer },
108 { "sosn", Frame::FT_Other },
109 { "\251too", Frame::FT_EncoderSettings },
110 { "\251wrk", Frame::FT_Work },
111 { "purd", Frame::FT_Other },
112 { "pcst", Frame::FT_Other },
113 { "keyw", Frame::FT_Other },
114 { "catg", Frame::FT_Other },
115 { "hdvd", Frame::FT_Other },
116 { "stik", Frame::FT_Other },
117 { "rtng", Frame::FT_Other },
118 { "apID", Frame::FT_Other },
119 { "akID", Frame::FT_Other },
120 { "sfID", Frame::FT_Other },
121 { "cnID", Frame::FT_Other },
122 { "atID", Frame::FT_Other },
123 { "plID", Frame::FT_Other },
124 { "geID", Frame::FT_Other },
125 { "purl", Frame::FT_Other },
126 { "egid", Frame::FT_Other },
127 #endif
128 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
129 { "cmID", Frame::FT_Other },
130 { "xid ", Frame::FT_Other },
131 #endif
132 { "covr", Frame::FT_Picture }
133 },
134 freeFormNameTypes[] = {
135 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105)
136 { "GROUPING", Frame::FT_Grouping },
137 #endif
138 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106)
139 { "ALBUMARTIST", Frame::FT_AlbumArtist },
140 #endif
141 { "ARRANGER", Frame::FT_Arranger },
142 { "AUTHOR", Frame::FT_Author },
143 { "CATALOGNUMBER", Frame::FT_CatalogNumber },
144 { "CONDUCTOR", Frame::FT_Conductor },
145 { "ENCODINGTIME", Frame::FT_EncodingTime },
146 { "INITIALKEY", Frame::FT_InitialKey },
147 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109)
148 { "COPYRIGHT", Frame::FT_Copyright },
149 #endif
150 { "ISRC", Frame::FT_Isrc },
151 { "LANGUAGE", Frame::FT_Language },
152 { "LYRICIST", Frame::FT_Lyricist },
153 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109)
154 { "LYRICS", Frame::FT_Lyrics },
155 #endif
156 { "MOOD", Frame::FT_Mood },
157 { "SOURCEMEDIA", Frame::FT_Media },
158 { "ORIGINALALBUM", Frame::FT_OriginalAlbum },
159 { "ORIGINALARTIST", Frame::FT_OriginalArtist },
160 { "ORIGINALDATE", Frame::FT_OriginalDate },
161 { "PERFORMER", Frame::FT_Performer },
162 { "PUBLISHER", Frame::FT_Publisher },
163 { "RELEASECOUNTRY", Frame::FT_ReleaseCountry },
164 { "REMIXER", Frame::FT_Remixer },
165 { "SUBTITLE", Frame::FT_Subtitle },
166 { "WEBSITE", Frame::FT_Website },
167 { "WWWAUDIOFILE", Frame::FT_WWWAudioFile },
168 { "WWWAUDIOSOURCE", Frame::FT_WWWAudioSource },
169 { "RELEASEDATE", Frame::FT_ReleaseDate },
170 { "rate", Frame::FT_Rating }
171 };
172
173 /**
174 * Get the predefined field name for a type.
175 *
176 * @param type frame type
177 *
178 * @return field name, QString::null if not defined.
179 */
getNameForType(Frame::Type type)180 QString getNameForType(Frame::Type type)
181 {
182 static QMap<Frame::Type, QString> typeNameMap;
183 if (typeNameMap.empty()) {
184 // first time initialization
185 for (const auto& nameType : nameTypes) {
186 if (nameType.type != Frame::FT_Other) {
187 typeNameMap.insert(nameType.type, QString::fromLatin1(nameType.name));
188 }
189 }
190 for (const auto& freeFormNameType : freeFormNameTypes) {
191 typeNameMap.insert(freeFormNameType.type,
192 QString::fromLatin1(freeFormNameType.name));
193 }
194 }
195 if (type != Frame::FT_Other) {
196 auto it = typeNameMap.constFind(type);
197 if (it != typeNameMap.constEnd()) {
198 return *it;
199 }
200 }
201 return QString();
202 }
203
204 /**
205 * Get the type for a predefined field name.
206 *
207 * @param name field name
208 * @param onlyPredefined if true, FT_Unknown is returned for fields which
209 * are not predefined, else FT_Other
210 *
211 * @return type, FT_Unknown or FT_Other if not predefined field.
212 */
getTypeForName(const QString & name,bool onlyPredefined=false)213 Frame::Type getTypeForName(const QString& name, bool onlyPredefined = false)
214 {
215 if (name.length() == 4) {
216 static QMap<QString, Frame::Type> nameTypeMap;
217 if (nameTypeMap.empty()) {
218 // first time initialization
219 for (const auto& nameType : nameTypes) {
220 nameTypeMap.insert(QString::fromLatin1(nameType.name), nameType.type);
221 }
222 }
223 auto it = nameTypeMap.constFind(name);
224 if (it != nameTypeMap.constEnd()) {
225 return *it;
226 }
227 }
228 if (!onlyPredefined) {
229 static QMap<QString, Frame::Type> freeFormNameTypeMap;
230 if (freeFormNameTypeMap.empty()) {
231 // first time initialization
232 for (const auto& freeFormNameType : freeFormNameTypes) {
233 freeFormNameTypeMap.insert(QString::fromLatin1(freeFormNameType.name),
234 freeFormNameType.type);
235 }
236 }
237 auto it = freeFormNameTypeMap.constFind(name);
238 if (it != freeFormNameTypeMap.constEnd()) {
239 return *it;
240 }
241 return Frame::FT_Other;
242 }
243 return Frame::FT_UnknownFrame;
244 }
245
246 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
247 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG
248 #else
249 /**
250 * Check if a name is a free form field.
251 *
252 * @param hFile handle
253 * @param name field name
254 *
255 * @return true if a free form field.
256 */
isFreeFormMetadata(MP4FileHandle hFile,const char * name)257 bool isFreeFormMetadata(MP4FileHandle hFile, const char* name)
258 {
259 bool result = false;
260 if (getTypeForName(name, true) == Frame::FT_UnknownFrame) {
261 uint8_t* pValue = 0;
262 uint32_t valueSize = 0;
263 result = MP4GetMetadataFreeForm(hFile, const_cast<char*>(name),
264 &pValue, &valueSize);
265 if (pValue && valueSize > 0) {
266 free(pValue);
267 }
268 }
269 return result;
270 }
271 #endif
272
273 /**
274 * Get a byte array for a value.
275 *
276 * @param name field name
277 * @param value field value
278 * @param size size of value in bytes
279 *
280 * @return byte array with string representation.
281 */
getValueByteArray(const char * name,const uint8_t * value,uint32_t size)282 QByteArray getValueByteArray(const char* name,
283 const uint8_t* value, uint32_t size)
284 {
285 QByteArray str;
286 if (name[0] == '\251') {
287 str = QByteArray(reinterpret_cast<const char*>(value), size);
288 } else if (std::strcmp(name, "trkn") == 0) {
289 if (size >= 6) {
290 unsigned track = value[3] + (value[2] << 8);
291 unsigned totalTracks = value[5] + (value[4] << 8);
292 str.setNum(track);
293 if (totalTracks > 0) {
294 str += '/';
295 str += QByteArray().setNum(totalTracks);
296 }
297 }
298 } else if (std::strcmp(name, "disk") == 0) {
299 if (size >= 6) {
300 unsigned disk = value[3] + (value[2] << 8);
301 unsigned totalDisks = value[5] + (value[4] << 8);
302 str.setNum(disk);
303 if (totalDisks > 0) {
304 str += '/';
305 str += QByteArray().setNum(totalDisks);
306 }
307 }
308 } else if (std::strcmp(name, "gnre") == 0) {
309 if (size >= 2) {
310 unsigned genreNum = value[1] + (value[0] << 8);
311 if (genreNum > 0) {
312 str = Genres::getName(genreNum - 1);
313 }
314 }
315 } else if (std::strcmp(name, "cpil") == 0) {
316 if (size >= 1) {
317 str.setNum(value[0]);
318 }
319 } else if (std::strcmp(name, "tmpo") == 0) {
320 if (size >= 2) {
321 unsigned bpm = value[1] + (value[0] << 8);
322 if (bpm > 0) {
323 str.setNum(bpm);
324 }
325 }
326 } else if (std::strcmp(name, "covr") == 0) {
327 QByteArray ba;
328 ba = QByteArray(reinterpret_cast<const char*>(value), size);
329 return ba;
330 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
331 } else if (std::strcmp(name, "pgap") == 0) {
332 if (size >= 1) {
333 str.setNum(value[0]);
334 }
335 #endif
336 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
337 } else if (std::strcmp(name, "tvsn") == 0 || std::strcmp(name, "tves") == 0 ||
338 std::strcmp(name, "sfID") == 0 || std::strcmp(name, "cnID") == 0 ||
339 std::strcmp(name, "atID") == 0 || std::strcmp(name, "geID") == 0
340 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
341 || std::strcmp(name, "cmID") == 0
342 #endif
343 ) {
344 if (size >= 4) {
345 uint val = value[3] + (value[2] << 8) +
346 (value[1] << 16) + (value[0] << 24);
347 if (val > 0) {
348 str.setNum(val);
349 }
350 }
351 } else if (std::strcmp(name, "pcst") == 0 || std::strcmp(name, "hdvd") == 0 ||
352 std::strcmp(name, "stik") == 0 || std::strcmp(name, "rtng") == 0 ||
353 std::strcmp(name, "akID") == 0) {
354 if (size >= 1) {
355 str.setNum(value[0]);
356 }
357 } else if (std::strcmp(name, "plID") == 0) {
358 if (size >= 8) {
359 qulonglong val =
360 static_cast<qulonglong>(value[7]) +
361 (static_cast<qulonglong>(value[6]) << 8) +
362 (static_cast<qulonglong>(value[5]) << 16) +
363 (static_cast<qulonglong>(value[4]) << 24) +
364 (static_cast<qulonglong>(value[3]) << 32) +
365 (static_cast<qulonglong>(value[2]) << 40) +
366 (static_cast<qulonglong>(value[1]) << 48) +
367 (static_cast<qulonglong>(value[0]) << 56);
368 if (val > 0) {
369 str.setNum(val);
370 }
371 }
372 #endif
373 } else {
374 str = QByteArray(reinterpret_cast<const char*>(value), size);
375 }
376 return str;
377 }
378
379 }
380
381 /**
382 * Constructor.
383 *
384 * @param idx index in file proxy model
385 */
M4aFile(const QPersistentModelIndex & idx)386 M4aFile::M4aFile(const QPersistentModelIndex& idx)
387 : TaggedFile(idx), m_fileRead(false)
388 {
389 }
390
391 /**
392 * Get key of tagged file format.
393 * @return "Mp4v2Metadata".
394 */
taggedFileKey() const395 QString M4aFile::taggedFileKey() const
396 {
397 return QLatin1String("Mp4v2Metadata");
398 }
399
400 /**
401 * Read tags from file.
402 *
403 * @param force true to force reading even if tags were already read.
404 */
readTags(bool force)405 void M4aFile::readTags(bool force)
406 {
407 bool priorIsTagInformationRead = isTagInformationRead();
408 if (force || !m_fileRead) {
409 m_metadata.clear();
410 m_pictures.clear();
411 markTagUnchanged(Frame::Tag_2);
412 m_fileRead = true;
413 QByteArray fnIn =
414 #ifdef Q_OS_WIN32
415 currentFilePath().toUtf8();
416 #else
417 QFile::encodeName(currentFilePath());
418 #endif
419
420 MP4FileHandle handle = MP4Read(fnIn);
421 if (handle != MP4_INVALID_FILE_HANDLE) {
422 m_fileInfo.read(handle);
423 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
424 MP4ItmfItemList* list = MP4ItmfGetItems(handle);
425 if (list) {
426 for (uint32_t i = 0; i < list->size; ++i) {
427 MP4ItmfItem& item = list->elements[i];
428 const char* key = nullptr;
429 if (memcmp(item.code, "----", 4) == 0) {
430 // free form tagfield
431 if (item.name) {
432 key = item.name;
433 }
434 } else {
435 key = item.code;
436 }
437 if (key) {
438 if (std::strcmp(key, "covr") == 0) {
439 if (item.dataList.size > 0) {
440 int i;
441 MP4ItmfData* element;
442 for (i = 0, element = item.dataList.elements;
443 i < static_cast<int>(item.dataList.size);
444 ++i, ++element) {
445 QString mimeType, imgFormat;
446 switch (element->typeCode) {
447 case MP4_ITMF_BT_PNG:
448 mimeType = QLatin1String("image/png");
449 imgFormat = QLatin1String("PNG");
450 break;
451 case MP4_ITMF_BT_BMP:
452 mimeType = QLatin1String("image/bmp");
453 imgFormat = QLatin1String("BMP");
454 break;
455 case MP4_ITMF_BT_GIF:
456 mimeType = QLatin1String("image/gif");
457 imgFormat = QLatin1String("GIF");
458 break;
459 case MP4_ITMF_BT_JPEG:
460 default:
461 mimeType = QLatin1String("image/jpeg");
462 imgFormat = QLatin1String("JPG");
463 }
464 PictureFrame frame(
465 getValueByteArray(key, element->value, element->valueSize),
466 QLatin1String(""), PictureFrame::PT_CoverFront, mimeType,
467 Frame::TE_ISO8859_1, imgFormat);
468 frame.setIndex(Frame::toNegativeIndex(i));
469 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Picture,
470 QLatin1String(key)));
471 m_pictures.append(frame);
472 }
473 }
474 } else {
475 QByteArray ba;
476 if (item.dataList.size > 0 &&
477 item.dataList.elements[0].value &&
478 item.dataList.elements[0].valueSize > 0) {
479 ba = getValueByteArray(key, item.dataList.elements[0].value,
480 item.dataList.elements[0].valueSize);
481 }
482 m_metadata[QString::fromLatin1(key)] = ba;
483 }
484 }
485 }
486 MP4ItmfItemListFree(list);
487 }
488 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG
489 static char notFreeFormStr[] = "NOFF";
490 static char freeFormStr[] = "----";
491 char* ppName;
492 uint8_t* ppValue = 0;
493 uint32_t pValueSize = 0;
494 uint32_t index = 0;
495 unsigned numEmptyEntries = 0;
496 for (index = 0; index < 64; ++index) {
497 ppName = notFreeFormStr;
498 bool ok = MP4GetMetadataByIndex(handle, index,
499 &ppName, &ppValue, &pValueSize);
500 if (ok && ppName && memcmp(ppName, "----", 4) == 0) {
501 // free form tagfield
502 free(ppName);
503 free(ppValue);
504 ppName = freeFormStr;
505 ppValue = 0;
506 pValueSize = 0;
507 ok = MP4GetMetadataByIndex(handle, index,
508 &ppName, &ppValue, &pValueSize);
509 }
510 if (ok) {
511 numEmptyEntries = 0;
512 if (ppName) {
513 QString key(ppName);
514 QByteArray ba;
515 if (ppValue && pValueSize > 0) {
516 ba = getValueByteArray(ppName, ppValue, pValueSize);
517 }
518 m_metadata[key] = ba;
519 free(ppName);
520 }
521 free(ppValue);
522 ppName = 0;
523 ppValue = 0;
524 pValueSize = 0;
525 } else {
526 // There are iTunes files with invalid fields in between,
527 // so we stop after 3 invalid indices.
528 if (++numEmptyEntries >= 3) {
529 break;
530 }
531 }
532 }
533 #else
534 const char* ppName = 0;
535 uint8_t* ppValue = 0;
536 uint32_t pValueSize = 0;
537 uint32_t index = 0;
538 unsigned numEmptyEntries = 0;
539 for (index = 0; index < 64; ++index) {
540 if (MP4GetMetadataByIndex(handle, index,
541 &ppName, &ppValue, &pValueSize)) {
542 numEmptyEntries = 0;
543 if (ppName) {
544 QString key(ppName);
545 QByteArray ba;
546 if (ppValue && pValueSize > 0) {
547 ba = getValueByteArray(ppName, ppValue, pValueSize);
548 }
549 m_metadata[key] = ba;
550
551 // If the field is free form, there are two memory leaks in mp4v2.
552 // The first is not accessible, the second can be freed.
553 if (isFreeFormMetadata(handle, ppName)) {
554 free(const_cast<char*>(ppName));
555 }
556 }
557 free(ppValue);
558 ppName = 0;
559 ppValue = 0;
560 pValueSize = 0;
561 } else {
562 // There are iTunes files with invalid fields in between,
563 // so we stop after 3 invalid indices.
564 if (++numEmptyEntries >= 3) {
565 break;
566 }
567 }
568 }
569 #endif
570 MP4Close(handle
571 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
572 , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE
573 #endif
574 );
575 }
576 }
577
578 if (force) {
579 setFilename(currentFilename());
580 }
581
582 notifyModelDataChanged(priorIsTagInformationRead);
583 }
584
585 /**
586 * Write tags to file and rename it if necessary.
587 *
588 * @param force true to force writing even if file was not changed.
589 * @param renamed will be set to true if the file was renamed,
590 * i.e. the file name is no longer valid, else *renamed
591 * is left unchanged
592 * @param preserve true to preserve file time stamps
593 *
594 * @return true if ok, false if the file could not be written or renamed.
595 */
writeTags(bool force,bool * renamed,bool preserve)596 bool M4aFile::writeTags(bool force, bool* renamed, bool preserve)
597 {
598 bool ok = true;
599 QString fnStr(currentFilePath());
600 if (isChanged() && !QFileInfo(fnStr).isWritable()) {
601 revertChangedFilename();
602 return false;
603 }
604
605 if (m_fileRead && (force || isTagChanged(Frame::Tag_2))) {
606 QByteArray fn = QFile::encodeName(fnStr);
607
608 // store time stamp if it has to be preserved
609 quint64 actime = 0, modtime = 0;
610 if (preserve) {
611 getFileTimeStamps(fnStr, actime, modtime);
612 }
613
614 MP4FileHandle handle = MP4Modify(fn);
615 if (handle != MP4_INVALID_FILE_HANDLE) {
616 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
617 MP4ItmfItemList* list = MP4ItmfGetItems(handle);
618 if (list) {
619 for (uint32_t i = 0; i < list->size; ++i) {
620 MP4ItmfRemoveItem(handle, &list->elements[i]);
621 }
622 MP4ItmfItemListFree(list);
623 }
624 const MP4Tags* tags = MP4TagsAlloc();
625 #else
626 // return code is not checked because it will fail if no metadata exists
627 MP4MetadataDelete(handle);
628 #endif
629
630 for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
631 const QByteArray& value = *it;
632 if (!value.isEmpty()) {
633 const QString& name = it.key();
634 const QByteArray& str = value;
635 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
636 // clazy:excludeall=qlatin1string-non-ascii
637 if (name == QLatin1String("\251nam")) {
638 MP4TagsSetName(tags, str);
639 } else if (name == QLatin1String("\251ART")) {
640 MP4TagsSetArtist(tags, str);
641 } else if (name == QLatin1String("\251wrt")) {
642 MP4TagsSetComposer(tags, str);
643 } else if (name == QLatin1String("\251cmt")) {
644 MP4TagsSetComments(tags, str);
645 } else if (name == QLatin1String("\251too")) {
646 MP4TagsSetEncodingTool(tags, str);
647 } else if (name == QLatin1String("\251day")) {
648 MP4TagsSetReleaseDate(tags, str);
649 } else if (name == QLatin1String("\251alb")) {
650 MP4TagsSetAlbum(tags, str);
651 } else if (name == QLatin1String("trkn")) {
652 MP4TagTrack indexTotal;
653 int slashPos = str.indexOf('/');
654 if (slashPos != -1) {
655 indexTotal.total = str.mid(slashPos + 1).toUShort();
656 indexTotal.index = str.mid(0, slashPos).toUShort();
657 } else {
658 indexTotal.total = 0;
659 indexTotal.index = str.toUShort();
660 }
661 MP4TagsSetTrack(tags, &indexTotal);
662 } else if (name == QLatin1String("disk")) {
663 MP4TagDisk indexTotal;
664 int slashPos = str.indexOf('/');
665 if (slashPos != -1) {
666 indexTotal.total = str.mid(slashPos + 1).toUShort();
667 indexTotal.index = str.mid(0, slashPos).toUShort();
668 } else {
669 indexTotal.total = 0;
670 indexTotal.index = str.toUShort();
671 }
672 MP4TagsSetDisk(tags, &indexTotal);
673 } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) {
674 MP4TagsSetGenre(tags, str);
675 } else if (name == QLatin1String("tmpo")) {
676 uint16_t tempo = str.toUShort();
677 MP4TagsSetTempo(tags, &tempo);
678 } else if (name == QLatin1String("cpil")) {
679 uint8_t cpl = str.toUShort();
680 MP4TagsSetCompilation(tags, &cpl);
681 } else if (name == QLatin1String("\251grp")) {
682 MP4TagsSetGrouping(tags, str);
683 } else if (name == QLatin1String("aART")) {
684 MP4TagsSetAlbumArtist(tags, str);
685 } else if (name == QLatin1String("pgap")) {
686 uint8_t pgap = str.toUShort();
687 MP4TagsSetGapless(tags, &pgap);
688 } else if (name == QLatin1String("tvsh")) {
689 MP4TagsSetTVShow(tags, str);
690 } else if (name == QLatin1String("tvnn")) {
691 MP4TagsSetTVNetwork(tags, str);
692 } else if (name == QLatin1String("tven")) {
693 MP4TagsSetTVEpisodeID(tags, str);
694 } else if (name == QLatin1String("tvsn")) {
695 uint32_t val = str.toULong();
696 MP4TagsSetTVSeason(tags, &val);
697 } else if (name == QLatin1String("tves")) {
698 uint32_t val = str.toULong();
699 MP4TagsSetTVEpisode(tags, &val);
700 } else if (name == QLatin1String("desc")) {
701 MP4TagsSetDescription(tags, str);
702 } else if (name == QLatin1String("ldes")) {
703 MP4TagsSetLongDescription(tags, str);
704 } else if (name == QLatin1String("\251lyr")) {
705 MP4TagsSetLyrics(tags, str);
706 } else if (name == QLatin1String("sonm")) {
707 MP4TagsSetSortName(tags, str);
708 } else if (name == QLatin1String("soar")) {
709 MP4TagsSetSortArtist(tags, str);
710 } else if (name == QLatin1String("soaa")) {
711 MP4TagsSetSortAlbumArtist(tags, str);
712 } else if (name == QLatin1String("soal")) {
713 MP4TagsSetSortAlbum(tags, str);
714 } else if (name == QLatin1String("soco")) {
715 MP4TagsSetSortComposer(tags, str);
716 } else if (name == QLatin1String("sosn")) {
717 MP4TagsSetSortTVShow(tags, str);
718 } else if (name == QLatin1String("cprt")) {
719 MP4TagsSetCopyright(tags, str);
720 } else if (name == QLatin1String("\251enc")) {
721 MP4TagsSetEncodedBy(tags, str);
722 } else if (name == QLatin1String("purd")) {
723 MP4TagsSetPurchaseDate(tags, str);
724 } else if (name == QLatin1String("pcst")) {
725 uint8_t val = str.toUShort();
726 MP4TagsSetPodcast(tags, &val);
727 } else if (name == QLatin1String("keyw")) {
728 MP4TagsSetKeywords(tags, str);
729 } else if (name == QLatin1String("catg")) {
730 MP4TagsSetCategory(tags, str);
731 } else if (name == QLatin1String("hdvd")) {
732 uint8_t val = str.toUShort();
733 MP4TagsSetHDVideo(tags, &val);
734 } else if (name == QLatin1String("stik")) {
735 uint8_t val = str.toUShort();
736 MP4TagsSetMediaType(tags, &val);
737 } else if (name == QLatin1String("rtng")) {
738 uint8_t val = str.toUShort();
739 MP4TagsSetContentRating(tags, &val);
740 } else if (name == QLatin1String("apID")) {
741 MP4TagsSetITunesAccount(tags, str);
742 } else if (name == QLatin1String("akID")) {
743 uint8_t val = str.toUShort();
744 MP4TagsSetITunesAccountType(tags, &val);
745 } else if (name == QLatin1String("sfID")) {
746 uint32_t val = str.toULong();
747 MP4TagsSetITunesCountry(tags, &val);
748 } else if (name == QLatin1String("cnID")) {
749 uint32_t val = str.toULong();
750 MP4TagsSetContentID(tags, &val);
751 } else if (name == QLatin1String("atID")) {
752 uint32_t val = str.toULong();
753 MP4TagsSetArtistID(tags, &val);
754 } else if (name == QLatin1String("plID")) {
755 uint64_t val = str.toULongLong();
756 MP4TagsSetPlaylistID(tags, &val);
757 } else if (name == QLatin1String("geID")) {
758 uint32_t val = str.toULong();
759 MP4TagsSetGenreID(tags, &val);
760 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
761 } else if (name == QLatin1String("cmID")) {
762 uint32_t val = str.toULong();
763 MP4TagsSetComposerID(tags, &val);
764 } else if (name == QLatin1String("xid ")) {
765 MP4TagsSetXID(tags, str);
766 #endif
767 } else {
768 MP4ItmfItem* item;
769 if (name.length() == 4 &&
770 (name.at(0).toLatin1() == '\251' ||
771 (name.at(0) >= QLatin1Char('a') &&
772 name.at(0) <= QLatin1Char('z')))) {
773 item = MP4ItmfItemAlloc(name.toLatin1().constData(), 1);
774 } else {
775 item = MP4ItmfItemAlloc("----", 1);
776 item->mean = strdup("com.apple.iTunes");
777 item->name = strdup(name.toUtf8().data());
778 }
779
780 MP4ItmfData& data = item->dataList.elements[0];
781 data.typeCode = MP4_ITMF_BT_UTF8;
782 data.valueSize = value.size();
783 data.value = reinterpret_cast<uint8_t*>(malloc(data.valueSize));
784 memcpy(data.value, value.data(), data.valueSize);
785
786 MP4ItmfAddItem(handle, item);
787 MP4ItmfItemFree(item);
788 }
789 #else
790 bool setOk;
791 if (name == QLatin1String("\251nam")) {
792 setOk = MP4SetMetadataName(handle, str);
793 } else if (name == QLatin1String("\251ART")) {
794 setOk = MP4SetMetadataArtist(handle, str);
795 } else if (name == QLatin1String("\251wrt")) {
796 setOk = MP4SetMetadataWriter(handle, str);
797 } else if (name == QLatin1String("\251cmt")) {
798 setOk = MP4SetMetadataComment(handle, str);
799 } else if (name == QLatin1String("\251too")) {
800 setOk = MP4SetMetadataTool(handle, str);
801 } else if (name == QLatin1String("\251day")) {
802 unsigned short year = str.toUShort();
803 if (year > 0) {
804 if (year < 1000) year += 2000;
805 else if (year > 9999) year = 9999;
806 setOk = MP4SetMetadataYear(handle, QByteArray().setNum(year));
807 if (setOk) {
808 if (year >= 0) {
809 setTextField(QLatin1String("\251day"),
810 year != 0 ? QString::number(year)
811 : QLatin1String(""), Frame::FT_Date);
812 }
813 }
814 } else {
815 setOk = true;
816 }
817 } else if (name == QLatin1String("\251alb")) {
818 setOk = MP4SetMetadataAlbum(handle, str);
819 } else if (name == QLatin1String("trkn")) {
820 uint16_t track = 0, totalTracks = 0;
821 int slashPos = str.indexOf('/');
822 if (slashPos != -1) {
823 totalTracks = str.mid(slashPos + 1).toUShort();
824 track = str.mid(0, slashPos).toUShort();
825 } else {
826 track = str.toUShort();
827 }
828 setOk = MP4SetMetadataTrack(handle, track, totalTracks);
829 } else if (name == QLatin1String("disk")) {
830 uint16_t disk = 0, totalDisks = 0;
831 int slashPos = str.indexOf('/');
832 if (slashPos != -1) {
833 totalDisks = str.mid(slashPos + 1).toUShort();
834 disk = str.mid(0, slashPos).toUShort();
835 } else {
836 disk = str.toUShort();
837 }
838 setOk = MP4SetMetadataDisk(handle, disk, totalDisks);
839 } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) {
840 setOk = MP4SetMetadataGenre(handle, str);
841 } else if (name == QLatin1String("tmpo")) {
842 uint16_t tempo = str.toUShort();
843 setOk = MP4SetMetadataTempo(handle, tempo);
844 } else if (name == QLatin1String("cpil")) {
845 uint8_t cpl = str.toUShort();
846 setOk = MP4SetMetadataCompilation(handle, cpl);
847 // While this works on Debian Etch with libmp4v2-dev 1.5.0.1-0.3 from
848 // www.debian-multimedia.org, linking on OpenSUSE 10.3 with
849 // libmp4v2-devel-1.5.0.1-6 from packman.links2linux.de fails with
850 // undefined reference to MP4SetMetadataGrouping. To avoid this,
851 // in the line below, 0x105 is replaced by 0x106.
852 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
853 } else if (name == QLatin1String("\251grp")) {
854 setOk = MP4SetMetadataGrouping(handle, str);
855 #endif
856 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
857 } else if (name == QLatin1String("aART")) {
858 setOk = MP4SetMetadataAlbumArtist(handle, str);
859 } else if (name == QLatin1String("pgap")) {
860 uint8_t pgap = str.toUShort();
861 setOk = MP4SetMetadataPartOfGaplessAlbum(handle, pgap);
862 #endif
863 } else {
864 setOk = MP4SetMetadataFreeForm(
865 handle, const_cast<char*>(name.toUtf8().data()),
866 reinterpret_cast<uint8_t*>(const_cast<char*>(value.data())),
867 value.size());
868 }
869 if (!setOk) {
870 qDebug("MP4SetMetadata %s failed", name.toLatin1().data());
871 ok = false;
872 }
873 #endif
874 }
875 }
876
877 const auto frames = m_pictures;
878 for (const Frame& frame : frames) {
879 QByteArray ba;
880 if (PictureFrame::getData(frame, ba)) {
881 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
882 MP4TagArtwork artwork;
883 artwork.data = ba.data();
884 artwork.size = static_cast<uint32_t>(ba.size());
885 artwork.type = MP4_ART_JPEG;
886 QString mimeType;
887 if (PictureFrame::getMimeType(frame, mimeType)) {
888 if (mimeType == QLatin1String("image/png")) {
889 artwork.type = MP4_ART_PNG;
890 } else if (mimeType == QLatin1String("image/bmp")) {
891 artwork.type = MP4_ART_BMP;
892 } else if (mimeType == QLatin1String("image/gif")) {
893 artwork.type = MP4_ART_GIF;
894 }
895 }
896 MP4TagsAddArtwork(tags, &artwork);
897 #else
898 MP4SetMetadataCoverArt(handle, reinterpret_cast<uint8_t*>(ba.data()),
899 ba.size());
900 #endif
901 }
902 }
903
904 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
905 MP4TagsStore(tags, handle);
906 MP4TagsFree(tags);
907 #endif
908
909 MP4Close(handle
910 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
911 , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE
912 #endif
913 );
914 if (ok) {
915 // without this, old tags stay in the file marked as free
916 MP4Optimize(fn);
917 markTagUnchanged(Frame::Tag_2);
918 }
919
920 // restore time stamp
921 if (actime || modtime) {
922 setFileTimeStamps(fnStr, actime, modtime);
923 }
924 } else {
925 qDebug("MP4Modify failed");
926 ok = false;
927 }
928 }
929
930 if (isFilenameChanged()) {
931 if (!renameFile()) {
932 return false;
933 }
934 markFilenameUnchanged();
935 // link tags to new file name
936 readTags(true);
937 *renamed = true;
938 }
939 return ok;
940 }
941
942 /**
943 * Free resources allocated when calling readTags().
944 *
945 * @param force true to force clearing even if the tags are modified
946 */
clearTags(bool force)947 void M4aFile::clearTags(bool force)
948 {
949 if (!m_fileRead || (isChanged() && !force))
950 return;
951
952 bool priorIsTagInformationRead = isTagInformationRead();
953 m_metadata.clear();
954 m_pictures.clear();
955 markTagUnchanged(Frame::Tag_2);
956 m_fileRead = false;
957 notifyModelDataChanged(priorIsTagInformationRead);
958 }
959
960 /**
961 * Remove frames.
962 *
963 * @param tagNr tag number
964 * @param flt filter specifying which frames to remove
965 */
deleteFrames(Frame::TagNumber tagNr,const FrameFilter & flt)966 void M4aFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt)
967 {
968 if (tagNr != Frame::Tag_2)
969 return;
970
971 if (flt.areAllEnabled()) {
972 m_metadata.clear();
973 m_pictures.clear();
974 markTagChanged(Frame::Tag_2, Frame::FT_UnknownFrame);
975 } else {
976 bool changed = false;
977 for (auto it = m_metadata.begin(); it != m_metadata.end();) { // clazy:exclude=detaching-member
978 QString name(it.key());
979 Frame::Type type = getTypeForName(name);
980 if (flt.isEnabled(type, name)) {
981 it = m_metadata.erase(it);
982 changed = true;
983 } else {
984 ++it;
985 }
986 }
987 if (flt.isEnabled(Frame::FT_Picture) && !m_pictures.isEmpty()) {
988 m_pictures.clear();
989 changed = true;
990 }
991 if (changed) {
992 markTagChanged(Frame::Tag_2, Frame::FT_UnknownFrame);
993 }
994 }
995 }
996
997 /**
998 * Get metadata field as string.
999 *
1000 * @param name field name
1001 *
1002 * @return value as string, "" if not found,
1003 * QString::null if the tags have not been read yet.
1004 */
getTextField(const QString & name) const1005 QString M4aFile::getTextField(const QString& name) const
1006 {
1007 if (m_fileRead) {
1008 auto it = m_metadata.constFind(name);
1009 if (it != m_metadata.constEnd()) {
1010 return QString::fromUtf8((*it).data(), (*it).size());
1011 }
1012 return QLatin1String("");
1013 }
1014 return QString();
1015 }
1016
1017 /**
1018 * Set text field.
1019 * If value is null if the tags have not been read yet, nothing is changed.
1020 * If value is different from the current value, tag 2 is marked as changed.
1021 *
1022 * @param name name
1023 * @param value value, "" to remove, QString::null to do nothing
1024 * @param type frame type
1025 */
setTextField(const QString & name,const QString & value,Frame::Type type)1026 void M4aFile::setTextField(const QString& name, const QString& value,
1027 Frame::Type type)
1028 {
1029 if (m_fileRead && !value.isNull()) {
1030 QByteArray str = value.toUtf8();
1031 auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1032 if (it != m_metadata.end()) {
1033 if (QString::fromUtf8((*it).data(), (*it).size()) != value) {
1034 *it = str;
1035 markTagChanged(Frame::Tag_2, type);
1036 }
1037 } else {
1038 m_metadata.insert(name, str);
1039 markTagChanged(Frame::Tag_2, type);
1040 }
1041 }
1042 }
1043
1044 /**
1045 * Check if tag information has already been read.
1046 *
1047 * @return true if information is available,
1048 * false if the tags have not been read yet, in which case
1049 * hasTag() does not return meaningful information.
1050 */
isTagInformationRead() const1051 bool M4aFile::isTagInformationRead() const
1052 {
1053 return m_fileRead;
1054 }
1055
1056 /**
1057 * Check if file has a tag.
1058 *
1059 * @param tagNr tag number
1060 * @return true if a V2 tag is available.
1061 * @see isTagInformationRead()
1062 */
hasTag(Frame::TagNumber tagNr) const1063 bool M4aFile::hasTag(Frame::TagNumber tagNr) const
1064 {
1065 return tagNr == Frame::Tag_2 && !m_metadata.empty();
1066 }
1067
1068 /**
1069 * Get file extension including the dot.
1070 *
1071 * @return file extension ".m4a".
1072 */
getFileExtension() const1073 QString M4aFile::getFileExtension() const
1074 {
1075 return QLatin1String(".m4a");
1076 }
1077
1078 /**
1079 * Get technical detail information.
1080 *
1081 * @param info the detail information is returned here
1082 */
getDetailInfo(DetailInfo & info) const1083 void M4aFile::getDetailInfo(DetailInfo& info) const
1084 {
1085 if (m_fileRead && m_fileInfo.valid) {
1086 info.valid = true;
1087 info.format = QLatin1String("MP4");
1088 info.bitrate = m_fileInfo.bitrate;
1089 info.sampleRate = m_fileInfo.sampleRate;
1090 info.channels = m_fileInfo.channels;
1091 info.duration = m_fileInfo.duration;
1092 } else {
1093 info.valid = false;
1094 }
1095 }
1096
1097 /**
1098 * Get duration of file.
1099 *
1100 * @return duration in seconds,
1101 * 0 if unknown.
1102 */
getDuration() const1103 unsigned M4aFile::getDuration() const
1104 {
1105 if (m_fileRead && m_fileInfo.valid) {
1106 return m_fileInfo.duration;
1107 }
1108 return 0;
1109 }
1110
1111 /**
1112 * Get the format of tag.
1113 *
1114 * @param tagNr tag number
1115 * @return "Vorbis".
1116 */
getTagFormat(Frame::TagNumber tagNr) const1117 QString M4aFile::getTagFormat(Frame::TagNumber tagNr) const
1118 {
1119 return hasTag(tagNr) ? QLatin1String("MP4") : QString();
1120 }
1121
1122 /**
1123 * Get a specific frame from the tags.
1124 *
1125 * @param tagNr tag number
1126 * @param type frame type
1127 * @param frame the frame is returned here
1128 *
1129 * @return true if ok.
1130 */
getFrame(Frame::TagNumber tagNr,Frame::Type type,Frame & frame) const1131 bool M4aFile::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const
1132 {
1133 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame ||
1134 tagNr > 1)
1135 return false;
1136
1137 if (tagNr == Frame::Tag_1) {
1138 frame.setValue(QString());
1139 } else {
1140 if (type == Frame::FT_Genre) {
1141 QString str(getTextField(QLatin1String("\251gen")));
1142 frame.setValue(str.isEmpty() ? getTextField(QLatin1String("gnre")) : str);
1143 } else {
1144 frame.setValue(getTextField(getNameForType(type)));
1145 }
1146 }
1147 frame.setType(type);
1148 return true;
1149 }
1150
1151 /**
1152 * Set a frame in the tags.
1153 *
1154 * @param tagNr tag number
1155 * @param frame frame to set
1156 *
1157 * @return true if ok.
1158 */
setFrame(Frame::TagNumber tagNr,const Frame & frame)1159 bool M4aFile::setFrame(Frame::TagNumber tagNr, const Frame& frame)
1160 {
1161 if (tagNr == Frame::Tag_2) {
1162 if (frame.getType() == Frame::FT_Picture) {
1163 int idx = Frame::fromNegativeIndex(frame.getIndex());
1164 if (idx >= 0 && idx < m_pictures.size()) {
1165 Frame newFrame(frame);
1166 if (PictureFrame::areFieldsEqual(m_pictures[idx], newFrame)) {
1167 m_pictures[idx].setValueChanged(false);
1168 } else {
1169 m_pictures[idx] = newFrame;
1170 markTagChanged(tagNr, Frame::FT_Picture);
1171 }
1172 return true;
1173 } else {
1174 return false;
1175 }
1176 }
1177 QString name(frame.getInternalName());
1178 auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1179 if (it != m_metadata.end()) {
1180 if (frame.getType() != Frame::FT_Picture) {
1181 QByteArray str = frame.getValue().toUtf8();
1182 if (*it != str) {
1183 *it = str;
1184 markTagChanged(Frame::Tag_2, frame.getType());
1185 }
1186 } else {
1187 if (PictureFrame::getData(frame, *it)) {
1188 markTagChanged(Frame::Tag_2, Frame::FT_Picture);
1189 }
1190 }
1191 return true;
1192 }
1193 }
1194
1195 // Try the basic method
1196 Frame::Type type = frame.getType();
1197 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame ||
1198 tagNr > 1)
1199 return false;
1200
1201 if (tagNr == Frame::Tag_2) {
1202 if (type == Frame::FT_Genre) {
1203 QString str = frame.getValue();
1204 QString oldStr(getTextField(QLatin1String("\251gen")));
1205 if (oldStr.isEmpty()) {
1206 oldStr = getTextField(QLatin1String("gnre"));
1207 }
1208 if (str != oldStr) {
1209 int genreNum = Genres::getNumber(str);
1210 if (genreNum != 255) {
1211 setTextField(QLatin1String("gnre"), str, Frame::FT_Genre);
1212 m_metadata.remove(QLatin1String("\251gen"));
1213 } else {
1214 setTextField(QLatin1String("\251gen"), str, Frame::FT_Genre);
1215 m_metadata.remove(QLatin1String("gnre"));
1216 }
1217 }
1218 } else if (type == Frame::FT_Track) {
1219 int numTracks;
1220 int num = splitNumberAndTotal(frame.getValue(), &numTracks);
1221 if (num >= 0) {
1222 QString str;
1223 if (num != 0) {
1224 str.setNum(num);
1225 if (numTracks == 0)
1226 numTracks = getTotalNumberOfTracksIfEnabled();
1227 if (numTracks > 0) {
1228 str += QLatin1Char('/');
1229 str += QString::number(numTracks);
1230 }
1231 } else {
1232 str = QLatin1String("");
1233 }
1234 setTextField(QLatin1String("trkn"), str, Frame::FT_Track);
1235 }
1236 } else {
1237 setTextField(getNameForType(type), frame.getValue(), type);
1238 }
1239 }
1240 return true;
1241 }
1242
1243 /**
1244 * Add a frame in the tags.
1245 *
1246 * @param tagNr tag number
1247 * @param frame frame to add
1248 *
1249 * @return true if ok.
1250 */
addFrame(Frame::TagNumber tagNr,Frame & frame)1251 bool M4aFile::addFrame(Frame::TagNumber tagNr, Frame& frame)
1252 {
1253 if (tagNr == Frame::Tag_2) {
1254 Frame::Type type = frame.getType();
1255 if (type == Frame::FT_Picture) {
1256 if (frame.getFieldList().empty()) {
1257 PictureFrame::setFields(frame);
1258 }
1259 frame.setIndex(Frame::toNegativeIndex(m_pictures.size()));
1260 m_pictures.append(frame);
1261 markTagChanged(tagNr, Frame::FT_Picture);
1262 return true;
1263 }
1264 QString name;
1265 if (type != Frame::FT_Other) {
1266 name = getNameForType(type);
1267 if (!name.isEmpty()) {
1268 frame.setExtendedType(Frame::ExtendedType(type, name));
1269 }
1270 }
1271 name = frame.getInternalName();
1272 m_metadata[name] = frame.getValue().toUtf8();
1273 markTagChanged(Frame::Tag_2, type);
1274 return true;
1275 }
1276 return false;
1277 }
1278
1279 /**
1280 * Delete a frame in the tags.
1281 *
1282 * @param tagNr tag number
1283 * @param frame frame to delete.
1284 *
1285 * @return true if ok.
1286 */
deleteFrame(Frame::TagNumber tagNr,const Frame & frame)1287 bool M4aFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame)
1288 {
1289 if (tagNr == Frame::Tag_2) {
1290 if (frame.getType() == Frame::FT_Picture) {
1291 int idx = Frame::fromNegativeIndex(frame.getIndex());
1292 if (idx >= 0 && idx < m_pictures.size()) {
1293 m_pictures.removeAt(idx);
1294 while (idx < m_pictures.size()) {
1295 m_pictures[idx].setIndex(Frame::toNegativeIndex(idx));
1296 ++idx;
1297 }
1298 markTagChanged(tagNr, Frame::FT_Picture);
1299 return true;
1300 }
1301 }
1302 QString name(frame.getInternalName());
1303 auto it = m_metadata.find(name); // clazy:exclude=detaching-member
1304 if (it != m_metadata.end()) {
1305 m_metadata.erase(it);
1306 markTagChanged(Frame::Tag_2, frame.getType());
1307 return true;
1308 }
1309 }
1310
1311 // Try the superclass method
1312 return TaggedFile::deleteFrame(tagNr, frame);
1313 }
1314
1315 /**
1316 * Get all frames in tag.
1317 *
1318 * @param tagNr tag number
1319 * @param frames frame collection to set.
1320 */
getAllFrames(Frame::TagNumber tagNr,FrameCollection & frames)1321 void M4aFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames)
1322 {
1323 if (tagNr == Frame::Tag_2) {
1324 frames.clear();
1325 QString name;
1326 QString value;
1327 int i = 0;
1328 for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
1329 name = it.key();
1330 Frame::Type type = getTypeForName(name);
1331 value = QString::fromUtf8((*it).data(), (*it).size());
1332 frames.insert(Frame(type, value, name, i++));
1333 }
1334 for (auto it = m_pictures.constBegin(); it != m_pictures.constEnd(); ++it) {
1335 frames.insert(*it);
1336 }
1337 frames.addMissingStandardFrames();
1338 return;
1339 }
1340
1341 TaggedFile::getAllFrames(tagNr, frames);
1342 }
1343
1344 /**
1345 * Get a list of frame IDs which can be added.
1346 * @param tagNr tag number
1347 * @return list with frame IDs.
1348 */
getFrameIds(Frame::TagNumber tagNr) const1349 QStringList M4aFile::getFrameIds(Frame::TagNumber tagNr) const
1350 {
1351 if (tagNr != Frame::Tag_2)
1352 return QStringList();
1353
1354 static const Frame::Type types[] = {
1355 Frame::FT_Title,
1356 Frame::FT_Artist,
1357 Frame::FT_Album,
1358 Frame::FT_Comment,
1359 Frame::FT_Compilation,
1360 Frame::FT_Date,
1361 Frame::FT_Track,
1362 Frame::FT_Genre,
1363 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
1364 Frame::FT_AlbumArtist,
1365 #endif
1366 Frame::FT_Bpm,
1367 Frame::FT_Composer,
1368 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1369 Frame::FT_Copyright,
1370 #endif
1371 Frame::FT_Description,
1372 Frame::FT_Disc,
1373 Frame::FT_EncodedBy,
1374 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1375 Frame::FT_EncoderSettings,
1376 #endif
1377 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105
1378 Frame::FT_Grouping,
1379 #endif
1380 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1381 Frame::FT_Lyrics,
1382 #endif
1383 Frame::FT_Picture,
1384 Frame::FT_Rating
1385 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1386 , Frame::FT_SortAlbum,
1387 Frame::FT_SortAlbumArtist,
1388 Frame::FT_SortArtist,
1389 Frame::FT_SortComposer,
1390 Frame::FT_SortName
1391 #endif
1392 };
1393
1394 QStringList lst;
1395 for (auto type : types) {
1396 lst.append(Frame::ExtendedType(type, QLatin1String("")). // clazy:exclude=reserve-candidates
1397 getName());
1398 }
1399 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106
1400 lst << QLatin1String("pgap");
1401 #endif
1402 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1403 lst << QLatin1String("akID") << QLatin1String("apID") << QLatin1String("atID") << QLatin1String("catg") << QLatin1String("cnID") <<
1404 QLatin1String("geID") << QLatin1String("hdvd") << QLatin1String("keyw") << QLatin1String("ldes") << QLatin1String("pcst") <<
1405 QLatin1String("plID") << QLatin1String("purd") << QLatin1String("rtng") << QLatin1String("sfID") <<
1406 QLatin1String("sosn") << QLatin1String("stik") << QLatin1String("tven") <<
1407 QLatin1String("tves") << QLatin1String("tvnn") << QLatin1String("tvsh") << QLatin1String("tvsn") <<
1408 QLatin1String("purl") << QLatin1String("egid");
1409 #endif
1410 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200
1411 lst << QLatin1String("cmID") << QLatin1String("xid ");
1412 #endif
1413 return lst;
1414 }
1415
1416
1417 /**
1418 * Read information about an MPEG-4 file.
1419 * @param fn file name
1420 * @return true if ok.
1421 */
read(MP4FileHandle handle)1422 bool M4aFile::FileInfo::read(MP4FileHandle handle)
1423 {
1424 valid = false;
1425 uint32_t numTracks = MP4GetNumberOfTracks(handle);
1426 for (uint32_t i = 0; i < numTracks; ++i) {
1427 MP4TrackId trackId = MP4FindTrackId(handle, i);
1428 const char* trackType = MP4GetTrackType(handle, trackId);
1429 if (std::strcmp(trackType, MP4_AUDIO_TRACK_TYPE) == 0) {
1430 valid = true;
1431 bitrate = (MP4GetTrackBitRate(handle, trackId) + 500) / 1000;
1432 sampleRate = MP4GetTrackTimeScale(handle, trackId);
1433 duration = MP4ConvertFromTrackDuration(
1434 handle, trackId,
1435 MP4GetTrackDuration(handle, trackId), MP4_MSECS_TIME_SCALE) / 1000;
1436 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109
1437 channels = MP4GetTrackAudioChannels(handle, trackId);
1438 #else
1439 channels = 2;
1440 #endif
1441 break;
1442 }
1443 }
1444 return valid;
1445 }
1446