File indexing completed on 2024-05-12 04:55:38
0001 /** 0002 * \file mp3file.cpp 0003 * Handling of tagged MP3 files. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 9 Jan 2003 0008 * 0009 * Copyright (C) 2003-2024 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 "mp3file.h" 0028 0029 #include <QDir> 0030 #include <QString> 0031 #if QT_VERSION >= 0x060000 0032 #include <QStringConverter> 0033 #else 0034 #include <QTextCodec> 0035 #endif 0036 #include <QByteArray> 0037 #include <QtEndian> 0038 0039 #include <cstring> 0040 #include <id3/tag.h> 0041 #ifdef Q_OS_WIN32 0042 #include <id3.h> 0043 #endif 0044 0045 #include "id3libconfig.h" 0046 #include "genres.h" 0047 #include "attributedata.h" 0048 0049 #ifdef Q_OS_WIN32 0050 /** 0051 * This will be set for id3lib versions with Unicode bugs. 0052 * ID3LIB_ symbols cannot be found on Windows ?! 0053 */ 0054 #define UNICODE_SUPPORT_BUGGY 1 0055 #else 0056 /** This will be set for id3lib versions with Unicode bugs. */ 0057 #define UNICODE_SUPPORT_BUGGY ((((ID3LIB_MAJOR_VERSION) << 16) + \ 0058 ((ID3LIB_MINOR_VERSION) << 8) + (ID3LIB_PATCH_VERSION)) <= 0x030803) 0059 #endif 0060 0061 #if defined __GNUC__ && (__GNUC__ * 100 + __GNUC_MINOR__) >= 407 0062 /** Defined if GCC is used and supports diagnostic pragmas */ 0063 #define GCC_HAS_DIAGNOSTIC_PRAGMA 0064 #endif 0065 0066 namespace { 0067 0068 #if QT_VERSION >= 0x060000 0069 /** String decoder for ID3v1 tags, default is ISO 8859-1 */ 0070 QStringDecoder s_decoderV1; 0071 QStringEncoder s_encoderV1; 0072 #else 0073 /** Text codec for ID3v1 tags, 0 to use default (ISO 8859-1) */ 0074 const QTextCodec* s_textCodecV1 = nullptr; 0075 #endif 0076 0077 /** Default text encoding */ 0078 ID3_TextEnc s_defaultTextEncoding = ID3TE_ISO8859_1; 0079 0080 /** 0081 * Get the default text encoding. 0082 * @return default text encoding. 0083 */ 0084 ID3_TextEnc getDefaultTextEncoding() { return s_defaultTextEncoding; } 0085 0086 } 0087 0088 /** 0089 * Constructor. 0090 * 0091 * @param idx index in tagged file system model 0092 */ 0093 Mp3File::Mp3File(const QPersistentModelIndex& idx) 0094 : TaggedFile(idx) 0095 { 0096 } 0097 0098 /** 0099 * Destructor. 0100 */ 0101 Mp3File::~Mp3File() 0102 { 0103 // Must not be inline because of forwared declared QScopedPointer. 0104 } 0105 0106 /** 0107 * Get key of tagged file format. 0108 * @return "Id3libMetadata". 0109 */ 0110 QString Mp3File::taggedFileKey() const 0111 { 0112 return QLatin1String("Id3libMetadata"); 0113 } 0114 0115 /** 0116 * Get features supported. 0117 * @return bit mask with Feature flags set. 0118 */ 0119 int Mp3File::taggedFileFeatures() const 0120 { 0121 return TF_ID3v11 | TF_ID3v23; 0122 } 0123 0124 /** 0125 * Read tags from file. 0126 * 0127 * @param force true to force reading even if tags were already read. 0128 */ 0129 void Mp3File::readTags(bool force) 0130 { 0131 bool priorIsTagInformationRead = isTagInformationRead(); 0132 QByteArray fn = QFile::encodeName(currentFilePath()); 0133 0134 if (force && m_tagV1) { 0135 m_tagV1->Clear(); 0136 m_tagV1->Link(fn, ID3TT_ID3V1); 0137 markTagUnchanged(Frame::Tag_1); 0138 } 0139 if (!m_tagV1) { 0140 m_tagV1.reset(new ID3_Tag); 0141 m_tagV1->Link(fn, ID3TT_ID3V1); 0142 markTagUnchanged(Frame::Tag_1); 0143 } 0144 0145 if (force && m_tagV2) { 0146 m_tagV2->Clear(); 0147 m_tagV2->Link(fn, ID3TT_ID3V2); 0148 markTagUnchanged(Frame::Tag_2); 0149 } 0150 if (!m_tagV2) { 0151 m_tagV2.reset(new ID3_Tag); 0152 m_tagV2->Link(fn, ID3TT_ID3V2); 0153 markTagUnchanged(Frame::Tag_2); 0154 } 0155 0156 if (force) { 0157 setFilename(currentFilename()); 0158 } 0159 0160 notifyModelDataChanged(priorIsTagInformationRead); 0161 } 0162 0163 /** 0164 * Write tags to file and rename it if necessary. 0165 * 0166 * @param force true to force writing even if file was not changed. 0167 * @param renamed will be set to true if the file was renamed, 0168 * i.e. the file name is no longer valid, else *renamed 0169 * is left unchanged 0170 * @param preserve true to preserve file time stamps 0171 * 0172 * @return true if ok, false if the file could not be written or renamed. 0173 */ 0174 bool Mp3File::writeTags(bool force, bool* renamed, bool preserve) 0175 { 0176 QString fnStr(currentFilePath()); 0177 if (isChanged() && !QFileInfo(fnStr).isWritable()) { 0178 revertChangedFilename(); 0179 return false; 0180 } 0181 0182 // store time stamp if it has to be preserved 0183 quint64 actime = 0, modtime = 0; 0184 if (preserve) { 0185 getFileTimeStamps(fnStr, actime, modtime); 0186 } 0187 0188 // There seems to be a bug in id3lib: The V1 genre is not 0189 // removed. So we check here and strip the whole header 0190 // if there are no frames. 0191 if (m_tagV1 && (force || isTagChanged(Frame::Tag_1)) && m_tagV1->NumFrames() == 0) { 0192 m_tagV1->Strip(ID3TT_ID3V1); 0193 markTagUnchanged(Frame::Tag_1); 0194 } 0195 // Even after removing all frames, HasV2Tag() still returns true, 0196 // so we strip the whole header. 0197 if (m_tagV2 && (force || isTagChanged(Frame::Tag_2)) && m_tagV2->NumFrames() == 0) { 0198 m_tagV2->Strip(ID3TT_ID3V2); 0199 markTagUnchanged(Frame::Tag_2); 0200 } 0201 // There seems to be a bug in id3lib: If I update an ID3v1 and then 0202 // strip the ID3v2 the ID3v1 is removed too and vice versa, so I 0203 // first make any stripping and then the updating. 0204 if (m_tagV1 && (force || isTagChanged(Frame::Tag_1)) && m_tagV1->NumFrames() > 0) { 0205 m_tagV1->Update(ID3TT_ID3V1); 0206 markTagUnchanged(Frame::Tag_1); 0207 } 0208 if (m_tagV2 && (force || isTagChanged(Frame::Tag_2)) && m_tagV2->NumFrames() > 0) { 0209 m_tagV2->Update(ID3TT_ID3V2); 0210 markTagUnchanged(Frame::Tag_2); 0211 } 0212 0213 // restore time stamp 0214 if (actime || modtime) { 0215 setFileTimeStamps(fnStr, actime, modtime); 0216 } 0217 0218 if (isFilenameChanged()) { 0219 if (!renameFile()) { 0220 return false; 0221 } 0222 markFilenameUnchanged(); 0223 // link tags to new file name 0224 readTags(true); 0225 *renamed = true; 0226 } 0227 return true; 0228 } 0229 0230 /** 0231 * Free resources allocated when calling readTags(). 0232 * 0233 * @param force true to force clearing even if the tags are modified 0234 */ 0235 void Mp3File::clearTags(bool force) 0236 { 0237 if (isChanged() && !force) 0238 return; 0239 0240 bool priorIsTagInformationRead = isTagInformationRead(); 0241 if (m_tagV1) { 0242 m_tagV1.reset(); 0243 markTagUnchanged(Frame::Tag_1); 0244 } 0245 if (m_tagV2) { 0246 m_tagV2.reset(); 0247 markTagUnchanged(Frame::Tag_2); 0248 } 0249 notifyModelDataChanged(priorIsTagInformationRead); 0250 } 0251 0252 namespace { 0253 0254 /** 0255 * Fix up a unicode string from id3lib. 0256 * 0257 * @param str unicode string 0258 * @param numChars number of characters in str 0259 * 0260 * @return string as QString. 0261 */ 0262 QString fixUpUnicode(const unicode_t* str, size_t numChars) 0263 { 0264 QString text; 0265 if (numChars > 0 && str && *str) { 0266 auto qcarray = new QChar[numChars]; 0267 // Unfortunately, Unicode support in id3lib is rather buggy 0268 // in the current version: The codes are mirrored. 0269 // In the hope that my patches will be included, I try here 0270 // to work around these bugs. 0271 size_t numZeroes = 0; 0272 for (size_t i = 0; i < numChars; i++) { 0273 qcarray[i] = UNICODE_SUPPORT_BUGGY 0274 ? static_cast<ushort>(((str[i] & 0x00ff) << 8) | 0275 ((str[i] & 0xff00) >> 8)) 0276 : static_cast<ushort>(str[i]); 0277 if (qcarray[i].isNull()) { ++numZeroes; } 0278 } 0279 // remove a single trailing zero character 0280 if (numZeroes == 1 && qcarray[numChars - 1].isNull()) { 0281 --numChars; 0282 } 0283 text = QString(qcarray, numChars); 0284 delete [] qcarray; 0285 } 0286 return text; 0287 } 0288 0289 /** 0290 * Get string from text field. 0291 * 0292 * @param field field 0293 * @param codec text codec to use, 0 for default 0294 * 0295 * @return string, 0296 * "" if the field does not exist. 0297 */ 0298 QString getString(ID3_Field* field, 0299 #if QT_VERSION >= 0x060000 0300 QStringDecoder* decoder = nullptr 0301 #else 0302 const QTextCodec* codec = nullptr 0303 #endif 0304 ) 0305 { 0306 QString text(QLatin1String("")); 0307 if (field != nullptr) { 0308 if (ID3_TextEnc enc = field->GetEncoding(); 0309 enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) { 0310 if (size_t numItems = field->GetNumTextItems(); numItems <= 1) { 0311 text = fixUpUnicode(field->GetRawUnicodeText(), 0312 field->Size() / sizeof(unicode_t)); 0313 } else { 0314 // if there are multiple items, put them into one string 0315 // separated by a special separator. 0316 // ID3_Field::GetRawUnicodeTextItem() returns a pointer to a temporary 0317 // object, so I do not use it. 0318 text = fixUpUnicode(field->GetRawUnicodeText(), 0319 field->Size() / sizeof(unicode_t)); 0320 text = Frame::joinStringList(text.split(QLatin1Char('\0'))); 0321 } 0322 } else { 0323 // (ID3TE_IS_SINGLE_BYTE_ENC(enc)) 0324 // (enc == ID3TE_ISO8859_1 || enc == ID3TE_UTF8) 0325 if (size_t numItems = field->GetNumTextItems(); numItems <= 1) { 0326 #if QT_VERSION >= 0x060000 0327 text = decoder ? decoder->decode(QByteArray(field->GetRawText(), field->Size())) 0328 : QString::fromLatin1(field->GetRawText()); 0329 #else 0330 text = codec ? codec->toUnicode(field->GetRawText(), field->Size()) 0331 : QString::fromLatin1(field->GetRawText()); 0332 #endif 0333 } else { 0334 // if there are multiple items, put them into one string 0335 // separated by a special separator. 0336 QStringList strs; 0337 strs.reserve(numItems); 0338 for (size_t itemNr = 0; itemNr < numItems; ++itemNr) { 0339 strs.append(QString::fromLatin1(field->GetRawTextItem(itemNr))); 0340 } 0341 text = Frame::joinStringList(strs); 0342 } 0343 } 0344 } 0345 return text; 0346 } 0347 0348 /** 0349 * Get text field. 0350 * 0351 * @param tag ID3 tag 0352 * @param id frame ID 0353 * @param codec text codec to use, 0 for default 0354 * @return string, 0355 * "" if the field does not exist, 0356 * QString::null if the tags do not exist. 0357 */ 0358 QString getTextField(const ID3_Tag* tag, ID3_FrameID id, 0359 #if QT_VERSION >= 0x060000 0360 QStringDecoder* decoder = nullptr 0361 #else 0362 const QTextCodec* codec = nullptr 0363 #endif 0364 ) 0365 { 0366 if (!tag) { 0367 return QString(); 0368 } 0369 QString str(QLatin1String("")); 0370 ID3_Field* fld; 0371 if (ID3_Frame* frame = tag->Find(id); 0372 frame && (fld = frame->GetField(ID3FN_TEXT)) != nullptr) { 0373 #if QT_VERSION >= 0x060000 0374 str = getString(fld, decoder); 0375 #else 0376 str = getString(fld, codec); 0377 #endif 0378 } 0379 return str; 0380 } 0381 0382 /** 0383 * Get year. 0384 * 0385 * @param tag ID3 tag 0386 * @return number, 0387 * 0 if the field does not exist, 0388 * -1 if the tags do not exist. 0389 */ 0390 int getYear(const ID3_Tag* tag) 0391 { 0392 QString str = getTextField(tag, ID3FID_YEAR); 0393 if (str.isNull()) return -1; 0394 if (str.isEmpty()) return 0; 0395 return str.toInt(); 0396 } 0397 0398 /** 0399 * Get track. 0400 * 0401 * @param tag ID3 tag 0402 * @return number, 0403 * 0 if the field does not exist, 0404 * -1 if the tags do not exist. 0405 */ 0406 int getTrackNum(const ID3_Tag* tag) 0407 { 0408 QString str = getTextField(tag, ID3FID_TRACKNUM); 0409 if (str.isNull()) return -1; 0410 if (str.isEmpty()) return 0; 0411 // handle "track/total number of tracks" format 0412 if (int slashPos = str.indexOf(QLatin1Char('/')); slashPos != -1) { 0413 str.truncate(slashPos); 0414 } 0415 return str.toInt(); 0416 } 0417 0418 /** 0419 * Get genre. 0420 * 0421 * @param tag ID3 tag 0422 * @return number, 0423 * 0xff if the field does not exist, 0424 * -1 if the tags do not exist. 0425 */ 0426 int getGenreNum(const ID3_Tag* tag) 0427 { 0428 QString str = getTextField(tag, ID3FID_CONTENTTYPE); 0429 if (str.isNull()) return -1; 0430 if (str.isEmpty()) return 0xff; 0431 int n = 0xff; 0432 if (int cpPos = 0; 0433 str[0] == QLatin1Char('(') && 0434 (cpPos = str.indexOf(QLatin1Char(')'), 2)) > 1) { 0435 bool ok; 0436 #if QT_VERSION >= 0x060000 0437 n = str.mid(1, cpPos - 1).toInt(&ok); 0438 #else 0439 n = str.midRef(1, cpPos - 1).toInt(&ok); 0440 #endif 0441 if (!ok || n > 0xff) { 0442 n = 0xff; 0443 } 0444 } else { 0445 // ID3v2 genres can be stored as "(9)", "(9)Metal" or "Metal". 0446 // If the string does not start with '(', try to get the genre number 0447 // from a string containing a genre text. 0448 n = Genres::getNumber(str); 0449 } 0450 return n; 0451 } 0452 0453 /** 0454 * Allocate a fixed up a unicode string for id3lib. 0455 * 0456 * @param text string 0457 * 0458 * @return new allocated unicode string, has to be freed with delete []. 0459 */ 0460 unicode_t* newFixedUpUnicode(const QString& text) 0461 { 0462 // Unfortunately, Unicode support in id3lib is rather buggy in the 0463 // current version: The codes are mirrored, a second different 0464 // BOM may be added, if the LSB >= 0x80, the MSB is set to 0xff. 0465 // If iconv is used (id3lib on Linux), the character do not come 0466 // back mirrored, but with a second (different)! BOM 0xfeff and 0467 // they are still written in the wrong order (big endian). 0468 // In the hope that my patches will be included, I try here to 0469 // work around these bugs, but there is no solution for the 0470 // LSB >= 0x80 bug. 0471 const QChar* qcarray = text.unicode(); 0472 int unicode_size = text.length(); 0473 auto unicode = new unicode_t[unicode_size + 1]; 0474 for (int i = 0; i < unicode_size; ++i) { 0475 unicode[i] = qcarray[i].unicode(); 0476 if (UNICODE_SUPPORT_BUGGY) { 0477 unicode[i] = static_cast<ushort>(((unicode[i] & 0x00ff) << 8) | 0478 ((unicode[i] & 0xff00) >> 8)); 0479 } 0480 } 0481 unicode[unicode_size] = 0; 0482 return unicode; 0483 } 0484 0485 /** 0486 * Set string list in text field. 0487 * 0488 * @param field field 0489 * @param lst string list to set 0490 */ 0491 void setStringList(ID3_Field* field, const QStringList& lst) 0492 { 0493 ID3_TextEnc enc = field->GetEncoding(); 0494 bool first = true; 0495 for (auto it = lst.constBegin(); it != lst.constEnd(); ++it) { 0496 if (first) { 0497 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) { 0498 if (unicode_t* unicode = newFixedUpUnicode(*it)) { 0499 field->Set(unicode); 0500 delete [] unicode; 0501 } 0502 } else if (enc == ID3TE_UTF8) { 0503 field->Set(it->toUtf8().data()); 0504 } else { 0505 // enc == ID3TE_ISO8859_1 0506 field->Set(it->toLatin1().data()); 0507 } 0508 first = false; 0509 } else { 0510 // This will not work with buggy id3lib. A BOM 0xfffe is written before 0511 // the first string, but not before the subsequent strings. Prepending a 0512 // BOM or changing the byte order does not help when id3lib rewrites 0513 // this field when another frame is changed. So you cannot use 0514 // string lists with Unicode encoding. 0515 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) { 0516 if (unicode_t* unicode = newFixedUpUnicode(*it)) { 0517 field->Add(unicode); 0518 delete [] unicode; 0519 } 0520 } else if (enc == ID3TE_UTF8) { 0521 field->Add(it->toUtf8().data()); 0522 } else { 0523 // enc == ID3TE_ISO8859_1 0524 field->Add(it->toLatin1().data()); 0525 } 0526 } 0527 } 0528 } 0529 0530 /** 0531 * Set string in text field. 0532 * 0533 * @param field field 0534 * @param text text to set 0535 * @param codec text codec to use, 0 for default 0536 */ 0537 void setString(ID3_Field* field, const QString& text, 0538 #if QT_VERSION >= 0x060000 0539 QStringEncoder* encoder = nullptr 0540 #else 0541 const QTextCodec* codec = nullptr 0542 #endif 0543 ) 0544 { 0545 if (text.indexOf(Frame::stringListSeparator()) == -1) { 0546 // (ID3TE_IS_DOUBLE_BYTE_ENC(enc)) 0547 if (ID3_TextEnc enc = field->GetEncoding(); 0548 enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) { 0549 if (unicode_t* unicode = newFixedUpUnicode(text)) { 0550 field->Set(unicode); 0551 delete [] unicode; 0552 } 0553 } else if (enc == ID3TE_UTF8) { 0554 field->Set(text.toUtf8().data()); 0555 } else { 0556 // enc == ID3TE_ISO8859_1 0557 #if QT_VERSION >= 0x060000 0558 field->Set(encoder ? static_cast<QByteArray>(encoder->encode(text)).constData() 0559 : text.toLatin1().constData()); 0560 #else 0561 field->Set(codec ? codec->fromUnicode(text).constData() 0562 : text.toLatin1().constData()); 0563 #endif 0564 } 0565 } else { 0566 setStringList(field, Frame::splitStringList(text)); 0567 } 0568 } 0569 0570 /** 0571 * Set text field. 0572 * 0573 * @param tag ID3 tag 0574 * @param id frame ID 0575 * @param text text to set 0576 * @param allowUnicode true to allow setting of Unicode encoding if necessary 0577 * @param replace true to replace an existing field 0578 * @param removeEmpty true to remove a field if text is empty 0579 * @param codec text codec to use, 0 for default 0580 * 0581 * @return true if the field was changed. 0582 */ 0583 bool setTextField(ID3_Tag* tag, ID3_FrameID id, const QString& text, 0584 bool allowUnicode = false, bool replace = true, 0585 bool removeEmpty = true, 0586 #if QT_VERSION >= 0x060000 0587 QStringEncoder* encoder = nullptr 0588 #else 0589 const QTextCodec* codec = nullptr 0590 #endif 0591 ) 0592 { 0593 bool changed = false; 0594 if (tag && !text.isNull()) { 0595 ID3_Frame* frame = nullptr; 0596 bool removeOnly = removeEmpty && text.isEmpty(); 0597 if (replace || removeOnly) { 0598 if (id == ID3FID_COMMENT && tag->HasV2Tag()) { 0599 frame = tag->Find(ID3FID_COMMENT, ID3FN_DESCRIPTION, ""); 0600 } else { 0601 frame = tag->Find(id); 0602 } 0603 if (frame) { 0604 frame = tag->RemoveFrame(frame); 0605 delete frame; 0606 changed = true; 0607 } 0608 } 0609 if (!removeOnly && (replace || tag->Find(id) == nullptr)) { 0610 frame = new ID3_Frame(id); 0611 if (frame) { 0612 if (ID3_Field* fld = frame->GetField(ID3FN_TEXT)) { 0613 ID3_TextEnc enc = tag->HasV2Tag() ? 0614 getDefaultTextEncoding() : ID3TE_ISO8859_1; 0615 if (allowUnicode && enc == ID3TE_ISO8859_1) { 0616 // check if information is lost if the string is not unicode 0617 int unicode_size = text.length(); 0618 const QChar* qcarray = text.unicode(); 0619 for (int i = 0; i < unicode_size; ++i) { 0620 if (char ch = qcarray[i].toLatin1(); 0621 ch == 0 || (ch & 0x80) != 0) { 0622 enc = ID3TE_UTF16; 0623 break; 0624 } 0625 } 0626 } 0627 if (ID3_Field* encfld = frame->GetField(ID3FN_TEXTENC)) { 0628 encfld->Set(enc); 0629 } 0630 fld->SetEncoding(enc); 0631 #if QT_VERSION >= 0x060000 0632 setString(fld, text, encoder); 0633 #else 0634 setString(fld, text, codec); 0635 #endif 0636 tag->AttachFrame(frame); 0637 } 0638 } 0639 changed = true; 0640 } 0641 } 0642 return changed; 0643 } 0644 0645 /** 0646 * Set year. 0647 * 0648 * @param tag ID3 tag 0649 * @param num number to set, 0 to remove field. 0650 * 0651 * @return true if the field was changed. 0652 */ 0653 bool setYear(ID3_Tag* tag, int num) 0654 { 0655 bool changed = false; 0656 if (num >= 0) { 0657 QString str; 0658 if (num != 0) { 0659 str.setNum(num); 0660 } else { 0661 str.clear(); 0662 } 0663 changed = getTextField(tag, ID3FID_YEAR) != str && 0664 setTextField(tag, ID3FID_YEAR, str); 0665 } 0666 return changed; 0667 } 0668 0669 } 0670 0671 /** 0672 * Set track. 0673 * 0674 * @param tag ID3 tag 0675 * @param num number to set, 0 to remove field. 0676 * @param numTracks total number of tracks, <=0 to ignore 0677 * 0678 * @return true if the field was changed. 0679 */ 0680 bool Mp3File::setTrackNum(ID3_Tag* tag, int num, int numTracks) const 0681 { 0682 bool changed = false; 0683 if (num >= 0 && getTrackNum(tag) != num) { 0684 QString str = trackNumberString(num, numTracks); 0685 changed = getTextField(tag, ID3FID_TRACKNUM) != str && 0686 setTextField(tag, ID3FID_TRACKNUM, str); 0687 } 0688 return changed; 0689 } 0690 0691 namespace { 0692 0693 /** 0694 * Set genre. 0695 * 0696 * @param tag ID3 tag 0697 * @param num number to set, 0xff to remove field. 0698 * 0699 * @return true if the field was changed. 0700 */ 0701 bool setGenreNum(ID3_Tag* tag, int num) 0702 { 0703 bool changed = false; 0704 if (num >= 0) { 0705 QString str; 0706 if (num != 0xff) { 0707 str = QString(QLatin1String("(%1)")).arg(num); 0708 } else { 0709 str = QLatin1String(""); 0710 } 0711 changed = getTextField(tag, ID3FID_CONTENTTYPE) != str && 0712 setTextField(tag, ID3FID_CONTENTTYPE, str); 0713 } 0714 return changed; 0715 } 0716 0717 } 0718 0719 /** 0720 * Check if tag information has already been read. 0721 * 0722 * @return true if information is available, 0723 * false if the tags have not been read yet, in which case 0724 * hasTag() does not return meaningful information. 0725 */ 0726 bool Mp3File::isTagInformationRead() const 0727 { 0728 return m_tagV1 || m_tagV2; 0729 } 0730 0731 /** 0732 * Check if tags are supported by the format of this file. 0733 * 0734 * @param tagNr tag number 0735 * @return true. 0736 */ 0737 bool Mp3File::isTagSupported(Frame::TagNumber tagNr) const 0738 { 0739 return tagNr == Frame::Tag_1 || tagNr == Frame::Tag_2; 0740 } 0741 0742 /** 0743 * Check if file has a tag. 0744 * 0745 * @param tagNr tag number 0746 * @return true if a tag is available. 0747 * @see isTagInformationRead() 0748 */ 0749 bool Mp3File::hasTag(Frame::TagNumber tagNr) const 0750 { 0751 return (tagNr == Frame::Tag_1 && m_tagV1 && m_tagV1->HasV1Tag()) || 0752 (tagNr == Frame::Tag_2 && m_tagV2 && m_tagV2->HasV2Tag()); 0753 } 0754 0755 /** 0756 * Get technical detail information. 0757 * 0758 * @param info the detail information is returned here 0759 */ 0760 void Mp3File::getDetailInfo(DetailInfo& info) const 0761 { 0762 if (getFilename().right(4).toLower() == QLatin1String(".aac")) { 0763 info.valid = true; 0764 info.format = QLatin1String("AAC"); 0765 return; 0766 } 0767 0768 const Mp3_Headerinfo* headerInfo = nullptr; 0769 if (m_tagV2) { 0770 headerInfo = m_tagV2->GetMp3HeaderInfo(); 0771 } 0772 if (!headerInfo && m_tagV1) { 0773 headerInfo = m_tagV1->GetMp3HeaderInfo(); 0774 } 0775 if (headerInfo) { 0776 info.valid = true; 0777 switch (headerInfo->version) { 0778 case MPEGVERSION_1: 0779 info.format = QLatin1String("MPEG 1 "); 0780 break; 0781 case MPEGVERSION_2: 0782 info.format = QLatin1String("MPEG 2 "); 0783 break; 0784 case MPEGVERSION_2_5: 0785 info.format = QLatin1String("MPEG 2.5 "); 0786 break; 0787 default: 0788 ; // nothing 0789 } 0790 switch (headerInfo->layer) { 0791 case MPEGLAYER_I: 0792 info.format += QLatin1String("Layer 1"); 0793 break; 0794 case MPEGLAYER_II: 0795 info.format += QLatin1String("Layer 2"); 0796 break; 0797 case MPEGLAYER_III: 0798 info.format += QLatin1String("Layer 3"); 0799 break; 0800 default: 0801 ; // nothing 0802 } 0803 info.bitrate = headerInfo->bitrate / 1000; 0804 #ifndef HAVE_NO_ID3LIB_VBR 0805 if (headerInfo->vbr_bitrate > 1000) { 0806 info.vbr = true; 0807 info.bitrate = headerInfo->vbr_bitrate / 1000; 0808 } 0809 #endif 0810 info.sampleRate = headerInfo->frequency; 0811 switch (headerInfo->channelmode) { 0812 case MP3CHANNELMODE_STEREO: 0813 info.channelMode = DetailInfo::CM_Stereo; 0814 info.channels = 2; 0815 break; 0816 case MP3CHANNELMODE_JOINT_STEREO: 0817 info.channelMode = DetailInfo::CM_JointStereo; 0818 info.channels = 2; 0819 break; 0820 case MP3CHANNELMODE_DUAL_CHANNEL: 0821 info.channels = 2; 0822 break; 0823 case MP3CHANNELMODE_SINGLE_CHANNEL: 0824 info.channels = 1; 0825 break; 0826 default: 0827 ; // nothing 0828 } 0829 info.duration = headerInfo->time; 0830 } else { 0831 info.valid = false; 0832 } 0833 } 0834 0835 /** 0836 * Get duration of file. 0837 * 0838 * @return duration in seconds, 0839 * 0 if unknown. 0840 */ 0841 unsigned Mp3File::getDuration() const 0842 { 0843 unsigned duration = 0; 0844 const Mp3_Headerinfo* info = nullptr; 0845 if (m_tagV2) { 0846 info = m_tagV2->GetMp3HeaderInfo(); 0847 } 0848 if (!info && m_tagV1) { 0849 info = m_tagV1->GetMp3HeaderInfo(); 0850 } 0851 if (info && info->time > 0) { 0852 duration = info->time; 0853 } 0854 return duration; 0855 } 0856 0857 /** 0858 * Get file extension including the dot. 0859 * 0860 * @return file extension ".mp3". 0861 */ 0862 QString Mp3File::getFileExtension() const 0863 { 0864 if (QString ext(getFilename().right(4).toLower()); 0865 ext == QLatin1String(".aac") || ext == QLatin1String(".mp2")) 0866 return ext; 0867 return QLatin1String(".mp3"); 0868 } 0869 0870 /** 0871 * Get the format of a tag. 0872 * 0873 * @param tagNr tag number 0874 * @return string describing format of tag, 0875 * e.g. "ID3v1.1". 0876 */ 0877 QString Mp3File::getTagFormat(Frame::TagNumber tagNr) const 0878 { 0879 if (tagNr == Frame::Tag_1 && m_tagV1 && m_tagV1->HasV1Tag()) { 0880 return QLatin1String("ID3v1.1"); 0881 } 0882 if (tagNr == Frame::Tag_2 && m_tagV2 && m_tagV2->HasV2Tag()) { 0883 switch (m_tagV2->GetSpec()) { 0884 case ID3V2_3_0: 0885 return QLatin1String("ID3v2.3.0"); 0886 case ID3V2_4_0: 0887 return QLatin1String("ID3v2.4.0"); 0888 case ID3V2_2_0: 0889 return QLatin1String("ID3v2.2.0"); 0890 case ID3V2_2_1: 0891 return QLatin1String("ID3v2.2.1"); 0892 default: 0893 break; 0894 } 0895 } 0896 return QString(); 0897 } 0898 0899 namespace { 0900 0901 /** Types and descriptions for id3lib frame IDs */ 0902 const struct TypeStrOfId { 0903 Frame::Type type; 0904 const char* str; 0905 } typeStrOfId[] = { 0906 { Frame::FT_UnknownFrame, nullptr }, /* ???? */ 0907 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "AENC - Audio encryption") }, /* AENC */ 0908 { Frame::FT_Picture, QT_TRANSLATE_NOOP("@default", "APIC - Attached picture") }, /* APIC */ 0909 { Frame::FT_Other, nullptr }, /* ASPI */ 0910 { Frame::FT_Comment, QT_TRANSLATE_NOOP("@default", "COMM - Comments") }, /* COMM */ 0911 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "COMR - Commercial") }, /* COMR */ 0912 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "ENCR - Encryption method registration") }, /* ENCR */ 0913 { Frame::FT_Other, nullptr }, /* EQU2 */ 0914 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "EQUA - Equalization") }, /* EQUA */ 0915 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "ETCO - Event timing codes") }, /* ETCO */ 0916 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "GEOB - General encapsulated object") }, /* GEOB */ 0917 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "GRID - Group identification registration") }, /* GRID */ 0918 { Frame::FT_Arranger, QT_TRANSLATE_NOOP("@default", "IPLS - Involved people list") }, /* IPLS */ 0919 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "LINK - Linked information") }, /* LINK */ 0920 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "MCDI - Music CD identifier") }, /* MCDI */ 0921 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "MLLT - MPEG location lookup table") }, /* MLLT */ 0922 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "OWNE - Ownership frame") }, /* OWNE */ 0923 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "PRIV - Private frame") }, /* PRIV */ 0924 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "PCNT - Play counter") }, /* PCNT */ 0925 { Frame::FT_Rating, QT_TRANSLATE_NOOP("@default", "POPM - Popularimeter") }, /* POPM */ 0926 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "POSS - Position synchronisation frame") }, /* POSS */ 0927 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RBUF - Recommended buffer size") }, /* RBUF */ 0928 { Frame::FT_Other, nullptr }, /* RVA2 */ 0929 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RVAD - Relative volume adjustment") }, /* RVAD */ 0930 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RVRB - Reverb") }, /* RVRB */ 0931 { Frame::FT_Other, nullptr }, /* SEEK */ 0932 { Frame::FT_Other, nullptr }, /* SIGN */ 0933 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "SYLT - Synchronized lyric/text") }, /* SYLT */ 0934 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "SYTC - Synchronized tempo codes") }, /* SYTC */ 0935 { Frame::FT_Album, QT_TRANSLATE_NOOP("@default", "TALB - Album/Movie/Show title") }, /* TALB */ 0936 { Frame::FT_Bpm, QT_TRANSLATE_NOOP("@default", "TBPM - BPM (beats per minute)") }, /* TBPM */ 0937 { Frame::FT_Composer, QT_TRANSLATE_NOOP("@default", "TCOM - Composer") }, /* TCOM */ 0938 { Frame::FT_Genre, QT_TRANSLATE_NOOP("@default", "TCON - Content type") }, /* TCON */ 0939 { Frame::FT_Copyright, QT_TRANSLATE_NOOP("@default", "TCOP - Copyright message") }, /* TCOP */ 0940 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TDAT - Date") }, /* TDAT */ 0941 { Frame::FT_Other, nullptr }, /* TDEN */ 0942 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TDLY - Playlist delay") }, /* TDLY */ 0943 { Frame::FT_Other, nullptr }, /* TDOR */ 0944 { Frame::FT_Other, nullptr }, /* TDRC */ 0945 { Frame::FT_Other, nullptr }, /* TDRL */ 0946 { Frame::FT_Other, nullptr }, /* TDTG */ 0947 { Frame::FT_Other, nullptr }, /* TIPL */ 0948 { Frame::FT_EncodedBy, QT_TRANSLATE_NOOP("@default", "TENC - Encoded by") }, /* TENC */ 0949 { Frame::FT_Lyricist, QT_TRANSLATE_NOOP("@default", "TEXT - Lyricist/Text writer") }, /* TEXT */ 0950 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TFLT - File type") }, /* TFLT */ 0951 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TIME - Time") }, /* TIME */ 0952 { Frame::FT_Work, QT_TRANSLATE_NOOP("@default", "TIT1 - Content group description") }, /* TIT1 */ 0953 { Frame::FT_Title, QT_TRANSLATE_NOOP("@default", "TIT2 - Title/songname/content description") }, /* TIT2 */ 0954 { Frame::FT_Description, QT_TRANSLATE_NOOP("@default", "TIT3 - Subtitle/Description refinement") }, /* TIT3 */ 0955 { Frame::FT_InitialKey, QT_TRANSLATE_NOOP("@default", "TKEY - Initial key") }, /* TKEY */ 0956 { Frame::FT_Language, QT_TRANSLATE_NOOP("@default", "TLAN - Language(s)") }, /* TLAN */ 0957 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TLEN - Length") }, /* TLEN */ 0958 { Frame::FT_Other, nullptr }, /* TMCL */ 0959 { Frame::FT_Media, QT_TRANSLATE_NOOP("@default", "TMED - Media type") }, /* TMED */ 0960 { Frame::FT_Other, nullptr }, /* TMOO */ 0961 { Frame::FT_OriginalAlbum, QT_TRANSLATE_NOOP("@default", "TOAL - Original album/movie/show title") }, /* TOAL */ 0962 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TOFN - Original filename") }, /* TOFN */ 0963 { Frame::FT_Author, QT_TRANSLATE_NOOP("@default", "TOLY - Original lyricist(s)/text writer(s)") }, /* TOLY */ 0964 { Frame::FT_OriginalArtist, QT_TRANSLATE_NOOP("@default", "TOPE - Original artist(s)/performer(s)") }, /* TOPE */ 0965 { Frame::FT_OriginalDate, QT_TRANSLATE_NOOP("@default", "TORY - Original release year") }, /* TORY */ 0966 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TOWN - File owner/licensee") }, /* TOWN */ 0967 { Frame::FT_Artist, QT_TRANSLATE_NOOP("@default", "TPE1 - Lead performer(s)/Soloist(s)") }, /* TPE1 */ 0968 { Frame::FT_AlbumArtist, QT_TRANSLATE_NOOP("@default", "TPE2 - Band/orchestra/accompaniment") }, /* TPE2 */ 0969 { Frame::FT_Conductor, QT_TRANSLATE_NOOP("@default", "TPE3 - Conductor/performer refinement") }, /* TPE3 */ 0970 { Frame::FT_Remixer, QT_TRANSLATE_NOOP("@default", "TPE4 - Interpreted, remixed, or otherwise modified by") }, /* TPE4 */ 0971 { Frame::FT_Disc, QT_TRANSLATE_NOOP("@default", "TPOS - Part of a set") }, /* TPOS */ 0972 { Frame::FT_Other, nullptr }, /* TPRO */ 0973 { Frame::FT_Publisher, QT_TRANSLATE_NOOP("@default", "TPUB - Publisher") }, /* TPUB */ 0974 { Frame::FT_Track, QT_TRANSLATE_NOOP("@default", "TRCK - Track number/Position in set") }, /* TRCK */ 0975 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRDA - Recording dates") }, /* TRDA */ 0976 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRSN - Internet radio station name") }, /* TRSN */ 0977 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRSO - Internet radio station owner") }, /* TRSO */ 0978 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TSIZ - Size") }, /* TSIZ */ 0979 { Frame::FT_Other, nullptr }, /* TSOA */ 0980 { Frame::FT_Other, nullptr }, /* TSOP */ 0981 { Frame::FT_Other, nullptr }, /* TSOT */ 0982 { Frame::FT_Isrc, QT_TRANSLATE_NOOP("@default", "TSRC - ISRC (international standard recording code)") }, /* TSRC */ 0983 { Frame::FT_EncoderSettings,QT_TRANSLATE_NOOP("@default", "TSSE - Software/Hardware and settings used for encoding") },/* TSSE */ 0984 { Frame::FT_Subtitle, nullptr }, /* TSST */ 0985 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TXXX - User defined text information") }, /* TXXX */ 0986 { Frame::FT_Date, QT_TRANSLATE_NOOP("@default", "TYER - Year") }, /* TYER */ 0987 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "UFID - Unique file identifier") }, /* UFID */ 0988 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "USER - Terms of use") }, /* USER */ 0989 { Frame::FT_Lyrics, QT_TRANSLATE_NOOP("@default", "USLT - Unsynchronized lyric/text transcription") }, /* USLT */ 0990 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WCOM - Commercial information") }, /* WCOM */ 0991 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WCOP - Copyright/Legal information") }, /* WCOP */ 0992 { Frame::FT_WWWAudioFile, QT_TRANSLATE_NOOP("@default", "WOAF - Official audio file webpage") }, /* WOAF */ 0993 { Frame::FT_Website, QT_TRANSLATE_NOOP("@default", "WOAR - Official artist/performer webpage") }, /* WOAR */ 0994 { Frame::FT_WWWAudioSource, QT_TRANSLATE_NOOP("@default", "WOAS - Official audio source webpage") }, /* WOAS */ 0995 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WORS - Official internet radio station homepage") }, /* WORS */ 0996 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WPAY - Payment") }, /* WPAY */ 0997 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WPUB - Official publisher webpage") }, /* WPUB */ 0998 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WXXX - User defined URL link") } /* WXXX */ 0999 }; 1000 Q_STATIC_ASSERT(std::size(typeStrOfId) == ID3FID_WWWUSER + 1); 1001 1002 /** 1003 * Get type and description of frame. 1004 * 1005 * @param id ID of frame 1006 * @param type the type is returned here 1007 * @param str the description is returned here, 0 if not available 1008 */ 1009 void getTypeStringForId3libFrameId(ID3_FrameID id, Frame::Type& type, 1010 const char*& str) 1011 { 1012 const auto& [t, s] = typeStrOfId[id <= ID3FID_WWWUSER ? id : 0]; 1013 type = t; 1014 str = s; 1015 } 1016 1017 /** 1018 * Get id3lib frame ID for frame type. 1019 * 1020 * @param type frame type 1021 * 1022 * @return id3lib frame ID or ID3FID_NOFRAME if type not found. 1023 */ 1024 ID3_FrameID getId3libFrameIdForType(Frame::Type type) 1025 { 1026 // IPLS is mapped to FT_Arranger and FT_Performer 1027 if (type == Frame::FT_Performer) { 1028 return ID3FID_INVOLVEDPEOPLE; 1029 } 1030 if (type == Frame::FT_CatalogNumber || 1031 type == Frame::FT_ReleaseCountry || 1032 type == Frame::FT_Grouping || 1033 type == Frame::FT_Subtitle || 1034 Frame::isCustomFrameType(type)) { 1035 return ID3FID_USERTEXT; 1036 } 1037 1038 static int typeIdMap[Frame::FT_LastFrame + 1] = { -1, }; 1039 if (typeIdMap[0] == -1) { 1040 // first time initialization 1041 for (int i = 0; i <= ID3FID_WWWUSER; ++i) { 1042 if (int t = typeStrOfId[i].type; t <= Frame::FT_LastFrame) { 1043 typeIdMap[t] = i; 1044 } 1045 } 1046 } 1047 return type <= Frame::FT_LastFrame ? 1048 static_cast<ID3_FrameID>(typeIdMap[type]) : ID3FID_NOFRAME; 1049 } 1050 1051 /** 1052 * Get id3lib frame ID for frame name. 1053 * 1054 * @param name frame name 1055 * 1056 * @return id3lib frame ID or ID3FID_NOFRAME if name not found. 1057 */ 1058 ID3_FrameID getId3libFrameIdForName(const QString& name) 1059 { 1060 if (name.length() >= 4) { 1061 QByteArray nameBytes = name.toLatin1(); 1062 const char* nameStr = nameBytes.constData(); 1063 for (int i = 0; i <= ID3FID_WWWUSER; ++i) { 1064 if (const char* s = typeStrOfId[i].str; 1065 s && ::strncmp(s, nameStr, 4) == 0) { 1066 return static_cast<ID3_FrameID>(i); 1067 } 1068 } 1069 } 1070 return ID3FID_NOFRAME; 1071 } 1072 1073 /** 1074 * Convert the binary field of a SYLT frame to a list of alternating 1075 * time stamps and strings. 1076 * @param bytes binary field 1077 * @param enc encoding 1078 * @return list containing time, text, time, text, ... 1079 */ 1080 QVariantList syltBytesToList(const QByteArray& bytes, ID3_TextEnc enc) 1081 { 1082 QVariantList timeEvents; 1083 const int numBytes = bytes.size(); 1084 int textBegin = 0, textEnd; 1085 while (textBegin < numBytes) { 1086 if (enc == ID3TE_ISO8859_1 || enc == ID3TE_UTF8) { 1087 textEnd = bytes.indexOf('\0', textBegin); 1088 if (textEnd != -1) { 1089 ++textEnd; 1090 } 1091 } else { 1092 auto unicode = 1093 reinterpret_cast<const ushort*>(bytes.constData() + textBegin); 1094 textEnd = textBegin; 1095 while (textEnd < numBytes) { 1096 textEnd += 2; 1097 if (*unicode++ == 0) { 1098 break; 1099 } 1100 } 1101 } 1102 if (textEnd < 0 || textEnd >= numBytes) 1103 break; 1104 1105 QString str; 1106 QByteArray text = bytes.mid(textBegin, textEnd - textBegin); 1107 switch (enc) { 1108 case ID3TE_UTF16BE: 1109 text.prepend(0xff); 1110 text.prepend(0xfe); 1111 // starting with FEFF BOM 1112 // fallthrough 1113 case ID3TE_UTF16: 1114 #if QT_VERSION >= 0x060000 1115 str = QString::fromUtf16(reinterpret_cast<const char16_t*>(text.constData())); 1116 #else 1117 str = QString::fromUtf16(reinterpret_cast<const ushort*>(text.constData())); 1118 #endif 1119 break; 1120 case ID3TE_UTF8: 1121 str = QString::fromUtf8(text.constData()); 1122 break; 1123 case ID3TE_ISO8859_1: 1124 default: 1125 str = QString::fromLatin1(text.constData()); 1126 } 1127 textBegin = textEnd + 4; 1128 if (textBegin > numBytes) 1129 break; 1130 1131 auto time = qFromBigEndian<quint32>( 1132 reinterpret_cast<const uchar*>(bytes.constData()) + textEnd); 1133 timeEvents.append(time); 1134 timeEvents.append(str); 1135 } 1136 return timeEvents; 1137 } 1138 1139 /** 1140 * Convert a list of alternating time stamps and strings to the binary field of 1141 * a SYLT frame. 1142 * @param synchedData list containing time, text, time, text, ... 1143 * @param enc text encoding 1144 * @return binary field bytes. 1145 */ 1146 QByteArray syltListToBytes(const QVariantList& synchedData, ID3_TextEnc enc) 1147 { 1148 QByteArray bytes; 1149 QListIterator it(synchedData); 1150 while (it.hasNext()) { 1151 quint32 milliseconds = it.next().toUInt(); 1152 if (!it.hasNext()) 1153 break; 1154 1155 QString str = it.next().toString(); 1156 switch (enc) { 1157 case ID3TE_UTF16: 1158 bytes.append(0xff); 1159 bytes.append(0xfe); 1160 // starting with FFFE BOM 1161 // fallthrough 1162 case ID3TE_UTF16BE: 1163 { 1164 const ushort* unicode = str.utf16(); 1165 do { 1166 uchar lsb = *unicode & 0xff; 1167 uchar msb = *unicode >> 8; 1168 if (enc == ID3TE_UTF16) { 1169 bytes.append(static_cast<char>(lsb)); 1170 bytes.append(static_cast<char>(msb)); 1171 } else { 1172 bytes.append(static_cast<char>(msb)); 1173 bytes.append(static_cast<char>(lsb)); 1174 } 1175 } while (*unicode++); 1176 break; 1177 } 1178 case ID3TE_UTF8: 1179 bytes.append(str.toUtf8()); 1180 bytes.append('\0'); 1181 break; 1182 case ID3TE_ISO8859_1: 1183 default: 1184 bytes.append(str.toLatin1()); 1185 bytes.append('\0'); 1186 } 1187 uchar timeStamp[4]; 1188 qToBigEndian(milliseconds, timeStamp); 1189 bytes.append(reinterpret_cast<const char*>(timeStamp), sizeof(timeStamp)); 1190 } 1191 if (bytes.isEmpty()) { 1192 // id3lib bug: Empty binary fields are not written, so add a minimal field 1193 bytes = QByteArray(4 + (enc == ID3TE_UTF16 || 1194 enc == ID3TE_UTF16BE ? 2 : 1), 1195 '\0'); 1196 } 1197 return bytes; 1198 } 1199 1200 /** 1201 * Convert the binary field of an ETCO frame to a list of alternating 1202 * time stamps and codes. 1203 * @param bytes binary field 1204 * @return list containing time, code, time, code, ... 1205 */ 1206 QVariantList etcoBytesToList(const QByteArray& bytes) 1207 { 1208 QVariantList timeEvents; 1209 const int numBytes = bytes.size(); 1210 // id3lib bug: There is only a single data field for ETCO frames, 1211 // but it should be preceded by an ID_TimestampFormat field. 1212 // Start with the second byte. 1213 int pos = 1; 1214 while (pos < numBytes) { 1215 int code = static_cast<uchar>(bytes.at(pos)); 1216 ++pos; 1217 if (pos + 4 > numBytes) 1218 break; 1219 1220 auto time = qFromBigEndian<quint32>( 1221 reinterpret_cast<const uchar*>(bytes.constData()) + pos); 1222 pos += 4; 1223 timeEvents.append(time); 1224 timeEvents.append(code); 1225 } 1226 return timeEvents; 1227 } 1228 1229 /** 1230 * Convert a list of alternating time stamps and codes to the binary field of 1231 * an ETCO frame. 1232 * @param synchedData list containing time, code, time, code, ... 1233 * @return binary field bytes. 1234 */ 1235 QByteArray etcoListToBytes(const QVariantList& synchedData) 1236 { 1237 QByteArray bytes; 1238 QListIterator it(synchedData); 1239 while (it.hasNext()) { 1240 quint32 milliseconds = it.next().toUInt(); 1241 if (!it.hasNext()) 1242 break; 1243 1244 int code = it.next().toInt(); 1245 bytes.append(static_cast<char>(code)); 1246 uchar timeStamp[4]; 1247 qToBigEndian(milliseconds, timeStamp); 1248 bytes.append(reinterpret_cast<const char*>(timeStamp), sizeof(timeStamp)); 1249 } 1250 return bytes; 1251 } 1252 1253 /** 1254 * Get the fields from an ID3v2 tag. 1255 * 1256 * @param id3Frame frame 1257 * @param fields the fields are appended to this list 1258 * 1259 * @return text representation of fields (Text or URL). 1260 */ 1261 QString getFieldsFromId3Frame(ID3_Frame* id3Frame, Frame::FieldList& fields) 1262 { 1263 QString text; 1264 ID3_Frame::Iterator* iter = id3Frame->CreateIterator(); 1265 ID3_FrameID id3Id = id3Frame->GetID(); 1266 ID3_TextEnc enc = ID3TE_NONE; 1267 ID3_Field* id3Field; 1268 Frame::Field field; 1269 while ((id3Field = iter->GetNext()) != nullptr) { 1270 ID3_FieldID id = id3Field->GetID(); 1271 ID3_FieldType type = id3Field->GetType(); 1272 field.m_id = id; 1273 if (type == ID3FTY_INTEGER) { 1274 uint32 intVal = id3Field->Get(); 1275 field.m_value = intVal; 1276 if (id == ID3FN_TEXTENC) { 1277 enc = static_cast<ID3_TextEnc>(intVal); 1278 } 1279 } 1280 else if (type == ID3FTY_BINARY) { 1281 QByteArray ba(reinterpret_cast<const char*>(id3Field->GetRawBinary()), 1282 static_cast<int>(id3Field->Size())); 1283 if (id3Id == ID3FID_SYNCEDLYRICS) { 1284 field.m_value = syltBytesToList(ba, enc); 1285 } else if (id3Id == ID3FID_EVENTTIMING) { 1286 field.m_value = etcoBytesToList(ba); 1287 } else { 1288 field.m_value = ba; 1289 } 1290 } 1291 else if (type == ID3FTY_TEXTSTRING) { 1292 if (id == ID3FN_TEXT || id == ID3FN_DESCRIPTION || id == ID3FN_URL) { 1293 text = getString(id3Field); 1294 if (id3Id == ID3FID_CONTENTTYPE) { 1295 text = Genres::getNameString(text); 1296 } 1297 field.m_value = text; 1298 } else { 1299 field.m_value = getString(id3Field); 1300 } 1301 } else { 1302 field.m_value.clear(); 1303 } 1304 fields.push_back(field); 1305 } 1306 #ifdef Q_OS_WIN32 1307 /* allocated in Windows DLL => must be freed in the same DLL */ 1308 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 1309 #else 1310 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1311 #pragma GCC diagnostic push 1312 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 1313 /* Is safe because iterator implementation has default destructor */ 1314 #endif 1315 delete iter; 1316 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1317 #pragma GCC diagnostic pop 1318 #endif 1319 #endif 1320 return text; 1321 } 1322 1323 /** 1324 * Get ID3v2 frame by index. 1325 * 1326 * @param tag ID3v2 tag 1327 * @param index index 1328 * 1329 * @return frame, 0 if index invalid. 1330 */ 1331 ID3_Frame* getId3v2Frame(ID3_Tag* tag, int index) 1332 { 1333 ID3_Frame* result = nullptr; 1334 if (tag) { 1335 ID3_Frame* frame; 1336 ID3_Tag::Iterator* iter = tag->CreateIterator(); 1337 int i = 0; 1338 while ((frame = iter->GetNext()) != nullptr) { 1339 if (i == index) { 1340 result = frame; 1341 break; 1342 } 1343 ++i; 1344 } 1345 #ifdef Q_OS_WIN32 1346 /* allocated in Windows DLL => must be freed in the same DLL */ 1347 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 1348 #else 1349 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1350 #pragma GCC diagnostic push 1351 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 1352 /* Is safe because iterator implementation has default destructor */ 1353 #endif 1354 delete iter; 1355 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1356 #pragma GCC diagnostic pop 1357 #endif 1358 #endif 1359 } 1360 return result; 1361 } 1362 1363 } 1364 1365 /** 1366 * Set the fields in an id3lib frame from the field in the frame. 1367 * 1368 * @param id3Frame id3lib frame 1369 * @param frame frame with fields 1370 */ 1371 void Mp3File::setId3v2Frame(ID3_Frame* id3Frame, const Frame& frame) const 1372 { 1373 ID3_Frame::Iterator* iter = id3Frame->CreateIterator(); 1374 ID3_FrameID id3Id = id3Frame->GetID(); 1375 ID3_TextEnc enc = ID3TE_NONE; 1376 for (auto fldIt = frame.getFieldList().constBegin(); 1377 fldIt != frame.getFieldList().constEnd(); 1378 ++fldIt) { 1379 ID3_Field* id3Field = iter->GetNext(); 1380 if (!id3Field) { 1381 qDebug("early end of ID3 fields"); 1382 break; 1383 } 1384 const Frame::Field& fld = *fldIt; 1385 #if QT_VERSION >= 0x060000 1386 switch (fld.m_value.typeId()) { 1387 case QMetaType::Int: 1388 case QMetaType::UInt: 1389 #else 1390 switch (fld.m_value.type()) { 1391 case QVariant::Int: 1392 case QVariant::UInt: 1393 #endif 1394 { 1395 int intVal = fld.m_value.toInt(); 1396 if (fld.m_id == ID3FN_TEXTENC) { 1397 if (intVal == ID3TE_UTF8) intVal = ID3TE_UTF16; 1398 enc = static_cast<ID3_TextEnc>(intVal); 1399 } 1400 id3Field->Set(intVal); 1401 break; 1402 } 1403 1404 #if QT_VERSION >= 0x060000 1405 case QMetaType::QString: 1406 #else 1407 case QVariant::String: 1408 #endif 1409 { 1410 if (enc != ID3TE_NONE) { 1411 id3Field->SetEncoding(enc); 1412 } 1413 QString value(fld.m_value.toString()); 1414 if (id3Id == ID3FID_CONTENTTYPE) { 1415 if (!TagConfig::instance().genreNotNumeric() || 1416 value.contains(Frame::stringListSeparator())) { 1417 value = Genres::getNumberString(value, true); 1418 } 1419 } else if (id3Id == ID3FID_TRACKNUM) { 1420 formatTrackNumberIfEnabled(value, true); 1421 } 1422 setString(id3Field, value); 1423 break; 1424 } 1425 1426 #if QT_VERSION >= 0x060000 1427 case QMetaType::QByteArray: 1428 #else 1429 case QVariant::ByteArray: 1430 #endif 1431 { 1432 const QByteArray& ba = fld.m_value.toByteArray(); 1433 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()), 1434 static_cast<size_t>(ba.size())); 1435 break; 1436 } 1437 1438 #if QT_VERSION >= 0x060000 1439 case QMetaType::QVariantList: 1440 #else 1441 case QVariant::List: 1442 #endif 1443 { 1444 if (id3Id == ID3FID_SYNCEDLYRICS) { 1445 QByteArray ba = syltListToBytes(fld.m_value.toList(), enc); 1446 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()), 1447 static_cast<size_t>(ba.size())); 1448 } else if (id3Id == ID3FID_EVENTTIMING) { 1449 QByteArray ba = etcoListToBytes(fld.m_value.toList()); 1450 // id3lib bug: There is only a single data field for ETCO frames, 1451 // but it should be preceded by an ID_TimestampFormat field. 1452 ba.prepend(2); 1453 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()), 1454 static_cast<size_t>(ba.size())); 1455 } else { 1456 qDebug("Unexpected QVariantList in field %d", fld.m_id); 1457 } 1458 break; 1459 } 1460 1461 default: 1462 #if QT_VERSION >= 0x060000 1463 qDebug("Unknown type %d in field %d", fld.m_value.typeId(), fld.m_id); 1464 #else 1465 qDebug("Unknown type %d in field %d", fld.m_value.type(), fld.m_id); 1466 #endif 1467 } 1468 } 1469 #ifdef Q_OS_WIN32 1470 /* allocated in Windows DLL => must be freed in the same DLL */ 1471 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 1472 #else 1473 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1474 #pragma GCC diagnostic push 1475 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 1476 /* Is safe because iterator implementation has default destructor */ 1477 #endif 1478 delete iter; 1479 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 1480 #pragma GCC diagnostic pop 1481 #endif 1482 #endif 1483 } 1484 1485 /** 1486 * Get a specific frame from the tags. 1487 * 1488 * @param tagNr tag number 1489 * @param type frame type 1490 * @param frame the frame is returned here 1491 * 1492 * @return true if ok. 1493 */ 1494 bool Mp3File::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const 1495 { 1496 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame) 1497 return false; 1498 1499 ID3_FrameID frameId = getId3libFrameIdForType(type); 1500 if (frameId == ID3FID_NOFRAME) 1501 return false; 1502 1503 const ID3_Tag* tag; 1504 #if QT_VERSION >= 0x060000 1505 QStringDecoder* decoder; 1506 #else 1507 const QTextCodec* codec; 1508 #endif 1509 if (tagNr == Frame::Tag_1) { 1510 tag = m_tagV1.data(); 1511 #if QT_VERSION >= 0x060000 1512 decoder = &s_decoderV1; 1513 #else 1514 codec = s_textCodecV1; 1515 #endif 1516 } else if (tagNr == Frame::Tag_2) { 1517 tag = m_tagV2.data(); 1518 #if QT_VERSION >= 0x060000 1519 decoder = nullptr; 1520 #else 1521 codec = nullptr; 1522 #endif 1523 } else { 1524 return false; 1525 } 1526 1527 switch (type) { 1528 case Frame::FT_Album: 1529 case Frame::FT_Artist: 1530 case Frame::FT_Comment: 1531 case Frame::FT_Title: 1532 #if QT_VERSION >= 0x060000 1533 frame.setValue(getTextField(tag, frameId, decoder)); 1534 #else 1535 frame.setValue(getTextField(tag, frameId, codec)); 1536 #endif 1537 break; 1538 case Frame::FT_Track: 1539 if (tagNr == Frame::Tag_1) { 1540 frame.setValueAsNumber(getTrackNum(tag)); 1541 } else { 1542 frame.setValue(getTextField(tag, frameId)); 1543 } 1544 break; 1545 case Frame::FT_Date: 1546 frame.setValueAsNumber(getYear(tag)); 1547 break; 1548 case Frame::FT_Genre: 1549 { 1550 int num = getGenreNum(tag); 1551 if (tagNr == Frame::Tag_1) { 1552 frame.setValue(num == -1 1553 ? QString() 1554 : num == 0xff 1555 ? QLatin1String("") 1556 : QString::fromLatin1(Genres::getName(num))); 1557 } else { 1558 frame.setValue(num != 0xff && num != -1 1559 ? QString::fromLatin1(Genres::getName(num)) 1560 : getTextField(tag, frameId)); 1561 } 1562 break; 1563 } 1564 default: 1565 return false; 1566 } 1567 frame.setType(type); 1568 return true; 1569 } 1570 1571 /** 1572 * Set a frame in the tags. 1573 * 1574 * @param tagNr tag number 1575 * @param frame frame to set 1576 * 1577 * @return true if ok. 1578 */ 1579 bool Mp3File::setFrame(Frame::TagNumber tagNr, const Frame& frame) 1580 { 1581 if (tagNr == Frame::Tag_2) { 1582 // If the frame has an index, change that specific frame 1583 if (int index = frame.getIndex(); index != -1 && m_tagV2) { 1584 if (ID3_Frame* id3Frame = getId3v2Frame(m_tagV2.data(), index)) { 1585 // If value is changed or field list is empty, 1586 // set from value, else from FieldList. 1587 if (frame.isValueChanged() || frame.getFieldList().empty()) { 1588 QString value(frame.getValue()); 1589 if (ID3_Field* fld; 1590 (fld = id3Frame->GetField(ID3FN_URL)) != nullptr) { 1591 if (getString(fld) != value) { 1592 fld->Set(value.toLatin1().data()); 1593 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1594 } 1595 return true; 1596 } else { 1597 if ((fld = id3Frame->GetField(ID3FN_TEXT)) != nullptr || 1598 (fld = id3Frame->GetField(ID3FN_DESCRIPTION)) != nullptr) { 1599 ID3_FrameID id = id3Frame->GetID(); 1600 if (id == ID3FID_CONTENTTYPE) { 1601 if (!TagConfig::instance().genreNotNumeric() || 1602 value.contains(Frame::stringListSeparator())) { 1603 value = Genres::getNumberString(value, true); 1604 } 1605 } else if (id == ID3FID_TRACKNUM) { 1606 formatTrackNumberIfEnabled(value, true); 1607 } 1608 bool hasEnc; 1609 const ID3_TextEnc enc = fld->GetEncoding(); 1610 auto newEnc = static_cast<ID3_TextEnc>( 1611 frame.getFieldValue(Frame::ID_TextEnc).toInt(&hasEnc)); 1612 if (!hasEnc) { 1613 newEnc = enc; 1614 } 1615 if (newEnc != ID3TE_ISO8859_1 && newEnc != ID3TE_UTF16) { 1616 // only ISO-8859-1 and UTF16 are allowed for ID3v2.3.0 1617 newEnc = ID3TE_UTF16; 1618 } 1619 if (newEnc == ID3TE_ISO8859_1) { 1620 // check if information is lost if the string is not unicode 1621 int unicode_size = value.length(); 1622 const QChar* qcarray = value.unicode(); 1623 for (int i = 0; i < unicode_size; i++) { 1624 if (char ch = qcarray[i].toLatin1(); 1625 ch == 0 || (ch & 0x80) != 0) { 1626 newEnc = ID3TE_UTF16; 1627 break; 1628 } 1629 } 1630 } 1631 if (enc != newEnc && id != ID3FID_SYNCEDLYRICS) { 1632 if (ID3_Field* encfld = id3Frame->GetField(ID3FN_TEXTENC)) { 1633 encfld->Set(newEnc); 1634 } 1635 fld->SetEncoding(newEnc); 1636 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1637 } 1638 if (getString(fld) != value) { 1639 setString(fld, value); 1640 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1641 } 1642 return true; 1643 } 1644 if (id3Frame->GetID() == ID3FID_PRIVATE && 1645 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) { 1646 ID3_Field* ownerFld = id3Frame->GetField(ID3FN_OWNER); 1647 QByteArray newData; 1648 if (QString owner; 1649 ownerFld && !(owner = getString(ownerFld)).isEmpty() && 1650 AttributeData(owner).toByteArray(value, newData)) { 1651 if (auto oldData = 1652 QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()), 1653 static_cast<int>(fld->Size())); 1654 newData != oldData) { 1655 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()), 1656 newData.size()); 1657 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1658 } 1659 return true; 1660 } 1661 } else if (id3Frame->GetID() == ID3FID_CDID && 1662 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) { 1663 QByteArray newData; 1664 if (AttributeData::isHexString(value, 'F', QLatin1String("+")) && 1665 AttributeData(AttributeData::Utf16).toByteArray(value, newData)) { 1666 if (auto oldData = 1667 QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()), 1668 static_cast<int>(fld->Size())); 1669 newData != oldData) { 1670 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()), 1671 static_cast<size_t>(newData.size())); 1672 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1673 } 1674 return true; 1675 } 1676 } else if (id3Frame->GetID() == ID3FID_UNIQUEFILEID && 1677 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) { 1678 QByteArray newData; 1679 if (AttributeData::isHexString(value, 'Z', QLatin1String("-"))) { 1680 newData = (value + QLatin1Char('\0')).toLatin1(); 1681 if (auto oldData = 1682 QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()), 1683 static_cast<int>(fld->Size())); 1684 newData != oldData) { 1685 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()), 1686 static_cast<size_t>(newData.size())); 1687 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1688 } 1689 return true; 1690 } 1691 } else if (id3Frame->GetID() == ID3FID_POPULARIMETER && 1692 (fld = id3Frame->GetField(ID3FN_RATING)) != nullptr) { 1693 if (getString(fld) != value) { 1694 fld->Set(value.toInt()); 1695 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1696 } 1697 return true; 1698 } 1699 } 1700 } else { 1701 setId3v2Frame(id3Frame, frame); 1702 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 1703 return true; 1704 } 1705 } 1706 } 1707 } 1708 1709 // Try the basic method 1710 Frame::Type type = frame.getType(); 1711 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame) 1712 return false; 1713 1714 ID3_FrameID frameId = getId3libFrameIdForType(type); 1715 if (frameId == ID3FID_NOFRAME) 1716 return false; 1717 1718 ID3_Tag* tag; 1719 #if QT_VERSION >= 0x060000 1720 QStringDecoder* decoder; 1721 QStringEncoder* encoder; 1722 #else 1723 const QTextCodec* codec; 1724 #endif 1725 bool allowUnicode; 1726 if (tagNr == Frame::Tag_1) { 1727 tag = m_tagV1.data(); 1728 #if QT_VERSION >= 0x060000 1729 decoder = &s_decoderV1; 1730 encoder = &s_encoderV1; 1731 #else 1732 codec = s_textCodecV1; 1733 #endif 1734 allowUnicode = false; 1735 } else if (tagNr == Frame::Tag_2) { 1736 tag = m_tagV2.data(); 1737 #if QT_VERSION >= 0x060000 1738 decoder = nullptr; 1739 encoder = nullptr; 1740 #else 1741 codec = nullptr; 1742 #endif 1743 allowUnicode = true; 1744 } else { 1745 return false; 1746 } 1747 1748 switch (type) { 1749 case Frame::FT_Album: 1750 case Frame::FT_Artist: 1751 case Frame::FT_Comment: 1752 case Frame::FT_Title: 1753 { 1754 QString str = frame.getValue(); 1755 #if QT_VERSION >= 0x060000 1756 if (getTextField(tag, frameId, decoder) != str && 1757 setTextField(tag, frameId, str, allowUnicode, true, true, encoder)) { 1758 #else 1759 if (getTextField(tag, frameId, codec) != str && 1760 setTextField(tag, frameId, str, allowUnicode, true, true, codec)) { 1761 #endif 1762 markTagChanged(tagNr, Frame::ExtendedType(type)); 1763 if (QString s = checkTruncation(tagNr, str, 1ULL << type, 1764 type == Frame::FT_Comment ? 28 : 30); 1765 !s.isNull()) 1766 #if QT_VERSION >= 0x060000 1767 setTextField(tag, frameId, s, allowUnicode, true, true, encoder); 1768 #else 1769 setTextField(tag, frameId, s, allowUnicode, true, true, codec); 1770 #endif 1771 } 1772 break; 1773 } 1774 case Frame::FT_Date: 1775 { 1776 if (int num = frame.getValueAsNumber(); setYear(tag, num)) { 1777 markTagChanged(tagNr, Frame::ExtendedType(type)); 1778 } 1779 break; 1780 } 1781 case Frame::FT_Genre: 1782 { 1783 QString str = frame.getValue(); 1784 if (tagNr == Frame::Tag_1) { 1785 if (!str.isNull()) { 1786 const auto genres = Frame::splitStringList(str); 1787 int num = 0xff; 1788 for (const auto& genre : genres) { 1789 num = Genres::getNumber(genre); 1790 if (num != 0xff) { 1791 break; 1792 } 1793 } 1794 if (setGenreNum(tag, num)) { 1795 markTagChanged(tagNr, Frame::ExtendedType(type)); 1796 } 1797 // if the string cannot be converted to a number, set the truncation flag 1798 checkTruncation(tagNr, num == 0xff && !str.isEmpty() ? 1 : 0, 1799 1ULL << type, 0); 1800 } 1801 } else { 1802 if (!str.isNull()) { 1803 int num = 0xff; 1804 if (str.contains(Frame::stringListSeparator())) { 1805 str = Genres::getNumberString(str, true); 1806 } else if (!TagConfig::instance().genreNotNumeric()) { 1807 num = Genres::getNumber(str); 1808 } 1809 if (num >= 0 && num != 0xff) { 1810 if (getGenreNum(tag) != num && 1811 setGenreNum(tag, num)) { 1812 markTagChanged(tagNr, Frame::ExtendedType(type)); 1813 } 1814 } else { 1815 #if QT_VERSION >= 0x060000 1816 if (getTextField(tag, frameId, decoder) != str && 1817 setTextField(tag, frameId, str, allowUnicode, true, true, encoder)) { 1818 #else 1819 if (getTextField(tag, frameId, codec) != str && 1820 setTextField(tag, frameId, str, allowUnicode, true, true, codec)) { 1821 #endif 1822 markTagChanged(tagNr, Frame::ExtendedType(type)); 1823 } 1824 } 1825 } 1826 } 1827 break; 1828 } 1829 case Frame::FT_Track: 1830 { 1831 if (tagNr == Frame::Tag_1) { 1832 if (int num = frame.getValueAsNumber(); setTrackNum(tag, num)) { 1833 markTagChanged(tagNr, Frame::ExtendedType(type)); 1834 if (int n = checkTruncation(tagNr, num, 1ULL << type); n != -1) 1835 setTrackNum(tag, n); 1836 } 1837 } else { 1838 QString str = frame.getValue(); 1839 int numTracks; 1840 if (int num = splitNumberAndTotal(str, &numTracks); 1841 setTrackNum(tag, num, numTracks)) { 1842 markTagChanged(tagNr, Frame::ExtendedType(type)); 1843 } 1844 } 1845 break; 1846 } 1847 default: 1848 return false; 1849 } 1850 return true; 1851 } 1852 1853 /** 1854 * Create an id3lib frame from a frame. 1855 * @param frame frame 1856 * @return id3lib frame, 0 if invalid. 1857 */ 1858 ID3_Frame* Mp3File::createId3FrameFromFrame(Frame& frame) const 1859 { 1860 ID3_Frame* id3Frame = nullptr; 1861 ID3_FrameID id; 1862 if (!Frame::isCustomFrameTypeOrOther(frame.getType())) { 1863 id = getId3libFrameIdForType(frame.getType()); 1864 } else { 1865 id = getId3libFrameIdForName(frame.getName()); 1866 if (id == ID3FID_NOFRAME) { 1867 if (frame.getName() == QLatin1String("AverageLevel") || 1868 frame.getName() == QLatin1String("PeakValue") || 1869 frame.getName().startsWith(QLatin1String("WM/"))) { 1870 id = ID3FID_PRIVATE; 1871 } else if (frame.getName().startsWith(QLatin1String("iTun"))) { 1872 id = ID3FID_COMMENT; 1873 } else { 1874 id = ID3FID_USERTEXT; 1875 } 1876 } 1877 } 1878 if (id != ID3FID_NOFRAME && id != ID3FID_SETSUBTITLE) { 1879 id3Frame = new ID3_Frame(id); 1880 ID3_Field* fld = id3Frame->GetField(ID3FN_TEXT); 1881 if (fld) { 1882 ID3_TextEnc enc = getDefaultTextEncoding(); 1883 if (ID3_Field* encfld = id3Frame->GetField(ID3FN_TEXTENC)) { 1884 encfld->Set(enc); 1885 } 1886 fld->SetEncoding(enc); 1887 } 1888 if (id == ID3FID_USERTEXT && 1889 !frame.getName().startsWith(QLatin1String("TXXX"))) { 1890 fld = id3Frame->GetField(ID3FN_DESCRIPTION); 1891 if (fld) { 1892 QString description; 1893 if (frame.getType() == Frame::FT_CatalogNumber) { 1894 description = QLatin1String("CATALOGNUMBER"); 1895 } else if (frame.getType() == Frame::FT_ReleaseCountry) { 1896 description = QLatin1String("RELEASECOUNTRY"); 1897 } else if (frame.getType() == Frame::FT_Grouping) { 1898 description = QLatin1String("GROUPING"); 1899 } else if (frame.getType() == Frame::FT_Subtitle) { 1900 description = QLatin1String("SUBTITLE"); 1901 } else if (Frame::isCustomFrameType(frame.getType())) { 1902 description = QString::fromLatin1( 1903 Frame::getNameForCustomFrame(frame.getType())); 1904 } else { 1905 description = frame.getName(); 1906 } 1907 setString(fld, description); 1908 } 1909 } else if (id == ID3FID_COMMENT) { 1910 fld = id3Frame->GetField(ID3FN_LANGUAGE); 1911 if (fld) { 1912 setString(fld, QLatin1String("eng")); 1913 } 1914 if (frame.getType() == Frame::FT_Other) { 1915 fld = id3Frame->GetField(ID3FN_DESCRIPTION); 1916 if (fld) { 1917 setString(fld, frame.getName()); 1918 } 1919 } 1920 } else if (id == ID3FID_PRIVATE && 1921 !frame.getName().startsWith(QLatin1String("PRIV"))) { 1922 fld = id3Frame->GetField(ID3FN_OWNER); 1923 if (fld) { 1924 setString(fld, frame.getName()); 1925 QByteArray data; 1926 if (AttributeData(frame.getName()).toByteArray(frame.getValue(), data)) { 1927 fld = id3Frame->GetField(ID3FN_DATA); 1928 if (fld) { 1929 fld->Set(reinterpret_cast<const unsigned char*>(data.data()), 1930 static_cast<size_t>(data.size())); 1931 } 1932 } 1933 } 1934 } else if (id == ID3FID_UNIQUEFILEID) { 1935 fld = id3Frame->GetField(ID3FN_OWNER); 1936 if (fld) { 1937 setString(fld, QLatin1String("http://www.id3.org/dummy/ufid.html")); 1938 } 1939 QByteArray data; 1940 if (AttributeData::isHexString(frame.getValue(), 'Z', QLatin1String("-"))) { 1941 data = (frame.getValue() + QLatin1Char('\0')).toLatin1(); 1942 fld = id3Frame->GetField(ID3FN_DATA); 1943 if (fld) { 1944 fld->Set(reinterpret_cast<const unsigned char*>(data.data()), 1945 static_cast<size_t>(data.size())); 1946 } 1947 } 1948 } else if (id == ID3FID_PICTURE) { 1949 fld = id3Frame->GetField(ID3FN_MIMETYPE); 1950 if (fld) { 1951 setString(fld, QLatin1String("image/jpeg")); 1952 } 1953 fld = id3Frame->GetField(ID3FN_PICTURETYPE); 1954 if (fld) { 1955 fld->Set(ID3PT_COVERFRONT); 1956 } 1957 } else if (id == ID3FID_SYNCEDLYRICS) { 1958 fld = id3Frame->GetField(ID3FN_LANGUAGE); 1959 if (fld) { 1960 setString(fld, QLatin1String("eng")); 1961 } 1962 fld = id3Frame->GetField(ID3FN_TIMESTAMPFORMAT); 1963 if (fld) { 1964 fld->Set(ID3TSF_MS); 1965 } 1966 fld = id3Frame->GetField(ID3FN_CONTENTTYPE); 1967 if (fld) { 1968 fld->Set(ID3CT_LYRICS); 1969 } 1970 } else if (id == ID3FID_UNSYNCEDLYRICS || id == ID3FID_TERMSOFUSE) { 1971 fld = id3Frame->GetField(ID3FN_LANGUAGE); 1972 if (fld) { 1973 setString(fld, QLatin1String("eng")); 1974 } 1975 } else if (id == ID3FID_POPULARIMETER) { 1976 fld = id3Frame->GetField(ID3FN_EMAIL); 1977 if (fld) { 1978 setString(fld, TagConfig::instance().defaultPopmEmail()); 1979 } 1980 } 1981 if (!frame.fieldList().empty()) { 1982 setId3v2Frame(id3Frame, frame); 1983 } 1984 Frame::Type type; 1985 const char* name; 1986 getTypeStringForId3libFrameId(id, type, name); 1987 if (type == Frame::FT_Other) { 1988 type = Frame::getTypeFromCustomFrameName(QByteArray(id3Frame->GetTextID())); 1989 } 1990 frame.setExtendedType(Frame::ExtendedType(type, QString::fromLatin1(name))); 1991 } 1992 return id3Frame; 1993 } 1994 1995 /** 1996 * Add a frame in the tags. 1997 * 1998 * @param tagNr tag number 1999 * @param frame frame to add, a field list may be added by this method 2000 * 2001 * @return true if ok. 2002 */ 2003 bool Mp3File::addFrame(Frame::TagNumber tagNr, Frame& frame) 2004 { 2005 if (tagNr == Frame::Tag_2) { 2006 // Add a new frame. 2007 if (ID3_Frame* id3Frame; 2008 m_tagV2 && (id3Frame = createId3FrameFromFrame(frame)) != nullptr) { 2009 m_tagV2->AttachFrame(id3Frame); 2010 frame.setIndex(m_tagV2->NumFrames() - 1); 2011 if (frame.fieldList().empty()) { 2012 // add field list to frame 2013 getFieldsFromId3Frame(id3Frame, frame.fieldList()); 2014 frame.setFieldListFromValue(); 2015 } 2016 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 2017 return true; 2018 } 2019 } 2020 2021 // Try the superclass method 2022 return TaggedFile::addFrame(tagNr, frame); 2023 } 2024 2025 /** 2026 * Delete a frame from the tags. 2027 * 2028 * @param tagNr tag number 2029 * @param frame frame to delete. 2030 * 2031 * @return true if ok. 2032 */ 2033 bool Mp3File::deleteFrame(Frame::TagNumber tagNr, const Frame& frame) 2034 { 2035 if (tagNr == Frame::Tag_2) { 2036 // If the frame has an index, delete that specific frame 2037 if (int index = frame.getIndex(); index != -1 && m_tagV2) { 2038 if (ID3_Frame* id3Frame = getId3v2Frame(m_tagV2.data(), index)) { 2039 m_tagV2->RemoveFrame(id3Frame); 2040 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 2041 return true; 2042 } 2043 } 2044 } 2045 2046 // Try the superclass method 2047 return TaggedFile::deleteFrame(tagNr, frame); 2048 } 2049 2050 namespace { 2051 2052 /** 2053 * Create a frame from an id3lib frame. 2054 * @param id3Frame id3lib frame 2055 * @param index -1 if not used 2056 * @return frame. 2057 */ 2058 Frame createFrameFromId3libFrame(ID3_Frame* id3Frame, int index) 2059 { 2060 Frame::Type type; 2061 const char* name; 2062 getTypeStringForId3libFrameId(id3Frame->GetID(), type, name); 2063 if (type == Frame::FT_Other) { 2064 type = Frame::getTypeFromCustomFrameName(QByteArray(id3Frame->GetTextID())); 2065 } 2066 2067 Frame frame(type, QLatin1String(""), QString::fromLatin1(name), index); 2068 frame.setValue(getFieldsFromId3Frame(id3Frame, frame.fieldList())); 2069 if (id3Frame->GetID() == ID3FID_USERTEXT || 2070 id3Frame->GetID() == ID3FID_WWWUSER || 2071 id3Frame->GetID() == ID3FID_COMMENT) { 2072 if (QVariant fieldValue = frame.getFieldValue(Frame::ID_Description); 2073 fieldValue.isValid()) { 2074 if (QString description = fieldValue.toString(); !description.isEmpty()) { 2075 if (description == QLatin1String("CATALOGNUMBER")) { 2076 frame.setType(Frame::FT_CatalogNumber); 2077 } else if (description == QLatin1String("RELEASECOUNTRY")) { 2078 frame.setType(Frame::FT_ReleaseCountry); 2079 } else if (description == QLatin1String("GROUPING")) { 2080 frame.setType(Frame::FT_Grouping); 2081 } else if (description == QLatin1String("SUBTITLE")) { 2082 frame.setType(Frame::FT_Subtitle); 2083 } else { 2084 frame.setExtendedType(Frame::ExtendedType( 2085 Frame::getTypeFromCustomFrameName(description.toLatin1()), 2086 frame.getInternalName() + QLatin1Char('\n') + description)); 2087 } 2088 } 2089 } 2090 } else if (id3Frame->GetID() == ID3FID_PRIVATE) { 2091 const Frame::FieldList& fields = frame.getFieldList(); 2092 QString owner; 2093 QByteArray data; 2094 for (auto it = fields.constBegin(); it != fields.constEnd(); ++it) { 2095 if (it->m_id == Frame::ID_Owner) { 2096 owner = it->m_value.toString(); 2097 if (!owner.isEmpty()) { 2098 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Other, 2099 frame.getInternalName() + QLatin1Char('\n') + owner)); 2100 } 2101 } else if (it->m_id == Frame::ID_Data) { 2102 data = it->m_value.toByteArray(); 2103 } 2104 } 2105 if (!owner.isEmpty() && !data.isEmpty()) { 2106 if (QString str; AttributeData(owner).toString(data, str)) { 2107 frame.setValue(str); 2108 } 2109 } 2110 } else if (id3Frame->GetID() == ID3FID_CDID) { 2111 if (QVariant fieldValue = frame.getFieldValue(Frame::ID_Data); 2112 fieldValue.isValid()) { 2113 if (QString str; AttributeData(AttributeData::Utf16) 2114 .toString(fieldValue.toByteArray(), str) && 2115 AttributeData::isHexString(str, 'F', QLatin1String("+"))) { 2116 frame.setValue(str); 2117 } 2118 } 2119 } else if (id3Frame->GetID() == ID3FID_UNIQUEFILEID) { 2120 if (QVariant fieldValue = frame.getFieldValue(Frame::ID_Data); 2121 fieldValue.isValid()) { 2122 QByteArray ba(fieldValue.toByteArray()); 2123 if (QString str(QString::fromLatin1(ba)); 2124 ba.size() - str.length() <= 1 && 2125 AttributeData::isHexString(str, 'Z', QLatin1String("-"))) { 2126 frame.setValue(str); 2127 } 2128 } 2129 } else if (id3Frame->GetID() == ID3FID_POPULARIMETER) { 2130 if (QVariant fieldValue = frame.getFieldValue(Frame::ID_Rating); 2131 fieldValue.isValid()) { 2132 if (QString str(fieldValue.toString()); !str.isEmpty()) { 2133 frame.setValue(str); 2134 } 2135 } 2136 } 2137 return frame; 2138 } 2139 2140 } 2141 2142 /** 2143 * Remove frames. 2144 * 2145 * @param tagNr tag number 2146 * @param flt filter specifying which frames to remove 2147 */ 2148 void Mp3File::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt) 2149 { 2150 if (tagNr == Frame::Tag_1) { 2151 if (m_tagV1) { 2152 if (flt.areAllEnabled()) { 2153 ID3_Tag::Iterator* iter = m_tagV1->CreateIterator(); 2154 ID3_Frame* frame; 2155 while ((frame = iter->GetNext()) != nullptr) { 2156 m_tagV1->RemoveFrame(frame); 2157 } 2158 #ifdef Q_OS_WIN32 2159 /* allocated in Windows DLL => must be freed in the same DLL */ 2160 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 2161 #else 2162 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2163 #pragma GCC diagnostic push 2164 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 2165 /* Is safe because iterator implementation has default destructor */ 2166 #endif 2167 delete iter; 2168 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2169 #pragma GCC diagnostic pop 2170 #endif 2171 #endif 2172 markTagChanged(Frame::Tag_1, Frame::ExtendedType()); 2173 clearTrunctionFlags(Frame::Tag_1); 2174 } else { 2175 TaggedFile::deleteFrames(Frame::Tag_1, flt); 2176 } 2177 } 2178 } else if (tagNr == Frame::Tag_2) { 2179 if (m_tagV2) { 2180 if (flt.areAllEnabled()) { 2181 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator(); 2182 ID3_Frame* frame; 2183 while ((frame = iter->GetNext()) != nullptr) { 2184 m_tagV2->RemoveFrame(frame); 2185 } 2186 #ifdef Q_OS_WIN32 2187 /* allocated in Windows DLL => must be freed in the same DLL */ 2188 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 2189 #else 2190 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2191 #pragma GCC diagnostic push 2192 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 2193 /* Is safe because iterator implementation has default destructor */ 2194 #endif 2195 delete iter; 2196 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2197 #pragma GCC diagnostic pop 2198 #endif 2199 #endif 2200 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 2201 } else { 2202 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator(); 2203 ID3_Frame* id3Frame; 2204 while ((id3Frame = iter->GetNext()) != nullptr) { 2205 if (Frame frame(createFrameFromId3libFrame(id3Frame, -1)); 2206 flt.isEnabled(frame.getType(), frame.getName())) { 2207 m_tagV2->RemoveFrame(id3Frame); 2208 } 2209 } 2210 #ifdef Q_OS_WIN32 2211 /* allocated in Windows DLL => must be freed in the same DLL */ 2212 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 2213 #else 2214 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2215 #pragma GCC diagnostic push 2216 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 2217 /* Is safe because iterator implementation has default destructor */ 2218 #endif 2219 delete iter; 2220 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2221 #pragma GCC diagnostic pop 2222 #endif 2223 #endif 2224 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 2225 } 2226 } 2227 } 2228 } 2229 2230 /** 2231 * Get all frames in tag. 2232 * 2233 * @param tagNr tag number 2234 * @param frames frame collection to set. 2235 */ 2236 void Mp3File::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames) 2237 { 2238 if (tagNr == Frame::Tag_2) { 2239 frames.clear(); 2240 if (m_tagV2) { 2241 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator(); 2242 ID3_Frame* id3Frame; 2243 int i = 0; 2244 while ((id3Frame = iter->GetNext()) != nullptr) { 2245 Frame frame(createFrameFromId3libFrame(id3Frame, i++)); 2246 frames.insert(frame); 2247 } 2248 #ifdef Q_OS_WIN32 2249 /* allocated in Windows DLL => must be freed in the same DLL */ 2250 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter)); 2251 #else 2252 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2253 #pragma GCC diagnostic push 2254 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" 2255 /* Is safe because iterator implementation has default destructor */ 2256 #endif 2257 delete iter; 2258 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA 2259 #pragma GCC diagnostic pop 2260 #endif 2261 #endif 2262 } 2263 updateMarkedState(tagNr, frames); 2264 frames.addMissingStandardFrames(); 2265 return; 2266 } 2267 2268 TaggedFile::getAllFrames(tagNr, frames); 2269 } 2270 2271 /** 2272 * Add a suitable field list for the frame if missing. 2273 * If a frame is created, its field list is empty. This method will create 2274 * a field list appropriate for the frame type and tagged file type if no 2275 * field list exists. 2276 * @param tagNr tag number 2277 * @param frame frame where field list is added 2278 */ 2279 void Mp3File::addFieldList(Frame::TagNumber tagNr, Frame& frame) const 2280 { 2281 if (tagNr == Frame::Tag_2 && frame.fieldList().isEmpty()) { 2282 if (ID3_Frame* id3Frame = createId3FrameFromFrame(frame)) { 2283 getFieldsFromId3Frame(id3Frame, frame.fieldList()); 2284 frame.setFieldListFromValue(); 2285 delete id3Frame; 2286 } 2287 } 2288 } 2289 2290 /** 2291 * Get a list of frame IDs which can be added. 2292 * @param tagNr tag number 2293 * @return list with frame IDs. 2294 */ 2295 QStringList Mp3File::getFrameIds(Frame::TagNumber tagNr) const 2296 { 2297 if (tagNr != Frame::Tag_2) 2298 return QStringList(); 2299 2300 QStringList lst; 2301 for (int type = Frame::FT_FirstFrame; type <= Frame::FT_LastFrame; ++type) { 2302 if (auto name = Frame::ExtendedType(static_cast<Frame::Type>(type), 2303 QLatin1String("")).getName(); 2304 !name.isEmpty()) { 2305 lst.append(name); 2306 } 2307 } 2308 for (int i = 0; i <= ID3FID_WWWUSER; ++i) { 2309 if (typeStrOfId[i].type == Frame::FT_Other) { 2310 if (const char* s = typeStrOfId[i].str) { 2311 lst.append(QString::fromLatin1(s)); 2312 } 2313 } 2314 } 2315 return lst; 2316 } 2317 2318 /** 2319 * Set the encoding to be used for tag 1. 2320 * 2321 * @param name of encoding, default is ISO 8859-1 2322 */ 2323 void Mp3File::setTextEncodingV1(const QString& name) 2324 { 2325 #if QT_VERSION >= 0x060000 2326 auto encoding = QStringConverter::encodingForName(name.toLatin1()); 2327 s_decoderV1 = QStringDecoder(encoding.value_or(QStringConverter::Latin1)); 2328 s_encoderV1 = QStringEncoder(encoding.value_or(QStringConverter::Latin1)); 2329 #else 2330 s_textCodecV1 = name != QLatin1String("ISO-8859-1") 2331 ? QTextCodec::codecForName(name.toLatin1().data()) : nullptr; 2332 #endif 2333 } 2334 2335 /** 2336 * Set the default text encoding. 2337 * 2338 * @param textEnc default text encoding 2339 */ 2340 void Mp3File::setDefaultTextEncoding(TagConfig::TextEncoding textEnc) 2341 { 2342 // UTF8 encoding is buggy, so UTF16 is used when UTF8 is configured 2343 s_defaultTextEncoding = textEnc == TagConfig::TE_ISO8859_1 ? 2344 ID3TE_ISO8859_1 : ID3TE_UTF16; 2345 } 2346 2347 /** 2348 * Notify about configuration change. 2349 * This method shall be called when the configuration changes. 2350 */ 2351 void Mp3File::notifyConfigurationChange() 2352 { 2353 setDefaultTextEncoding( 2354 static_cast<TagConfig::TextEncoding>(TagConfig::instance().textEncoding())); 2355 setTextEncodingV1(TagConfig::instance().textEncodingV1()); 2356 }