1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 //    * Redistributions of source code must retain the above copyright notice,
11 //      this list of conditions and the following disclaimer.
12 //
13 //    * Redistributions in binary form must reproduce the above copyright
14 //      notice, this list of conditions and the following disclaimer in the
15 //      documentation and/or other materials provided with the distribution.
16 //
17 //    * Neither the name of the author nor the names of other contributors may
18 //      be used to endorse or promote products derived from this software
19 //      without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34 
35 #include "TaglibMetadataReader.h"
36 
37 #ifdef WIN32
38     #include <taglib/toolkit/tlist.h>
39     #include <taglib/toolkit/tfile.h>
40     #include <taglib/tag.h>
41     #include <taglib/fileref.h>
42     #include <taglib/audioproperties.h>
43     #include <taglib/mpeg/mpegfile.h>
44     #include <taglib/mpeg/id3v1/id3v1tag.h>
45     #include <taglib/mpeg/id3v1/id3v1genres.h>
46     #include <taglib/mpeg/id3v2/id3v2tag.h>
47     #include <taglib/mpeg/id3v2/id3v2header.h>
48     #include <taglib/mpeg/id3v2/id3v2frame.h>
49     #include <taglib/mpeg/id3v2/frames/attachedpictureframe.h>
50     #include <taglib/mpeg/id3v2/frames/commentsframe.h>
51     #include <taglib/mpeg/id3v2/frames/textidentificationframe.h>
52     #include <taglib/mp4/mp4file.h>
53     #include <taglib/ogg/oggfile.h>
54     #include <taglib/ogg/xiphcomment.h>
55     #include <taglib/ogg/opus/opusfile.h>
56     #include <taglib/flac/flacfile.h>
57     #include <taglib/toolkit/tpropertymap.h>
58     #include <taglib/wavpack/wavpackfile.h>
59     #include <taglib/riff/aiff/aifffile.h>
60     #include <taglib/riff/wav/wavfile.h>
61 #else
62     #include <taglib/tlist.h>
63     #include <taglib/tfile.h>
64     #include <taglib/tag.h>
65     #include <taglib/fileref.h>
66     #include <taglib/audioproperties.h>
67     #include <taglib/mpegfile.h>
68     #include <taglib/id3v1tag.h>
69     #include <taglib/id3v1genres.h>
70     #include <taglib/id3v2tag.h>
71     #include <taglib/id3v2header.h>
72     #include <taglib/id3v2frame.h>
73     #include <taglib/attachedpictureframe.h>
74     #include <taglib/commentsframe.h>
75     #include <taglib/mp4file.h>
76     #include <taglib/oggfile.h>
77     #include <taglib/opusfile.h>
78     #include <taglib/flacfile.h>
79     #include <taglib/wavpackfile.h>
80     #include <taglib/xiphcomment.h>
81     #include <taglib/tpropertymap.h>
82     #include <taglib/aifffile.h>
83     #include <taglib/wavfile.h>
84     #include <taglib/textidentificationframe.h>
85 #endif
86 
87 #include <vector>
88 #include <string>
89 #include <iostream>
90 #include <functional>
91 #include <cctype>
92 #include <algorithm>
93 #include <string.h>
94 
95 #ifdef WIN32
96 #define ENABLE_FFMPEG
97 #endif
98 
99 using namespace musik::core::sdk;
100 
101 namespace str {
lower(std::string input)102     static std::string lower(std::string input) {
103         std::transform(input.begin(), input.end(), input.begin(), ::tolower);
104         return input;
105     }
106 
isSpace(const char c)107     static inline bool isSpace(const char c) {
108         return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\v' || c == '\f';
109     }
110 
trim(const std::string & str)111     std::string trim(const std::string& str) {
112         if (str.size()) {
113             int start = 0;
114             for (size_t i = 0; i < str.length(); i++) {
115                 if (!isSpace(str[i])) {
116                     break;
117                 }
118                 ++start;
119             }
120             int end = (int)str.length();
121             for (size_t i = str.length() - 1; i >= 0; i--) {
122                 if (!isSpace(str[i])) {
123                     break;
124                 }
125                 --end;
126             }
127             if (end > start) {
128                 std::string result = str.substr((size_t)start, (size_t)end - start);
129                 return result;
130             }
131         }
132         return str;
133     }
134 
split(const std::string & in,const std::string & delim)135     static std::vector<std::string> split(
136         const std::string& in, const std::string& delim)
137     {
138         std::vector<std::string> result;
139         size_t last = 0, next = 0;
140         while ((next = in.find(delim, last)) != std::string::npos) {
141             result.push_back(std::move(trim(in.substr(last, next - last))));
142             last = next + 1;
143         }
144         result.push_back(std::move(trim(in.substr(last))));
145         return result;
146     }
147 }
148 
149 #ifdef WIN32
utf8to16(const char * utf8)150 static inline std::wstring utf8to16(const char* utf8) {
151     int size = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, 0, 0);
152     if (size <= 0) return L"";
153     wchar_t* buffer = new wchar_t[size];
154     MultiByteToWideChar(CP_UTF8, 0, utf8, -1, buffer, size);
155     std::wstring utf16fn(buffer);
156     delete[] buffer;
157     return utf16fn;
158 }
159 #endif
160 
resolveOggType(const char * uri)161 static TagLib::FileRef resolveOggType(const char* uri) {
162     try {
163 #ifdef WIN32
164         FILE* file = _wfopen(utf8to16(uri).c_str(), L"rb");
165 #else
166         FILE* file = fopen(uri, "rb");
167 #endif
168         if (file) {
169             static const char OGG_OPUS_HEADER[8] = {'O','p','u','s','H','e','a','d'};
170 
171             char buffer[512];
172             size_t read = fread(buffer, 1, sizeof(buffer), file);
173             fclose(file);
174 
175             if (read == sizeof(buffer)) {
176                 auto it = std::search(
177                     std::begin(buffer), std::end(buffer),
178                     std::begin(OGG_OPUS_HEADER), std::end(OGG_OPUS_HEADER));
179 
180                 if (it != std::end(buffer)) {
181 #ifdef WIN32
182                     const std::wstring uri16 = utf8to16(uri);
183                     return TagLib::FileRef(new TagLib::Ogg::Opus::File(uri16.c_str()));
184 #else
185                     return TagLib::FileRef(new TagLib::Ogg::Opus::File(uri));
186 #endif
187                 }
188             }
189 
190         }
191     }
192     catch (...) {
193         /* ugh. yay. */
194     }
195     return TagLib::FileRef();
196 }
197 
isValidYear(const std::string & year)198 static bool isValidYear(const std::string& year) {
199     return std::stoi(year) > 0;
200 }
201 
toReplayGainFloat(const std::string & input)202 static float toReplayGainFloat(const std::string& input) {
203     /* trim any trailing " db" or "db" noise... */
204     std::string lower = str::lower(input);
205     if (lower.find(" db") == lower.length() - 3) {
206         lower = lower.substr(0, lower.length() - 3);
207     }
208     else if (lower.find("db") == lower.length() - 2) {
209         lower = lower.substr(0, lower.length() - 2);
210     }
211 
212     try {
213         return std::stof(lower);
214     }
215     catch (...) {
216         /* nothing we can do... */
217     }
218 
219     return 1.0f;
220 }
221 
initReplayGain(ReplayGain & rg)222 static inline void initReplayGain(ReplayGain& rg) {
223     rg.albumGain = rg.albumPeak = 1.0f;
224     rg.trackGain = rg.trackPeak = 1.0f;
225 }
226 
replayGainValid(ReplayGain & rg)227 static inline bool replayGainValid(ReplayGain& rg) {
228     return rg.albumGain != 1.0 || rg.albumPeak != 1.0 ||
229         rg.trackGain != 1.0 || rg.trackPeak != 1.0;
230 }
231 
TaglibMetadataReader()232 TaglibMetadataReader::TaglibMetadataReader() {
233 }
234 
~TaglibMetadataReader()235 TaglibMetadataReader::~TaglibMetadataReader() {
236 }
237 
Release()238 void TaglibMetadataReader::Release() {
239     delete this;
240 }
241 
CanRead(const char * extension)242 bool TaglibMetadataReader::CanRead(const char *extension) {
243     if (extension && strlen(extension)) {
244         std::string ext(str::lower(extension[0] == '.' ? &extension[1] : extension));
245         return
246 #ifdef ENABLE_FFMPEG
247             ext.compare("opus") == 0 ||
248             ext.compare("wv") == 0 ||
249             ext.compare("wma") == 0 ||
250             ext.compare("ape") == 0 ||
251             ext.compare("mpc") == 0 ||
252             ext.compare("aac") == 0 ||
253             ext.compare("alac") == 0 ||
254             ext.compare("wav") == 0 ||
255             ext.compare("wave") == 0 ||
256             ext.compare("aif") == 0 ||
257             ext.compare("aiff") == 0 ||
258 #endif
259             ext.compare("mp3") == 0 ||
260             ext.compare("ogg") == 0 ||
261             ext.compare("m4a") == 0 ||
262             ext.compare("flac") == 0;
263     }
264 
265     return false;
266 }
267 
Read(const char * uri,ITagStore * track)268 bool TaglibMetadataReader::Read(const char* uri, ITagStore *track) {
269     std::string path(uri);
270     std::string extension;
271 
272     std::string::size_type lastDot = path.find_last_of(".");
273     if (lastDot != std::string::npos) {
274         extension = path.substr(lastDot + 1).c_str();
275     }
276 
277     /* first, process everything except ID3v2. this logic applies
278     to everything except ID3v2 (including ID3v1) */
279     try {
280         this->ReadGeneric(uri, extension, track);
281     }
282     catch (...) {
283         std::cerr << "generic tag read for " << uri << "failed!";
284     }
285 
286     /* ID3v2 is a trainwreck, so it requires special processing */
287     if (extension.size()) {
288         if (str::lower(extension) == "mp3") {
289             this->ReadID3V2(uri, track);
290         }
291     }
292 
293     return true;
294 }
295 
ReadGeneric(const char * uri,const std::string & extension,ITagStore * target)296 bool TaglibMetadataReader::ReadGeneric(
297     const char* uri, const std::string& extension, ITagStore *target)
298 {
299 #ifdef WIN32
300     TagLib::FileRef file(utf8to16(uri).c_str());
301 #else
302     TagLib::FileRef file(uri);
303 #endif
304 
305     /* ogg is a container format, but taglib sees the extension and
306     assumes it's a vorbis file. in this case, we'll read the header
307     and try to guess the filetype */
308     if (file.isNull() && extension == "ogg") {
309         file = TagLib::FileRef(); /* closes the file */
310         file = resolveOggType(uri);
311     }
312 
313     if (file.isNull()) {
314         this->SetTagValue("title", uri, target);
315     }
316     else {
317         TagLib::Tag *tag = file.tag();
318         if (tag) {
319             this->ReadBasicData(file.tag(), uri, target);
320 
321             /* wav files can have metadata in the RIFF header, or, in some cases,
322             with an embedded id3v2 tag */
323             auto wavFile = dynamic_cast<TagLib::RIFF::WAV::File*>(file.file());
324             if (wavFile) {
325                 if (wavFile->hasInfoTag()) {
326                     this->ReadBasicData(wavFile->InfoTag(), uri, target);
327                 }
328                 if (wavFile->hasID3v2Tag()) {
329                     this->ReadID3V2(wavFile->ID3v2Tag(), target);
330                 }
331             }
332 
333             /* aif files are similar to wav files, but for some reason taglib
334             doesn't seem to expose non-id3v2 tags */
335             auto aifFile = dynamic_cast<TagLib::RIFF::AIFF::File*>(file.file());
336             if (aifFile) {
337                 if (aifFile->hasID3v2Tag()) {
338                     this->ReadID3V2(aifFile->tag(), target);
339                 }
340             }
341 
342             /* taglib hides certain properties (like album artist) in the XiphComment's
343             field list. if we're dealing with a straight-up Xiph tag, process it now */
344             auto xiphTag = dynamic_cast<TagLib::Ogg::XiphComment*>(tag);
345             if (xiphTag) {
346                 this->ReadFromMap(xiphTag->fieldListMap(), target);
347                 this->ExtractReplayGain(xiphTag->fieldListMap(), target);
348             }
349 
350             /* if this isn't a xiph tag, the file format may have some other custom
351             properties. let's see if we can pull them out here... */
352             if (!xiphTag) {
353                 bool handled = false;
354 
355                 /* flac files may have more than one type of tag embedded. see if there's
356                 see if there's a xiph comment burried deep. */
357                 auto flacFile = dynamic_cast<TagLib::FLAC::File*>(file.file());
358                 if (flacFile && flacFile->hasXiphComment()) {
359                     this->ReadFromMap(flacFile->xiphComment()->fieldListMap(), target);
360                     this->ExtractReplayGain(flacFile->xiphComment()->fieldListMap(), target);
361                     handled = true;
362                 }
363 
364                 /* similarly, mp4 buries disc number and album artist. however, taglib does
365                 NOT exposed a map with normalized keys, so we have to do special property
366                 handling here... */
367                 if (!handled) {
368                     auto mp4File = dynamic_cast<TagLib::MP4::File*>(file.file());
369                     if (mp4File && mp4File->hasMP4Tag()) {
370                         auto mp4TagMap = static_cast<TagLib::MP4::Tag*>(tag)->itemListMap();
371                         this->ExtractValueForKey(mp4TagMap, "aART", "album_artist", target);
372                         this->ExtractValueForKey(mp4TagMap, "disk", "disc", target);
373                         this->ExtractReplayGain(mp4TagMap, target);
374                         handled = true;
375                     }
376                 }
377 
378                 if (!handled) {
379                     auto wvFile = dynamic_cast<TagLib::WavPack::File*>(file.file());
380                     if (wvFile && wvFile->hasAPETag()) {
381                         this->ReadFromMap(wvFile->properties(), target);
382                         this->ExtractReplayGain(wvFile->properties(), target);
383                         handled = true;
384                     }
385                 }
386             }
387 
388             TagLib::AudioProperties *audio = file.audioProperties();
389             this->SetAudioProperties(audio, target);
390         }
391     }
392 
393     return true;
394 }
395 
ExtractValueForKey(const TagLib::MP4::ItemMap & map,const std::string & inputKey,const std::string & outputKey,ITagStore * target)396 void TaglibMetadataReader::ExtractValueForKey(
397     const TagLib::MP4::ItemMap& map,
398     const std::string& inputKey,
399     const std::string& outputKey,
400     ITagStore *target)
401 {
402     if (map.contains(inputKey.c_str())) {
403         TagLib::StringList value = map[inputKey.c_str()].toStringList();
404         if (value.size()) {
405             this->SetTagValue(outputKey.c_str(), value[0], target);
406         }
407     }
408 }
409 
ExtractValueForKey(const TagLib::MP4::ItemMap & map,const std::string & inputKey,const std::string & defaultValue)410 std::string TaglibMetadataReader::ExtractValueForKey(
411     const TagLib::MP4::ItemMap& map,
412     const std::string& inputKey,
413     const std::string& defaultValue)
414 {
415     if (map.contains(inputKey.c_str())) {
416         TagLib::StringList value = map[inputKey.c_str()].toStringList();
417         if (value.size()) {
418             return value[0].to8Bit();
419         }
420     }
421     return defaultValue;
422 }
423 
424 template <typename T>
ExtractValueForKey(const T & map,const std::string & inputKey,const std::string & outputKey,ITagStore * target)425 void TaglibMetadataReader::ExtractValueForKey(
426     const T& map,
427     const std::string& inputKey,
428     const std::string& outputKey,
429     ITagStore *target)
430 {
431     if (map.contains(inputKey.c_str())) {
432         TagLib::StringList value = map[inputKey.c_str()];
433         if (value.size()) {
434             this->SetTagValue(outputKey.c_str(), value[0], target);
435         }
436     }
437 }
438 
439 template <typename T>
ReadFromMap(const T & map,ITagStore * target)440 void TaglibMetadataReader::ReadFromMap(const T& map, ITagStore *target) {
441     ExtractValueForKey(map, "DISCNUMBER", "disc", target);
442     ExtractValueForKey(map, "ALBUM ARTIST", "album_artist", target);
443     ExtractValueForKey(map, "ALBUMARTIST", "album_artist", target);
444 }
445 
446 template<typename T>
ReadBasicData(const T * tag,const char * uri,ITagStore * target)447 void TaglibMetadataReader::ReadBasicData(const T* tag, const char* uri, ITagStore *target) {
448     if (tag) {
449         if (!tag->title().isEmpty()) {
450             this->SetTagValue("title", tag->title(), target);
451         }
452         else {
453             this->SetTagValue("title", uri, target);
454         }
455 
456         this->SetTagValue("album", tag->album(), target);
457         this->SetSlashSeparatedValues("artist", tag->artist(), target);
458         this->SetTagValue("genre", tag->genre(), target);
459         this->SetTagValue("comment", tag->comment(), target);
460 
461         if (tag->track()) {
462             this->SetTagValue("track", tag->track(), target);
463         }
464 
465         if (tag->year()) {
466             this->SetTagValue("year", tag->year(), target);
467         }
468 
469         /* read some generic key/value pairs that don't have direct accessors */
470         this->ReadFromMap(tag->properties(), target);
471     }
472 }
473 
474 template <typename T>
ExtractValueForKey(const T & map,const std::string & inputKey,const std::string & defaultValue)475 std::string TaglibMetadataReader::ExtractValueForKey(
476     const T& map,
477     const std::string& inputKey,
478     const std::string& defaultValue)
479 {
480     if (map.contains(inputKey.c_str())) {
481         TagLib::StringList value = map[inputKey.c_str()];
482         if (value.size()) {
483             return value[0].to8Bit();
484         }
485     }
486     return defaultValue;
487 }
488 
489 template <typename T>
ExtractReplayGain(const T & map,ITagStore * target)490 void TaglibMetadataReader::ExtractReplayGain(const T& map, ITagStore *target)
491 {
492     try {
493         ReplayGain replayGain;
494         initReplayGain(replayGain);
495         replayGain.trackGain = toReplayGainFloat(ExtractValueForKey(map, "REPLAYGAIN_TRACK_GAIN", "1.0"));
496         replayGain.trackPeak = toReplayGainFloat(ExtractValueForKey(map, "REPLAYGAIN_TRACK_PEAK", "1.0"));
497         replayGain.albumGain = toReplayGainFloat(ExtractValueForKey(map, "REPLAYGAIN_ALBUM_GAIN", "1.0"));
498         replayGain.albumPeak = toReplayGainFloat(ExtractValueForKey(map, "REPLAYGAIN_ALBUM_PEAK", "1.0"));
499 
500         if (replayGainValid(replayGain)) {
501             target->SetReplayGain(replayGain);
502         }
503     }
504     catch (...) {
505         /* let's not allow weird replay gain tags to crash indexing... */
506     }
507 }
508 
SetTagValueWithPossibleTotal(const std::string & value,const std::string & valueKey,const std::string & totalKey,ITagStore * track)509 void TaglibMetadataReader::SetTagValueWithPossibleTotal(
510     const std::string& value, const std::string& valueKey, const std::string& totalKey, ITagStore* track)
511 {
512     std::vector<std::string> parts = str::split(value, "/");
513     this->SetTagValue(valueKey.c_str(), parts[0].c_str(), track);
514     if (parts.size() > 1) {
515         this->SetTagValue(totalKey.c_str(), parts[1].c_str(), track);
516     }
517 }
518 
ReadID3V2(const char * uri,ITagStore * track)519 bool TaglibMetadataReader::ReadID3V2(const char* uri, ITagStore *track) {
520     TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8);
521 
522 #ifdef WIN32
523     TagLib::MPEG::File file(utf8to16(uri).c_str());
524 #else
525     TagLib::MPEG::File file(uri);
526 #endif
527 
528     /* audio properties include things like bitrate, channels, and duration */
529     TagLib::AudioProperties *audio = file.audioProperties();
530     if (audio) {
531         this->SetAudioProperties(audio, track);
532     }
533 
534     auto id3v2 = file.ID3v2Tag();
535     if (id3v2) {
536         return this->ReadID3V2(id3v2, track);
537     }
538 
539     return false;
540 }
541 
ReadID3V2(TagLib::ID3v2::Tag * id3v2,ITagStore * track)542 bool TaglibMetadataReader::ReadID3V2(TagLib::ID3v2::Tag *id3v2, ITagStore *track) {
543     try {
544         if (id3v2) {
545             TagLib::ID3v2::FrameListMap allTags = id3v2->frameListMap();
546 
547             if (!id3v2->title().isEmpty()) {
548                 this->SetTagValue("title", id3v2->title(), track);
549             }
550 
551             this->SetTagValue("album", id3v2->album(), track);
552 
553             /* year */
554 
555             if (!track->Contains("year") && !allTags["TYER"].isEmpty()) { /* ID3v2.3*/
556                 auto year = allTags["TYER"].front()->toString().substr(0, 4);
557                 if (isValidYear(year.to8Bit())) {
558                     this->SetTagValue("year", year, track);
559                 }
560             }
561 
562             if (!track->Contains("year") && !allTags["TDRC"].isEmpty()) { /* ID3v2.4*/
563                 auto year = allTags["TDRC"].front()->toString().substr(0, 4);
564                 if (isValidYear(year.to8Bit())) {
565                     this->SetTagValue("year", year, track);
566                 }
567             }
568 
569             if (!track->Contains("year") && !allTags["TCOP"].isEmpty()) { /* ID3v2.3*/
570                 auto year = allTags["TCOP"].front()->toString().substr(0, 4);
571                 if (isValidYear(year.to8Bit())) {
572                     this->SetTagValue("year", year, track);
573                 }
574             }
575 
576             /* replay gain */
577 
578             auto txxx = allTags["TXXX"];
579             if (!txxx.isEmpty()) {
580                 ReplayGain replayGain;
581                 initReplayGain(replayGain);
582 
583                 for (auto current : txxx) {
584                     using UTIF = TagLib::ID3v2::UserTextIdentificationFrame;
585                     UTIF* utif = dynamic_cast<UTIF*>(current);
586                     if (utif) {
587                         auto name = utif->description().upper();
588                         auto values = utif->fieldList();
589                         if (values.size() > 0) {
590                             if (name == "REPLAYGAIN_TRACK_GAIN") {
591                                 replayGain.trackGain = toReplayGainFloat(utif->fieldList().back().to8Bit());
592                             }
593                             else if (name == "REPLAYGAIN_TRACK_PEAK") {
594                                 replayGain.trackPeak = toReplayGainFloat(utif->fieldList().back().to8Bit());
595                             }
596                             else if (name == "REPLAYGAIN_ALBUM_GAIN") {
597                                 replayGain.albumGain = toReplayGainFloat(utif->fieldList().back().to8Bit());
598                             }
599                             else if (name == "REPLAYGAIN_ALBUM_PEAK") {
600                                 replayGain.albumPeak = toReplayGainFloat(utif->fieldList().back().to8Bit());
601                             }
602                         }
603                     }
604                 }
605 
606                 if (replayGainValid(replayGain)) {
607                     track->SetReplayGain(replayGain);
608                 }
609             }
610 
611             /* TRCK is the track number (or "trackNum/totalTracks") */
612             if (!allTags["TRCK"].isEmpty()) {
613                 std::string trackNumber = allTags["TRCK"].front()->toString().toCString(true);
614                 this->SetTagValueWithPossibleTotal(trackNumber, "track", "totaltracks", track);
615             }
616 
617             /* TPOS is the disc number (or "discNum/totalDiscs") */
618             if (!allTags["TPOS"].isEmpty()) {
619                 std::string discNumber = allTags["TPOS"].front()->toString().toCString(true);
620                 this->SetTagValueWithPossibleTotal(discNumber, "disc", "totaldiscs", track);
621             }
622             else {
623                 this->SetTagValue("disc", "1", track);
624                 this->SetTagValue("totaldiscs", "1", track);
625             }
626 
627             this->SetTagValues("bpm", allTags["TBPM"], track);
628             this->SetSlashSeparatedValues("composer", allTags["TCOM"], track);
629             this->SetTagValues("copyright", allTags["TCOP"], track);
630             this->SetTagValues("encoder", allTags["TENC"], track);
631             this->SetTagValues("writer", allTags["TEXT"], track);
632             this->SetTagValues("org.writer", allTags["TOLY"], track);
633             this->SetSlashSeparatedValues("publisher", allTags["TPUB"], track);
634             this->SetTagValues("mood", allTags["TMOO"], track);
635             this->SetSlashSeparatedValues("org.artist", allTags["TOPE"], track);
636             this->SetTagValues("language", allTags["TLAN"], track);
637             this->SetTagValues("lyrics", allTags["USLT"], track);
638             this->SetTagValues("disc", allTags["TPOS"], track);
639 
640             /* genre. note that multiple genres may be present */
641 
642             if (!allTags["TCON"].isEmpty()) {
643                 TagLib::ID3v2::FrameList genres = allTags["TCON"];
644 
645                 TagLib::ID3v2::FrameList::ConstIterator it = genres.begin();
646 
647                 for (; it != genres.end(); ++it) {
648                     TagLib::String genreString = (*it)->toString();
649 
650                     if (!genreString.isEmpty()) {
651                         /* note1: apparently genres will already be de-duped */
652                         int numberLength = 0;
653                         bool isNumber = true;
654 
655                         TagLib::String::ConstIterator charIt = genreString.begin();
656                         for (; isNumber && charIt != genreString.end(); ++charIt) {
657                             isNumber = (*charIt >= '0' && *charIt <= '9');
658 
659                             if (isNumber) {
660                                 ++numberLength;
661                             }
662                         }
663 
664                         if (isNumber) { /* old ID3v1 tags had numbers for genres. */
665                             int genreNumber = genreString.toInt();
666                             if (genreNumber >= 0 && genreNumber <= 255) {
667                                 genreString = TagLib::ID3v1::genre(genreNumber);
668                             }
669                         }
670                         else {
671                             if (numberLength > 0) { /* genre may start with a number. */
672                                 if (genreString.substr(numberLength, 1) == " ") {
673                                     int genreNumber = genreString.substr(0, numberLength).toInt();
674                                     if (genreNumber >= 0 && genreNumber <= 255) {
675                                         this->SetTagValue("genre", TagLib::ID3v1::genre(genreNumber), track);
676                                     }
677 
678                                     /* strip the number */
679                                     genreString = genreString.substr(numberLength + 1);
680                                 }
681                             }
682 
683                             if (!genreString.isEmpty()) {
684                                 this->SetTagValue("genre", genreString, track);
685                             }
686                         }
687                     }
688                 }
689             }
690 
691             /* artists */
692 
693             this->SetSlashSeparatedValues("artist", allTags["TPE1"], track);
694             this->SetSlashSeparatedValues("album_artist", allTags["TPE2"], track);
695             this->SetSlashSeparatedValues("conductor", allTags["TPE3"], track);
696             this->SetSlashSeparatedValues("interpreted", allTags["TPE4"], track);
697 
698             /* comments, mood, and rating */
699 
700             TagLib::ID3v2::FrameList comments = allTags["COMM"];
701 
702             TagLib::ID3v2::FrameList::Iterator it = comments.begin();
703             for (; it != comments.end(); ++it) {
704                 TagLib::ID3v2::CommentsFrame *comment
705                     = dynamic_cast<TagLib::ID3v2::CommentsFrame*> (*it);
706 
707                 TagLib::String temp = comment->description();
708                 std::string description(temp.begin(), temp.end());
709 
710                 if (description.empty()) {
711                     this->SetTagValue("comment", comment->toString(), track);
712                 }
713                 else if (description.compare("MusicMatch_Mood") == 0) {
714                     this->SetTagValue("mood", comment->toString(), track);
715                 }
716                 else if (description.compare("MusicMatch_Preference") == 0) {
717                     this->SetTagValue("textrating", comment->toString(), track);
718                 }
719             }
720 
721             /* thumbnail -- should come last, otherwise ::ContainsThumbnail() may
722             not be reliable; the thumbnails are computed and stored at the album level
723             so the album and album artist names need to have already been parsed. */
724 
725             if (!track->ContainsThumbnail()) {
726                 TagLib::ID3v2::FrameList pictures = allTags["APIC"];
727                 if (!pictures.isEmpty()) {
728                     /* there can be multiple pictures, apparently. let's just use
729                     the first one. */
730 
731                     TagLib::ID3v2::AttachedPictureFrame *picture =
732                         static_cast<TagLib::ID3v2::AttachedPictureFrame*>(pictures.front());
733 
734                     TagLib::ByteVector pictureData = picture->picture();
735                     long long size = pictureData.size();
736 
737                     if (size > 32) {    /* noticed that some id3tags have like a 4-8 byte size with no thumbnail */
738                         track->SetThumbnail(pictureData.data(), size);
739                     }
740                 }
741             }
742 
743             return true;
744         }
745     }
746     catch (...) {
747         /* not much we can do... */
748     }
749     return false;
750 }
751 
SetTagValue(const char * key,const TagLib::String tagString,ITagStore * track)752 void TaglibMetadataReader::SetTagValue(
753     const char* key,
754     const TagLib::String tagString,
755     ITagStore *track)
756 {
757     std::string value(tagString.to8Bit(true));
758     track->SetValue(key, value.c_str());
759 }
760 
SetTagValue(const char * key,const char * string,ITagStore * track)761 void TaglibMetadataReader::SetTagValue(
762     const char* key,
763     const char* string,
764     ITagStore *track)
765 {
766     std::string temp(string);
767     track->SetValue(key, temp.c_str());
768 }
769 
SetTagValue(const char * key,const int tagInt,ITagStore * target)770 void TaglibMetadataReader::SetTagValue(
771     const char* key, const int tagInt, ITagStore *target)
772 {
773     target->SetValue(key, std::to_string(tagInt).c_str());
774 }
775 
SetTagValues(const char * key,const TagLib::ID3v2::FrameList & frame,ITagStore * target)776 void TaglibMetadataReader::SetTagValues(
777     const char* key,
778     const TagLib::ID3v2::FrameList &frame,
779     ITagStore *target)
780 {
781     if (!frame.isEmpty()) {
782         TagLib::ID3v2::FrameList::ConstIterator value = frame.begin();
783 
784         for ( ; value != frame.end(); ++value) {
785             TagLib::String tagString = (*value)->toString();
786             if(!tagString.isEmpty()) {
787                 std::string value(tagString.to8Bit(true));
788                 target->SetValue(key, value.c_str());
789             }
790         }
791     }
792 }
793 
SetSlashSeparatedValues(const char * key,TagLib::String tagString,ITagStore * track)794 void TaglibMetadataReader::SetSlashSeparatedValues(
795     const char* key, TagLib::String tagString, ITagStore *track)
796 {
797     if(!tagString.isEmpty()) {
798         std::string value(tagString.to8Bit(true));
799         std::vector<std::string> splitValues = str::split(value, "/");
800         std::vector<std::string>::iterator it = splitValues.begin();
801         for( ; it != splitValues.end(); ++it) {
802             track->SetValue(key, it->c_str());
803         }
804     }
805 }
806 
SetSlashSeparatedValues(const char * key,const TagLib::ID3v2::FrameList & frame,ITagStore * track)807 void TaglibMetadataReader::SetSlashSeparatedValues(
808     const char* key,
809     const TagLib::ID3v2::FrameList &frame,
810     ITagStore *track)
811 {
812     if(!frame.isEmpty()) {
813         TagLib::ID3v2::FrameList::ConstIterator value = frame.begin();
814         for ( ; value != frame.end(); ++value) {
815             TagLib::String tagString = (*value)->toString();
816             this->SetSlashSeparatedValues(key, tagString, track);
817         }
818     }
819 }
820 
SetAudioProperties(TagLib::AudioProperties * audioProperties,ITagStore * track)821 void TaglibMetadataReader::SetAudioProperties(
822     TagLib::AudioProperties *audioProperties, ITagStore *track)
823 {
824     if (audioProperties) {
825         std::string duration = std::to_string(audioProperties->length());
826         int bitrate = audioProperties->bitrate();
827         int channels = audioProperties->channels();
828 
829         this->SetTagValue("duration", duration, track);
830 
831         if (bitrate) {
832             this->SetTagValue("bitrate", std::to_string(bitrate), track);
833         }
834 
835         if (channels) {
836             this->SetTagValue("channels", std::to_string(channels), track);
837         }
838     }
839 }
840