File indexing completed on 2024-05-12 04:55:40
0001 /** 0002 * \file m4afile.cpp 0003 * Handling of MPEG-4 audio files. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 25 Oct 2007 0008 * 0009 * Copyright (C) 2007-2023 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * 0013 * Kid3 is free software; you can redistribute it and/or modify 0014 * it under the terms of the GNU General Public License as published by 0015 * the Free Software Foundation; either version 2 of the License, or 0016 * (at your option) any later version. 0017 * 0018 * Kid3 is distributed in the hope that it will be useful, 0019 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0021 * GNU General Public License for more details. 0022 * 0023 * You should have received a copy of the GNU General Public License 0024 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0025 */ 0026 0027 #include "m4afile.h" 0028 #include "mp4v2config.h" 0029 0030 #include <QFile> 0031 #include <QDir> 0032 #include <QByteArray> 0033 #include <stdio.h> 0034 #ifdef HAVE_MP4V2_MP4V2_H 0035 #include <mp4v2/mp4v2.h> 0036 #else 0037 #include <mp4.h> 0038 #endif 0039 #include <cstdlib> 0040 #include <cstring> 0041 #include "genres.h" 0042 #include "pictureframe.h" 0043 0044 /** MPEG4IP version as 16-bit hex number with major and minor version. */ 0045 #if defined MP4V2_PROJECT_version_major && defined MP4V2_PROJECT_version_minor 0046 #define MPEG4IP_MAJOR_MINOR_VERSION ((MP4V2_PROJECT_version_major << 8) | \ 0047 MP4V2_PROJECT_version_minor) 0048 #elif defined MPEG4IP_MAJOR_VERSION && defined MPEG4IP_MINOR_VERSION 0049 #define MPEG4IP_MAJOR_MINOR_VERSION ((MPEG4IP_MAJOR_VERSION << 8) | \ 0050 MPEG4IP_MINOR_VERSION) 0051 #else 0052 #define MPEG4IP_MAJOR_MINOR_VERSION 0x0009 0053 #endif 0054 0055 #if MPEG4IP_MAJOR_MINOR_VERSION < 0x0200 0056 /** Set content ID. */ 0057 #define MP4TagsSetContentID MP4TagsSetCNID 0058 /** Set artist ID. */ 0059 #define MP4TagsSetArtistID MP4TagsSetATID 0060 /** Set playlist ID. */ 0061 #define MP4TagsSetPlaylistID MP4TagsSetPLID 0062 /** Set genre ID. */ 0063 #define MP4TagsSetGenreID MP4TagsSetGEID 0064 #endif 0065 0066 namespace { 0067 0068 /** Mapping between frame types and field names. */ 0069 const struct { 0070 const char* name; 0071 Frame::Type type; 0072 } nameTypes[] = { 0073 { "\251nam", Frame::FT_Title }, 0074 { "\251ART", Frame::FT_Artist }, 0075 { "\251wrt", Frame::FT_Composer }, 0076 { "\251alb", Frame::FT_Album }, 0077 { "\251day", Frame::FT_Date }, 0078 { "\251enc", Frame::FT_EncodedBy }, 0079 { "\251cmt", Frame::FT_Comment }, 0080 { "\251gen", Frame::FT_Genre }, 0081 { "trkn", Frame::FT_Track }, 0082 { "disk", Frame::FT_Disc }, 0083 { "gnre", Frame::FT_Genre }, 0084 { "cpil", Frame::FT_Compilation }, 0085 { "tmpo", Frame::FT_Bpm }, 0086 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105 0087 { "\251grp", Frame::FT_Grouping }, 0088 #endif 0089 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 0090 { "aART", Frame::FT_AlbumArtist }, 0091 { "pgap", Frame::FT_Other }, 0092 #endif 0093 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0094 { "cprt", Frame::FT_Copyright }, 0095 { "\251lyr", Frame::FT_Lyrics }, 0096 { "tvsh", Frame::FT_Other }, 0097 { "tvnn", Frame::FT_Other }, 0098 { "tven", Frame::FT_Other }, 0099 { "tvsn", Frame::FT_Other }, 0100 { "tves", Frame::FT_Other }, 0101 { "desc", Frame::FT_Description }, 0102 { "ldes", Frame::FT_Other }, 0103 { "sonm", Frame::FT_SortName }, 0104 { "soar", Frame::FT_SortArtist }, 0105 { "soaa", Frame::FT_SortAlbumArtist }, 0106 { "soal", Frame::FT_SortAlbum }, 0107 { "soco", Frame::FT_SortComposer }, 0108 { "sosn", Frame::FT_Other }, 0109 { "\251too", Frame::FT_EncoderSettings }, 0110 { "\251wrk", Frame::FT_Work }, 0111 { "purd", Frame::FT_Other }, 0112 { "pcst", Frame::FT_Other }, 0113 { "keyw", Frame::FT_Other }, 0114 { "catg", Frame::FT_Other }, 0115 { "hdvd", Frame::FT_Other }, 0116 { "stik", Frame::FT_Other }, 0117 { "rtng", Frame::FT_Other }, 0118 { "apID", Frame::FT_Other }, 0119 { "akID", Frame::FT_Other }, 0120 { "sfID", Frame::FT_Other }, 0121 { "cnID", Frame::FT_Other }, 0122 { "atID", Frame::FT_Other }, 0123 { "plID", Frame::FT_Other }, 0124 { "geID", Frame::FT_Other }, 0125 { "purl", Frame::FT_Other }, 0126 { "egid", Frame::FT_Other }, 0127 { "cmID", Frame::FT_Other }, 0128 { "xid ", Frame::FT_Other }, 0129 #endif 0130 { "covr", Frame::FT_Picture } 0131 }, 0132 freeFormNameTypes[] = { 0133 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105) 0134 { "GROUPING", Frame::FT_Grouping }, 0135 #endif 0136 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106) 0137 { "ALBUMARTIST", Frame::FT_AlbumArtist }, 0138 #endif 0139 { "ARRANGER", Frame::FT_Arranger }, 0140 { "AUTHOR", Frame::FT_Author }, 0141 { "CATALOGNUMBER", Frame::FT_CatalogNumber }, 0142 { "CONDUCTOR", Frame::FT_Conductor }, 0143 { "ENCODINGTIME", Frame::FT_EncodingTime }, 0144 { "INITIALKEY", Frame::FT_InitialKey }, 0145 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109) 0146 { "COPYRIGHT", Frame::FT_Copyright }, 0147 #endif 0148 { "ISRC", Frame::FT_Isrc }, 0149 { "LANGUAGE", Frame::FT_Language }, 0150 { "LYRICIST", Frame::FT_Lyricist }, 0151 #if !(MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109) 0152 { "LYRICS", Frame::FT_Lyrics }, 0153 #endif 0154 { "MOOD", Frame::FT_Mood }, 0155 { "SOURCEMEDIA", Frame::FT_Media }, 0156 { "ORIGINALALBUM", Frame::FT_OriginalAlbum }, 0157 { "ORIGINALARTIST", Frame::FT_OriginalArtist }, 0158 { "ORIGINALDATE", Frame::FT_OriginalDate }, 0159 { "PERFORMER", Frame::FT_Performer }, 0160 { "PUBLISHER", Frame::FT_Publisher }, 0161 { "RELEASECOUNTRY", Frame::FT_ReleaseCountry }, 0162 { "REMIXER", Frame::FT_Remixer }, 0163 { "SUBTITLE", Frame::FT_Subtitle }, 0164 { "WEBSITE", Frame::FT_Website }, 0165 { "WWWAUDIOFILE", Frame::FT_WWWAudioFile }, 0166 { "WWWAUDIOSOURCE", Frame::FT_WWWAudioSource }, 0167 { "RELEASEDATE", Frame::FT_ReleaseDate }, 0168 { "rate", Frame::FT_Rating } 0169 }; 0170 0171 /** 0172 * Get the predefined field name for a type. 0173 * 0174 * @param type frame type 0175 * 0176 * @return field name, QString::null if not defined. 0177 */ 0178 QString getNameForType(Frame::Type type) 0179 { 0180 static QMap<Frame::Type, QString> typeNameMap; 0181 if (typeNameMap.empty()) { 0182 // first time initialization 0183 for (const auto& nameType : nameTypes) { 0184 if (nameType.type != Frame::FT_Other) { 0185 typeNameMap.insert(nameType.type, QString::fromLatin1(nameType.name)); 0186 } 0187 } 0188 for (const auto& freeFormNameType : freeFormNameTypes) { 0189 typeNameMap.insert(freeFormNameType.type, 0190 QString::fromLatin1(freeFormNameType.name)); 0191 } 0192 } 0193 if (type != Frame::FT_Other) { 0194 auto it = typeNameMap.constFind(type); 0195 if (it != typeNameMap.constEnd()) { 0196 return *it; 0197 } else { 0198 auto customFrameName = Frame::getNameForCustomFrame(type); 0199 if (!customFrameName.isEmpty()) { 0200 return QString::fromLatin1(customFrameName); 0201 } 0202 } 0203 } 0204 return QString(); 0205 } 0206 0207 /** 0208 * Get the type for a predefined field name. 0209 * 0210 * @param name field name 0211 * @param onlyPredefined if true, FT_Unknown is returned for fields which 0212 * are not predefined, else FT_Other 0213 * 0214 * @return type, FT_Unknown or FT_Other if not predefined field. 0215 */ 0216 Frame::Type getTypeForName(const QString& name, bool onlyPredefined = false) 0217 { 0218 if (name.length() == 4) { 0219 static QMap<QString, Frame::Type> nameTypeMap; 0220 if (nameTypeMap.empty()) { 0221 // first time initialization 0222 for (const auto& nameType : nameTypes) { 0223 nameTypeMap.insert(QString::fromLatin1(nameType.name), nameType.type); 0224 } 0225 } 0226 auto it = nameTypeMap.constFind(name); 0227 if (it != nameTypeMap.constEnd()) { 0228 Frame::Type type = *it; 0229 if (type == Frame::FT_Other) { 0230 type = Frame::getTypeFromCustomFrameName(name.toLatin1()); 0231 } 0232 return type; 0233 } 0234 } 0235 if (!onlyPredefined) { 0236 static QMap<QString, Frame::Type> freeFormNameTypeMap; 0237 if (freeFormNameTypeMap.empty()) { 0238 // first time initialization 0239 for (const auto& freeFormNameType : freeFormNameTypes) { 0240 freeFormNameTypeMap.insert(QString::fromLatin1(freeFormNameType.name), 0241 freeFormNameType.type); 0242 } 0243 } 0244 auto it = freeFormNameTypeMap.constFind(name); 0245 if (it != freeFormNameTypeMap.constEnd()) { 0246 return *it; 0247 } 0248 return Frame::getTypeFromCustomFrameName(name.toLatin1()); 0249 } 0250 return Frame::FT_UnknownFrame; 0251 } 0252 0253 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0254 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG 0255 #else 0256 /** 0257 * Check if a name is a free form field. 0258 * 0259 * @param hFile handle 0260 * @param name field name 0261 * 0262 * @return true if a free form field. 0263 */ 0264 bool isFreeFormMetadata(MP4FileHandle hFile, const char* name) 0265 { 0266 bool result = false; 0267 if (getTypeForName(name, true) == Frame::FT_UnknownFrame) { 0268 uint8_t* pValue = 0; 0269 uint32_t valueSize = 0; 0270 result = MP4GetMetadataFreeForm(hFile, const_cast<char*>(name), 0271 &pValue, &valueSize); 0272 if (pValue && valueSize > 0) { 0273 free(pValue); 0274 } 0275 } 0276 return result; 0277 } 0278 #endif 0279 0280 /** 0281 * Get a byte array for a value. 0282 * 0283 * @param name field name 0284 * @param value field value 0285 * @param size size of value in bytes 0286 * 0287 * @return byte array with string representation. 0288 */ 0289 QByteArray getValueByteArray(const char* name, 0290 const uint8_t* value, uint32_t size) 0291 { 0292 QByteArray str; 0293 if (name[0] == '\251') { 0294 str = QByteArray(reinterpret_cast<const char*>(value), size); 0295 } else if (std::strcmp(name, "trkn") == 0) { 0296 if (size >= 6) { 0297 unsigned track = value[3] + (value[2] << 8); 0298 unsigned totalTracks = value[5] + (value[4] << 8); 0299 str.setNum(track); 0300 if (totalTracks > 0) { 0301 str += '/'; 0302 str += QByteArray().setNum(totalTracks); 0303 } 0304 } 0305 } else if (std::strcmp(name, "disk") == 0) { 0306 if (size >= 6) { 0307 unsigned disk = value[3] + (value[2] << 8); 0308 unsigned totalDisks = value[5] + (value[4] << 8); 0309 str.setNum(disk); 0310 if (totalDisks > 0) { 0311 str += '/'; 0312 str += QByteArray().setNum(totalDisks); 0313 } 0314 } 0315 } else if (std::strcmp(name, "gnre") == 0) { 0316 if (size >= 2) { 0317 unsigned genreNum = value[1] + (value[0] << 8); 0318 if (genreNum > 0) { 0319 str = Genres::getName(genreNum - 1); 0320 } 0321 } 0322 } else if (std::strcmp(name, "cpil") == 0) { 0323 if (size >= 1) { 0324 str.setNum(value[0]); 0325 } 0326 } else if (std::strcmp(name, "tmpo") == 0) { 0327 if (size >= 2) { 0328 unsigned bpm = value[1] + (value[0] << 8); 0329 if (bpm > 0) { 0330 str.setNum(bpm); 0331 } 0332 } 0333 } else if (std::strcmp(name, "covr") == 0) { 0334 QByteArray ba; 0335 ba = QByteArray(reinterpret_cast<const char*>(value), size); 0336 return ba; 0337 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 0338 } else if (std::strcmp(name, "pgap") == 0) { 0339 if (size >= 1) { 0340 str.setNum(value[0]); 0341 } 0342 #endif 0343 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0344 } else if (std::strcmp(name, "tvsn") == 0 || std::strcmp(name, "tves") == 0 || 0345 std::strcmp(name, "sfID") == 0 || std::strcmp(name, "cnID") == 0 || 0346 std::strcmp(name, "atID") == 0 || std::strcmp(name, "geID") == 0 || 0347 std::strcmp(name, "cmID") == 0) { 0348 if (size >= 4) { 0349 uint val = value[3] + (value[2] << 8) + 0350 (value[1] << 16) + (value[0] << 24); 0351 if (val > 0) { 0352 str.setNum(val); 0353 } 0354 } 0355 } else if (std::strcmp(name, "pcst") == 0 || std::strcmp(name, "hdvd") == 0 || 0356 std::strcmp(name, "stik") == 0 || std::strcmp(name, "rtng") == 0 || 0357 std::strcmp(name, "akID") == 0) { 0358 if (size >= 1) { 0359 str.setNum(value[0]); 0360 } 0361 } else if (std::strcmp(name, "plID") == 0) { 0362 if (size >= 8) { 0363 qulonglong val = 0364 static_cast<qulonglong>(value[7]) + 0365 (static_cast<qulonglong>(value[6]) << 8) + 0366 (static_cast<qulonglong>(value[5]) << 16) + 0367 (static_cast<qulonglong>(value[4]) << 24) + 0368 (static_cast<qulonglong>(value[3]) << 32) + 0369 (static_cast<qulonglong>(value[2]) << 40) + 0370 (static_cast<qulonglong>(value[1]) << 48) + 0371 (static_cast<qulonglong>(value[0]) << 56); 0372 if (val > 0) { 0373 str.setNum(val); 0374 } 0375 } 0376 #endif 0377 } else { 0378 str = QByteArray(reinterpret_cast<const char*>(value), size); 0379 } 0380 return str; 0381 } 0382 0383 /** 0384 * Set a SYLT frame with data from MP4 chapters. 0385 * @param frame frame to set 0386 * @param data list with time stamps and chapter titles 0387 */ 0388 void setMp4ChaptersFields(Frame& frame, 0389 const QVariantList& data = QVariantList()) 0390 { 0391 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Other, 0392 QLatin1String("Chapters"))); 0393 frame.setValue(QString()); 0394 0395 Frame::Field field; 0396 Frame::FieldList& fields = frame.fieldList(); 0397 fields.clear(); 0398 0399 field.m_id = Frame::ID_TimestampFormat; 0400 field.m_value = 2; // milliseconds 0401 fields.append(field); 0402 0403 field.m_id = Frame::ID_ContentType; 0404 field.m_value = 0; // other 0405 fields.append(field); 0406 0407 field.m_id = Frame::ID_Description; 0408 field.m_value = QString(); 0409 fields.append(field); 0410 0411 field.m_id = Frame::ID_Data; 0412 field.m_value = data; 0413 fields.append(field); 0414 } 0415 0416 /** 0417 * Set a SYLT frame from MP4 chapters. 0418 * @param chapterList MP4 chapters 0419 * @param chapterCount number of elements in chapterList 0420 * @param frame the SYLT frame is returned here 0421 */ 0422 void mp4ChaptersToFrame(const MP4Chapter_t* chapterList, uint32_t chapterCount, 0423 Frame& frame) 0424 { 0425 QVariantList data; 0426 quint32 time = 0; 0427 for (uint32_t i = 0; i < chapterCount; ++i) { 0428 MP4Chapter_t chapter = chapterList[i]; 0429 data.append(time); 0430 data.append(QString::fromUtf8(chapter.title)); 0431 time += chapter.duration; 0432 } 0433 data.append(time); 0434 data.append(QString()); 0435 setMp4ChaptersFields(frame, data); 0436 } 0437 0438 /** 0439 * Set MP4 chapters from a SYLT frame. 0440 * @param frame SYLT frame 0441 * @param chapterList the chapters are returned here and must be freed using 0442 * delete[] afterwards 0443 * @param chapterCount the number of elements in @a chapterList is returned here 0444 */ 0445 void frameToMp4Chapters(const Frame& frame, 0446 MP4Chapter_t*& chapterList, uint32_t& chapterCount) 0447 { 0448 QVariantList data = Frame::getField(frame, Frame::ID_Data).toList(); 0449 int dataLen = data.size(); 0450 if (dataLen >= 2) { 0451 quint32 lastTime = data.at(dataLen - 2).toUInt(); 0452 QString lastTitle = data.at(dataLen - 1).toString(); 0453 if (!lastTitle.trimmed().isEmpty()) { 0454 data.append(lastTime); 0455 data.append(QString()); 0456 dataLen += 2; 0457 } 0458 } 0459 if (dataLen > 2 && (dataLen & 1) == 0) { 0460 chapterCount = (dataLen - 2) / 2; 0461 chapterList = new MP4Chapter_t[chapterCount]; 0462 quint32 lastTime = 0; 0463 uint32_t i = 0; 0464 QListIterator<QVariant> it(data); 0465 while (it.hasNext()) { 0466 quint32 time = it.next().toUInt(); 0467 if (!it.hasNext()) 0468 break; 0469 0470 QByteArray chapterTitle = it.next().toString().trimmed().toUtf8(); 0471 if (i < chapterCount) { 0472 MP4Chapter_t* mp4Chapter = &chapterList[i]; 0473 qstrncpy(mp4Chapter->title, chapterTitle.constData(), 0474 sizeof(mp4Chapter->title) - 1); 0475 mp4Chapter->title[sizeof(mp4Chapter->title) - 1] = '\0'; 0476 } 0477 if (i > 0 && i <= chapterCount) { 0478 chapterList[i - 1].duration = time - lastTime; 0479 } 0480 lastTime = time; 0481 ++i; 0482 } 0483 } else { 0484 chapterCount = 0; 0485 chapterList = nullptr; 0486 } 0487 } 0488 0489 /** 0490 * Check if two chapters frames are equal. 0491 * @param f1 first chapters frame 0492 * @param f2 second chapters frame 0493 * @return true if equal. 0494 */ 0495 bool areMp4ChaptersFieldsEqual(const Frame& f1, const Frame& f2) 0496 { 0497 return Frame::getField(f1, Frame::ID_Data) == Frame::getField(f2, Frame::ID_Data); 0498 } 0499 0500 } 0501 0502 /** 0503 * Constructor. 0504 * 0505 * @param idx index in tagged file system model 0506 */ 0507 M4aFile::M4aFile(const QPersistentModelIndex& idx) 0508 : TaggedFile(idx), m_fileRead(false) 0509 { 0510 } 0511 0512 /** 0513 * Get key of tagged file format. 0514 * @return "Mp4v2Metadata". 0515 */ 0516 QString M4aFile::taggedFileKey() const 0517 { 0518 return QLatin1String("Mp4v2Metadata"); 0519 } 0520 0521 /** 0522 * Read tags from file. 0523 * 0524 * @param force true to force reading even if tags were already read. 0525 */ 0526 void M4aFile::readTags(bool force) 0527 { 0528 bool priorIsTagInformationRead = isTagInformationRead(); 0529 if (force || !m_fileRead) { 0530 m_metadata.clear(); 0531 m_extraFrames.clear(); 0532 markTagUnchanged(Frame::Tag_2); 0533 m_fileRead = true; 0534 QByteArray fnIn = 0535 #ifdef Q_OS_WIN32 0536 currentFilePath().toUtf8(); 0537 #else 0538 QFile::encodeName(currentFilePath()); 0539 #endif 0540 0541 MP4FileHandle handle = MP4Read(fnIn); 0542 if (handle != MP4_INVALID_FILE_HANDLE) { 0543 m_fileInfo.read(handle); 0544 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0545 MP4ItmfItemList* list = MP4ItmfGetItems(handle); 0546 if (list) { 0547 for (uint32_t i = 0; i < list->size; ++i) { 0548 MP4ItmfItem& item = list->elements[i]; 0549 const char* key = nullptr; 0550 if (memcmp(item.code, "----", 4) == 0) { 0551 // free form tagfield 0552 if (item.name) { 0553 key = item.name; 0554 } 0555 } else { 0556 key = item.code; 0557 } 0558 if (key) { 0559 if (std::strcmp(key, "covr") == 0) { 0560 if (item.dataList.size > 0) { 0561 int i; 0562 MP4ItmfData* element; 0563 for (i = 0, element = item.dataList.elements; 0564 i < static_cast<int>(item.dataList.size); 0565 ++i, ++element) { 0566 QString mimeType, imgFormat; 0567 switch (element->typeCode) { 0568 case MP4_ITMF_BT_PNG: 0569 mimeType = QLatin1String("image/png"); 0570 imgFormat = QLatin1String("PNG"); 0571 break; 0572 case MP4_ITMF_BT_BMP: 0573 mimeType = QLatin1String("image/bmp"); 0574 imgFormat = QLatin1String("BMP"); 0575 break; 0576 case MP4_ITMF_BT_GIF: 0577 mimeType = QLatin1String("image/gif"); 0578 imgFormat = QLatin1String("GIF"); 0579 break; 0580 case MP4_ITMF_BT_JPEG: 0581 default: 0582 mimeType = QLatin1String("image/jpeg"); 0583 imgFormat = QLatin1String("JPG"); 0584 } 0585 PictureFrame frame( 0586 getValueByteArray(key, element->value, element->valueSize), 0587 QLatin1String(""), PictureFrame::PT_CoverFront, mimeType, 0588 Frame::TE_ISO8859_1, imgFormat); 0589 frame.setIndex(Frame::toNegativeIndex(i)); 0590 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Picture, 0591 QLatin1String(key))); 0592 m_extraFrames.append(frame); 0593 } 0594 } 0595 } else { 0596 QByteArray ba; 0597 if (item.dataList.size > 0 && 0598 item.dataList.elements[0].value && 0599 item.dataList.elements[0].valueSize > 0) { 0600 ba = getValueByteArray(key, item.dataList.elements[0].value, 0601 item.dataList.elements[0].valueSize); 0602 } 0603 m_metadata[QString::fromLatin1(key)] = ba; 0604 } 0605 } 0606 } 0607 MP4ItmfItemListFree(list); 0608 } 0609 0610 MP4Chapter_t* chapterList = nullptr; 0611 uint32_t chapterCount = 0; 0612 MP4GetChapters(handle, &chapterList, &chapterCount, MP4ChapterTypeQt); 0613 if (chapterList) { 0614 Frame frame; 0615 mp4ChaptersToFrame(chapterList, chapterCount, frame); 0616 frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size())); 0617 m_extraFrames.append(frame); 0618 MP4Free(chapterList); 0619 } 0620 #elif defined HAVE_MP4V2_MP4GETMETADATABYINDEX_CHARPP_ARG 0621 static char notFreeFormStr[] = "NOFF"; 0622 static char freeFormStr[] = "----"; 0623 char* ppName; 0624 uint8_t* ppValue = 0; 0625 uint32_t pValueSize = 0; 0626 uint32_t index = 0; 0627 unsigned numEmptyEntries = 0; 0628 for (index = 0; index < 64; ++index) { 0629 ppName = notFreeFormStr; 0630 bool ok = MP4GetMetadataByIndex(handle, index, 0631 &ppName, &ppValue, &pValueSize); 0632 if (ok && ppName && memcmp(ppName, "----", 4) == 0) { 0633 // free form tagfield 0634 free(ppName); 0635 free(ppValue); 0636 ppName = freeFormStr; 0637 ppValue = 0; 0638 pValueSize = 0; 0639 ok = MP4GetMetadataByIndex(handle, index, 0640 &ppName, &ppValue, &pValueSize); 0641 } 0642 if (ok) { 0643 numEmptyEntries = 0; 0644 if (ppName) { 0645 QString key(ppName); 0646 QByteArray ba; 0647 if (ppValue && pValueSize > 0) { 0648 ba = getValueByteArray(ppName, ppValue, pValueSize); 0649 } 0650 m_metadata[key] = ba; 0651 free(ppName); 0652 } 0653 free(ppValue); 0654 ppName = 0; 0655 ppValue = 0; 0656 pValueSize = 0; 0657 } else { 0658 // There are iTunes files with invalid fields in between, 0659 // so we stop after 3 invalid indices. 0660 if (++numEmptyEntries >= 3) { 0661 break; 0662 } 0663 } 0664 } 0665 #else 0666 const char* ppName = 0; 0667 uint8_t* ppValue = 0; 0668 uint32_t pValueSize = 0; 0669 uint32_t index = 0; 0670 unsigned numEmptyEntries = 0; 0671 for (index = 0; index < 64; ++index) { 0672 if (MP4GetMetadataByIndex(handle, index, 0673 &ppName, &ppValue, &pValueSize)) { 0674 numEmptyEntries = 0; 0675 if (ppName) { 0676 QString key(ppName); 0677 QByteArray ba; 0678 if (ppValue && pValueSize > 0) { 0679 ba = getValueByteArray(ppName, ppValue, pValueSize); 0680 } 0681 m_metadata[key] = ba; 0682 0683 // If the field is free form, there are two memory leaks in mp4v2. 0684 // The first is not accessible, the second can be freed. 0685 if (isFreeFormMetadata(handle, ppName)) { 0686 free(const_cast<char*>(ppName)); 0687 } 0688 } 0689 free(ppValue); 0690 ppName = 0; 0691 ppValue = 0; 0692 pValueSize = 0; 0693 } else { 0694 // There are iTunes files with invalid fields in between, 0695 // so we stop after 3 invalid indices. 0696 if (++numEmptyEntries >= 3) { 0697 break; 0698 } 0699 } 0700 } 0701 #endif 0702 MP4Close(handle 0703 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200 0704 , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE 0705 #endif 0706 ); 0707 } 0708 } 0709 0710 if (force) { 0711 setFilename(currentFilename()); 0712 } 0713 0714 notifyModelDataChanged(priorIsTagInformationRead); 0715 } 0716 0717 /** 0718 * Write tags to file and rename it if necessary. 0719 * 0720 * @param force true to force writing even if file was not changed. 0721 * @param renamed will be set to true if the file was renamed, 0722 * i.e. the file name is no longer valid, else *renamed 0723 * is left unchanged 0724 * @param preserve true to preserve file time stamps 0725 * 0726 * @return true if ok, false if the file could not be written or renamed. 0727 */ 0728 bool M4aFile::writeTags(bool force, bool* renamed, bool preserve) 0729 { 0730 bool ok = true; 0731 QString fnStr(currentFilePath()); 0732 if (isChanged() && !QFileInfo(fnStr).isWritable()) { 0733 revertChangedFilename(); 0734 return false; 0735 } 0736 0737 if (m_fileRead && (force || isTagChanged(Frame::Tag_2))) { 0738 QByteArray fn = 0739 #ifdef Q_OS_WIN32 0740 fnStr.toUtf8(); 0741 #else 0742 QFile::encodeName(fnStr); 0743 #endif 0744 0745 // store time stamp if it has to be preserved 0746 quint64 actime = 0, modtime = 0; 0747 if (preserve) { 0748 getFileTimeStamps(fnStr, actime, modtime); 0749 } 0750 0751 MP4FileHandle handle = MP4Modify(fn); 0752 if (handle != MP4_INVALID_FILE_HANDLE) { 0753 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0754 MP4ItmfItemList* list = MP4ItmfGetItems(handle); 0755 if (list) { 0756 for (uint32_t i = 0; i < list->size; ++i) { 0757 MP4ItmfRemoveItem(handle, &list->elements[i]); 0758 } 0759 MP4ItmfItemListFree(list); 0760 } 0761 const MP4Tags* tags = MP4TagsAlloc(); 0762 #else 0763 // return code is not checked because it will fail if no metadata exists 0764 MP4MetadataDelete(handle); 0765 #endif 0766 0767 for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) { 0768 const QByteArray& value = *it; 0769 if (!value.isEmpty()) { 0770 const QString& name = it.key(); 0771 const QByteArray& str = value; 0772 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 0773 // clazy:excludeall=qlatin1string-non-ascii 0774 if (name == QLatin1String("\251nam")) { 0775 MP4TagsSetName(tags, str); 0776 } else if (name == QLatin1String("\251ART")) { 0777 MP4TagsSetArtist(tags, str); 0778 } else if (name == QLatin1String("\251wrt")) { 0779 MP4TagsSetComposer(tags, str); 0780 } else if (name == QLatin1String("\251cmt")) { 0781 MP4TagsSetComments(tags, str); 0782 } else if (name == QLatin1String("\251too")) { 0783 MP4TagsSetEncodingTool(tags, str); 0784 } else if (name == QLatin1String("\251day")) { 0785 MP4TagsSetReleaseDate(tags, str); 0786 } else if (name == QLatin1String("\251alb")) { 0787 MP4TagsSetAlbum(tags, str); 0788 } else if (name == QLatin1String("trkn")) { 0789 MP4TagTrack indexTotal; 0790 int slashPos = str.indexOf('/'); 0791 if (slashPos != -1) { 0792 indexTotal.total = str.mid(slashPos + 1).toUShort(); 0793 indexTotal.index = str.mid(0, slashPos).toUShort(); 0794 } else { 0795 indexTotal.total = 0; 0796 indexTotal.index = str.toUShort(); 0797 } 0798 MP4TagsSetTrack(tags, &indexTotal); 0799 } else if (name == QLatin1String("disk")) { 0800 MP4TagDisk indexTotal; 0801 int slashPos = str.indexOf('/'); 0802 if (slashPos != -1) { 0803 indexTotal.total = str.mid(slashPos + 1).toUShort(); 0804 indexTotal.index = str.mid(0, slashPos).toUShort(); 0805 } else { 0806 indexTotal.total = 0; 0807 indexTotal.index = str.toUShort(); 0808 } 0809 MP4TagsSetDisk(tags, &indexTotal); 0810 } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) { 0811 MP4TagsSetGenre(tags, str); 0812 } else if (name == QLatin1String("tmpo")) { 0813 uint16_t tempo = str.toUShort(); 0814 MP4TagsSetTempo(tags, &tempo); 0815 } else if (name == QLatin1String("cpil")) { 0816 uint8_t cpl = str.toUShort(); 0817 MP4TagsSetCompilation(tags, &cpl); 0818 } else if (name == QLatin1String("\251grp")) { 0819 MP4TagsSetGrouping(tags, str); 0820 } else if (name == QLatin1String("aART")) { 0821 MP4TagsSetAlbumArtist(tags, str); 0822 } else if (name == QLatin1String("pgap")) { 0823 uint8_t pgap = str.toUShort(); 0824 MP4TagsSetGapless(tags, &pgap); 0825 } else if (name == QLatin1String("tvsh")) { 0826 MP4TagsSetTVShow(tags, str); 0827 } else if (name == QLatin1String("tvnn")) { 0828 MP4TagsSetTVNetwork(tags, str); 0829 } else if (name == QLatin1String("tven")) { 0830 MP4TagsSetTVEpisodeID(tags, str); 0831 } else if (name == QLatin1String("tvsn")) { 0832 uint32_t val = str.toULong(); 0833 MP4TagsSetTVSeason(tags, &val); 0834 } else if (name == QLatin1String("tves")) { 0835 uint32_t val = str.toULong(); 0836 MP4TagsSetTVEpisode(tags, &val); 0837 } else if (name == QLatin1String("desc")) { 0838 MP4TagsSetDescription(tags, str); 0839 } else if (name == QLatin1String("ldes")) { 0840 MP4TagsSetLongDescription(tags, str); 0841 } else if (name == QLatin1String("\251lyr")) { 0842 MP4TagsSetLyrics(tags, str); 0843 } else if (name == QLatin1String("sonm")) { 0844 MP4TagsSetSortName(tags, str); 0845 } else if (name == QLatin1String("soar")) { 0846 MP4TagsSetSortArtist(tags, str); 0847 } else if (name == QLatin1String("soaa")) { 0848 MP4TagsSetSortAlbumArtist(tags, str); 0849 } else if (name == QLatin1String("soal")) { 0850 MP4TagsSetSortAlbum(tags, str); 0851 } else if (name == QLatin1String("soco")) { 0852 MP4TagsSetSortComposer(tags, str); 0853 } else if (name == QLatin1String("sosn")) { 0854 MP4TagsSetSortTVShow(tags, str); 0855 } else if (name == QLatin1String("cprt")) { 0856 MP4TagsSetCopyright(tags, str); 0857 } else if (name == QLatin1String("\251enc")) { 0858 MP4TagsSetEncodedBy(tags, str); 0859 } else if (name == QLatin1String("purd")) { 0860 MP4TagsSetPurchaseDate(tags, str); 0861 } else if (name == QLatin1String("pcst")) { 0862 uint8_t val = str.toUShort(); 0863 MP4TagsSetPodcast(tags, &val); 0864 } else if (name == QLatin1String("keyw")) { 0865 MP4TagsSetKeywords(tags, str); 0866 } else if (name == QLatin1String("catg")) { 0867 MP4TagsSetCategory(tags, str); 0868 } else if (name == QLatin1String("hdvd")) { 0869 uint8_t val = str.toUShort(); 0870 MP4TagsSetHDVideo(tags, &val); 0871 } else if (name == QLatin1String("stik")) { 0872 uint8_t val = str.toUShort(); 0873 MP4TagsSetMediaType(tags, &val); 0874 } else if (name == QLatin1String("rtng")) { 0875 uint8_t val = str.toUShort(); 0876 MP4TagsSetContentRating(tags, &val); 0877 } else if (name == QLatin1String("apID")) { 0878 MP4TagsSetITunesAccount(tags, str); 0879 } else if (name == QLatin1String("akID")) { 0880 uint8_t val = str.toUShort(); 0881 MP4TagsSetITunesAccountType(tags, &val); 0882 } else if (name == QLatin1String("sfID")) { 0883 uint32_t val = str.toULong(); 0884 MP4TagsSetITunesCountry(tags, &val); 0885 } else if (name == QLatin1String("cnID")) { 0886 uint32_t val = str.toULong(); 0887 MP4TagsSetContentID(tags, &val); 0888 } else if (name == QLatin1String("atID")) { 0889 uint32_t val = str.toULong(); 0890 MP4TagsSetArtistID(tags, &val); 0891 } else if (name == QLatin1String("plID")) { 0892 uint64_t val = str.toULongLong(); 0893 MP4TagsSetPlaylistID(tags, &val); 0894 } else if (name == QLatin1String("geID")) { 0895 uint32_t val = str.toULong(); 0896 MP4TagsSetGenreID(tags, &val); 0897 } else if (name == QLatin1String("cmID")) { 0898 uint32_t val = str.toULong(); 0899 MP4TagsSetComposerID(tags, &val); 0900 } else if (name == QLatin1String("xid ")) { 0901 MP4TagsSetXID(tags, str); 0902 } else { 0903 MP4ItmfItem* item; 0904 if (name.length() == 4 && 0905 (name.at(0).toLatin1() == '\251' || 0906 (name.at(0) >= QLatin1Char('a') && 0907 name.at(0) <= QLatin1Char('z')))) { 0908 item = MP4ItmfItemAlloc(name.toLatin1().constData(), 1); 0909 } else { 0910 item = MP4ItmfItemAlloc("----", 1); 0911 item->mean = strdup("com.apple.iTunes"); 0912 item->name = strdup(name.toUtf8().data()); 0913 } 0914 0915 MP4ItmfData& data = item->dataList.elements[0]; 0916 data.typeCode = MP4_ITMF_BT_UTF8; 0917 data.valueSize = value.size(); 0918 data.value = reinterpret_cast<uint8_t*>(malloc(data.valueSize)); 0919 memcpy(data.value, value.data(), data.valueSize); 0920 0921 MP4ItmfAddItem(handle, item); 0922 MP4ItmfItemFree(item); 0923 } 0924 #else 0925 bool setOk; 0926 if (name == QLatin1String("\251nam")) { 0927 setOk = MP4SetMetadataName(handle, str); 0928 } else if (name == QLatin1String("\251ART")) { 0929 setOk = MP4SetMetadataArtist(handle, str); 0930 } else if (name == QLatin1String("\251wrt")) { 0931 setOk = MP4SetMetadataWriter(handle, str); 0932 } else if (name == QLatin1String("\251cmt")) { 0933 setOk = MP4SetMetadataComment(handle, str); 0934 } else if (name == QLatin1String("\251too")) { 0935 setOk = MP4SetMetadataTool(handle, str); 0936 } else if (name == QLatin1String("\251day")) { 0937 unsigned short year = str.toUShort(); 0938 if (year > 0) { 0939 if (year < 1000) year += 2000; 0940 else if (year > 9999) year = 9999; 0941 setOk = MP4SetMetadataYear(handle, QByteArray().setNum(year)); 0942 if (setOk) { 0943 if (year >= 0) { 0944 setTextField(QLatin1String("\251day"), 0945 year != 0 ? QString::number(year) 0946 : QLatin1String(""), Frame::FT_Date); 0947 } 0948 } 0949 } else { 0950 setOk = true; 0951 } 0952 } else if (name == QLatin1String("\251alb")) { 0953 setOk = MP4SetMetadataAlbum(handle, str); 0954 } else if (name == QLatin1String("trkn")) { 0955 uint16_t track = 0, totalTracks = 0; 0956 int slashPos = str.indexOf('/'); 0957 if (slashPos != -1) { 0958 totalTracks = str.mid(slashPos + 1).toUShort(); 0959 track = str.mid(0, slashPos).toUShort(); 0960 } else { 0961 track = str.toUShort(); 0962 } 0963 setOk = MP4SetMetadataTrack(handle, track, totalTracks); 0964 } else if (name == QLatin1String("disk")) { 0965 uint16_t disk = 0, totalDisks = 0; 0966 int slashPos = str.indexOf('/'); 0967 if (slashPos != -1) { 0968 totalDisks = str.mid(slashPos + 1).toUShort(); 0969 disk = str.mid(0, slashPos).toUShort(); 0970 } else { 0971 disk = str.toUShort(); 0972 } 0973 setOk = MP4SetMetadataDisk(handle, disk, totalDisks); 0974 } else if (name == QLatin1String("\251gen") || name == QLatin1String("gnre")) { 0975 setOk = MP4SetMetadataGenre(handle, str); 0976 } else if (name == QLatin1String("tmpo")) { 0977 uint16_t tempo = str.toUShort(); 0978 setOk = MP4SetMetadataTempo(handle, tempo); 0979 } else if (name == QLatin1String("cpil")) { 0980 uint8_t cpl = str.toUShort(); 0981 setOk = MP4SetMetadataCompilation(handle, cpl); 0982 // While this works on Debian Etch with libmp4v2-dev 1.5.0.1-0.3 from 0983 // www.debian-multimedia.org, linking on OpenSUSE 10.3 with 0984 // libmp4v2-devel-1.5.0.1-6 from packman.links2linux.de fails with 0985 // undefined reference to MP4SetMetadataGrouping. To avoid this, 0986 // in the line below, 0x105 is replaced by 0x106. 0987 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 0988 } else if (name == QLatin1String("\251grp")) { 0989 setOk = MP4SetMetadataGrouping(handle, str); 0990 #endif 0991 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 0992 } else if (name == QLatin1String("aART")) { 0993 setOk = MP4SetMetadataAlbumArtist(handle, str); 0994 } else if (name == QLatin1String("pgap")) { 0995 uint8_t pgap = str.toUShort(); 0996 setOk = MP4SetMetadataPartOfGaplessAlbum(handle, pgap); 0997 #endif 0998 } else { 0999 setOk = MP4SetMetadataFreeForm( 1000 handle, const_cast<char*>(name.toUtf8().data()), 1001 reinterpret_cast<uint8_t*>(const_cast<char*>(value.data())), 1002 value.size()); 1003 } 1004 if (!setOk) { 1005 qDebug("MP4SetMetadata %s failed", name.toLatin1().data()); 1006 ok = false; 1007 } 1008 #endif 1009 } 1010 } 1011 1012 bool hasChapters = false; 1013 const auto frames = m_extraFrames; 1014 for (const Frame& frame : frames) { 1015 if (frame.getType() == Frame::FT_Other && 1016 frame.getName() == QLatin1String("Chapters")) { 1017 uint32_t chapterCount = 0; 1018 MP4Chapter_t* chapterList = nullptr; 1019 frameToMp4Chapters(frame, chapterList, chapterCount); 1020 MP4SetChapters(handle, chapterList, chapterCount, MP4ChapterTypeQt); 1021 hasChapters = true; 1022 delete [] chapterList; 1023 } else { 1024 QByteArray ba; 1025 if (PictureFrame::getData(frame, ba)) { 1026 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1027 MP4TagArtwork artwork; 1028 artwork.data = ba.data(); 1029 artwork.size = static_cast<uint32_t>(ba.size()); 1030 artwork.type = MP4_ART_JPEG; 1031 QString mimeType; 1032 if (PictureFrame::getMimeType(frame, mimeType)) { 1033 if (mimeType == QLatin1String("image/png")) { 1034 artwork.type = MP4_ART_PNG; 1035 } else if (mimeType == QLatin1String("image/bmp")) { 1036 artwork.type = MP4_ART_BMP; 1037 } else if (mimeType == QLatin1String("image/gif")) { 1038 artwork.type = MP4_ART_GIF; 1039 } 1040 } 1041 MP4TagsAddArtwork(tags, &artwork); 1042 #else 1043 MP4SetMetadataCoverArt(handle, reinterpret_cast<uint8_t*>(ba.data()), 1044 ba.size()); 1045 #endif 1046 } 1047 } 1048 } 1049 if (!hasChapters) { 1050 MP4DeleteChapters(handle); 1051 } 1052 1053 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1054 MP4TagsStore(tags, handle); 1055 MP4TagsFree(tags); 1056 #endif 1057 1058 MP4Close(handle 1059 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0200 1060 , MP4_CLOSE_DO_NOT_COMPUTE_BITRATE 1061 #endif 1062 ); 1063 if (ok) { 1064 // without this, old tags stay in the file marked as free 1065 MP4Optimize(fn); 1066 markTagUnchanged(Frame::Tag_2); 1067 } 1068 1069 // restore time stamp 1070 if (actime || modtime) { 1071 setFileTimeStamps(fnStr, actime, modtime); 1072 } 1073 } else { 1074 qDebug("MP4Modify failed"); 1075 ok = false; 1076 } 1077 } 1078 1079 if (isFilenameChanged()) { 1080 if (!renameFile()) { 1081 return false; 1082 } 1083 markFilenameUnchanged(); 1084 // link tags to new file name 1085 readTags(true); 1086 *renamed = true; 1087 } 1088 return ok; 1089 } 1090 1091 /** 1092 * Free resources allocated when calling readTags(). 1093 * 1094 * @param force true to force clearing even if the tags are modified 1095 */ 1096 void M4aFile::clearTags(bool force) 1097 { 1098 if (!m_fileRead || (isChanged() && !force)) 1099 return; 1100 1101 bool priorIsTagInformationRead = isTagInformationRead(); 1102 m_metadata.clear(); 1103 m_extraFrames.clear(); 1104 markTagUnchanged(Frame::Tag_2); 1105 m_fileRead = false; 1106 notifyModelDataChanged(priorIsTagInformationRead); 1107 } 1108 1109 /** 1110 * Remove frames. 1111 * 1112 * @param tagNr tag number 1113 * @param flt filter specifying which frames to remove 1114 */ 1115 void M4aFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt) 1116 { 1117 if (tagNr != Frame::Tag_2) 1118 return; 1119 1120 if (flt.areAllEnabled()) { 1121 m_metadata.clear(); 1122 m_extraFrames.clear(); 1123 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 1124 } else { 1125 bool changed = false; 1126 for (auto it = m_metadata.begin(); it != m_metadata.end();) { // clazy:exclude=detaching-member 1127 QString name(it.key()); 1128 Frame::Type type = getTypeForName(name); 1129 if (flt.isEnabled(type, name)) { 1130 it = m_metadata.erase(it); 1131 changed = true; 1132 } else { 1133 ++it; 1134 } 1135 } 1136 bool pictureEnabled = flt.isEnabled(Frame::FT_Picture); 1137 bool chaptersEnabled = flt.isEnabled(Frame::FT_Other, 1138 QLatin1String("Chapters")); 1139 if ((pictureEnabled || chaptersEnabled) && !m_extraFrames.isEmpty()) { 1140 for (auto it = m_extraFrames.begin(); it != m_extraFrames.end();) { 1141 Frame::Type type = it->getType(); 1142 if ((pictureEnabled && type == Frame::FT_Picture) || 1143 (chaptersEnabled && type == Frame::FT_Other && 1144 it->getName() == QLatin1String("Chapters"))) { 1145 it = m_extraFrames.erase(it); 1146 changed = true; 1147 } else { 1148 ++it; 1149 } 1150 } 1151 } 1152 if (changed) { 1153 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 1154 } 1155 } 1156 } 1157 1158 /** 1159 * Get metadata field as string. 1160 * 1161 * @param name field name 1162 * 1163 * @return value as string, "" if not found, 1164 * QString::null if the tags have not been read yet. 1165 */ 1166 QString M4aFile::getTextField(const QString& name) const 1167 { 1168 if (m_fileRead) { 1169 auto it = m_metadata.constFind(name); 1170 if (it != m_metadata.constEnd()) { 1171 return QString::fromUtf8((*it).data(), (*it).size()); 1172 } 1173 return QLatin1String(""); 1174 } 1175 return QString(); 1176 } 1177 1178 /** 1179 * Set text field. 1180 * If value is null if the tags have not been read yet, nothing is changed. 1181 * If value is different from the current value, tag 2 is marked as changed. 1182 * 1183 * @param name name 1184 * @param value value, "" to remove, QString::null to do nothing 1185 * @param type frame type 1186 */ 1187 void M4aFile::setTextField(const QString& name, const QString& value, 1188 const Frame::ExtendedType& type) 1189 { 1190 if (m_fileRead && !value.isNull()) { 1191 QByteArray str = value.toUtf8(); 1192 auto it = m_metadata.find(name); // clazy:exclude=detaching-member 1193 if (it != m_metadata.end()) { 1194 if (QString::fromUtf8((*it).data(), (*it).size()) != value) { 1195 *it = str; 1196 markTagChanged(Frame::Tag_2, type); 1197 } 1198 } else { 1199 m_metadata.insert(name, str); 1200 markTagChanged(Frame::Tag_2, type); 1201 } 1202 } 1203 } 1204 1205 /** 1206 * Check if tag information has already been read. 1207 * 1208 * @return true if information is available, 1209 * false if the tags have not been read yet, in which case 1210 * hasTag() does not return meaningful information. 1211 */ 1212 bool M4aFile::isTagInformationRead() const 1213 { 1214 return m_fileRead; 1215 } 1216 1217 /** 1218 * Check if file has a tag. 1219 * 1220 * @param tagNr tag number 1221 * @return true if a V2 tag is available. 1222 * @see isTagInformationRead() 1223 */ 1224 bool M4aFile::hasTag(Frame::TagNumber tagNr) const 1225 { 1226 return tagNr == Frame::Tag_2 && !m_metadata.empty(); 1227 } 1228 1229 /** 1230 * Get file extension including the dot. 1231 * 1232 * @return file extension ".m4a". 1233 */ 1234 QString M4aFile::getFileExtension() const 1235 { 1236 return QLatin1String(".m4a"); 1237 } 1238 1239 /** 1240 * Get technical detail information. 1241 * 1242 * @param info the detail information is returned here 1243 */ 1244 void M4aFile::getDetailInfo(DetailInfo& info) const 1245 { 1246 if (m_fileRead && m_fileInfo.valid) { 1247 info.valid = true; 1248 info.format = QLatin1String("MP4"); 1249 info.bitrate = m_fileInfo.bitrate; 1250 info.sampleRate = m_fileInfo.sampleRate; 1251 info.channels = m_fileInfo.channels; 1252 info.duration = m_fileInfo.duration; 1253 } else { 1254 info.valid = false; 1255 } 1256 } 1257 1258 /** 1259 * Get duration of file. 1260 * 1261 * @return duration in seconds, 1262 * 0 if unknown. 1263 */ 1264 unsigned M4aFile::getDuration() const 1265 { 1266 if (m_fileRead && m_fileInfo.valid) { 1267 return m_fileInfo.duration; 1268 } 1269 return 0; 1270 } 1271 1272 /** 1273 * Get the format of tag. 1274 * 1275 * @param tagNr tag number 1276 * @return "Vorbis". 1277 */ 1278 QString M4aFile::getTagFormat(Frame::TagNumber tagNr) const 1279 { 1280 return hasTag(tagNr) ? QLatin1String("MP4") : QString(); 1281 } 1282 1283 /** 1284 * Get a specific frame from the tags. 1285 * 1286 * @param tagNr tag number 1287 * @param type frame type 1288 * @param frame the frame is returned here 1289 * 1290 * @return true if ok. 1291 */ 1292 bool M4aFile::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const 1293 { 1294 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame || 1295 tagNr > 1) 1296 return false; 1297 1298 if (tagNr == Frame::Tag_1) { 1299 frame.setValue(QString()); 1300 } else { 1301 if (type == Frame::FT_Genre) { 1302 QString str(getTextField(QLatin1String("\251gen"))); 1303 frame.setValue(str.isEmpty() ? getTextField(QLatin1String("gnre")) : str); 1304 } else { 1305 frame.setValue(getTextField(getNameForType(type))); 1306 } 1307 } 1308 frame.setType(type); 1309 return true; 1310 } 1311 1312 /** 1313 * Set a frame in the tags. 1314 * 1315 * @param tagNr tag number 1316 * @param frame frame to set 1317 * 1318 * @return true if ok. 1319 */ 1320 bool M4aFile::setFrame(Frame::TagNumber tagNr, const Frame& frame) 1321 { 1322 if (tagNr == Frame::Tag_2) { 1323 if ((frame.getType() == Frame::FT_Picture) || 1324 (frame.getType() == Frame::FT_Other && 1325 frame.getName() == QLatin1String("Chapters"))) { 1326 int idx = Frame::fromNegativeIndex(frame.getIndex()); 1327 if (idx >= 0 && idx < m_extraFrames.size()) { 1328 Frame newFrame(frame); 1329 if ((frame.getType() == Frame::FT_Picture && 1330 PictureFrame::areFieldsEqual(m_extraFrames[idx], newFrame)) || 1331 (frame.getType() == Frame::FT_Other && 1332 frame.getName() == QLatin1String("Chapters") && 1333 areMp4ChaptersFieldsEqual(m_extraFrames[idx], newFrame))) { 1334 m_extraFrames[idx].setValueChanged(false); 1335 } else { 1336 m_extraFrames[idx] = newFrame; 1337 markTagChanged(tagNr, frame.getExtendedType()); 1338 } 1339 return true; 1340 } else { 1341 return false; 1342 } 1343 } 1344 QString name = fixUpTagKey(frame.getInternalName(), TT_Mp4); 1345 auto it = m_metadata.find(name); // clazy:exclude=detaching-member 1346 if (it != m_metadata.end()) { 1347 if (frame.getType() != Frame::FT_Picture) { 1348 QByteArray str = frame.getValue().toUtf8(); 1349 if (*it != str) { 1350 *it = str; 1351 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1352 } 1353 } else { 1354 if (PictureFrame::getData(frame, *it)) { 1355 markTagChanged(Frame::Tag_2, 1356 Frame::ExtendedType(Frame::FT_Picture, name)); 1357 } 1358 } 1359 return true; 1360 } 1361 } 1362 1363 // Try the basic method 1364 Frame::Type type = frame.getType(); 1365 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame || 1366 tagNr > 1) 1367 return false; 1368 1369 if (tagNr == Frame::Tag_2) { 1370 if (type == Frame::FT_Genre) { 1371 QString str = frame.getValue(); 1372 QString oldStr(getTextField(QLatin1String("\251gen"))); 1373 if (oldStr.isEmpty()) { 1374 oldStr = getTextField(QLatin1String("gnre")); 1375 } 1376 if (str != oldStr) { 1377 int genreNum = Genres::getNumber(str); 1378 if (genreNum != 255) { 1379 const QString genreName(QLatin1String("gnre")); 1380 setTextField(genreName, str, 1381 Frame::ExtendedType(Frame::FT_Genre, genreName)); 1382 m_metadata.remove(QLatin1String("\251gen")); 1383 } else { 1384 const QString genreName(QLatin1String("\251gen")); 1385 setTextField(genreName, str, 1386 Frame::ExtendedType(Frame::FT_Genre, genreName)); 1387 m_metadata.remove(QLatin1String("gnre")); 1388 } 1389 } 1390 } else if (type == Frame::FT_Track) { 1391 int numTracks; 1392 int num = splitNumberAndTotal(frame.getValue(), &numTracks); 1393 if (num >= 0) { 1394 QString str; 1395 if (num != 0) { 1396 str.setNum(num); 1397 if (numTracks == 0) 1398 numTracks = getTotalNumberOfTracksIfEnabled(); 1399 if (numTracks > 0) { 1400 str += QLatin1Char('/'); 1401 str += QString::number(numTracks); 1402 } 1403 } else { 1404 str = QLatin1String(""); 1405 } 1406 const QString trackName(QLatin1String("trkn")); 1407 setTextField(trackName, str, 1408 Frame::ExtendedType(Frame::FT_Track, trackName)); 1409 } 1410 } else { 1411 const QString fieldName = getNameForType(type); 1412 setTextField(fieldName, frame.getValue(), 1413 Frame::ExtendedType(type, fieldName)); 1414 } 1415 } 1416 return true; 1417 } 1418 1419 /** 1420 * Add a frame in the tags. 1421 * 1422 * @param tagNr tag number 1423 * @param frame frame to add 1424 * 1425 * @return true if ok. 1426 */ 1427 bool M4aFile::addFrame(Frame::TagNumber tagNr, Frame& frame) 1428 { 1429 if (tagNr == Frame::Tag_2) { 1430 Frame::ExtendedType extendedType = frame.getExtendedType(); 1431 Frame::Type type = extendedType.getType(); 1432 if (type == Frame::FT_Picture) { 1433 if (frame.getFieldList().empty()) { 1434 PictureFrame::setFields(frame); 1435 } 1436 frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size())); 1437 m_extraFrames.append(frame); 1438 markTagChanged(tagNr, extendedType); 1439 return true; 1440 } 1441 if (type == Frame::FT_Other && 1442 frame.getName() == QLatin1String("Chapters")) { 1443 if (frame.getFieldList().empty()) { 1444 setMp4ChaptersFields(frame); 1445 } 1446 frame.setIndex(Frame::toNegativeIndex(m_extraFrames.size())); 1447 m_extraFrames.append(frame); 1448 markTagChanged(Frame::Tag_2, extendedType); 1449 return true;; 1450 } 1451 QString name; 1452 if (type != Frame::FT_Other) { 1453 name = getNameForType(type); 1454 if (!name.isEmpty()) { 1455 extendedType = Frame::ExtendedType(type, name); 1456 frame.setExtendedType(extendedType); 1457 } 1458 } 1459 name = fixUpTagKey(frame.getInternalName(), TT_Mp4); 1460 m_metadata[name] = frame.getValue().toUtf8(); 1461 markTagChanged(Frame::Tag_2, extendedType); 1462 return true; 1463 } 1464 return false; 1465 } 1466 1467 /** 1468 * Delete a frame in the tags. 1469 * 1470 * @param tagNr tag number 1471 * @param frame frame to delete. 1472 * 1473 * @return true if ok. 1474 */ 1475 bool M4aFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame) 1476 { 1477 if (tagNr == Frame::Tag_2) { 1478 if ((frame.getType() == Frame::FT_Picture) || 1479 (frame.getType() == Frame::FT_Other && 1480 frame.getName() == QLatin1String("Chapters"))) { 1481 int idx = Frame::fromNegativeIndex(frame.getIndex()); 1482 if (idx >= 0 && idx < m_extraFrames.size()) { 1483 m_extraFrames.removeAt(idx); 1484 while (idx < m_extraFrames.size()) { 1485 m_extraFrames[idx].setIndex(Frame::toNegativeIndex(idx)); 1486 ++idx; 1487 } 1488 markTagChanged(tagNr, frame.getExtendedType()); 1489 return true; 1490 } 1491 } 1492 QString name(frame.getInternalName()); 1493 auto it = m_metadata.find(name); // clazy:exclude=detaching-member 1494 if (it != m_metadata.end()) { 1495 m_metadata.erase(it); 1496 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1497 return true; 1498 } 1499 } 1500 1501 // Try the superclass method 1502 return TaggedFile::deleteFrame(tagNr, frame); 1503 } 1504 1505 /** 1506 * Get all frames in tag. 1507 * 1508 * @param tagNr tag number 1509 * @param frames frame collection to set. 1510 */ 1511 void M4aFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames) 1512 { 1513 if (tagNr == Frame::Tag_2) { 1514 frames.clear(); 1515 QString name; 1516 QString value; 1517 int i = 0; 1518 for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) { 1519 name = it.key(); 1520 Frame::Type type = getTypeForName(name); 1521 value = QString::fromUtf8((*it).data(), (*it).size()); 1522 frames.insert(Frame(type, value, name, i++)); 1523 } 1524 for (auto it = m_extraFrames.constBegin(); it != m_extraFrames.constEnd(); ++it) { 1525 frames.insert(*it); 1526 } 1527 frames.addMissingStandardFrames(); 1528 return; 1529 } 1530 1531 TaggedFile::getAllFrames(tagNr, frames); 1532 } 1533 1534 /** 1535 * Get a list of frame IDs which can be added. 1536 * @param tagNr tag number 1537 * @return list with frame IDs. 1538 */ 1539 QStringList M4aFile::getFrameIds(Frame::TagNumber tagNr) const 1540 { 1541 if (tagNr != Frame::Tag_2) 1542 return QStringList(); 1543 1544 static const Frame::Type types[] = { 1545 Frame::FT_Title, 1546 Frame::FT_Artist, 1547 Frame::FT_Album, 1548 Frame::FT_Comment, 1549 Frame::FT_Compilation, 1550 Frame::FT_Date, 1551 Frame::FT_Track, 1552 Frame::FT_Genre, 1553 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 1554 Frame::FT_AlbumArtist, 1555 #endif 1556 Frame::FT_Bpm, 1557 Frame::FT_Composer, 1558 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1559 Frame::FT_Copyright, 1560 #endif 1561 Frame::FT_Description, 1562 Frame::FT_Disc, 1563 Frame::FT_EncodedBy, 1564 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1565 Frame::FT_EncoderSettings, 1566 #endif 1567 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0105 1568 Frame::FT_Grouping, 1569 #endif 1570 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1571 Frame::FT_Lyrics, 1572 #endif 1573 Frame::FT_Picture, 1574 Frame::FT_Rating 1575 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1576 , Frame::FT_SortAlbum, 1577 Frame::FT_SortAlbumArtist, 1578 Frame::FT_SortArtist, 1579 Frame::FT_SortComposer, 1580 Frame::FT_SortName 1581 #endif 1582 }; 1583 1584 QStringList lst; 1585 for (auto type : types) { 1586 lst.append(Frame::ExtendedType(type, QLatin1String("")). // clazy:exclude=reserve-candidates 1587 getName()); 1588 } 1589 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0106 1590 lst << QLatin1String("pgap"); 1591 #endif 1592 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1593 lst << QLatin1String("akID") << QLatin1String("apID") << QLatin1String("atID") << QLatin1String("catg") << QLatin1String("cnID") << 1594 QLatin1String("geID") << QLatin1String("hdvd") << QLatin1String("keyw") << QLatin1String("ldes") << QLatin1String("pcst") << 1595 QLatin1String("plID") << QLatin1String("purd") << QLatin1String("rtng") << QLatin1String("sfID") << 1596 QLatin1String("sosn") << QLatin1String("stik") << QLatin1String("tven") << 1597 QLatin1String("tves") << QLatin1String("tvnn") << QLatin1String("tvsh") << QLatin1String("tvsn") << 1598 QLatin1String("purl") << QLatin1String("egid") << QLatin1String("cmID") << QLatin1String("xid "); 1599 #endif 1600 lst << QLatin1String("Chapters"); 1601 return lst; 1602 } 1603 1604 1605 /** 1606 * Read information about an MPEG-4 file. 1607 * @param fn file name 1608 * @return true if ok. 1609 */ 1610 bool M4aFile::FileInfo::read(MP4FileHandle handle) 1611 { 1612 valid = false; 1613 uint32_t numTracks = MP4GetNumberOfTracks(handle); 1614 for (uint32_t i = 0; i < numTracks; ++i) { 1615 MP4TrackId trackId = MP4FindTrackId(handle, i); 1616 const char* trackType = MP4GetTrackType(handle, trackId); 1617 if (std::strcmp(trackType, MP4_AUDIO_TRACK_TYPE) == 0) { 1618 valid = true; 1619 bitrate = (MP4GetTrackBitRate(handle, trackId) + 500) / 1000; 1620 sampleRate = MP4GetTrackTimeScale(handle, trackId); 1621 duration = MP4ConvertFromTrackDuration( 1622 handle, trackId, 1623 MP4GetTrackDuration(handle, trackId), MP4_MSECS_TIME_SCALE) / 1000; 1624 #if MPEG4IP_MAJOR_MINOR_VERSION >= 0x0109 1625 channels = MP4GetTrackAudioChannels(handle, trackId); 1626 #else 1627 channels = 2; 1628 #endif 1629 break; 1630 } 1631 } 1632 return valid; 1633 }