1 /**
2 * \file mp3file.cpp
3 * Handling of tagged MP3 files.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 9 Jan 2003
8 *
9 * Copyright (C) 2003-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "mp3file.h"
28
29 #include <QDir>
30 #include <QString>
31 #if QT_VERSION >= 0x060000
32 #include <QStringConverter>
33 #else
34 #include <QTextCodec>
35 #endif
36 #include <QByteArray>
37 #include <QtEndian>
38
39 #include <cstring>
40 #include <id3/tag.h>
41 #ifdef Q_OS_WIN32
42 #include <id3.h>
43 #endif
44
45 #include "id3libconfig.h"
46 #include "genres.h"
47 #include "attributedata.h"
48
49 #ifdef Q_OS_WIN32
50 /**
51 * This will be set for id3lib versions with Unicode bugs.
52 * ID3LIB_ symbols cannot be found on Windows ?!
53 */
54 #define UNICODE_SUPPORT_BUGGY 1
55 #else
56 /** This will be set for id3lib versions with Unicode bugs. */
57 #define UNICODE_SUPPORT_BUGGY ((((ID3LIB_MAJOR_VERSION) << 16) + \
58 ((ID3LIB_MINOR_VERSION) << 8) + (ID3LIB_PATCH_VERSION)) <= 0x030803)
59 #endif
60
61 #if defined __GNUC__ && (__GNUC__ * 100 + __GNUC_MINOR__) >= 407
62 /** Defined if GCC is used and supports diagnostic pragmas */
63 #define GCC_HAS_DIAGNOSTIC_PRAGMA
64 #endif
65
66 namespace {
67
68 #if QT_VERSION >= 0x060000
69 /** String decoder for ID3v1 tags, default is ISO 8859-1 */
70 QStringDecoder s_decoderV1;
71 QStringEncoder s_encoderV1;
72 #else
73 /** Text codec for ID3v1 tags, 0 to use default (ISO 8859-1) */
74 const QTextCodec* s_textCodecV1 = nullptr;
75 #endif
76
77 /** Default text encoding */
78 ID3_TextEnc s_defaultTextEncoding = ID3TE_ISO8859_1;
79
80 /**
81 * Get the default text encoding.
82 * @return default text encoding.
83 */
getDefaultTextEncoding()84 ID3_TextEnc getDefaultTextEncoding() { return s_defaultTextEncoding; }
85
86 }
87
88 /**
89 * Constructor.
90 *
91 * @param idx index in file proxy model
92 */
Mp3File(const QPersistentModelIndex & idx)93 Mp3File::Mp3File(const QPersistentModelIndex& idx)
94 : TaggedFile(idx)
95 {
96 }
97
98 /**
99 * Destructor.
100 */
~Mp3File()101 Mp3File::~Mp3File()
102 {
103 // Must not be inline because of forwared declared QScopedPointer.
104 }
105
106 /**
107 * Get key of tagged file format.
108 * @return "Id3libMetadata".
109 */
taggedFileKey() const110 QString Mp3File::taggedFileKey() const
111 {
112 return QLatin1String("Id3libMetadata");
113 }
114
115 /**
116 * Get features supported.
117 * @return bit mask with Feature flags set.
118 */
taggedFileFeatures() const119 int Mp3File::taggedFileFeatures() const
120 {
121 return TF_ID3v11 | TF_ID3v23;
122 }
123
124 /**
125 * Read tags from file.
126 *
127 * @param force true to force reading even if tags were already read.
128 */
readTags(bool force)129 void Mp3File::readTags(bool force)
130 {
131 bool priorIsTagInformationRead = isTagInformationRead();
132 QByteArray fn = QFile::encodeName(currentFilePath());
133
134 if (force && m_tagV1) {
135 m_tagV1->Clear();
136 m_tagV1->Link(fn, ID3TT_ID3V1);
137 markTagUnchanged(Frame::Tag_1);
138 }
139 if (!m_tagV1) {
140 m_tagV1.reset(new ID3_Tag);
141 m_tagV1->Link(fn, ID3TT_ID3V1);
142 markTagUnchanged(Frame::Tag_1);
143 }
144
145 if (force && m_tagV2) {
146 m_tagV2->Clear();
147 m_tagV2->Link(fn, ID3TT_ID3V2);
148 markTagUnchanged(Frame::Tag_2);
149 }
150 if (!m_tagV2) {
151 m_tagV2.reset(new ID3_Tag);
152 m_tagV2->Link(fn, ID3TT_ID3V2);
153 markTagUnchanged(Frame::Tag_2);
154 }
155
156 if (force) {
157 setFilename(currentFilename());
158 }
159
160 notifyModelDataChanged(priorIsTagInformationRead);
161 }
162
163 /**
164 * Write tags to file and rename it if necessary.
165 *
166 * @param force true to force writing even if file was not changed.
167 * @param renamed will be set to true if the file was renamed,
168 * i.e. the file name is no longer valid, else *renamed
169 * is left unchanged
170 * @param preserve true to preserve file time stamps
171 *
172 * @return true if ok, false if the file could not be written or renamed.
173 */
writeTags(bool force,bool * renamed,bool preserve)174 bool Mp3File::writeTags(bool force, bool* renamed, bool preserve)
175 {
176 QString fnStr(currentFilePath());
177 if (isChanged() && !QFileInfo(fnStr).isWritable()) {
178 revertChangedFilename();
179 return false;
180 }
181
182 // store time stamp if it has to be preserved
183 quint64 actime = 0, modtime = 0;
184 if (preserve) {
185 getFileTimeStamps(fnStr, actime, modtime);
186 }
187
188 // There seems to be a bug in id3lib: The V1 genre is not
189 // removed. So we check here and strip the whole header
190 // if there are no frames.
191 if (m_tagV1 && (force || isTagChanged(Frame::Tag_1)) && (m_tagV1->NumFrames() == 0)) {
192 m_tagV1->Strip(ID3TT_ID3V1);
193 markTagUnchanged(Frame::Tag_1);
194 }
195 // Even after removing all frames, HasV2Tag() still returns true,
196 // so we strip the whole header.
197 if (m_tagV2 && (force || isTagChanged(Frame::Tag_2)) && (m_tagV2->NumFrames() == 0)) {
198 m_tagV2->Strip(ID3TT_ID3V2);
199 markTagUnchanged(Frame::Tag_2);
200 }
201 // There seems to be a bug in id3lib: If I update an ID3v1 and then
202 // strip the ID3v2 the ID3v1 is removed too and vice versa, so I
203 // first make any stripping and then the updating.
204 if (m_tagV1 && (force || isTagChanged(Frame::Tag_1)) && (m_tagV1->NumFrames() > 0)) {
205 m_tagV1->Update(ID3TT_ID3V1);
206 markTagUnchanged(Frame::Tag_1);
207 }
208 if (m_tagV2 && (force || isTagChanged(Frame::Tag_2)) && (m_tagV2->NumFrames() > 0)) {
209 m_tagV2->Update(ID3TT_ID3V2);
210 markTagUnchanged(Frame::Tag_2);
211 }
212
213 // restore time stamp
214 if (actime || modtime) {
215 setFileTimeStamps(fnStr, actime, modtime);
216 }
217
218 if (isFilenameChanged()) {
219 if (!renameFile()) {
220 return false;
221 }
222 markFilenameUnchanged();
223 // link tags to new file name
224 readTags(true);
225 *renamed = true;
226 }
227 return true;
228 }
229
230 /**
231 * Free resources allocated when calling readTags().
232 *
233 * @param force true to force clearing even if the tags are modified
234 */
clearTags(bool force)235 void Mp3File::clearTags(bool force)
236 {
237 if (isChanged() && !force)
238 return;
239
240 bool priorIsTagInformationRead = isTagInformationRead();
241 if (m_tagV1) {
242 m_tagV1.reset();
243 markTagUnchanged(Frame::Tag_1);
244 }
245 if (m_tagV2) {
246 m_tagV2.reset();
247 markTagUnchanged(Frame::Tag_2);
248 }
249 notifyModelDataChanged(priorIsTagInformationRead);
250 }
251
252 namespace {
253
254 /**
255 * Fix up a unicode string from id3lib.
256 *
257 * @param str unicode string
258 * @param numChars number of characters in str
259 *
260 * @return string as QString.
261 */
fixUpUnicode(const unicode_t * str,size_t numChars)262 QString fixUpUnicode(const unicode_t* str, size_t numChars)
263 {
264 QString text;
265 if (numChars > 0 && str && *str) {
266 auto qcarray = new QChar[numChars];
267 // Unfortunately, Unicode support in id3lib is rather buggy
268 // in the current version: The codes are mirrored.
269 // In the hope that my patches will be included, I try here
270 // to work around these bugs.
271 size_t numZeroes = 0;
272 for (size_t i = 0; i < numChars; i++) {
273 qcarray[i] = UNICODE_SUPPORT_BUGGY
274 ? static_cast<ushort>(((str[i] & 0x00ff) << 8) |
275 ((str[i] & 0xff00) >> 8))
276 : static_cast<ushort>(str[i]);
277 if (qcarray[i].isNull()) { ++numZeroes; }
278 }
279 // remove a single trailing zero character
280 if (numZeroes == 1 && qcarray[numChars - 1].isNull()) {
281 --numChars;
282 }
283 text = QString(qcarray, numChars);
284 delete [] qcarray;
285 }
286 return text;
287 }
288
289 /**
290 * Get string from text field.
291 *
292 * @param field field
293 * @param codec text codec to use, 0 for default
294 *
295 * @return string,
296 * "" if the field does not exist.
297 */
getString(ID3_Field * field,QStringDecoder * decoder=nullptr)298 QString getString(ID3_Field* field,
299 #if QT_VERSION >= 0x060000
300 QStringDecoder* decoder = nullptr
301 #else
302 const QTextCodec* codec = nullptr
303 #endif
304 )
305 {
306 QString text(QLatin1String(""));
307 if (field != nullptr) {
308 ID3_TextEnc enc = field->GetEncoding();
309 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) {
310 size_t numItems = field->GetNumTextItems();
311 if (numItems <= 1) {
312 text = fixUpUnicode(field->GetRawUnicodeText(),
313 field->Size() / sizeof(unicode_t));
314 } else {
315 // if there are multiple items, put them into one string
316 // separated by a special separator.
317 // ID3_Field::GetRawUnicodeTextItem() returns a pointer to a temporary
318 // object, so I do not use it.
319 text = fixUpUnicode(field->GetRawUnicodeText(),
320 field->Size() / sizeof(unicode_t));
321 text.replace(QLatin1Char('\0'), Frame::stringListSeparator());
322 }
323 } else {
324 // (ID3TE_IS_SINGLE_BYTE_ENC(enc))
325 // (enc == ID3TE_ISO8859_1 || enc == ID3TE_UTF8)
326 size_t numItems = field->GetNumTextItems();
327 if (numItems <= 1) {
328 #if QT_VERSION >= 0x060000
329 text = decoder ? decoder->decode(QByteArray(field->GetRawText(), field->Size()))
330 : QString::fromLatin1(field->GetRawText());
331 #else
332 text = codec ? codec->toUnicode(field->GetRawText(), field->Size())
333 : QString::fromLatin1(field->GetRawText());
334 #endif
335 } else {
336 // if there are multiple items, put them into one string
337 // separated by a special separator.
338 for (size_t itemNr = 0; itemNr < numItems; ++itemNr) {
339 if (itemNr == 0) {
340 text = QString::fromLatin1(field->GetRawTextItem(0));
341 } else {
342 text += Frame::stringListSeparator();
343 text += QString::fromLatin1(field->GetRawTextItem(itemNr));
344 }
345 }
346 }
347 }
348 }
349 return text;
350 }
351
352 /**
353 * Get text field.
354 *
355 * @param tag ID3 tag
356 * @param id frame ID
357 * @param codec text codec to use, 0 for default
358 * @return string,
359 * "" if the field does not exist,
360 * QString::null if the tags do not exist.
361 */
getTextField(const ID3_Tag * tag,ID3_FrameID id,QStringDecoder * decoder=nullptr)362 QString getTextField(const ID3_Tag* tag, ID3_FrameID id,
363 #if QT_VERSION >= 0x060000
364 QStringDecoder* decoder = nullptr
365 #else
366 const QTextCodec* codec = nullptr
367 #endif
368 )
369 {
370 if (!tag) {
371 return QString();
372 }
373 QString str(QLatin1String(""));
374 ID3_Field* fld;
375 ID3_Frame* frame = tag->Find(id);
376 if (frame && ((fld = frame->GetField(ID3FN_TEXT)) != nullptr)) {
377 #if QT_VERSION >= 0x060000
378 str = getString(fld, decoder);
379 #else
380 str = getString(fld, codec);
381 #endif
382 }
383 return str;
384 }
385
386 /**
387 * Get year.
388 *
389 * @param tag ID3 tag
390 * @return number,
391 * 0 if the field does not exist,
392 * -1 if the tags do not exist.
393 */
getYear(const ID3_Tag * tag)394 int getYear(const ID3_Tag* tag)
395 {
396 QString str = getTextField(tag, ID3FID_YEAR);
397 if (str.isNull()) return -1;
398 if (str.isEmpty()) return 0;
399 return str.toInt();
400 }
401
402 /**
403 * Get track.
404 *
405 * @param tag ID3 tag
406 * @return number,
407 * 0 if the field does not exist,
408 * -1 if the tags do not exist.
409 */
getTrackNum(const ID3_Tag * tag)410 int getTrackNum(const ID3_Tag* tag)
411 {
412 QString str = getTextField(tag, ID3FID_TRACKNUM);
413 if (str.isNull()) return -1;
414 if (str.isEmpty()) return 0;
415 // handle "track/total number of tracks" format
416 int slashPos = str.indexOf(QLatin1Char('/'));
417 if (slashPos != -1) {
418 str.truncate(slashPos);
419 }
420 return str.toInt();
421 }
422
423 /**
424 * Get genre.
425 *
426 * @param tag ID3 tag
427 * @return number,
428 * 0xff if the field does not exist,
429 * -1 if the tags do not exist.
430 */
getGenreNum(const ID3_Tag * tag)431 int getGenreNum(const ID3_Tag* tag)
432 {
433 QString str = getTextField(tag, ID3FID_CONTENTTYPE);
434 if (str.isNull()) return -1;
435 if (str.isEmpty()) return 0xff;
436 int cpPos = 0, n = 0xff;
437 if ((str[0] == QLatin1Char('(')) &&
438 ((cpPos = str.indexOf(QLatin1Char(')'), 2)) > 1)) {
439 bool ok;
440 #if QT_VERSION >= 0x060000
441 n = str.mid(1, cpPos - 1).toInt(&ok);
442 #else
443 n = str.midRef(1, cpPos - 1).toInt(&ok);
444 #endif
445 if (!ok || n > 0xff) {
446 n = 0xff;
447 }
448 } else {
449 // ID3v2 genres can be stored as "(9)", "(9)Metal" or "Metal".
450 // If the string does not start with '(', try to get the genre number
451 // from a string containing a genre text.
452 n = Genres::getNumber(str);
453 }
454 return n;
455 }
456
457 /**
458 * Allocate a fixed up a unicode string for id3lib.
459 *
460 * @param text string
461 *
462 * @return new allocated unicode string, has to be freed with delete [].
463 */
newFixedUpUnicode(const QString & text)464 unicode_t* newFixedUpUnicode(const QString& text)
465 {
466 // Unfortunately, Unicode support in id3lib is rather buggy in the
467 // current version: The codes are mirrored, a second different
468 // BOM may be added, if the LSB >= 0x80, the MSB is set to 0xff.
469 // If iconv is used (id3lib on Linux), the character do not come
470 // back mirrored, but with a second (different)! BOM 0xfeff and
471 // they are still written in the wrong order (big endian).
472 // In the hope that my patches will be included, I try here to
473 // work around these bugs, but there is no solution for the
474 // LSB >= 0x80 bug.
475 const QChar* qcarray = text.unicode();
476 int unicode_size = text.length();
477 auto unicode = new unicode_t[unicode_size + 1];
478 for (int i = 0; i < unicode_size; ++i) {
479 unicode[i] = qcarray[i].unicode();
480 if (UNICODE_SUPPORT_BUGGY) {
481 unicode[i] = static_cast<ushort>(((unicode[i] & 0x00ff) << 8) |
482 ((unicode[i] & 0xff00) >> 8));
483 }
484 }
485 unicode[unicode_size] = 0;
486 return unicode;
487 }
488
489 /**
490 * Set string list in text field.
491 *
492 * @param field field
493 * @param lst string list to set
494 */
setStringList(ID3_Field * field,const QStringList & lst)495 void setStringList(ID3_Field* field, const QStringList& lst)
496 {
497 ID3_TextEnc enc = field->GetEncoding();
498 bool first = true;
499 for (auto it = lst.constBegin(); it != lst.constEnd(); ++it) {
500 if (first) {
501 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) {
502 unicode_t* unicode = newFixedUpUnicode(*it);
503 if (unicode) {
504 field->Set(unicode);
505 delete [] unicode;
506 }
507 } else if (enc == ID3TE_UTF8) {
508 field->Set((*it).toUtf8().data());
509 } else {
510 // enc == ID3TE_ISO8859_1
511 field->Set((*it).toLatin1().data());
512 }
513 first = false;
514 } else {
515 // This will not work with buggy id3lib. A BOM 0xfffe is written before
516 // the first string, but not before the subsequent strings. Prepending a
517 // BOM or changing the byte order does not help when id3lib rewrites
518 // this field when another frame is changed. So you cannot use
519 // string lists with Unicode encoding.
520 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) {
521 unicode_t* unicode = newFixedUpUnicode(*it);
522 if (unicode) {
523 field->Add(unicode);
524 delete [] unicode;
525 }
526 } else if (enc == ID3TE_UTF8) {
527 field->Add((*it).toUtf8().data());
528 } else {
529 // enc == ID3TE_ISO8859_1
530 field->Add((*it).toLatin1().data());
531 }
532 }
533 }
534 }
535
536 /**
537 * Set string in text field.
538 *
539 * @param field field
540 * @param text text to set
541 * @param codec text codec to use, 0 for default
542 */
setString(ID3_Field * field,const QString & text,QStringEncoder * encoder=nullptr)543 void setString(ID3_Field* field, const QString& text,
544 #if QT_VERSION >= 0x060000
545 QStringEncoder* encoder = nullptr
546 #else
547 const QTextCodec* codec = nullptr
548 #endif
549 )
550 {
551 if (text.indexOf(Frame::stringListSeparator()) == -1) {
552 ID3_TextEnc enc = field->GetEncoding();
553 // (ID3TE_IS_DOUBLE_BYTE_ENC(enc))
554 if (enc == ID3TE_UTF16 || enc == ID3TE_UTF16BE) {
555 unicode_t* unicode = newFixedUpUnicode(text);
556 if (unicode) {
557 field->Set(unicode);
558 delete [] unicode;
559 }
560 } else if (enc == ID3TE_UTF8) {
561 field->Set(text.toUtf8().data());
562 } else {
563 // enc == ID3TE_ISO8859_1
564 #if QT_VERSION >= 0x060000
565 field->Set(encoder ? static_cast<QByteArray>(encoder->encode(text)).constData()
566 : text.toLatin1().constData());
567 #else
568 field->Set(codec ? codec->fromUnicode(text).constData()
569 : text.toLatin1().constData());
570 #endif
571 }
572 } else {
573 setStringList(field, text.split(Frame::stringListSeparator()));
574 }
575 }
576
577 /**
578 * Set text field.
579 *
580 * @param tag ID3 tag
581 * @param id frame ID
582 * @param text text to set
583 * @param allowUnicode true to allow setting of Unicode encoding if necessary
584 * @param replace true to replace an existing field
585 * @param removeEmpty true to remove a field if text is empty
586 * @param codec text codec to use, 0 for default
587 *
588 * @return true if the field was changed.
589 */
setTextField(ID3_Tag * tag,ID3_FrameID id,const QString & text,bool allowUnicode=false,bool replace=true,bool removeEmpty=true,QStringEncoder * encoder=nullptr)590 bool setTextField(ID3_Tag* tag, ID3_FrameID id, const QString& text,
591 bool allowUnicode = false, bool replace = true,
592 bool removeEmpty = true,
593 #if QT_VERSION >= 0x060000
594 QStringEncoder* encoder = nullptr
595 #else
596 const QTextCodec* codec = nullptr
597 #endif
598 )
599 {
600 bool changed = false;
601 if (tag && !text.isNull()) {
602 ID3_Frame* frame = nullptr;
603 bool removeOnly = removeEmpty && text.isEmpty();
604 if (replace || removeOnly) {
605 if (id == ID3FID_COMMENT && tag->HasV2Tag()) {
606 frame = tag->Find(ID3FID_COMMENT, ID3FN_DESCRIPTION, "");
607 } else {
608 frame = tag->Find(id);
609 }
610 if (frame) {
611 frame = tag->RemoveFrame(frame);
612 delete frame;
613 changed = true;
614 }
615 }
616 if (!removeOnly && (replace || tag->Find(id) == nullptr)) {
617 frame = new ID3_Frame(id);
618 if (frame) {
619 ID3_Field* fld = frame->GetField(ID3FN_TEXT);
620 if (fld) {
621 ID3_TextEnc enc = tag->HasV2Tag() ?
622 getDefaultTextEncoding() : ID3TE_ISO8859_1;
623 if (allowUnicode && enc == ID3TE_ISO8859_1) {
624 // check if information is lost if the string is not unicode
625 int i, unicode_size = text.length();
626 const QChar* qcarray = text.unicode();
627 for (i = 0; i < unicode_size; ++i) {
628 char ch = qcarray[i].toLatin1();
629 if (ch == 0 || (ch & 0x80) != 0) {
630 enc = ID3TE_UTF16;
631 break;
632 }
633 }
634 }
635 ID3_Field* encfld = frame->GetField(ID3FN_TEXTENC);
636 if (encfld) {
637 encfld->Set(enc);
638 }
639 fld->SetEncoding(enc);
640 #if QT_VERSION >= 0x060000
641 setString(fld, text, encoder);
642 #else
643 setString(fld, text, codec);
644 #endif
645 tag->AttachFrame(frame);
646 }
647 }
648 changed = true;
649 }
650 }
651 return changed;
652 }
653
654 /**
655 * Set year.
656 *
657 * @param tag ID3 tag
658 * @param num number to set, 0 to remove field.
659 *
660 * @return true if the field was changed.
661 */
setYear(ID3_Tag * tag,int num)662 bool setYear(ID3_Tag* tag, int num)
663 {
664 bool changed = false;
665 if (num >= 0) {
666 QString str;
667 if (num != 0) {
668 str.setNum(num);
669 } else {
670 str.clear();
671 }
672 changed = getTextField(tag, ID3FID_YEAR) != str &&
673 setTextField(tag, ID3FID_YEAR, str);
674 }
675 return changed;
676 }
677
678 }
679
680 /**
681 * Set track.
682 *
683 * @param tag ID3 tag
684 * @param num number to set, 0 to remove field.
685 * @param numTracks total number of tracks, <=0 to ignore
686 *
687 * @return true if the field was changed.
688 */
setTrackNum(ID3_Tag * tag,int num,int numTracks) const689 bool Mp3File::setTrackNum(ID3_Tag* tag, int num, int numTracks) const
690 {
691 bool changed = false;
692 if (num >= 0 && getTrackNum(tag) != num) {
693 QString str = trackNumberString(num, numTracks);
694 changed = getTextField(tag, ID3FID_TRACKNUM) != str &&
695 setTextField(tag, ID3FID_TRACKNUM, str);
696 }
697 return changed;
698 }
699
700 namespace {
701
702 /**
703 * Set genre.
704 *
705 * @param tag ID3 tag
706 * @param num number to set, 0xff to remove field.
707 *
708 * @return true if the field was changed.
709 */
setGenreNum(ID3_Tag * tag,int num)710 bool setGenreNum(ID3_Tag* tag, int num)
711 {
712 bool changed = false;
713 if (num >= 0) {
714 QString str;
715 if (num != 0xff) {
716 str = QString(QLatin1String("(%1)")).arg(num);
717 } else {
718 str = QLatin1String("");
719 }
720 changed = getTextField(tag, ID3FID_CONTENTTYPE) != str &&
721 setTextField(tag, ID3FID_CONTENTTYPE, str);
722 }
723 return changed;
724 }
725
726 }
727
728 /**
729 * Check if tag information has already been read.
730 *
731 * @return true if information is available,
732 * false if the tags have not been read yet, in which case
733 * hasTag() does not return meaningful information.
734 */
isTagInformationRead() const735 bool Mp3File::isTagInformationRead() const
736 {
737 return m_tagV1 || m_tagV2;
738 }
739
740 /**
741 * Check if tags are supported by the format of this file.
742 *
743 * @param tagNr tag number
744 * @return true.
745 */
isTagSupported(Frame::TagNumber tagNr) const746 bool Mp3File::isTagSupported(Frame::TagNumber tagNr) const
747 {
748 return tagNr == Frame::Tag_1 || tagNr == Frame::Tag_2;
749 }
750
751 /**
752 * Check if file has a tag.
753 *
754 * @param tagNr tag number
755 * @return true if a tag is available.
756 * @see isTagInformationRead()
757 */
hasTag(Frame::TagNumber tagNr) const758 bool Mp3File::hasTag(Frame::TagNumber tagNr) const
759 {
760 return (tagNr == Frame::Tag_1 && m_tagV1 && m_tagV1->HasV1Tag()) ||
761 (tagNr == Frame::Tag_2 && m_tagV2 && m_tagV2->HasV2Tag());
762 }
763
764 /**
765 * Get technical detail information.
766 *
767 * @param info the detail information is returned here
768 */
getDetailInfo(DetailInfo & info) const769 void Mp3File::getDetailInfo(DetailInfo& info) const
770 {
771 if (getFilename().right(4).toLower() == QLatin1String(".aac")) {
772 info.valid = true;
773 info.format = QLatin1String("AAC");
774 return;
775 }
776
777 const Mp3_Headerinfo* headerInfo = nullptr;
778 if (m_tagV2) {
779 headerInfo = m_tagV2->GetMp3HeaderInfo();
780 }
781 if (!headerInfo && m_tagV1) {
782 headerInfo = m_tagV1->GetMp3HeaderInfo();
783 }
784 if (headerInfo) {
785 info.valid = true;
786 switch (headerInfo->version) {
787 case MPEGVERSION_1:
788 info.format = QLatin1String("MPEG 1 ");
789 break;
790 case MPEGVERSION_2:
791 info.format = QLatin1String("MPEG 2 ");
792 break;
793 case MPEGVERSION_2_5:
794 info.format = QLatin1String("MPEG 2.5 ");
795 break;
796 default:
797 ; // nothing
798 }
799 switch (headerInfo->layer) {
800 case MPEGLAYER_I:
801 info.format += QLatin1String("Layer 1");
802 break;
803 case MPEGLAYER_II:
804 info.format += QLatin1String("Layer 2");
805 break;
806 case MPEGLAYER_III:
807 info.format += QLatin1String("Layer 3");
808 break;
809 default:
810 ; // nothing
811 }
812 info.bitrate = headerInfo->bitrate / 1000;
813 #ifndef HAVE_NO_ID3LIB_VBR
814 if (headerInfo->vbr_bitrate > 1000) {
815 info.vbr = true;
816 info.bitrate = headerInfo->vbr_bitrate / 1000;
817 }
818 #endif
819 info.sampleRate = headerInfo->frequency;
820 switch (headerInfo->channelmode) {
821 case MP3CHANNELMODE_STEREO:
822 info.channelMode = DetailInfo::CM_Stereo;
823 info.channels = 2;
824 break;
825 case MP3CHANNELMODE_JOINT_STEREO:
826 info.channelMode = DetailInfo::CM_JointStereo;
827 info.channels = 2;
828 break;
829 case MP3CHANNELMODE_DUAL_CHANNEL:
830 info.channels = 2;
831 break;
832 case MP3CHANNELMODE_SINGLE_CHANNEL:
833 info.channels = 1;
834 break;
835 default:
836 ; // nothing
837 }
838 info.duration = headerInfo->time;
839 } else {
840 info.valid = false;
841 }
842 }
843
844 /**
845 * Get duration of file.
846 *
847 * @return duration in seconds,
848 * 0 if unknown.
849 */
getDuration() const850 unsigned Mp3File::getDuration() const
851 {
852 unsigned duration = 0;
853 const Mp3_Headerinfo* info = nullptr;
854 if (m_tagV2) {
855 info = m_tagV2->GetMp3HeaderInfo();
856 }
857 if (!info && m_tagV1) {
858 info = m_tagV1->GetMp3HeaderInfo();
859 }
860 if (info && info->time > 0) {
861 duration = info->time;
862 }
863 return duration;
864 }
865
866 /**
867 * Get file extension including the dot.
868 *
869 * @return file extension ".mp3".
870 */
getFileExtension() const871 QString Mp3File::getFileExtension() const
872 {
873 QString ext(getFilename().right(4).toLower());
874 if (ext == QLatin1String(".aac") || ext == QLatin1String(".mp2"))
875 return ext;
876 else
877 return QLatin1String(".mp3");
878 }
879
880 /**
881 * Get the format of a tag.
882 *
883 * @param tagNr tag number
884 * @return string describing format of tag,
885 * e.g. "ID3v1.1".
886 */
getTagFormat(Frame::TagNumber tagNr) const887 QString Mp3File::getTagFormat(Frame::TagNumber tagNr) const
888 {
889 if (tagNr == Frame::Tag_1 && m_tagV1 && m_tagV1->HasV1Tag()) {
890 return QLatin1String("ID3v1.1");
891 } else if (tagNr == Frame::Tag_2 && m_tagV2 && m_tagV2->HasV2Tag()) {
892 switch (m_tagV2->GetSpec()) {
893 case ID3V2_3_0:
894 return QLatin1String("ID3v2.3.0");
895 case ID3V2_4_0:
896 return QLatin1String("ID3v2.4.0");
897 case ID3V2_2_0:
898 return QLatin1String("ID3v2.2.0");
899 case ID3V2_2_1:
900 return QLatin1String("ID3v2.2.1");
901 default:
902 break;
903 }
904 }
905 return QString();
906 }
907
908 namespace {
909
910 /** Types and descriptions for id3lib frame IDs */
911 const struct TypeStrOfId {
912 Frame::Type type;
913 const char* str;
914 } typeStrOfId[] = {
915 { Frame::FT_UnknownFrame, nullptr }, /* ???? */
916 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "AENC - Audio encryption") }, /* AENC */
917 { Frame::FT_Picture, QT_TRANSLATE_NOOP("@default", "APIC - Attached picture") }, /* APIC */
918 { Frame::FT_Other, nullptr }, /* ASPI */
919 { Frame::FT_Comment, QT_TRANSLATE_NOOP("@default", "COMM - Comments") }, /* COMM */
920 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "COMR - Commercial") }, /* COMR */
921 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "ENCR - Encryption method registration") }, /* ENCR */
922 { Frame::FT_Other, nullptr }, /* EQU2 */
923 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "EQUA - Equalization") }, /* EQUA */
924 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "ETCO - Event timing codes") }, /* ETCO */
925 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "GEOB - General encapsulated object") }, /* GEOB */
926 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "GRID - Group identification registration") }, /* GRID */
927 { Frame::FT_Arranger, QT_TRANSLATE_NOOP("@default", "IPLS - Involved people list") }, /* IPLS */
928 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "LINK - Linked information") }, /* LINK */
929 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "MCDI - Music CD identifier") }, /* MCDI */
930 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "MLLT - MPEG location lookup table") }, /* MLLT */
931 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "OWNE - Ownership frame") }, /* OWNE */
932 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "PRIV - Private frame") }, /* PRIV */
933 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "PCNT - Play counter") }, /* PCNT */
934 { Frame::FT_Rating, QT_TRANSLATE_NOOP("@default", "POPM - Popularimeter") }, /* POPM */
935 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "POSS - Position synchronisation frame") }, /* POSS */
936 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RBUF - Recommended buffer size") }, /* RBUF */
937 { Frame::FT_Other, nullptr }, /* RVA2 */
938 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RVAD - Relative volume adjustment") }, /* RVAD */
939 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "RVRB - Reverb") }, /* RVRB */
940 { Frame::FT_Other, nullptr }, /* SEEK */
941 { Frame::FT_Other, nullptr }, /* SIGN */
942 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "SYLT - Synchronized lyric/text") }, /* SYLT */
943 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "SYTC - Synchronized tempo codes") }, /* SYTC */
944 { Frame::FT_Album, QT_TRANSLATE_NOOP("@default", "TALB - Album/Movie/Show title") }, /* TALB */
945 { Frame::FT_Bpm, QT_TRANSLATE_NOOP("@default", "TBPM - BPM (beats per minute)") }, /* TBPM */
946 { Frame::FT_Composer, QT_TRANSLATE_NOOP("@default", "TCOM - Composer") }, /* TCOM */
947 { Frame::FT_Genre, QT_TRANSLATE_NOOP("@default", "TCON - Content type") }, /* TCON */
948 { Frame::FT_Copyright, QT_TRANSLATE_NOOP("@default", "TCOP - Copyright message") }, /* TCOP */
949 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TDAT - Date") }, /* TDAT */
950 { Frame::FT_Other, nullptr }, /* TDEN */
951 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TDLY - Playlist delay") }, /* TDLY */
952 { Frame::FT_Other, nullptr }, /* TDOR */
953 { Frame::FT_Other, nullptr }, /* TDRC */
954 { Frame::FT_Other, nullptr }, /* TDRL */
955 { Frame::FT_Other, nullptr }, /* TDTG */
956 { Frame::FT_Other, nullptr }, /* TIPL */
957 { Frame::FT_EncodedBy, QT_TRANSLATE_NOOP("@default", "TENC - Encoded by") }, /* TENC */
958 { Frame::FT_Lyricist, QT_TRANSLATE_NOOP("@default", "TEXT - Lyricist/Text writer") }, /* TEXT */
959 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TFLT - File type") }, /* TFLT */
960 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TIME - Time") }, /* TIME */
961 { Frame::FT_Work, QT_TRANSLATE_NOOP("@default", "TIT1 - Content group description") }, /* TIT1 */
962 { Frame::FT_Title, QT_TRANSLATE_NOOP("@default", "TIT2 - Title/songname/content description") }, /* TIT2 */
963 { Frame::FT_Description, QT_TRANSLATE_NOOP("@default", "TIT3 - Subtitle/Description refinement") }, /* TIT3 */
964 { Frame::FT_InitialKey, QT_TRANSLATE_NOOP("@default", "TKEY - Initial key") }, /* TKEY */
965 { Frame::FT_Language, QT_TRANSLATE_NOOP("@default", "TLAN - Language(s)") }, /* TLAN */
966 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TLEN - Length") }, /* TLEN */
967 { Frame::FT_Other, nullptr }, /* TMCL */
968 { Frame::FT_Media, QT_TRANSLATE_NOOP("@default", "TMED - Media type") }, /* TMED */
969 { Frame::FT_Other, nullptr }, /* TMOO */
970 { Frame::FT_OriginalAlbum, QT_TRANSLATE_NOOP("@default", "TOAL - Original album/movie/show title") }, /* TOAL */
971 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TOFN - Original filename") }, /* TOFN */
972 { Frame::FT_Author, QT_TRANSLATE_NOOP("@default", "TOLY - Original lyricist(s)/text writer(s)") }, /* TOLY */
973 { Frame::FT_OriginalArtist, QT_TRANSLATE_NOOP("@default", "TOPE - Original artist(s)/performer(s)") }, /* TOPE */
974 { Frame::FT_OriginalDate, QT_TRANSLATE_NOOP("@default", "TORY - Original release year") }, /* TORY */
975 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TOWN - File owner/licensee") }, /* TOWN */
976 { Frame::FT_Artist, QT_TRANSLATE_NOOP("@default", "TPE1 - Lead performer(s)/Soloist(s)") }, /* TPE1 */
977 { Frame::FT_AlbumArtist, QT_TRANSLATE_NOOP("@default", "TPE2 - Band/orchestra/accompaniment") }, /* TPE2 */
978 { Frame::FT_Conductor, QT_TRANSLATE_NOOP("@default", "TPE3 - Conductor/performer refinement") }, /* TPE3 */
979 { Frame::FT_Remixer, QT_TRANSLATE_NOOP("@default", "TPE4 - Interpreted, remixed, or otherwise modified by") }, /* TPE4 */
980 { Frame::FT_Disc, QT_TRANSLATE_NOOP("@default", "TPOS - Part of a set") }, /* TPOS */
981 { Frame::FT_Other, nullptr }, /* TPRO */
982 { Frame::FT_Publisher, QT_TRANSLATE_NOOP("@default", "TPUB - Publisher") }, /* TPUB */
983 { Frame::FT_Track, QT_TRANSLATE_NOOP("@default", "TRCK - Track number/Position in set") }, /* TRCK */
984 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRDA - Recording dates") }, /* TRDA */
985 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRSN - Internet radio station name") }, /* TRSN */
986 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TRSO - Internet radio station owner") }, /* TRSO */
987 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TSIZ - Size") }, /* TSIZ */
988 { Frame::FT_Other, nullptr }, /* TSOA */
989 { Frame::FT_Other, nullptr }, /* TSOP */
990 { Frame::FT_Other, nullptr }, /* TSOT */
991 { Frame::FT_Isrc, QT_TRANSLATE_NOOP("@default", "TSRC - ISRC (international standard recording code)") }, /* TSRC */
992 { Frame::FT_EncoderSettings,QT_TRANSLATE_NOOP("@default", "TSSE - Software/Hardware and settings used for encoding") },/* TSSE */
993 { Frame::FT_Subtitle, nullptr }, /* TSST */
994 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "TXXX - User defined text information") }, /* TXXX */
995 { Frame::FT_Date, QT_TRANSLATE_NOOP("@default", "TYER - Year") }, /* TYER */
996 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "UFID - Unique file identifier") }, /* UFID */
997 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "USER - Terms of use") }, /* USER */
998 { Frame::FT_Lyrics, QT_TRANSLATE_NOOP("@default", "USLT - Unsynchronized lyric/text transcription") }, /* USLT */
999 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WCOM - Commercial information") }, /* WCOM */
1000 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WCOP - Copyright/Legal information") }, /* WCOP */
1001 { Frame::FT_WWWAudioFile, QT_TRANSLATE_NOOP("@default", "WOAF - Official audio file webpage") }, /* WOAF */
1002 { Frame::FT_Website, QT_TRANSLATE_NOOP("@default", "WOAR - Official artist/performer webpage") }, /* WOAR */
1003 { Frame::FT_WWWAudioSource, QT_TRANSLATE_NOOP("@default", "WOAS - Official audio source webpage") }, /* WOAS */
1004 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WORS - Official internet radio station homepage") }, /* WORS */
1005 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WPAY - Payment") }, /* WPAY */
1006 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WPUB - Official publisher webpage") }, /* WPUB */
1007 { Frame::FT_Other, QT_TRANSLATE_NOOP("@default", "WXXX - User defined URL link") } /* WXXX */
1008 };
1009 Q_STATIC_ASSERT(sizeof(typeStrOfId) / sizeof(typeStrOfId[0]) == ID3FID_WWWUSER + 1);
1010
1011 /**
1012 * Get type and description of frame.
1013 *
1014 * @param id ID of frame
1015 * @param type the type is returned here
1016 * @param str the description is returned here, 0 if not available
1017 */
getTypeStringForId3libFrameId(ID3_FrameID id,Frame::Type & type,const char * & str)1018 void getTypeStringForId3libFrameId(ID3_FrameID id, Frame::Type& type,
1019 const char*& str)
1020 {
1021 const TypeStrOfId& ts = typeStrOfId[id <= ID3FID_WWWUSER ? id : 0];
1022 type = ts.type;
1023 str = ts.str;
1024 }
1025
1026 /**
1027 * Get id3lib frame ID for frame type.
1028 *
1029 * @param type frame type
1030 *
1031 * @return id3lib frame ID or ID3FID_NOFRAME if type not found.
1032 */
getId3libFrameIdForType(Frame::Type type)1033 ID3_FrameID getId3libFrameIdForType(Frame::Type type)
1034 {
1035 // IPLS is mapped to FT_Arranger and FT_Performer
1036 if (type == Frame::FT_Performer) {
1037 return ID3FID_INVOLVEDPEOPLE;
1038 } else if (type == Frame::FT_CatalogNumber ||
1039 type == Frame::FT_ReleaseCountry ||
1040 type == Frame::FT_Grouping ||
1041 type == Frame::FT_Subtitle) {
1042 return ID3FID_USERTEXT;
1043 }
1044
1045 static int typeIdMap[Frame::FT_LastFrame + 1] = { -1, };
1046 if (typeIdMap[0] == -1) {
1047 // first time initialization
1048 for (int i = 0; i <= ID3FID_WWWUSER; ++i) {
1049 int t = typeStrOfId[i].type;
1050 if (t <= Frame::FT_LastFrame) {
1051 typeIdMap[t] = i;
1052 }
1053 }
1054 }
1055 return type <= Frame::FT_LastFrame ?
1056 static_cast<ID3_FrameID>(typeIdMap[type]) : ID3FID_NOFRAME;
1057 }
1058
1059 /**
1060 * Get id3lib frame ID for frame name.
1061 *
1062 * @param name frame name
1063 *
1064 * @return id3lib frame ID or ID3FID_NOFRAME if name not found.
1065 */
getId3libFrameIdForName(const QString & name)1066 ID3_FrameID getId3libFrameIdForName(const QString& name)
1067 {
1068 if (name.length() >= 4) {
1069 QByteArray nameBytes = name.toLatin1();
1070 const char* nameStr = nameBytes.constData();
1071 for (int i = 0; i <= ID3FID_WWWUSER; ++i) {
1072 const char* s = typeStrOfId[i].str;
1073 if (s && ::strncmp(s, nameStr, 4) == 0) {
1074 return static_cast<ID3_FrameID>(i);
1075 }
1076 }
1077 }
1078 return ID3FID_NOFRAME;
1079 }
1080
1081 /**
1082 * Convert the binary field of a SYLT frame to a list of alternating
1083 * time stamps and strings.
1084 * @param bytes binary field
1085 * @param enc encoding
1086 * @return list containing time, text, time, text, ...
1087 */
syltBytesToList(const QByteArray & bytes,ID3_TextEnc enc)1088 QVariantList syltBytesToList(const QByteArray& bytes, ID3_TextEnc enc)
1089 {
1090 QVariantList timeEvents;
1091 const int numBytes = bytes.size();
1092 int textBegin = 0, textEnd;
1093 while (textBegin < numBytes) {
1094 if (enc == ID3TE_ISO8859_1 || enc == ID3TE_UTF8) {
1095 textEnd = bytes.indexOf('\0', textBegin);
1096 if (textEnd != -1) {
1097 ++textEnd;
1098 }
1099 } else {
1100 const ushort* unicode =
1101 reinterpret_cast<const ushort*>(bytes.constData() + textBegin);
1102 textEnd = textBegin;
1103 while (textEnd < numBytes) {
1104 textEnd += 2;
1105 if (*unicode++ == 0) {
1106 break;
1107 }
1108 }
1109 }
1110 if (textEnd < 0 || textEnd >= numBytes)
1111 break;
1112
1113 QString str;
1114 QByteArray text = bytes.mid(textBegin, textEnd - textBegin);
1115 switch (enc) {
1116 case ID3TE_UTF16BE:
1117 text.prepend(0xff);
1118 text.prepend(0xfe);
1119 // starting with FEFF BOM
1120 // fallthrough
1121 case ID3TE_UTF16:
1122 #if QT_VERSION >= 0x060000
1123 str = QString::fromUtf16(reinterpret_cast<const char16_t*>(text.constData()));
1124 #else
1125 str = QString::fromUtf16(reinterpret_cast<const ushort*>(text.constData()));
1126 #endif
1127 break;
1128 case ID3TE_UTF8:
1129 str = QString::fromUtf8(text.constData());
1130 break;
1131 case ID3TE_ISO8859_1:
1132 default:
1133 str = QString::fromLatin1(text.constData());
1134 }
1135 textBegin = textEnd + 4;
1136 if (textBegin > numBytes)
1137 break;
1138
1139 auto time = qFromBigEndian<quint32>(
1140 reinterpret_cast<const uchar*>(bytes.constData()) + textEnd);
1141 timeEvents.append(time);
1142 timeEvents.append(str);
1143 }
1144 return timeEvents;
1145 }
1146
1147 /**
1148 * Convert a list of alternating time stamps and strings to the binary field of
1149 * a SYLT frame.
1150 * @param synchedData list containing time, text, time, text, ...
1151 * @return binary field bytes.
1152 */
syltListToBytes(const QVariantList & synchedData,ID3_TextEnc enc)1153 QByteArray syltListToBytes(const QVariantList& synchedData, ID3_TextEnc enc)
1154 {
1155 QByteArray bytes;
1156 QListIterator<QVariant> it(synchedData);
1157 while (it.hasNext()) {
1158 quint32 milliseconds = it.next().toUInt();
1159 if (!it.hasNext())
1160 break;
1161
1162 QString str = it.next().toString();
1163 switch (enc) {
1164 case ID3TE_UTF16:
1165 bytes.append(0xff);
1166 bytes.append(0xfe);
1167 // starting with FFFE BOM
1168 // fallthrough
1169 case ID3TE_UTF16BE:
1170 {
1171 const ushort* unicode = str.utf16();
1172 do {
1173 uchar lsb = *unicode & 0xff;
1174 uchar msb = *unicode >> 8;
1175 if (enc == ID3TE_UTF16) {
1176 bytes.append(static_cast<char>(lsb));
1177 bytes.append(static_cast<char>(msb));
1178 } else {
1179 bytes.append(static_cast<char>(msb));
1180 bytes.append(static_cast<char>(lsb));
1181 }
1182 } while (*unicode++);
1183 break;
1184 }
1185 case ID3TE_UTF8:
1186 bytes.append(str.toUtf8());
1187 bytes.append('\0');
1188 break;
1189 case ID3TE_ISO8859_1:
1190 default:
1191 bytes.append(str.toLatin1());
1192 bytes.append('\0');
1193 }
1194 uchar timeStamp[4];
1195 qToBigEndian(milliseconds, timeStamp);
1196 bytes.append(reinterpret_cast<const char*>(timeStamp), sizeof(timeStamp));
1197 }
1198 if (bytes.isEmpty()) {
1199 // id3lib bug: Empty binary fields are not written, so add a minimal field
1200 bytes = QByteArray(4 + (enc == ID3TE_UTF16 ||
1201 enc == ID3TE_UTF16BE ? 2 : 1),
1202 '\0');
1203 }
1204 return bytes;
1205 }
1206
1207 /**
1208 * Convert the binary field of an ETCO frame to a list of alternating
1209 * time stamps and codes.
1210 * @param bytes binary field
1211 * @return list containing time, code, time, code, ...
1212 */
etcoBytesToList(const QByteArray & bytes)1213 QVariantList etcoBytesToList(const QByteArray& bytes)
1214 {
1215 QVariantList timeEvents;
1216 const int numBytes = bytes.size();
1217 // id3lib bug: There is only a single data field for ETCO frames,
1218 // but it should be preceded by an ID_TimestampFormat field.
1219 // Start with the second byte.
1220 int pos = 1;
1221 while (pos < numBytes) {
1222 int code = static_cast<uchar>(bytes.at(pos));
1223 ++pos;
1224 if (pos + 4 > numBytes)
1225 break;
1226
1227 auto time = qFromBigEndian<quint32>(
1228 reinterpret_cast<const uchar*>(bytes.constData()) + pos);
1229 pos += 4;
1230 timeEvents.append(time);
1231 timeEvents.append(code);
1232 }
1233 return timeEvents;
1234 }
1235
1236 /**
1237 * Convert a list of alternating time stamps and codes to the binary field of
1238 * an ETCO frame.
1239 * @param synchedData list containing time, code, time, code, ...
1240 * @return binary field bytes.
1241 */
etcoListToBytes(const QVariantList & synchedData)1242 QByteArray etcoListToBytes(const QVariantList& synchedData)
1243 {
1244 QByteArray bytes;
1245 QListIterator<QVariant> it(synchedData);
1246 while (it.hasNext()) {
1247 quint32 milliseconds = it.next().toUInt();
1248 if (!it.hasNext())
1249 break;
1250
1251 int code = it.next().toInt();
1252 bytes.append(static_cast<char>(code));
1253 uchar timeStamp[4];
1254 qToBigEndian(milliseconds, timeStamp);
1255 bytes.append(reinterpret_cast<const char*>(timeStamp), sizeof(timeStamp));
1256 }
1257 return bytes;
1258 }
1259
1260 /**
1261 * Get the fields from an ID3v2 tag.
1262 *
1263 * @param id3Frame frame
1264 * @param fields the fields are appended to this list
1265 *
1266 * @return text representation of fields (Text or URL).
1267 */
getFieldsFromId3Frame(ID3_Frame * id3Frame,Frame::FieldList & fields)1268 QString getFieldsFromId3Frame(ID3_Frame* id3Frame, Frame::FieldList& fields)
1269 {
1270 QString text;
1271 ID3_Frame::Iterator* iter = id3Frame->CreateIterator();
1272 ID3_FrameID id3Id = id3Frame->GetID();
1273 ID3_TextEnc enc = ID3TE_NONE;
1274 ID3_Field* id3Field;
1275 Frame::Field field;
1276 while ((id3Field = iter->GetNext()) != nullptr) {
1277 ID3_FieldID id = id3Field->GetID();
1278 ID3_FieldType type = id3Field->GetType();
1279 field.m_id = id;
1280 if (type == ID3FTY_INTEGER) {
1281 uint32 intVal = id3Field->Get();
1282 field.m_value = intVal;
1283 if (id == ID3FN_TEXTENC) {
1284 enc = static_cast<ID3_TextEnc>(intVal);
1285 }
1286 }
1287 else if (type == ID3FTY_BINARY) {
1288 QByteArray ba(reinterpret_cast<const char*>(id3Field->GetRawBinary()),
1289 static_cast<int>(id3Field->Size()));
1290 if (id3Id == ID3FID_SYNCEDLYRICS) {
1291 field.m_value = syltBytesToList(ba, enc);
1292 } else if (id3Id == ID3FID_EVENTTIMING) {
1293 field.m_value = etcoBytesToList(ba);
1294 } else {
1295 field.m_value = ba;
1296 }
1297 }
1298 else if (type == ID3FTY_TEXTSTRING) {
1299 if (id == ID3FN_TEXT || id == ID3FN_DESCRIPTION || id == ID3FN_URL) {
1300 text = getString(id3Field);
1301 if (id3Id == ID3FID_CONTENTTYPE) {
1302 text = Genres::getNameString(text);
1303 }
1304 field.m_value = text;
1305 } else {
1306 field.m_value = getString(id3Field);
1307 }
1308 } else {
1309 field.m_value.clear();
1310 }
1311 fields.push_back(field);
1312 }
1313 #ifdef Q_OS_WIN32
1314 /* allocated in Windows DLL => must be freed in the same DLL */
1315 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
1316 #else
1317 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1318 #pragma GCC diagnostic push
1319 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
1320 /* Is safe because iterator implementation has default destructor */
1321 #endif
1322 delete iter;
1323 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1324 #pragma GCC diagnostic pop
1325 #endif
1326 #endif
1327 return text;
1328 }
1329
1330 /**
1331 * Get ID3v2 frame by index.
1332 *
1333 * @param tag ID3v2 tag
1334 * @param index index
1335 *
1336 * @return frame, 0 if index invalid.
1337 */
getId3v2Frame(ID3_Tag * tag,int index)1338 ID3_Frame* getId3v2Frame(ID3_Tag* tag, int index)
1339 {
1340 ID3_Frame* result = nullptr;
1341 if (tag) {
1342 ID3_Frame* frame;
1343 ID3_Tag::Iterator* iter = tag->CreateIterator();
1344 int i = 0;
1345 while ((frame = iter->GetNext()) != nullptr) {
1346 if (i == index) {
1347 result = frame;
1348 break;
1349 }
1350 ++i;
1351 }
1352 #ifdef Q_OS_WIN32
1353 /* allocated in Windows DLL => must be freed in the same DLL */
1354 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
1355 #else
1356 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1357 #pragma GCC diagnostic push
1358 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
1359 /* Is safe because iterator implementation has default destructor */
1360 #endif
1361 delete iter;
1362 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1363 #pragma GCC diagnostic pop
1364 #endif
1365 #endif
1366 }
1367 return result;
1368 }
1369
1370 }
1371
1372 /**
1373 * Set the fields in an id3lib frame from the field in the frame.
1374 *
1375 * @param id3Frame id3lib frame
1376 * @param frame frame with fields
1377 */
setId3v2Frame(ID3_Frame * id3Frame,const Frame & frame) const1378 void Mp3File::setId3v2Frame(ID3_Frame* id3Frame, const Frame& frame) const
1379 {
1380 ID3_Frame::Iterator* iter = id3Frame->CreateIterator();
1381 ID3_FrameID id3Id = id3Frame->GetID();
1382 ID3_TextEnc enc = ID3TE_NONE;
1383 for (auto fldIt = frame.getFieldList().constBegin();
1384 fldIt != frame.getFieldList().constEnd();
1385 ++fldIt) {
1386 ID3_Field* id3Field = iter->GetNext();
1387 if (!id3Field) {
1388 qDebug("early end of ID3 fields");
1389 break;
1390 }
1391 const Frame::Field& fld = *fldIt;
1392 #if QT_VERSION >= 0x060000
1393 switch (fld.m_value.typeId()) {
1394 case QMetaType::Int:
1395 case QMetaType::UInt:
1396 #else
1397 switch (fld.m_value.type()) {
1398 case QVariant::Int:
1399 case QVariant::UInt:
1400 #endif
1401 {
1402 int intVal = fld.m_value.toInt();
1403 if (fld.m_id == ID3FN_TEXTENC) {
1404 if (intVal == ID3TE_UTF8) intVal = ID3TE_UTF16;
1405 enc = static_cast<ID3_TextEnc>(intVal);
1406 }
1407 id3Field->Set(intVal);
1408 break;
1409 }
1410
1411 #if QT_VERSION >= 0x060000
1412 case QMetaType::QString:
1413 #else
1414 case QVariant::String:
1415 #endif
1416 {
1417 if (enc != ID3TE_NONE) {
1418 id3Field->SetEncoding(enc);
1419 }
1420 QString value(fld.m_value.toString());
1421 if (id3Id == ID3FID_CONTENTTYPE) {
1422 if (!TagConfig::instance().genreNotNumeric() ||
1423 value.contains(Frame::stringListSeparator())) {
1424 value = Genres::getNumberString(value, true);
1425 }
1426 } else if (id3Id == ID3FID_TRACKNUM) {
1427 formatTrackNumberIfEnabled(value, true);
1428 }
1429 setString(id3Field, value);
1430 break;
1431 }
1432
1433 #if QT_VERSION >= 0x060000
1434 case QMetaType::QByteArray:
1435 #else
1436 case QVariant::ByteArray:
1437 #endif
1438 {
1439 const QByteArray& ba = fld.m_value.toByteArray();
1440 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()),
1441 static_cast<size_t>(ba.size()));
1442 break;
1443 }
1444
1445 #if QT_VERSION >= 0x060000
1446 case QMetaType::QVariantList:
1447 #else
1448 case QVariant::List:
1449 #endif
1450 {
1451 if (id3Id == ID3FID_SYNCEDLYRICS) {
1452 QByteArray ba = syltListToBytes(fld.m_value.toList(), enc);
1453 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()),
1454 static_cast<size_t>(ba.size()));
1455 } else if (id3Id == ID3FID_EVENTTIMING) {
1456 QByteArray ba = etcoListToBytes(fld.m_value.toList());
1457 // id3lib bug: There is only a single data field for ETCO frames,
1458 // but it should be preceded by an ID_TimestampFormat field.
1459 ba.prepend(2);
1460 id3Field->Set(reinterpret_cast<const unsigned char*>(ba.data()),
1461 static_cast<size_t>(ba.size()));
1462 } else {
1463 qDebug("Unexpected QVariantList in field %d", fld.m_id);
1464 }
1465 break;
1466 }
1467
1468 default:
1469 #if QT_VERSION >= 0x060000
1470 qDebug("Unknown type %d in field %d", fld.m_value.typeId(), fld.m_id);
1471 #else
1472 qDebug("Unknown type %d in field %d", fld.m_value.type(), fld.m_id);
1473 #endif
1474 }
1475 }
1476 #ifdef Q_OS_WIN32
1477 /* allocated in Windows DLL => must be freed in the same DLL */
1478 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
1479 #else
1480 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1481 #pragma GCC diagnostic push
1482 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
1483 /* Is safe because iterator implementation has default destructor */
1484 #endif
1485 delete iter;
1486 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
1487 #pragma GCC diagnostic pop
1488 #endif
1489 #endif
1490 }
1491
1492 /**
1493 * Get a specific frame from the tags.
1494 *
1495 * @param tagNr tag number
1496 * @param type frame type
1497 * @param frame the frame is returned here
1498 *
1499 * @return true if ok.
1500 */
1501 bool Mp3File::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const
1502 {
1503 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame)
1504 return false;
1505
1506 ID3_FrameID frameId = getId3libFrameIdForType(type);
1507 if (frameId == ID3FID_NOFRAME)
1508 return false;
1509
1510 const ID3_Tag* tag;
1511 #if QT_VERSION >= 0x060000
1512 QStringDecoder* decoder;
1513 #else
1514 const QTextCodec* codec;
1515 #endif
1516 if (tagNr == Frame::Tag_1) {
1517 tag = m_tagV1.data();
1518 #if QT_VERSION >= 0x060000
1519 decoder = &s_decoderV1;
1520 #else
1521 codec = s_textCodecV1;
1522 #endif
1523 } else if (tagNr == Frame::Tag_2) {
1524 tag = m_tagV2.data();
1525 #if QT_VERSION >= 0x060000
1526 decoder = nullptr;
1527 #else
1528 codec = nullptr;
1529 #endif
1530 } else {
1531 return false;
1532 }
1533
1534 switch (type) {
1535 case Frame::FT_Album:
1536 case Frame::FT_Artist:
1537 case Frame::FT_Comment:
1538 case Frame::FT_Title:
1539 #if QT_VERSION >= 0x060000
1540 frame.setValue(getTextField(tag, frameId, decoder));
1541 #else
1542 frame.setValue(getTextField(tag, frameId, codec));
1543 #endif
1544 break;
1545 case Frame::FT_Track:
1546 if (tagNr == Frame::Tag_1) {
1547 frame.setValueAsNumber(getTrackNum(tag));
1548 } else {
1549 frame.setValue(getTextField(tag, frameId));
1550 }
1551 break;
1552 case Frame::FT_Date:
1553 frame.setValueAsNumber(getYear(tag));
1554 break;
1555 case Frame::FT_Genre:
1556 {
1557 int num = getGenreNum(tag);
1558 if (tagNr == Frame::Tag_1) {
1559 frame.setValue(num == -1
1560 ? QString()
1561 : num == 0xff
1562 ? QLatin1String("")
1563 : QString::fromLatin1(Genres::getName(num)));
1564 } else {
1565 frame.setValue(num != 0xff && num != -1
1566 ? QString::fromLatin1(Genres::getName(num))
1567 : getTextField(tag, frameId));
1568 }
1569 break;
1570 }
1571 default:
1572 return false;
1573 }
1574 frame.setType(type);
1575 return true;
1576 }
1577
1578 /**
1579 * Set a frame in the tags.
1580 *
1581 * @param tagNr tag number
1582 * @param frame frame to set
1583 *
1584 * @return true if ok.
1585 */
1586 bool Mp3File::setFrame(Frame::TagNumber tagNr, const Frame& frame)
1587 {
1588 if (tagNr == Frame::Tag_2) {
1589 // If the frame has an index, change that specific frame
1590 int index = frame.getIndex();
1591 if (index != -1 && m_tagV2) {
1592 ID3_Frame* id3Frame = getId3v2Frame(m_tagV2.data(), index);
1593 if (id3Frame) {
1594 // If value is changed or field list is empty,
1595 // set from value, else from FieldList.
1596 if (frame.isValueChanged() || frame.getFieldList().empty()) {
1597 QString value(frame.getValue());
1598 ID3_Field* fld;
1599 if ((fld = id3Frame->GetField(ID3FN_URL)) != nullptr) {
1600 if (getString(fld) != value) {
1601 fld->Set(value.toLatin1().data());
1602 markTagChanged(Frame::Tag_2, frame.getType());
1603 }
1604 return true;
1605 } else if ((fld = id3Frame->GetField(ID3FN_TEXT)) != nullptr ||
1606 (fld = id3Frame->GetField(ID3FN_DESCRIPTION)) != nullptr) {
1607 ID3_FrameID id = id3Frame->GetID();
1608 if (id == ID3FID_CONTENTTYPE) {
1609 if (!TagConfig::instance().genreNotNumeric() ||
1610 value.contains(Frame::stringListSeparator())) {
1611 value = Genres::getNumberString(value, true);
1612 }
1613 } else if (id == ID3FID_TRACKNUM) {
1614 formatTrackNumberIfEnabled(value, true);
1615 }
1616 bool hasEnc;
1617 const ID3_TextEnc enc = fld->GetEncoding();
1618 ID3_TextEnc newEnc = static_cast<ID3_TextEnc>(
1619 frame.getFieldValue(Frame::ID_TextEnc).toInt(&hasEnc));
1620 if (!hasEnc) {
1621 newEnc = enc;
1622 }
1623 if (newEnc != ID3TE_ISO8859_1 && newEnc != ID3TE_UTF16) {
1624 // only ISO-8859-1 and UTF16 are allowed for ID3v2.3.0
1625 newEnc = ID3TE_UTF16;
1626 }
1627 if (newEnc == ID3TE_ISO8859_1) {
1628 // check if information is lost if the string is not unicode
1629 int i, unicode_size = value.length();
1630 const QChar* qcarray = value.unicode();
1631 for (i = 0; i < unicode_size; i++) {
1632 char ch = qcarray[i].toLatin1();
1633 if (ch == 0 || (ch & 0x80) != 0) {
1634 newEnc = ID3TE_UTF16;
1635 break;
1636 }
1637 }
1638 }
1639 if (enc != newEnc && id != ID3FID_SYNCEDLYRICS) {
1640 ID3_Field* encfld = id3Frame->GetField(ID3FN_TEXTENC);
1641 if (encfld) {
1642 encfld->Set(newEnc);
1643 }
1644 fld->SetEncoding(newEnc);
1645 markTagChanged(Frame::Tag_2, frame.getType());
1646 }
1647 if (getString(fld) != value) {
1648 setString(fld, value);
1649 markTagChanged(Frame::Tag_2, frame.getType());
1650 }
1651 return true;
1652 } else if (id3Frame->GetID() == ID3FID_PRIVATE &&
1653 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) {
1654 ID3_Field* ownerFld = id3Frame->GetField(ID3FN_OWNER);
1655 QString owner;
1656 QByteArray newData, oldData;
1657 if (ownerFld && !(owner = getString(ownerFld)).isEmpty() &&
1658 AttributeData(owner).toByteArray(value, newData)) {
1659 oldData =
1660 QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()),
1661 static_cast<int>(fld->Size()));
1662 if (newData != oldData) {
1663 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()),
1664 newData.size());
1665 markTagChanged(Frame::Tag_2, frame.getType());
1666 }
1667 return true;
1668 }
1669 } else if (id3Frame->GetID() == ID3FID_CDID &&
1670 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) {
1671 QByteArray newData, oldData;
1672 if (AttributeData::isHexString(value, 'F', QLatin1String("+")) &&
1673 AttributeData(AttributeData::Utf16).toByteArray(value, newData)) {
1674 oldData = QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()),
1675 static_cast<int>(fld->Size()));
1676 if (newData != oldData) {
1677 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()),
1678 static_cast<size_t>(newData.size()));
1679 markTagChanged(Frame::Tag_2, frame.getType());
1680 }
1681 return true;
1682 }
1683 } else if (id3Frame->GetID() == ID3FID_UNIQUEFILEID &&
1684 (fld = id3Frame->GetField(ID3FN_DATA)) != nullptr) {
1685 QByteArray newData, oldData;
1686 if (AttributeData::isHexString(value, 'Z', QLatin1String("-"))) {
1687 newData = (value + QLatin1Char('\0')).toLatin1();
1688 oldData =
1689 QByteArray(reinterpret_cast<const char*>(fld->GetRawBinary()),
1690 static_cast<int>(fld->Size()));
1691 if (newData != oldData) {
1692 fld->Set(reinterpret_cast<const unsigned char*>(newData.data()),
1693 static_cast<size_t>(newData.size()));
1694 markTagChanged(Frame::Tag_2, frame.getType());
1695 }
1696 return true;
1697 }
1698 } else if (id3Frame->GetID() == ID3FID_POPULARIMETER &&
1699 (fld = id3Frame->GetField(ID3FN_RATING)) != nullptr) {
1700 if (getString(fld) != value) {
1701 fld->Set(value.toInt());
1702 markTagChanged(Frame::Tag_2, frame.getType());
1703 }
1704 return true;
1705 }
1706 } else {
1707 setId3v2Frame(id3Frame, frame);
1708 markTagChanged(Frame::Tag_2, frame.getType());
1709 return true;
1710 }
1711 }
1712 }
1713 }
1714
1715 // Try the basic method
1716 Frame::Type type = frame.getType();
1717 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame)
1718 return false;
1719
1720 ID3_FrameID frameId = getId3libFrameIdForType(type);
1721 if (frameId == ID3FID_NOFRAME)
1722 return false;
1723
1724 ID3_Tag* tag;
1725 #if QT_VERSION >= 0x060000
1726 QStringDecoder* decoder;
1727 QStringEncoder* encoder;
1728 #else
1729 const QTextCodec* codec;
1730 #endif
1731 bool allowUnicode;
1732 if (tagNr == Frame::Tag_1) {
1733 tag = m_tagV1.data();
1734 #if QT_VERSION >= 0x060000
1735 decoder = &s_decoderV1;
1736 encoder = &s_encoderV1;
1737 #else
1738 codec = s_textCodecV1;
1739 #endif
1740 allowUnicode = false;
1741 } else if (tagNr == Frame::Tag_2) {
1742 tag = m_tagV2.data();
1743 #if QT_VERSION >= 0x060000
1744 decoder = nullptr;
1745 encoder = nullptr;
1746 #else
1747 codec = nullptr;
1748 #endif
1749 allowUnicode = true;
1750 } else {
1751 return false;
1752 }
1753
1754 switch (type) {
1755 case Frame::FT_Album:
1756 case Frame::FT_Artist:
1757 case Frame::FT_Comment:
1758 case Frame::FT_Title:
1759 {
1760 QString str = frame.getValue();
1761 #if QT_VERSION >= 0x060000
1762 if (getTextField(tag, frameId, decoder) != str &&
1763 setTextField(tag, frameId, str, allowUnicode, true, true, encoder)) {
1764 #else
1765 if (getTextField(tag, frameId, codec) != str &&
1766 setTextField(tag, frameId, str, allowUnicode, true, true, codec)) {
1767 #endif
1768 markTagChanged(tagNr, type);
1769 QString s = checkTruncation(tagNr, str, 1ULL << type,
1770 type == Frame::FT_Comment ? 28 : 30);
1771 if (!s.isNull())
1772 #if QT_VERSION >= 0x060000
1773 setTextField(tag, frameId, s, allowUnicode, true, true, encoder);
1774 #else
1775 setTextField(tag, frameId, s, allowUnicode, true, true, codec);
1776 #endif
1777 }
1778 break;
1779 }
1780 case Frame::FT_Date:
1781 {
1782 int num = frame.getValueAsNumber();
1783 if (setYear(tag, num)) {
1784 markTagChanged(tagNr, type);
1785 }
1786 break;
1787 }
1788 case Frame::FT_Genre:
1789 {
1790 QString str = frame.getValue();
1791 if (tagNr == Frame::Tag_1) {
1792 if (!str.isNull()) {
1793 int num = Genres::getNumber(str);
1794 if (setGenreNum(tag, num)) {
1795 markTagChanged(tagNr, 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, 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, type);
1823 }
1824 }
1825 }
1826 }
1827 break;
1828 }
1829 case Frame::FT_Track:
1830 {
1831 if (tagNr == Frame::Tag_1) {
1832 int num = frame.getValueAsNumber();
1833 if (setTrackNum(tag, num)) {
1834 markTagChanged(tagNr, type);
1835 int n = checkTruncation(tagNr, num, 1ULL << type);
1836 if (n != -1) setTrackNum(tag, n);
1837 }
1838 } else {
1839 QString str = frame.getValue();
1840 int numTracks;
1841 int num = splitNumberAndTotal(str, &numTracks);
1842 if (setTrackNum(tag, num, numTracks)) {
1843 markTagChanged(tagNr, type);
1844 }
1845 }
1846 break;
1847 }
1848 default:
1849 return false;
1850 }
1851 return true;
1852 }
1853
1854 /**
1855 * Create an id3lib frame from a frame.
1856 * @param frame frame
1857 * @return id3lib frame, 0 if invalid.
1858 */
1859 ID3_Frame* Mp3File::createId3FrameFromFrame(Frame& frame) const
1860 {
1861 ID3_Frame* id3Frame = nullptr;
1862 ID3_FrameID id;
1863 if (frame.getType() != Frame::FT_Other) {
1864 id = getId3libFrameIdForType(frame.getType());
1865 } else {
1866 id = getId3libFrameIdForName(frame.getName());
1867 if (id == ID3FID_NOFRAME) {
1868 if (frame.getName() == QLatin1String("AverageLevel") ||
1869 frame.getName() == QLatin1String("PeakValue") ||
1870 frame.getName().startsWith(QLatin1String("WM/"))) {
1871 id = ID3FID_PRIVATE;
1872 } else if (frame.getName().startsWith(QLatin1String("iTun"))) {
1873 id = ID3FID_COMMENT;
1874 } else {
1875 id = ID3FID_USERTEXT;
1876 }
1877 }
1878 }
1879 if (id != ID3FID_NOFRAME && id != ID3FID_SETSUBTITLE) {
1880 id3Frame = new ID3_Frame(id);
1881 ID3_Field* fld = id3Frame->GetField(ID3FN_TEXT);
1882 if (fld) {
1883 ID3_TextEnc enc = getDefaultTextEncoding();
1884 ID3_Field* encfld = id3Frame->GetField(ID3FN_TEXTENC);
1885 if (encfld) {
1886 encfld->Set(enc);
1887 }
1888 fld->SetEncoding(enc);
1889 }
1890 if (id == ID3FID_USERTEXT &&
1891 !frame.getName().startsWith(QLatin1String("TXXX"))) {
1892 fld = id3Frame->GetField(ID3FN_DESCRIPTION);
1893 if (fld) {
1894 QString description;
1895 if (frame.getType() == Frame::FT_CatalogNumber) {
1896 description = QLatin1String("CATALOGNUMBER");
1897 } else if (frame.getType() == Frame::FT_ReleaseCountry) {
1898 description = QLatin1String("RELEASECOUNTRY");
1899 } else if (frame.getType() == Frame::FT_Grouping) {
1900 description = QLatin1String("GROUPING");
1901 } else if (frame.getType() == Frame::FT_Subtitle) {
1902 description = QLatin1String("SUBTITLE");
1903 } else {
1904 description = frame.getName();
1905 }
1906 setString(fld, description);
1907 }
1908 } else if (id == ID3FID_COMMENT) {
1909 fld = id3Frame->GetField(ID3FN_LANGUAGE);
1910 if (fld) {
1911 setString(fld, QLatin1String("eng"));
1912 }
1913 if (frame.getType() == Frame::FT_Other) {
1914 fld = id3Frame->GetField(ID3FN_DESCRIPTION);
1915 if (fld) {
1916 setString(fld, frame.getName());
1917 }
1918 }
1919 } else if (id == ID3FID_PRIVATE &&
1920 !frame.getName().startsWith(QLatin1String("PRIV"))) {
1921 fld = id3Frame->GetField(ID3FN_OWNER);
1922 if (fld) {
1923 setString(fld, frame.getName());
1924 QByteArray data;
1925 if (AttributeData(frame.getName()).toByteArray(frame.getValue(), data)) {
1926 fld = id3Frame->GetField(ID3FN_DATA);
1927 if (fld) {
1928 fld->Set(reinterpret_cast<const unsigned char*>(data.data()),
1929 static_cast<size_t>(data.size()));
1930 }
1931 }
1932 }
1933 } else if (id == ID3FID_UNIQUEFILEID) {
1934 fld = id3Frame->GetField(ID3FN_OWNER);
1935 if (fld) {
1936 setString(fld, QLatin1String("http://www.id3.org/dummy/ufid.html"));
1937 }
1938 QByteArray data;
1939 if (AttributeData::isHexString(frame.getValue(), 'Z', QLatin1String("-"))) {
1940 data = (frame.getValue() + QLatin1Char('\0')).toLatin1();
1941 fld = id3Frame->GetField(ID3FN_DATA);
1942 if (fld) {
1943 fld->Set(reinterpret_cast<const unsigned char*>(data.data()),
1944 static_cast<size_t>(data.size()));
1945 }
1946 }
1947 } else if (id == ID3FID_PICTURE) {
1948 fld = id3Frame->GetField(ID3FN_MIMETYPE);
1949 if (fld) {
1950 setString(fld, QLatin1String("image/jpeg"));
1951 }
1952 fld = id3Frame->GetField(ID3FN_PICTURETYPE);
1953 if (fld) {
1954 fld->Set(ID3PT_COVERFRONT);
1955 }
1956 } else if (id == ID3FID_SYNCEDLYRICS) {
1957 fld = id3Frame->GetField(ID3FN_LANGUAGE);
1958 if (fld) {
1959 setString(fld, QLatin1String("eng"));
1960 }
1961 fld = id3Frame->GetField(ID3FN_TIMESTAMPFORMAT);
1962 if (fld) {
1963 fld->Set(ID3TSF_MS);
1964 }
1965 fld = id3Frame->GetField(ID3FN_CONTENTTYPE);
1966 if (fld) {
1967 fld->Set(ID3CT_LYRICS);
1968 }
1969 } else if (id == ID3FID_UNSYNCEDLYRICS || id == ID3FID_TERMSOFUSE) {
1970 fld = id3Frame->GetField(ID3FN_LANGUAGE);
1971 if (fld) {
1972 setString(fld, QLatin1String("eng"));
1973 }
1974 } else if (id == ID3FID_POPULARIMETER) {
1975 fld = id3Frame->GetField(ID3FN_EMAIL);
1976 if (fld) {
1977 setString(fld, TagConfig::instance().defaultPopmEmail());
1978 }
1979 }
1980 if (!frame.fieldList().empty()) {
1981 setId3v2Frame(id3Frame, frame);
1982 }
1983 Frame::Type type;
1984 const char* name;
1985 getTypeStringForId3libFrameId(id, type, name);
1986 frame.setExtendedType(Frame::ExtendedType(type, QString::fromLatin1(name)));
1987 }
1988 return id3Frame;
1989 }
1990
1991 /**
1992 * Add a frame in the tags.
1993 *
1994 * @param tagNr tag number
1995 * @param frame frame to add, a field list may be added by this method
1996 *
1997 * @return true if ok.
1998 */
1999 bool Mp3File::addFrame(Frame::TagNumber tagNr, Frame& frame)
2000 {
2001 if (tagNr == Frame::Tag_2) {
2002 // Add a new frame.
2003 ID3_Frame* id3Frame;
2004 if (m_tagV2 && (id3Frame = createId3FrameFromFrame(frame)) != nullptr) {
2005 m_tagV2->AttachFrame(id3Frame);
2006 frame.setIndex(m_tagV2->NumFrames() - 1);
2007 if (frame.fieldList().empty()) {
2008 // add field list to frame
2009 getFieldsFromId3Frame(id3Frame, frame.fieldList());
2010 frame.setFieldListFromValue();
2011 }
2012 markTagChanged(Frame::Tag_2, frame.getType());
2013 return true;
2014 }
2015 }
2016
2017 // Try the superclass method
2018 return TaggedFile::addFrame(tagNr, frame);
2019 }
2020
2021 /**
2022 * Delete a frame from the tags.
2023 *
2024 * @param tagNr tag number
2025 * @param frame frame to delete.
2026 *
2027 * @return true if ok.
2028 */
2029 bool Mp3File::deleteFrame(Frame::TagNumber tagNr, const Frame& frame)
2030 {
2031 if (tagNr == Frame::Tag_2) {
2032 // If the frame has an index, delete that specific frame
2033 int index = frame.getIndex();
2034 if (index != -1 && m_tagV2) {
2035 ID3_Frame* id3Frame = getId3v2Frame(m_tagV2.data(), index);
2036 if (id3Frame) {
2037 m_tagV2->RemoveFrame(id3Frame);
2038 markTagChanged(Frame::Tag_2, frame.getType());
2039 return true;
2040 }
2041 }
2042 }
2043
2044 // Try the superclass method
2045 return TaggedFile::deleteFrame(tagNr, frame);
2046 }
2047
2048 namespace {
2049
2050 /**
2051 * Create a frame from an id3lib frame.
2052 * @param id3Frame id3lib frame
2053 * @param index, -1 if not used
2054 * @return frame.
2055 */
2056 Frame createFrameFromId3libFrame(ID3_Frame* id3Frame, int index)
2057 {
2058 Frame::Type type;
2059 const char* name;
2060 getTypeStringForId3libFrameId(id3Frame->GetID(), type, name);
2061 Frame frame(type, QLatin1String(""), QString::fromLatin1(name), index);
2062 frame.setValue(getFieldsFromId3Frame(id3Frame, frame.fieldList()));
2063 if (id3Frame->GetID() == ID3FID_USERTEXT ||
2064 id3Frame->GetID() == ID3FID_WWWUSER ||
2065 id3Frame->GetID() == ID3FID_COMMENT) {
2066 QVariant fieldValue = frame.getFieldValue(Frame::ID_Description);
2067 if (fieldValue.isValid()) {
2068 QString description = fieldValue.toString();
2069 if (!description.isEmpty()) {
2070 if (description == QLatin1String("CATALOGNUMBER")) {
2071 frame.setType(Frame::FT_CatalogNumber);
2072 } else if (description == QLatin1String("RELEASECOUNTRY")) {
2073 frame.setType(Frame::FT_ReleaseCountry);
2074 } else if (description == QLatin1String("GROUPING")) {
2075 frame.setType(Frame::FT_Grouping);
2076 } else if (description == QLatin1String("SUBTITLE")) {
2077 frame.setType(Frame::FT_Subtitle);
2078 } else {
2079 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Other,
2080 frame.getInternalName() + QLatin1Char('\n') + description));
2081 }
2082 }
2083 }
2084 } else if (id3Frame->GetID() == ID3FID_PRIVATE) {
2085 const Frame::FieldList& fields = frame.getFieldList();
2086 QString owner;
2087 QByteArray data;
2088 for (auto it = fields.constBegin(); it != fields.constEnd(); ++it) {
2089 if ((*it).m_id == Frame::ID_Owner) {
2090 owner = (*it).m_value.toString();
2091 if (!owner.isEmpty()) {
2092 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Other,
2093 frame.getInternalName() + QLatin1Char('\n') + owner));
2094 }
2095 } else if ((*it).m_id == Frame::ID_Data) {
2096 data = (*it).m_value.toByteArray();
2097 }
2098 }
2099 if (!owner.isEmpty() && !data.isEmpty()) {
2100 QString str;
2101 if (AttributeData(owner).toString(data, str)) {
2102 frame.setValue(str);
2103 }
2104 }
2105 } else if (id3Frame->GetID() == ID3FID_CDID) {
2106 QVariant fieldValue = frame.getFieldValue(Frame::ID_Data);
2107 if (fieldValue.isValid()) {
2108 QString str;
2109 if (AttributeData(AttributeData::Utf16)
2110 .toString(fieldValue.toByteArray(), str) &&
2111 AttributeData::isHexString(str, 'F', QLatin1String("+"))) {
2112 frame.setValue(str);
2113 }
2114 }
2115 } else if (id3Frame->GetID() == ID3FID_UNIQUEFILEID) {
2116 QVariant fieldValue = frame.getFieldValue(Frame::ID_Data);
2117 if (fieldValue.isValid()) {
2118 QByteArray ba(fieldValue.toByteArray());
2119 QString str(QString::fromLatin1(ba));
2120 if (ba.size() - str.length() <= 1 &&
2121 AttributeData::isHexString(str, 'Z', QLatin1String("-"))) {
2122 frame.setValue(str);
2123 }
2124 }
2125 } else if (id3Frame->GetID() == ID3FID_POPULARIMETER) {
2126 QVariant fieldValue = frame.getFieldValue(Frame::ID_Rating);
2127 if (fieldValue.isValid()) {
2128 QString str(fieldValue.toString());
2129 if (!str.isEmpty()) {
2130 frame.setValue(str);
2131 }
2132 }
2133 }
2134 return frame;
2135 }
2136
2137 }
2138
2139 /**
2140 * Remove frames.
2141 *
2142 * @param tagNr tag number
2143 * @param flt filter specifying which frames to remove
2144 */
2145 void Mp3File::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt)
2146 {
2147 if (tagNr == Frame::Tag_1) {
2148 if (m_tagV1) {
2149 if (flt.areAllEnabled()) {
2150 ID3_Tag::Iterator* iter = m_tagV1->CreateIterator();
2151 ID3_Frame* frame;
2152 while ((frame = iter->GetNext()) != nullptr) {
2153 m_tagV1->RemoveFrame(frame);
2154 }
2155 #ifdef Q_OS_WIN32
2156 /* allocated in Windows DLL => must be freed in the same DLL */
2157 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
2158 #else
2159 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2160 #pragma GCC diagnostic push
2161 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
2162 /* Is safe because iterator implementation has default destructor */
2163 #endif
2164 delete iter;
2165 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2166 #pragma GCC diagnostic pop
2167 #endif
2168 #endif
2169 markTagChanged(Frame::Tag_1, Frame::FT_UnknownFrame);
2170 clearTrunctionFlags(Frame::Tag_1);
2171 } else {
2172 TaggedFile::deleteFrames(Frame::Tag_1, flt);
2173 }
2174 }
2175 } else if (tagNr == Frame::Tag_2) {
2176 if (m_tagV2) {
2177 if (flt.areAllEnabled()) {
2178 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator();
2179 ID3_Frame* frame;
2180 while ((frame = iter->GetNext()) != nullptr) {
2181 m_tagV2->RemoveFrame(frame);
2182 }
2183 #ifdef Q_OS_WIN32
2184 /* allocated in Windows DLL => must be freed in the same DLL */
2185 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
2186 #else
2187 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2188 #pragma GCC diagnostic push
2189 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
2190 /* Is safe because iterator implementation has default destructor */
2191 #endif
2192 delete iter;
2193 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2194 #pragma GCC diagnostic pop
2195 #endif
2196 #endif
2197 markTagChanged(Frame::Tag_2, Frame::FT_UnknownFrame);
2198 } else {
2199 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator();
2200 ID3_Frame* id3Frame;
2201 while ((id3Frame = iter->GetNext()) != nullptr) {
2202 Frame frame(createFrameFromId3libFrame(id3Frame, -1));
2203 if (flt.isEnabled(frame.getType(), frame.getName())) {
2204 m_tagV2->RemoveFrame(id3Frame);
2205 }
2206 }
2207 #ifdef Q_OS_WIN32
2208 /* allocated in Windows DLL => must be freed in the same DLL */
2209 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
2210 #else
2211 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2212 #pragma GCC diagnostic push
2213 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
2214 /* Is safe because iterator implementation has default destructor */
2215 #endif
2216 delete iter;
2217 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2218 #pragma GCC diagnostic pop
2219 #endif
2220 #endif
2221 markTagChanged(Frame::Tag_2, Frame::FT_UnknownFrame);
2222 }
2223 }
2224 }
2225 }
2226
2227 /**
2228 * Get all frames in tag.
2229 *
2230 * @param tagNr tag number
2231 * @param frames frame collection to set.
2232 */
2233 void Mp3File::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames)
2234 {
2235 if (tagNr == Frame::Tag_2) {
2236 frames.clear();
2237 if (m_tagV2) {
2238 ID3_Tag::Iterator* iter = m_tagV2->CreateIterator();
2239 ID3_Frame* id3Frame;
2240 int i = 0;
2241 while ((id3Frame = iter->GetNext()) != nullptr) {
2242 Frame frame(createFrameFromId3libFrame(id3Frame, i++));
2243 frames.insert(frame);
2244 }
2245 #ifdef Q_OS_WIN32
2246 /* allocated in Windows DLL => must be freed in the same DLL */
2247 ID3TagIterator_Delete(reinterpret_cast<ID3TagIterator*>(iter));
2248 #else
2249 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2250 #pragma GCC diagnostic push
2251 #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
2252 /* Is safe because iterator implementation has default destructor */
2253 #endif
2254 delete iter;
2255 #ifdef GCC_HAS_DIAGNOSTIC_PRAGMA
2256 #pragma GCC diagnostic pop
2257 #endif
2258 #endif
2259 }
2260 updateMarkedState(tagNr, frames);
2261 frames.addMissingStandardFrames();
2262 return;
2263 }
2264
2265 TaggedFile::getAllFrames(tagNr, frames);
2266 }
2267
2268 /**
2269 * Add a suitable field list for the frame if missing.
2270 * If a frame is created, its field list is empty. This method will create
2271 * a field list appropriate for the frame type and tagged file type if no
2272 * field list exists.
2273 * @param tagNr tag number
2274 * @param frame frame where field list is added
2275 */
2276 void Mp3File::addFieldList(Frame::TagNumber tagNr, Frame& frame) const
2277 {
2278 if (tagNr == Frame::Tag_2 && frame.fieldList().isEmpty()) {
2279 ID3_Frame* id3Frame = createId3FrameFromFrame(frame);
2280 getFieldsFromId3Frame(id3Frame, frame.fieldList());
2281 frame.setFieldListFromValue();
2282 delete id3Frame;
2283 }
2284 }
2285
2286 /**
2287 * Get a list of frame IDs which can be added.
2288 * @param tagNr tag number
2289 * @return list with frame IDs.
2290 */
2291 QStringList Mp3File::getFrameIds(Frame::TagNumber tagNr) const
2292 {
2293 if (tagNr != Frame::Tag_2)
2294 return QStringList();
2295
2296 QStringList lst;
2297 for (int type = Frame::FT_FirstFrame; type <= Frame::FT_LastFrame; ++type) {
2298 lst.append(Frame::ExtendedType(static_cast<Frame::Type>(type),
2299 QLatin1String("")).getName());
2300 }
2301 for (int i = 0; i <= ID3FID_WWWUSER; ++i) {
2302 if (typeStrOfId[i].type == Frame::FT_Other) {
2303 const char* s = typeStrOfId[i].str;
2304 if (s) {
2305 lst.append(QString::fromLatin1(s));
2306 }
2307 }
2308 }
2309 return lst;
2310 }
2311
2312 /**
2313 * Set the encoding to be used for tag 1.
2314 *
2315 * @param name of encoding, default is ISO 8859-1
2316 */
2317 void Mp3File::setTextEncodingV1(const QString& name)
2318 {
2319 #if QT_VERSION >= 0x060000
2320 auto encoding = QStringConverter::encodingForName(name.toLatin1());
2321 s_decoderV1 = QStringDecoder(encoding ? encoding.value()
2322 : QStringConverter::Latin1);
2323 s_encoderV1 = QStringEncoder(encoding ? encoding.value()
2324 : QStringConverter::Latin1);
2325 #else
2326 s_textCodecV1 = name != QLatin1String("ISO-8859-1")
2327 ? QTextCodec::codecForName(name.toLatin1().data()) : nullptr;
2328 #endif
2329 }
2330
2331 /**
2332 * Set the default text encoding.
2333 *
2334 * @param textEnc default text encoding
2335 */
2336 void Mp3File::setDefaultTextEncoding(TagConfig::TextEncoding textEnc)
2337 {
2338 // UTF8 encoding is buggy, so UTF16 is used when UTF8 is configured
2339 s_defaultTextEncoding = textEnc == TagConfig::TE_ISO8859_1 ?
2340 ID3TE_ISO8859_1 : ID3TE_UTF16;
2341 }
2342
2343 /**
2344 * Notify about configuration change.
2345 * This method shall be called when the configuration changes.
2346 */
2347 void Mp3File::notifyConfigurationChange()
2348 {
2349 setDefaultTextEncoding(
2350 static_cast<TagConfig::TextEncoding>(TagConfig::instance().textEncoding()));
2351 setTextEncodingV1(TagConfig::instance().textEncodingV1());
2352 }
2353