1 /* This file is part of Clementine.
2 Copyright 2013, David Sansome <me@davidsansome.com>
3
4 Clementine is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 Clementine is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "tagreader.h"
19
20 #include <memory>
21
22 #include <QCoreApplication>
23 #include <QDateTime>
24 #include <QFileInfo>
25 #include <QTextCodec>
26 #include <QUrl>
27 #include <QVector>
28
29 #include <aifffile.h>
30 #include <apefile.h>
31 #include <asffile.h>
32 #include <attachedpictureframe.h>
33 #include <commentsframe.h>
34 #include <fileref.h>
35 #include <flacfile.h>
36 #include <id3v2tag.h>
37 #include <mp4file.h>
38 #include <mp4tag.h>
39 #include <mpcfile.h>
40 #include <mpegfile.h>
41 #include <oggfile.h>
42 #ifdef TAGLIB_HAS_OPUS
43 #include <opusfile.h>
44 #endif
45 #include <apetag.h>
46 #include <oggflacfile.h>
47 #include <popularimeterframe.h>
48 #include <speexfile.h>
49 #include <tag.h>
50 #include <textidentificationframe.h>
51 #include <trueaudiofile.h>
52 #include <tstring.h>
53 #include <unsynchronizedlyricsframe.h>
54 #include <vorbisfile.h>
55 #include <wavfile.h>
56 #include <wavpackfile.h>
57
58 #include <sys/stat.h>
59
60 #include "core/logging.h"
61 #include "core/messagehandler.h"
62 #include "core/timeconstants.h"
63 #include "fmpsparser.h"
64 #include "gmereader.h"
65
66 // Taglib added support for FLAC pictures in 1.7.0
67 #if (TAGLIB_MAJOR_VERSION > 1) || \
68 (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 7)
69 #define TAGLIB_HAS_FLAC_PICTURELIST
70 #endif
71
72 #ifdef HAVE_GOOGLE_DRIVE
73 #include "cloudstream.h"
74 #endif
75
76 #define NumberToASFAttribute(x) \
77 TagLib::ASF::Attribute(QStringToTaglibString(QString::number(x)))
78
79 class TagLibFileRefFactory : public FileRefFactory {
80 public:
GetFileRef(const QString & filename)81 virtual TagLib::FileRef* GetFileRef(const QString& filename) {
82 #ifdef Q_OS_WIN32
83 return new TagLib::FileRef(filename.toStdWString().c_str());
84 #else
85 return new TagLib::FileRef(QFile::encodeName(filename).constData());
86 #endif
87 }
88 };
89
90 namespace {
91
StdStringToTaglibString(const std::string & s)92 TagLib::String StdStringToTaglibString(const std::string& s) {
93 return TagLib::String(s.c_str(), TagLib::String::UTF8);
94 }
95
QStringToTaglibString(const QString & s)96 TagLib::String QStringToTaglibString(const QString& s) {
97 return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
98 }
99 } // namespace
100
101 const char* TagReader::kMP4_FMPS_Rating_ID =
102 "----:com.apple.iTunes:FMPS_Rating";
103 const char* TagReader::kMP4_FMPS_Playcount_ID =
104 "----:com.apple.iTunes:FMPS_Playcount";
105 const char* TagReader::kMP4_FMPS_Score_ID =
106 "----:com.apple.iTunes:FMPS_Rating_Amarok_Score";
107
108 namespace {
109 // Tags containing the year the album was originally released (in contrast to
110 // other tags that contain the release year of the current edition)
111 const char* kMP4_OriginalYear_ID = "----:com.apple.iTunes:ORIGINAL YEAR";
112 const char* kASF_OriginalDate_ID = "WM/OriginalReleaseTime";
113 const char* kASF_OriginalYear_ID = "WM/OriginalReleaseYear";
114 } // namespace
115
TagReader()116 TagReader::TagReader()
117 : factory_(new TagLibFileRefFactory),
118 kEmbeddedCover("(embedded)") {}
119
ReadFile(const QString & filename,pb::tagreader::SongMetadata * song) const120 void TagReader::ReadFile(const QString& filename,
121 pb::tagreader::SongMetadata* song) const {
122 const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
123 const QFileInfo info(filename);
124
125 song->set_basefilename(DataCommaSizeFromQString(info.fileName()));
126 song->set_url(url.constData(), url.size());
127 song->set_filesize(info.size());
128
129 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
130 qint64 mtime = info.lastModified().toSecsSinceEpoch();
131 qint64 btime = mtime;
132 if (info.birthTime().isValid()) {
133 btime = info.birthTime().toSecsSinceEpoch();
134 }
135 #elif QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
136 qint64 mtime = info.lastModified().toSecsSinceEpoch();
137 qint64 btime = info.created().toSecsSinceEpoch();
138 #else
139 // Legacy 32bit API.
140 uint mtime = info.lastModified().toTime_t();
141 uint btime = info.created().toTime_t();
142 #endif
143
144 song->set_mtime(mtime);
145 // NOTE: birthtime isn't supported by all filesystems or NFS implementations.
146 // -1 is often returned if not supported. Note further that for the
147 // toTime_t() call this returns an unsigned int, i.e. UINT_MAX.
148 if (btime == -1) {
149 btime = mtime;
150 }
151 song->set_ctime(btime);
152
153 qLog(Debug) << "Reading tags from" << filename << ". Got tags:"
154 << "size=" << info.size() << "; mtime=" << mtime
155 << "; birthtime=" << btime;
156
157 std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
158 if (fileref->isNull()) {
159 qLog(Info) << "TagLib hasn't been able to read " << filename << " file";
160
161 // Try fallback -- GME filetypes
162 GME::ReadFile(info, song);
163 return;
164 }
165
166 TagLib::Tag* tag = fileref->tag();
167 if (tag) {
168 Decode(tag->title(), nullptr, song->mutable_title());
169 Decode(tag->artist(), nullptr, song->mutable_artist()); // TPE1
170 Decode(tag->album(), nullptr, song->mutable_album());
171 Decode(tag->genre(), nullptr, song->mutable_genre());
172 song->set_year(tag->year());
173 song->set_track(tag->track());
174 song->set_valid(true);
175 }
176
177 QString disc;
178 QString compilation;
179 QString lyrics;
180
181 auto parseApeTag = [&](TagLib::APE::Tag* tag) {
182 const TagLib::APE::ItemListMap& items = tag->itemListMap();
183
184 // Find album artists
185 TagLib::APE::ItemListMap::ConstIterator it = items.find("ALBUM ARTIST");
186 if (it != items.end()) {
187 TagLib::StringList album_artists = it->second.toStringList();
188 if (!album_artists.isEmpty()) {
189 Decode(album_artists.front(), nullptr, song->mutable_albumartist());
190 }
191 }
192
193 // Find album cover art
194 if (items.find("COVER ART (FRONT)") != items.end()) {
195 song->set_art_automatic(kEmbeddedCover);
196 }
197
198 if (items.contains("COMPILATION")) {
199 compilation = TStringToQString(
200 TagLib::String::number(items["COMPILATION"].toString().toInt()));
201 }
202
203 if (items.contains("DISC")) {
204 disc = TStringToQString(
205 TagLib::String::number(items["DISC"].toString().toInt()));
206 }
207
208 if (items.contains("FMPS_RATING")) {
209 float rating =
210 TStringToQString(items["FMPS_RATING"].toString()).toFloat();
211 if (song->rating() <= 0 && rating > 0) {
212 song->set_rating(rating);
213 }
214 }
215 if (items.contains("FMPS_PLAYCOUNT")) {
216 int playcount =
217 TStringToQString(items["FMPS_PLAYCOUNT"].toString()).toFloat();
218 if (song->playcount() <= 0 && playcount > 0) {
219 song->set_playcount(playcount);
220 }
221 }
222 if (items.contains("FMPS_RATING_AMAROK_SCORE")) {
223 int score = TStringToQString(items["FMPS_RATING_AMAROK_SCORE"].toString())
224 .toFloat() *
225 100;
226 if (song->score() <= 0 && score > 0) {
227 song->set_score(score);
228 }
229 }
230
231 if (items.contains("BPM")) {
232 Decode(items["BPM"].toStringList().toString(", "), nullptr,
233 song->mutable_performer());
234 }
235
236 if (items.contains("PERFORMER")) {
237 Decode(items["PERFORMER"].toStringList().toString(", "), nullptr,
238 song->mutable_performer());
239 }
240
241 if (items.contains("COMPOSER")) {
242 Decode(items["COMPOSER"].toStringList().toString(", "), nullptr,
243 song->mutable_composer());
244 }
245
246 if (items.contains("GROUPING")) {
247 Decode(items["GROUPING"].toStringList().toString(" "), nullptr,
248 song->mutable_grouping());
249 }
250
251 if (items.contains("LYRICS")) {
252 Decode(items["LYRICS"].toString(), nullptr, song->mutable_lyrics());
253 }
254
255 Decode(tag->comment(), nullptr, song->mutable_comment());
256 };
257
258 // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same
259 // way;
260 // apart, so we keep specific behavior for some formats by adding another
261 // "else if" block below.
262 if (TagLib::Ogg::XiphComment* tag =
263 dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
264 ParseOggTag(tag->fieldListMap(), nullptr, &disc, &compilation, song);
265 #if TAGLIB_MAJOR_VERSION >= 1 && TAGLIB_MINOR_VERSION >= 11
266 if (!tag->pictureList().isEmpty()) song->set_art_automatic(kEmbeddedCover);
267 #endif
268 }
269
270 if (TagLib::MPEG::File* file =
271 dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
272 if (file->ID3v2Tag()) {
273 const TagLib::ID3v2::FrameListMap& map = file->ID3v2Tag()->frameListMap();
274
275 if (!map["TPOS"].isEmpty())
276 disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
277
278 if (!map["TBPM"].isEmpty())
279 song->set_bpm(TStringToQString(map["TBPM"].front()->toString())
280 .trimmed()
281 .toFloat());
282
283 if (!map["TCOM"].isEmpty())
284 Decode(map["TCOM"].front()->toString(), nullptr,
285 song->mutable_composer());
286
287 if (!map["TIT1"].isEmpty()) // content group
288 Decode(map["TIT1"].front()->toString(), nullptr,
289 song->mutable_grouping());
290
291 if (!map["TOPE"].isEmpty()) // original artist/performer
292 Decode(map["TOPE"].front()->toString(), nullptr,
293 song->mutable_performer());
294
295 // Skip TPE1 (which is the artist) here because we already fetched it
296
297 if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
298 Decode(map["TPE2"].front()->toString(), nullptr,
299 song->mutable_albumartist());
300
301 if (!map["TCMP"].isEmpty())
302 compilation =
303 TStringToQString(map["TCMP"].front()->toString()).trimmed();
304
305 if (!map["TDOR"].isEmpty()) {
306 song->set_originalyear(
307 map["TDOR"].front()->toString().substr(0, 4).toInt());
308 } else if (!map["TORY"].isEmpty()) {
309 song->set_originalyear(
310 map["TORY"].front()->toString().substr(0, 4).toInt());
311 }
312
313 if (!map["USLT"].isEmpty()) {
314 Decode(map["USLT"].front()->toString(), nullptr,
315 song->mutable_lyrics());
316 } else if (!map["SYLT"].isEmpty()) {
317 Decode(map["SYLT"].front()->toString(), nullptr,
318 song->mutable_lyrics());
319 }
320
321 if (!map["APIC"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
322
323 // Find a suitable comment tag. For now we ignore iTunNORM comments.
324 for (int i = 0; i < map["COMM"].size(); ++i) {
325 const TagLib::ID3v2::CommentsFrame* frame =
326 dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
327
328 if (frame && TStringToQString(frame->description()) != "iTunNORM") {
329 Decode(frame->text(), nullptr, song->mutable_comment());
330 break;
331 }
332 }
333
334 // Parse FMPS frames
335 for (int i = 0; i < map["TXXX"].size(); ++i) {
336 const TagLib::ID3v2::UserTextIdentificationFrame* frame =
337 dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(
338 map["TXXX"][i]);
339
340 if (frame && frame->description().startsWith("FMPS_")) {
341 ParseFMPSFrame(TStringToQString(frame->description()),
342 TStringToQString(frame->fieldList()[1]), song);
343 }
344 }
345
346 // Check POPM tags
347 // We do this after checking FMPS frames, so FMPS have precedence, as we
348 // will consider POPM tags iff song has no rating/playcount already set.
349 if (!map["POPM"].isEmpty()) {
350 const TagLib::ID3v2::PopularimeterFrame* frame =
351 dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(
352 map["POPM"].front());
353 if (frame) {
354 // Take a user rating only if there's no rating already set
355 if (song->rating() <= 0 && frame->rating() > 0) {
356 song->set_rating(ConvertPOPMRating(frame->rating()));
357 }
358 if (song->playcount() <= 0 && frame->counter() > 0) {
359 song->set_playcount(frame->counter());
360 }
361 }
362 }
363 }
364 } else if (TagLib::FLAC::File* file =
365 dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
366 if (file->xiphComment()) {
367 ParseOggTag(file->xiphComment()->fieldListMap(), nullptr, &disc,
368 &compilation, song);
369 #ifdef TAGLIB_HAS_FLAC_PICTURELIST
370 if (!file->pictureList().isEmpty()) {
371 song->set_art_automatic(kEmbeddedCover);
372 }
373 #endif
374 }
375 Decode(tag->comment(), nullptr, song->mutable_comment());
376 } else if (TagLib::MP4::File* file =
377 dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
378 if (file->tag()) {
379 TagLib::MP4::Tag* mp4_tag = file->tag();
380 const TagLib::MP4::ItemListMap& items = mp4_tag->itemListMap();
381
382 // Find album artists
383 TagLib::MP4::ItemListMap::ConstIterator it = items.find("aART");
384 if (it != items.end()) {
385 TagLib::StringList album_artists = it->second.toStringList();
386 if (!album_artists.isEmpty()) {
387 Decode(album_artists.front(), nullptr, song->mutable_albumartist());
388 }
389 }
390
391 // Find album cover art
392 if (items.find("covr") != items.end()) {
393 song->set_art_automatic(kEmbeddedCover);
394 }
395
396 if (items.contains("disk")) {
397 disc = TStringToQString(
398 TagLib::String::number(items["disk"].toIntPair().first));
399 }
400
401 if (items.contains(kMP4_FMPS_Rating_ID)) {
402 float rating =
403 TStringToQString(
404 items[kMP4_FMPS_Rating_ID].toStringList().toString('\n'))
405 .toFloat();
406 if (song->rating() <= 0 && rating > 0) {
407 song->set_rating(rating);
408 }
409 }
410 if (items.contains(kMP4_FMPS_Playcount_ID)) {
411 int playcount =
412 TStringToQString(
413 items[kMP4_FMPS_Playcount_ID].toStringList().toString('\n'))
414 .toFloat();
415 if (song->playcount() <= 0 && playcount > 0) {
416 song->set_playcount(playcount);
417 }
418 }
419 if (items.contains(kMP4_FMPS_Playcount_ID)) {
420 int score = TStringToQString(
421 items[kMP4_FMPS_Score_ID].toStringList().toString('\n'))
422 .toFloat() *
423 100;
424 if (song->score() <= 0 && score > 0) {
425 song->set_score(score);
426 }
427 }
428
429 if (items.contains("\251wrt")) {
430 Decode(items["\251wrt"].toStringList().toString(", "), nullptr,
431 song->mutable_composer());
432 }
433 if (items.contains("\251grp")) {
434 Decode(items["\251grp"].toStringList().toString(" "), nullptr,
435 song->mutable_grouping());
436 }
437
438 if (items.contains(kMP4_OriginalYear_ID)) {
439 song->set_originalyear(
440 TStringToQString(
441 items[kMP4_OriginalYear_ID].toStringList().toString('\n'))
442 .left(4)
443 .toInt());
444 }
445
446 Decode(mp4_tag->comment(), nullptr, song->mutable_comment());
447 }
448 } else if (TagLib::APE::File* file =
449 dynamic_cast<TagLib::APE::File*>(fileref->file())) {
450 if (file->tag()) {
451 parseApeTag(file->APETag());
452 }
453 } else if (TagLib::MPC::File* file =
454 dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
455 if (file->tag()) {
456 parseApeTag(file->APETag());
457 }
458 } else if (TagLib::WavPack::File* file =
459 dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
460 if (file->tag()) {
461 parseApeTag(file->APETag());
462 }
463 }
464 #ifdef TAGLIB_WITH_ASF
465 else if (TagLib::ASF::File* file =
466 dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
467 const TagLib::ASF::AttributeListMap& attributes_map =
468 file->tag()->attributeListMap();
469 if (attributes_map.contains("FMPS/Rating")) {
470 const TagLib::ASF::AttributeList& attributes =
471 attributes_map["FMPS/Rating"];
472 if (!attributes.isEmpty()) {
473 float rating =
474 TStringToQString(attributes.front().toString()).toFloat();
475 if (song->rating() <= 0 && rating > 0) {
476 song->set_rating(rating);
477 }
478 }
479 }
480 if (attributes_map.contains("FMPS/Playcount")) {
481 const TagLib::ASF::AttributeList& attributes =
482 attributes_map["FMPS/Playcount"];
483 if (!attributes.isEmpty()) {
484 int playcount = TStringToQString(attributes.front().toString()).toInt();
485 if (song->playcount() <= 0 && playcount > 0) {
486 song->set_playcount(playcount);
487 }
488 }
489 }
490 if (attributes_map.contains("FMPS/Rating_Amarok_Score")) {
491 const TagLib::ASF::AttributeList& attributes =
492 attributes_map["FMPS/Rating_Amarok_Score"];
493 if (!attributes.isEmpty()) {
494 int score =
495 TStringToQString(attributes.front().toString()).toFloat() * 100;
496 if (song->score() <= 0 && score > 0) {
497 song->set_score(score);
498 }
499 }
500 }
501
502 if (attributes_map.contains(kASF_OriginalDate_ID)) {
503 const TagLib::ASF::AttributeList& attributes =
504 attributes_map[kASF_OriginalDate_ID];
505 if (!attributes.isEmpty()) {
506 song->set_originalyear(
507 TStringToQString(attributes.front().toString()).left(4).toInt());
508 }
509 } else if (attributes_map.contains(kASF_OriginalYear_ID)) {
510 const TagLib::ASF::AttributeList& attributes =
511 attributes_map[kASF_OriginalYear_ID];
512 if (!attributes.isEmpty()) {
513 song->set_originalyear(
514 TStringToQString(attributes.front().toString()).left(4).toInt());
515 }
516 }
517 }
518 #endif
519 else if (tag) {
520 Decode(tag->comment(), nullptr, song->mutable_comment());
521 }
522
523 if (!disc.isEmpty()) {
524 const int i = disc.indexOf('/');
525 if (i != -1) {
526 // disc.right( i ).toInt() is total number of discs, we don't use this at
527 // the moment
528 song->set_disc(disc.left(i).toInt());
529 } else {
530 song->set_disc(disc.toInt());
531 }
532 }
533
534 if (compilation.isEmpty()) {
535 // well, it wasn't set, but if the artist is VA assume it's a compilation
536 if (QStringFromStdString(song->artist()).toLower() == "various artists") {
537 song->set_compilation(true);
538 }
539 } else {
540 song->set_compilation(compilation.toInt() == 1);
541 }
542
543 if (!lyrics.isEmpty()) song->set_lyrics(lyrics.toStdString());
544
545 if (fileref->audioProperties()) {
546 song->set_bitrate(fileref->audioProperties()->bitrate());
547 song->set_samplerate(fileref->audioProperties()->sampleRate());
548 song->set_length_nanosec(fileref->audioProperties()->length() *
549 kNsecPerSec);
550 }
551
552 // Get the filetype if we can
553 song->set_type(GuessFileType(fileref.get()));
554
555 // Set integer fields to -1 if they're not valid
556 #define SetDefault(field) \
557 if (song->field() <= 0) { \
558 song->set_##field(-1); \
559 }
560 SetDefault(track);
561 SetDefault(disc);
562 SetDefault(bpm);
563 SetDefault(year);
564 SetDefault(bitrate);
565 SetDefault(samplerate);
566 SetDefault(lastplayed);
567 #undef SetDefault
568 }
569
Decode(const TagLib::String & tag,const QTextCodec * codec,std::string * output)570 void TagReader::Decode(const TagLib::String& tag, const QTextCodec* codec,
571 std::string* output) {
572 QString tmp;
573
574 if (codec && tag.isLatin1()) { // Never override UTF-8.
575 const std::string fixed =
576 QString::fromUtf8(tag.toCString(true)).toStdString();
577 tmp = codec->toUnicode(fixed.c_str()).trimmed();
578 } else {
579 tmp = TStringToQString(tag).trimmed();
580 }
581
582 output->assign(DataCommaSizeFromQString(tmp));
583 }
584
Decode(const QString & tag,const QTextCodec * codec,std::string * output)585 void TagReader::Decode(const QString& tag, const QTextCodec* codec,
586 std::string* output) {
587 if (!codec) {
588 output->assign(DataCommaSizeFromQString(tag));
589 } else {
590 const QString decoded(codec->toUnicode(tag.toUtf8()));
591 output->assign(DataCommaSizeFromQString(decoded));
592 }
593 }
594
ParseFMPSFrame(const QString & name,const QString & value,pb::tagreader::SongMetadata * song) const595 void TagReader::ParseFMPSFrame(const QString& name, const QString& value,
596 pb::tagreader::SongMetadata* song) const {
597 qLog(Debug) << "Parsing FMPSFrame" << name << ", " << value;
598 FMPSParser parser;
599 if (!parser.Parse(value) || parser.is_empty()) return;
600
601 QVariant var;
602 if (name == "FMPS_Rating") {
603 var = parser.result()[0][0];
604 if (var.type() == QVariant::Double) {
605 song->set_rating(var.toDouble());
606 }
607 } else if (name == "FMPS_Rating_User") {
608 // Take a user rating only if there's no rating already set
609 if (song->rating() == -1 && parser.result()[0].count() >= 2) {
610 var = parser.result()[0][1];
611 if (var.type() == QVariant::Double) {
612 song->set_rating(var.toDouble());
613 }
614 }
615 } else if (name == "FMPS_PlayCount") {
616 var = parser.result()[0][0];
617 if (var.type() == QVariant::Double) {
618 song->set_playcount(var.toDouble());
619 }
620 } else if (name == "FMPS_PlayCount_User") {
621 // Take a user playcount only if there's no playcount already set
622 if (song->playcount() == 0 && parser.result()[0].count() >= 2) {
623 var = parser.result()[0][1];
624 if (var.type() == QVariant::Double) {
625 song->set_playcount(var.toDouble());
626 }
627 }
628 } else if (name == "FMPS_Rating_Amarok_Score") {
629 var = parser.result()[0][0];
630 if (var.type() == QVariant::Double) {
631 song->set_score(var.toFloat() * 100);
632 }
633 }
634 }
635
ParseOggTag(const TagLib::Ogg::FieldListMap & map,const QTextCodec * codec,QString * disc,QString * compilation,pb::tagreader::SongMetadata * song) const636 void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
637 const QTextCodec* codec, QString* disc,
638 QString* compilation,
639 pb::tagreader::SongMetadata* song) const {
640 if (!map["COMPOSER"].isEmpty())
641 Decode(map["COMPOSER"].front(), codec, song->mutable_composer());
642 if (!map["PERFORMER"].isEmpty())
643 Decode(map["PERFORMER"].front(), codec, song->mutable_performer());
644 if (!map["CONTENT GROUP"].isEmpty())
645 Decode(map["CONTENT GROUP"].front(), codec, song->mutable_grouping());
646
647 if (!map["ALBUMARTIST"].isEmpty()) {
648 Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist());
649 } else if (!map["ALBUM ARTIST"].isEmpty()) {
650 Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist());
651 }
652
653 if (!map["ORIGINALDATE"].isEmpty())
654 song->set_originalyear(
655 TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt());
656 else if (!map["ORIGINALYEAR"].isEmpty())
657 song->set_originalyear(
658 TStringToQString(map["ORIGINALYEAR"].front()).toInt());
659
660 if (!map["BPM"].isEmpty())
661 song->set_bpm(TStringToQString(map["BPM"].front()).trimmed().toFloat());
662
663 if (!map["DISCNUMBER"].isEmpty())
664 *disc = TStringToQString(map["DISCNUMBER"].front()).trimmed();
665
666 if (!map["COMPILATION"].isEmpty())
667 *compilation = TStringToQString(map["COMPILATION"].front()).trimmed();
668
669 if (!map["COVERART"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
670
671 if (!map["METADATA_BLOCK_PICTURE"].isEmpty())
672 song->set_art_automatic(kEmbeddedCover);
673
674 if (!map["FMPS_RATING"].isEmpty() && song->rating() <= 0)
675 song->set_rating(
676 TStringToQString(map["FMPS_RATING"].front()).trimmed().toFloat());
677
678 if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0)
679 song->set_playcount(
680 TStringToQString(map["FMPS_PLAYCOUNT"].front()).trimmed().toFloat());
681
682 if (!map["FMPS_RATING_AMAROK_SCORE"].isEmpty() && song->score() <= 0)
683 song->set_score(TStringToQString(map["FMPS_RATING_AMAROK_SCORE"].front())
684 .trimmed()
685 .toFloat() *
686 100);
687
688 if (!map["LYRICS"].isEmpty())
689 Decode(map["LYRICS"].front(), codec, song->mutable_lyrics());
690 else if (!map["UNSYNCEDLYRICS"].isEmpty())
691 Decode(map["UNSYNCEDLYRICS"].front(), codec, song->mutable_lyrics());
692 }
693
SetVorbisComments(TagLib::Ogg::XiphComment * vorbis_comments,const pb::tagreader::SongMetadata & song) const694 void TagReader::SetVorbisComments(
695 TagLib::Ogg::XiphComment* vorbis_comments,
696 const pb::tagreader::SongMetadata& song) const {
697 vorbis_comments->addField("COMPOSER",
698 StdStringToTaglibString(song.composer()), true);
699 vorbis_comments->addField("PERFORMER",
700 StdStringToTaglibString(song.performer()), true);
701 vorbis_comments->addField("CONTENT GROUP",
702 StdStringToTaglibString(song.grouping()), true);
703 vorbis_comments->addField(
704 "BPM",
705 QStringToTaglibString(song.bpm() <= 0 - 1 ? QString()
706 : QString::number(song.bpm())),
707 true);
708 vorbis_comments->addField(
709 "DISCNUMBER",
710 QStringToTaglibString(song.disc() <= 0 ? QString()
711 : QString::number(song.disc())),
712 true);
713 vorbis_comments->addField(
714 "COMPILATION",
715 QStringToTaglibString(song.compilation() ? QString::number(1)
716 : QString()),
717 true);
718
719 // Try to be coherent, the two forms are used but the first one is preferred
720
721 vorbis_comments->addField("ALBUMARTIST",
722 StdStringToTaglibString(song.albumartist()), true);
723 vorbis_comments->removeField("ALBUM ARTIST");
724
725 vorbis_comments->addField("LYRICS", StdStringToTaglibString(song.lyrics()),
726 true);
727 vorbis_comments->removeField("UNSYNCEDLYRICS");
728 }
729
SetFMPSStatisticsVorbisComments(TagLib::Ogg::XiphComment * vorbis_comments,const pb::tagreader::SongMetadata & song) const730 void TagReader::SetFMPSStatisticsVorbisComments(
731 TagLib::Ogg::XiphComment* vorbis_comments,
732 const pb::tagreader::SongMetadata& song) const {
733 if (song.playcount())
734 vorbis_comments->addField("FMPS_PLAYCOUNT",
735 TagLib::String::number(song.playcount()), true);
736 if (song.score())
737 vorbis_comments->addField(
738 "FMPS_RATING_AMAROK_SCORE",
739 QStringToTaglibString(QString::number(song.score() / 100.0)), true);
740 }
741
SetFMPSRatingVorbisComments(TagLib::Ogg::XiphComment * vorbis_comments,const pb::tagreader::SongMetadata & song) const742 void TagReader::SetFMPSRatingVorbisComments(
743 TagLib::Ogg::XiphComment* vorbis_comments,
744 const pb::tagreader::SongMetadata& song) const {
745 vorbis_comments->addField(
746 "FMPS_RATING", QStringToTaglibString(QString::number(song.rating())),
747 true);
748 }
749
GuessFileType(TagLib::FileRef * fileref) const750 pb::tagreader::SongMetadata_Type TagReader::GuessFileType(
751 TagLib::FileRef* fileref) const {
752 #ifdef TAGLIB_WITH_ASF
753 if (dynamic_cast<TagLib::ASF::File*>(fileref->file()))
754 return pb::tagreader::SongMetadata_Type_ASF;
755 #endif
756 if (dynamic_cast<TagLib::FLAC::File*>(fileref->file()))
757 return pb::tagreader::SongMetadata_Type_FLAC;
758 #ifdef TAGLIB_WITH_MP4
759 if (dynamic_cast<TagLib::MP4::File*>(fileref->file()))
760 return pb::tagreader::SongMetadata_Type_MP4;
761 #endif
762 if (dynamic_cast<TagLib::MPC::File*>(fileref->file()))
763 return pb::tagreader::SongMetadata_Type_MPC;
764 if (dynamic_cast<TagLib::MPEG::File*>(fileref->file()))
765 return pb::tagreader::SongMetadata_Type_MPEG;
766 if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file()))
767 return pb::tagreader::SongMetadata_Type_OGGFLAC;
768 if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file()))
769 return pb::tagreader::SongMetadata_Type_OGGSPEEX;
770 if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file()))
771 return pb::tagreader::SongMetadata_Type_OGGVORBIS;
772 #ifdef TAGLIB_HAS_OPUS
773 if (dynamic_cast<TagLib::Ogg::Opus::File*>(fileref->file()))
774 return pb::tagreader::SongMetadata_Type_OGGOPUS;
775 #endif
776 if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file()))
777 return pb::tagreader::SongMetadata_Type_AIFF;
778 if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file()))
779 return pb::tagreader::SongMetadata_Type_WAV;
780 if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file()))
781 return pb::tagreader::SongMetadata_Type_TRUEAUDIO;
782 if (dynamic_cast<TagLib::WavPack::File*>(fileref->file()))
783 return pb::tagreader::SongMetadata_Type_WAVPACK;
784 if (dynamic_cast<TagLib::APE::File*>(fileref->file()))
785 return pb::tagreader::SongMetadata_Type_APE;
786
787 return pb::tagreader::SongMetadata_Type_UNKNOWN;
788 }
789
SaveFile(const QString & filename,const pb::tagreader::SongMetadata & song) const790 bool TagReader::SaveFile(const QString& filename,
791 const pb::tagreader::SongMetadata& song) const {
792 if (filename.isNull()) return false;
793
794 qLog(Debug) << "Saving tags to" << filename;
795
796 std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
797
798 if (!fileref || fileref->isNull()) // The file probably doesn't exist
799 return false;
800
801 fileref->tag()->setTitle(StdStringToTaglibString(song.title()));
802 fileref->tag()->setArtist(StdStringToTaglibString(song.artist())); // TPE1
803 fileref->tag()->setAlbum(StdStringToTaglibString(song.album()));
804 fileref->tag()->setGenre(StdStringToTaglibString(song.genre()));
805 fileref->tag()->setComment(StdStringToTaglibString(song.comment()));
806 fileref->tag()->setYear(song.year() <= 0 - 1 ? 0 : song.year());
807 fileref->tag()->setTrack(song.track() <= 0 - 1 ? 0 : song.track());
808
809 auto saveApeTag = [&](TagLib::APE::Tag* tag) {
810 tag->addValue(
811 "disc",
812 QStringToTaglibString(song.disc() <= 0 ? QString()
813 : QString::number(song.disc())),
814 true);
815 tag->addValue("bpm",
816 QStringToTaglibString(song.bpm() <= 0 - 1
817 ? QString()
818 : QString::number(song.bpm())),
819 true);
820 tag->setItem("composer",
821 TagLib::APE::Item(
822 "composer", TagLib::StringList(song.composer().c_str())));
823 tag->setItem("grouping",
824 TagLib::APE::Item(
825 "grouping", TagLib::StringList(song.grouping().c_str())));
826 tag->setItem("performer",
827 TagLib::APE::Item("performer", TagLib::StringList(
828 song.performer().c_str())));
829 tag->setItem(
830 "album artist",
831 TagLib::APE::Item("album artist",
832 TagLib::StringList(song.albumartist().c_str())));
833 tag->setItem("lyrics",
834 TagLib::APE::Item("lyrics", TagLib::String(song.lyrics())));
835 tag->addValue("compilation",
836 QStringToTaglibString(song.compilation() ? QString::number(1)
837 : QString()),
838 true);
839 };
840
841 if (TagLib::MPEG::File* file =
842 dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
843 TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
844 SetTextFrame("TPOS",
845 song.disc() <= 0 ? QString() : QString::number(song.disc()),
846 tag);
847 SetTextFrame("TBPM",
848 song.bpm() <= 0 - 1 ? QString() : QString::number(song.bpm()),
849 tag);
850 SetTextFrame("TCOM", song.composer(), tag);
851 SetTextFrame("TIT1", song.grouping(), tag);
852 SetTextFrame("TOPE", song.performer(), tag);
853 SetUnsyncLyricsFrame(song.lyrics(), tag);
854 // Skip TPE1 (which is the artist) here because we already set it
855 SetTextFrame("TPE2", song.albumartist(), tag);
856 SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(),
857 tag);
858 } else if (TagLib::FLAC::File* file =
859 dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
860 TagLib::Ogg::XiphComment* tag = file->xiphComment();
861 SetVorbisComments(tag, song);
862 } else if (TagLib::MP4::File* file =
863 dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
864 TagLib::MP4::Tag* tag = file->tag();
865 tag->itemListMap()["disk"] =
866 TagLib::MP4::Item(song.disc() <= 0 - 1 ? 0 : song.disc(), 0);
867 tag->itemListMap()["tmpo"] = TagLib::StringList(
868 song.bpm() <= 0 - 1 ? "0" : TagLib::String::number(song.bpm()));
869 tag->itemListMap()["\251wrt"] = TagLib::StringList(song.composer().c_str());
870 tag->itemListMap()["\251grp"] = TagLib::StringList(song.grouping().c_str());
871 tag->itemListMap()["aART"] = TagLib::StringList(song.albumartist().c_str());
872 tag->itemListMap()["cpil"] =
873 TagLib::StringList(song.compilation() ? "1" : "0");
874 } else if (TagLib::APE::File* file =
875 dynamic_cast<TagLib::APE::File*>(fileref->file())) {
876 saveApeTag(file->APETag(true));
877 } else if (TagLib::MPC::File* file =
878 dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
879 saveApeTag(file->APETag(true));
880 } else if (TagLib::WavPack::File* file =
881 dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
882 saveApeTag(file->APETag(true));
883 }
884
885 // Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same
886 // way;
887 // apart, so we keep specific behavior for some formats by adding another
888 // "else if" block above.
889 if (TagLib::Ogg::XiphComment* tag =
890 dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
891 SetVorbisComments(tag, song);
892 }
893
894 bool ret = fileref->save();
895 #ifdef Q_OS_LINUX
896 if (ret) {
897 // Linux: inotify doesn't seem to notice the change to the file unless we
898 // change the timestamps as well. (this is what touch does)
899 utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
900 }
901 #endif // Q_OS_LINUX
902
903 return ret;
904 }
905
SaveSongStatisticsToFile(const QString & filename,const pb::tagreader::SongMetadata & song) const906 bool TagReader::SaveSongStatisticsToFile(
907 const QString& filename, const pb::tagreader::SongMetadata& song) const {
908 if (filename.isNull()) return false;
909
910 qLog(Debug) << "Saving song statistics tags to" << filename;
911
912 std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
913
914 if (!fileref || fileref->isNull()) // The file probably doesn't exist
915 return false;
916
917 auto saveApeSongStats = [&](TagLib::APE::Tag* tag) {
918 if (song.score())
919 tag->setItem(
920 "FMPS_Rating_Amarok_Score",
921 TagLib::APE::Item(
922 "FMPS_Rating_Amarok_Score",
923 QStringToTaglibString(QString::number(song.score() / 100.0))));
924 if (song.playcount())
925 tag->setItem("FMPS_PlayCount",
926 TagLib::APE::Item("FMPS_PlayCount",
927 TagLib::String::number(song.playcount())));
928 };
929
930 if (TagLib::MPEG::File* file =
931 dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
932 TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
933
934 if (song.playcount()) {
935 // Save as FMPS
936 SetUserTextFrame("FMPS_PlayCount", QString::number(song.playcount()),
937 tag);
938
939 // Also save as POPM
940 TagLib::ID3v2::PopularimeterFrame* frame = GetPOPMFrameFromTag(tag);
941 frame->setCounter(song.playcount());
942 }
943
944 if (song.score())
945 SetUserTextFrame("FMPS_Rating_Amarok_Score",
946 QString::number(song.score() / 100.0), tag);
947
948 } else if (TagLib::FLAC::File* file =
949 dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
950 TagLib::Ogg::XiphComment* vorbis_comments = file->xiphComment(true);
951 SetFMPSStatisticsVorbisComments(vorbis_comments, song);
952 } else if (TagLib::Ogg::XiphComment* tag =
953 dynamic_cast<TagLib::Ogg::XiphComment*>(
954 fileref->file()->tag())) {
955 SetFMPSStatisticsVorbisComments(tag, song);
956 }
957 #ifdef TAGLIB_WITH_ASF
958 else if (TagLib::ASF::File* file =
959 dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
960 TagLib::ASF::Tag* tag = file->tag();
961 if (song.playcount())
962 tag->addAttribute("FMPS/Playcount",
963 NumberToASFAttribute(song.playcount()));
964 if (song.score())
965 tag->addAttribute("FMPS/Rating_Amarok_Score",
966 NumberToASFAttribute(song.score() / 100.0));
967 }
968 #endif
969 else if (TagLib::MP4::File* file =
970 dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
971 TagLib::MP4::Tag* tag = file->tag();
972 if (song.score())
973 tag->itemListMap()[kMP4_FMPS_Score_ID] = TagLib::MP4::Item(
974 QStringToTaglibString(QString::number(song.score() / 100.0)));
975 if (song.playcount())
976 tag->itemListMap()[kMP4_FMPS_Playcount_ID] =
977 TagLib::MP4::Item(TagLib::String::number(song.playcount()));
978 } else if (TagLib::APE::File* file =
979 dynamic_cast<TagLib::APE::File*>(fileref->file())) {
980 saveApeSongStats(file->APETag(true));
981 } else if (TagLib::MPC::File* file =
982 dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
983 saveApeSongStats(file->APETag(true));
984 } else if (TagLib::WavPack::File* file =
985 dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
986 saveApeSongStats(file->APETag(true));
987 } else {
988 // Nothing to save: stop now
989 return true;
990 }
991
992 bool ret = fileref->save();
993 #ifdef Q_OS_LINUX
994 if (ret) {
995 // Linux: inotify doesn't seem to notice the change to the file unless we
996 // change the timestamps as well. (this is what touch does)
997 utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
998 }
999 #endif // Q_OS_LINUX
1000 return ret;
1001 }
1002
SaveSongRatingToFile(const QString & filename,const pb::tagreader::SongMetadata & song) const1003 bool TagReader::SaveSongRatingToFile(
1004 const QString& filename, const pb::tagreader::SongMetadata& song) const {
1005 if (filename.isNull()) return false;
1006
1007 qLog(Debug) << "Saving song rating tags to" << filename;
1008 if (song.rating() < 0) {
1009 // The FMPS spec says unrated == "tag not present". For us, no rating
1010 // results in rating being -1, so don't write anything in that case.
1011 // Actually, we should also remove tag set in this case, but in
1012 // Clementine it is not possible to unset rating i.e. make a song "unrated".
1013 qLog(Debug) << "Unrated: do nothing";
1014 return true;
1015 }
1016
1017 std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
1018
1019 if (!fileref || fileref->isNull()) // The file probably doesn't exist
1020 return false;
1021
1022 auto saveApeSongRating = [&](TagLib::APE::Tag* tag) {
1023 tag->setItem("FMPS_Rating",
1024 TagLib::APE::Item("FMPS_Rating",
1025 TagLib::StringList(QStringToTaglibString(
1026 QString::number(song.rating())))));
1027 };
1028
1029 if (TagLib::MPEG::File* file =
1030 dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
1031 TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
1032
1033 // Save as FMPS
1034 SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag);
1035
1036 // Also save as POPM
1037 TagLib::ID3v2::PopularimeterFrame* frame = GetPOPMFrameFromTag(tag);
1038 frame->setRating(ConvertToPOPMRating(song.rating()));
1039
1040 } else if (TagLib::FLAC::File* file =
1041 dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
1042 TagLib::Ogg::XiphComment* vorbis_comments = file->xiphComment(true);
1043 SetFMPSRatingVorbisComments(vorbis_comments, song);
1044 } else if (TagLib::Ogg::XiphComment* tag =
1045 dynamic_cast<TagLib::Ogg::XiphComment*>(
1046 fileref->file()->tag())) {
1047 SetFMPSRatingVorbisComments(tag, song);
1048 }
1049 #ifdef TAGLIB_WITH_ASF
1050 else if (TagLib::ASF::File* file =
1051 dynamic_cast<TagLib::ASF::File*>(fileref->file())) {
1052 TagLib::ASF::Tag* tag = file->tag();
1053 tag->addAttribute("FMPS/Rating", NumberToASFAttribute(song.rating()));
1054 }
1055 #endif
1056 else if (TagLib::MP4::File* file =
1057 dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
1058 TagLib::MP4::Tag* tag = file->tag();
1059 tag->itemListMap()[kMP4_FMPS_Rating_ID] = TagLib::StringList(
1060 QStringToTaglibString(QString::number(song.rating())));
1061 } else if (TagLib::APE::File* file =
1062 dynamic_cast<TagLib::APE::File*>(fileref->file())) {
1063 saveApeSongRating(file->APETag(true));
1064 } else if (TagLib::MPC::File* file =
1065 dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
1066 saveApeSongRating(file->APETag(true));
1067 } else if (TagLib::WavPack::File* file =
1068 dynamic_cast<TagLib::WavPack::File*>(fileref->file())) {
1069 saveApeSongRating(file->APETag(true));
1070 } else {
1071 // Nothing to save: stop now
1072 return true;
1073 }
1074
1075 bool ret = fileref->save();
1076 #ifdef Q_OS_LINUX
1077 if (ret) {
1078 // Linux: inotify doesn't seem to notice the change to the file unless we
1079 // change the timestamps as well. (this is what touch does)
1080 utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
1081 }
1082 #endif // Q_OS_LINUX
1083 return ret;
1084 }
1085
SetUserTextFrame(const QString & description,const QString & value,TagLib::ID3v2::Tag * tag) const1086 void TagReader::SetUserTextFrame(const QString& description,
1087 const QString& value,
1088 TagLib::ID3v2::Tag* tag) const {
1089 const QByteArray descr_utf8(description.toUtf8());
1090 const QByteArray value_utf8(value.toUtf8());
1091 qLog(Debug) << "Setting FMPSFrame:" << description << ", " << value;
1092 SetUserTextFrame(std::string(descr_utf8.constData(), descr_utf8.length()),
1093 std::string(value_utf8.constData(), value_utf8.length()),
1094 tag);
1095 }
1096
SetUserTextFrame(const std::string & description,const std::string & value,TagLib::ID3v2::Tag * tag) const1097 void TagReader::SetUserTextFrame(const std::string& description,
1098 const std::string& value,
1099 TagLib::ID3v2::Tag* tag) const {
1100 const TagLib::String t_description = StdStringToTaglibString(description);
1101 // Remove the frame if it already exists
1102 TagLib::ID3v2::UserTextIdentificationFrame* frame =
1103 TagLib::ID3v2::UserTextIdentificationFrame::find(tag, t_description);
1104 if (frame) {
1105 tag->removeFrame(frame);
1106 }
1107
1108 // Create and add a new frame
1109 frame = new TagLib::ID3v2::UserTextIdentificationFrame(TagLib::String::UTF8);
1110
1111 frame->setDescription(t_description);
1112 frame->setText(StdStringToTaglibString(value));
1113 tag->addFrame(frame);
1114 }
1115
SetTextFrame(const char * id,const QString & value,TagLib::ID3v2::Tag * tag) const1116 void TagReader::SetTextFrame(const char* id, const QString& value,
1117 TagLib::ID3v2::Tag* tag) const {
1118 const QByteArray utf8(value.toUtf8());
1119 SetTextFrame(id, std::string(utf8.constData(), utf8.length()), tag);
1120 }
1121
SetTextFrame(const char * id,const std::string & value,TagLib::ID3v2::Tag * tag) const1122 void TagReader::SetTextFrame(const char* id, const std::string& value,
1123 TagLib::ID3v2::Tag* tag) const {
1124 TagLib::ByteVector id_vector(id);
1125 QVector<TagLib::ByteVector> frames_buffer;
1126
1127 // Store and clear existing frames
1128 while (tag->frameListMap().contains(id_vector) &&
1129 tag->frameListMap()[id_vector].size() != 0) {
1130 frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render());
1131 tag->removeFrame(tag->frameListMap()[id_vector].front());
1132 }
1133
1134 // If no frames stored create empty frame
1135 if (frames_buffer.isEmpty()) {
1136 TagLib::ID3v2::TextIdentificationFrame frame(id_vector,
1137 TagLib::String::UTF8);
1138 frames_buffer.push_back(frame.render());
1139 }
1140
1141 // Update and add the frames
1142 for (int lyrics_index = 0; lyrics_index < frames_buffer.size();
1143 lyrics_index++) {
1144 TagLib::ID3v2::TextIdentificationFrame* frame =
1145 new TagLib::ID3v2::TextIdentificationFrame(
1146 frames_buffer.at(lyrics_index));
1147 if (lyrics_index == 0) {
1148 frame->setText(StdStringToTaglibString(value));
1149 }
1150 // add frame takes ownership and clears the memory
1151 tag->addFrame(frame);
1152 }
1153 }
1154
SetUnsyncLyricsFrame(const std::string & value,TagLib::ID3v2::Tag * tag) const1155 void TagReader::SetUnsyncLyricsFrame(const std::string& value,
1156 TagLib::ID3v2::Tag* tag) const {
1157 TagLib::ByteVector id_vector("USLT");
1158 QVector<TagLib::ByteVector> frames_buffer;
1159
1160 // Store and clear existing frames
1161 while (tag->frameListMap().contains(id_vector) &&
1162 tag->frameListMap()[id_vector].size() != 0) {
1163 frames_buffer.push_back(tag->frameListMap()[id_vector].front()->render());
1164 tag->removeFrame(tag->frameListMap()[id_vector].front());
1165 }
1166
1167 // If no frames stored create empty frame
1168 if (frames_buffer.isEmpty()) {
1169 TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8);
1170 frame.setDescription("Clementine editor");
1171 frames_buffer.push_back(frame.render());
1172 }
1173
1174 // Update and add the frames
1175 for (int lyrics_index = 0; lyrics_index < frames_buffer.size();
1176 lyrics_index++) {
1177 TagLib::ID3v2::UnsynchronizedLyricsFrame* frame =
1178 new TagLib::ID3v2::UnsynchronizedLyricsFrame(
1179 frames_buffer.at(lyrics_index));
1180 if (lyrics_index == 0) {
1181 frame->setText(StdStringToTaglibString(value));
1182 }
1183 // add frame takes ownership and clears the memory
1184 tag->addFrame(frame);
1185 }
1186 }
1187
IsMediaFile(const QString & filename) const1188 bool TagReader::IsMediaFile(const QString& filename) const {
1189 qLog(Debug) << "Checking for valid file" << filename;
1190
1191 std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
1192 return !fileref->isNull() && fileref->tag();
1193 }
1194
LoadEmbeddedArt(const QString & filename) const1195 QByteArray TagReader::LoadEmbeddedArt(const QString& filename) const {
1196 if (filename.isEmpty()) return QByteArray();
1197
1198 qLog(Debug) << "Loading art from" << filename;
1199
1200 #ifdef Q_OS_WIN32
1201 TagLib::FileRef ref(filename.toStdWString().c_str());
1202 #else
1203 TagLib::FileRef ref(QFile::encodeName(filename).constData());
1204 #endif
1205
1206 if (ref.isNull() || !ref.file()) return QByteArray();
1207
1208 // MP3
1209 TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file());
1210 if (file && file->ID3v2Tag()) {
1211 TagLib::ID3v2::FrameList apic_frames =
1212 file->ID3v2Tag()->frameListMap()["APIC"];
1213 if (apic_frames.isEmpty()) return QByteArray();
1214
1215 TagLib::ID3v2::AttachedPictureFrame* pic =
1216 static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
1217
1218 return QByteArray((const char*)pic->picture().data(),
1219 pic->picture().size());
1220 }
1221
1222 // Ogg vorbis/speex
1223 TagLib::Ogg::XiphComment* xiph_comment =
1224 dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
1225
1226 if (xiph_comment) {
1227 TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
1228
1229 #if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
1230 // Other than the below mentioned non-standard COVERART,
1231 // METADATA_BLOCK_PICTURE
1232 // is the proposed tag for cover pictures.
1233 // (see http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE)
1234 if (map.contains("METADATA_BLOCK_PICTURE")) {
1235 TagLib::StringList pict_list = map["METADATA_BLOCK_PICTURE"];
1236 for (std::list<TagLib::String>::iterator it = pict_list.begin();
1237 it != pict_list.end(); ++it) {
1238 QByteArray data(QByteArray::fromBase64(it->toCString()));
1239 TagLib::ByteVector tdata(data.data(), data.size());
1240 TagLib::FLAC::Picture p(tdata);
1241 if (p.type() == TagLib::FLAC::Picture::FrontCover)
1242 return QByteArray(p.data().data(), p.data().size());
1243 }
1244 // If there was no specific front cover, just take the first picture
1245 QByteArray data(QByteArray::fromBase64(
1246 map["METADATA_BLOCK_PICTURE"].front().toCString()));
1247 TagLib::ByteVector tdata(data.data(), data.size());
1248 TagLib::FLAC::Picture p(tdata);
1249 return QByteArray(p.data().data(), p.data().size());
1250 }
1251 #else
1252 TagLib::List<TagLib::FLAC::Picture*> pics = xiph_comment->pictureList();
1253 if (!pics.isEmpty()) {
1254 for (auto p : pics) {
1255 if (p->type() == TagLib::FLAC::Picture::FrontCover)
1256 return QByteArray(p->data().data(), p->data().size());
1257 }
1258 // If there was no specific front cover, just take the first picture
1259 std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
1260 TagLib::FLAC::Picture* picture = *it;
1261
1262 return QByteArray(picture->data().data(), picture->data().size());
1263 }
1264 #endif
1265
1266 // Ogg lacks a definitive standard for embedding cover art, but it seems
1267 // b64 encoding a field called COVERART is the general convention
1268 if (!map.contains("COVERART")) return QByteArray();
1269
1270 return QByteArray::fromBase64(map["COVERART"].toString().toCString());
1271 }
1272
1273 #ifdef TAGLIB_HAS_FLAC_PICTURELIST
1274 // Flac
1275 TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file());
1276 if (flac_file && flac_file->xiphComment()) {
1277 TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
1278 if (!pics.isEmpty()) {
1279 // Use the first picture in the file - this could be made cleverer and
1280 // pick the front cover if it's present.
1281
1282 std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
1283 TagLib::FLAC::Picture* picture = *it;
1284
1285 return QByteArray(picture->data().data(), picture->data().size());
1286 }
1287 }
1288 #endif
1289
1290 // MP4/AAC
1291 TagLib::MP4::File* aac_file = dynamic_cast<TagLib::MP4::File*>(ref.file());
1292 if (aac_file) {
1293 TagLib::MP4::Tag* tag = aac_file->tag();
1294 const TagLib::MP4::ItemListMap& items = tag->itemListMap();
1295 TagLib::MP4::ItemListMap::ConstIterator it = items.find("covr");
1296 if (it != items.end()) {
1297 const TagLib::MP4::CoverArtList& art_list = it->second.toCoverArtList();
1298
1299 if (!art_list.isEmpty()) {
1300 // Just take the first one for now
1301 const TagLib::MP4::CoverArt& art = art_list.front();
1302 return QByteArray(art.data().data(), art.data().size());
1303 }
1304 }
1305 }
1306
1307 // APE formats
1308 auto apeTagCover = [&](TagLib::APE::Tag* tag) {
1309 QByteArray cover;
1310 const TagLib::APE::ItemListMap& items = tag->itemListMap();
1311 TagLib::APE::ItemListMap::ConstIterator it =
1312 items.find("COVER ART (FRONT)");
1313 if (it != items.end()) {
1314 TagLib::ByteVector data = it->second.binaryData();
1315
1316 int pos = data.find('\0') + 1;
1317 if ((pos > 0) && (pos < data.size())) {
1318 cover = QByteArray(data.data() + pos, data.size() - pos);
1319 }
1320 }
1321
1322 return cover;
1323 };
1324
1325 TagLib::APE::File* ape_file = dynamic_cast<TagLib::APE::File*>(ref.file());
1326 if (ape_file) {
1327 return apeTagCover(ape_file->APETag());
1328 }
1329
1330 TagLib::MPC::File* mpc_file = dynamic_cast<TagLib::MPC::File*>(ref.file());
1331 if (mpc_file) {
1332 return apeTagCover(mpc_file->APETag());
1333 }
1334
1335 TagLib::WavPack::File* wavPack_file =
1336 dynamic_cast<TagLib::WavPack::File*>(ref.file());
1337 if (wavPack_file) {
1338 return apeTagCover(wavPack_file->APETag());
1339 }
1340
1341 return QByteArray();
1342 }
1343
1344 #ifdef HAVE_GOOGLE_DRIVE
ReadCloudFile(const QUrl & download_url,const QString & title,int size,const QString & mime_type,const QString & authorisation_header,pb::tagreader::SongMetadata * song) const1345 bool TagReader::ReadCloudFile(const QUrl& download_url, const QString& title,
1346 int size, const QString& mime_type,
1347 const QString& authorisation_header,
1348 pb::tagreader::SongMetadata* song) const {
1349 qLog(Debug) << "Loading tags from" << title;
1350
1351 std::unique_ptr<CloudStream> stream(
1352 new CloudStream(download_url, title, size, authorisation_header));
1353 stream->Precache();
1354 std::unique_ptr<TagLib::File> tag;
1355 if (mime_type == "audio/mpeg" &&
1356 title.endsWith(".mp3", Qt::CaseInsensitive)) {
1357 tag.reset(new TagLib::MPEG::File(stream.get(),
1358 TagLib::ID3v2::FrameFactory::instance(),
1359 TagLib::AudioProperties::Accurate));
1360 } else if (mime_type == "audio/mp4" ||
1361 (mime_type == "audio/mpeg" &&
1362 title.endsWith(".m4a", Qt::CaseInsensitive))) {
1363 tag.reset(new TagLib::MP4::File(stream.get(), true,
1364 TagLib::AudioProperties::Accurate));
1365 }
1366 #ifdef TAGLIB_HAS_OPUS
1367 else if ((mime_type == "application/opus" || mime_type == "audio/opus" ||
1368 mime_type == "application/ogg" || mime_type == "audio/ogg") &&
1369 title.endsWith(".opus", Qt::CaseInsensitive)) {
1370 tag.reset(new TagLib::Ogg::Opus::File(stream.get(), true,
1371 TagLib::AudioProperties::Accurate));
1372 }
1373 #endif
1374 else if (mime_type == "application/ogg" || mime_type == "audio/ogg") {
1375 tag.reset(new TagLib::Ogg::Vorbis::File(stream.get(), true,
1376 TagLib::AudioProperties::Accurate));
1377 } else if (mime_type == "application/x-flac" || mime_type == "audio/flac" ||
1378 mime_type == "audio/x-flac") {
1379 tag.reset(new TagLib::FLAC::File(stream.get(),
1380 TagLib::ID3v2::FrameFactory::instance(),
1381 true, TagLib::AudioProperties::Accurate));
1382 } else if (mime_type == "audio/x-ms-wma") {
1383 tag.reset(new TagLib::ASF::File(stream.get(), true,
1384 TagLib::AudioProperties::Accurate));
1385 } else {
1386 qLog(Debug) << "Unknown mime type for tagging:" << mime_type;
1387 return false;
1388 }
1389
1390 if (stream->num_requests() > 2) {
1391 // Warn if pre-caching failed.
1392 qLog(Warning) << "Total requests for file:" << title
1393 << stream->num_requests() << stream->cached_bytes();
1394 }
1395
1396 if (tag->tag() && !tag->tag()->isEmpty()) {
1397 song->set_title(tag->tag()->title().toCString(true));
1398 song->set_artist(tag->tag()->artist().toCString(true));
1399 song->set_album(tag->tag()->album().toCString(true));
1400 song->set_filesize(size);
1401
1402 if (tag->tag()->track() != 0) {
1403 song->set_track(tag->tag()->track());
1404 }
1405 if (tag->tag()->year() != 0) {
1406 song->set_year(tag->tag()->year());
1407 }
1408
1409 song->set_type(pb::tagreader::SongMetadata_Type_STREAM);
1410
1411 if (tag->audioProperties()) {
1412 song->set_length_nanosec(tag->audioProperties()->length() * kNsecPerSec);
1413 }
1414 return true;
1415 }
1416
1417 return false;
1418 }
1419 #endif // HAVE_GOOGLE_DRIVE
1420
GetPOPMFrameFromTag(TagLib::ID3v2::Tag * tag)1421 TagLib::ID3v2::PopularimeterFrame* TagReader::GetPOPMFrameFromTag(
1422 TagLib::ID3v2::Tag* tag) {
1423 TagLib::ID3v2::PopularimeterFrame* frame = nullptr;
1424
1425 const TagLib::ID3v2::FrameListMap& map = tag->frameListMap();
1426 if (!map["POPM"].isEmpty()) {
1427 frame =
1428 dynamic_cast<TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
1429 }
1430
1431 if (!frame) {
1432 frame = new TagLib::ID3v2::PopularimeterFrame();
1433 tag->addFrame(frame);
1434 }
1435 return frame;
1436 }
1437
ConvertPOPMRating(const int POPM_rating)1438 float TagReader::ConvertPOPMRating(const int POPM_rating) {
1439 if (POPM_rating < 0x01) {
1440 return 0.0;
1441 } else if (POPM_rating < 0x40) {
1442 return 0.20; // 1 star
1443 } else if (POPM_rating < 0x80) {
1444 return 0.40; // 2 stars
1445 } else if (POPM_rating < 0xC0) {
1446 return 0.60; // 3 stars
1447 } else if (POPM_rating < 0xFC) { // some players store 5 stars as 0xFC
1448 return 0.80; // 4 stars
1449 }
1450 return 1.0; // 5 stars
1451 }
1452
ConvertToPOPMRating(const float rating)1453 int TagReader::ConvertToPOPMRating(const float rating) {
1454 if (rating < 0.20) {
1455 return 0x00;
1456 } else if (rating < 0.40) {
1457 return 0x01;
1458 } else if (rating < 0.60) {
1459 return 0x40;
1460 } else if (rating < 0.80) {
1461 return 0x80;
1462 } else if (rating < 1.0) {
1463 return 0xC0;
1464 }
1465 return 0xFF;
1466 }
1467