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