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