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