1 /*MT*
2 
3     MediaTomb - http://www.mediatomb.cc/
4 
5     taglib_handler.cc - this file is part of MediaTomb.
6 
7     Copyright (C) 2005 Gena Batyan <bgeradz@mediatomb.cc>,
8                        Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>
9 
10     Copyright (C) 2006-2010 Gena Batyan <bgeradz@mediatomb.cc>,
11                             Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>,
12                             Leonhard Wimmer <leo@mediatomb.cc>
13 
14     MediaTomb is free software; you can redistribute it and/or modify
15     it under the terms of the GNU General Public License version 2
16     as published by the Free Software Foundation.
17 
18     MediaTomb is distributed in the hope that it will be useful,
19     but WITHOUT ANY WARRANTY; without even the implied warranty of
20     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21     GNU General Public License for more details.
22 
23     You should have received a copy of the GNU General Public License
24     version 2 along with MediaTomb; if not, write to the Free Software
25     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
26 
27     $Id$
28 */
29 
30 /// \file taglib_handler.cc
31 
32 #ifdef HAVE_TAGLIB
33 #include "taglib_handler.h" // API
34 
35 #include <taglib/aifffile.h>
36 #include <taglib/apefile.h>
37 #include <taglib/asffile.h>
38 #include <taglib/attachedpictureframe.h>
39 #include <taglib/flacfile.h>
40 #include <taglib/id3v2tag.h>
41 #include <taglib/mp4file.h>
42 #include <taglib/mpegfile.h>
43 #include <taglib/textidentificationframe.h>
44 #include <taglib/tfilestream.h>
45 #include <taglib/tiostream.h>
46 #include <taglib/tpropertymap.h>
47 #include <taglib/vorbisfile.h>
48 #include <taglib/wavpackfile.h>
49 
50 #include "cds_objects.h"
51 #include "config/config_manager.h"
52 #include "iohandler/mem_io_handler.h"
53 #include "util/mime.h"
54 #include "util/tools.h"
55 
TagLibHandler(const std::shared_ptr<Context> & context)56 TagLibHandler::TagLibHandler(const std::shared_ptr<Context>& context)
57     : MetadataHandler(context)
58 {
59     entrySeparator = this->config->getOption(CFG_IMPORT_LIBOPTS_ENTRY_SEP);
60     legacyEntrySeparator = this->config->getOption(CFG_IMPORT_LIBOPTS_ENTRY_LEGACY_SEP);
61     trimStringInPlace(legacyEntrySeparator);
62     if (legacyEntrySeparator.empty())
63         legacyEntrySeparator = "\n";
64     specialPropertyMap = this->config->getDictionaryOption(CFG_IMPORT_LIBOPTS_ID3_METADATA_TAGS_LIST);
65 }
66 
addField(metadata_fields_t field,const TagLib::File & file,const TagLib::Tag * tag,const std::shared_ptr<CdsItem> & item) const67 void TagLibHandler::addField(metadata_fields_t field, const TagLib::File& file, const TagLib::Tag* tag, const std::shared_ptr<CdsItem>& item) const
68 {
69     if (!tag || tag->isEmpty())
70         return;
71 
72     auto sc = StringConverter::i2i(config); // sure is sure
73 
74     std::vector<std::string> value;
75 
76     auto propertyMap = std::map<metadata_fields_t, std::string> {
77         { M_ARTIST, "ARTIST" },
78         { M_ALBUMARTIST, "ALBUMARTIST" },
79         { M_COMPOSER, "COMPOSER" },
80         { M_CONDUCTOR, "CONDUCTOR" },
81         { M_ORCHESTRA, "ORCHESTRA" },
82     };
83 
84     switch (field) {
85     case M_TITLE:
86         value.push_back(tag->title().toCString(true));
87         break;
88     case M_ALBUM:
89         value.push_back(tag->album().toCString(true));
90         break;
91     case M_DATE:
92     case M_UPNP_DATE: {
93         unsigned int i = tag->year();
94         if (i == 0)
95             return;
96         value.push_back(fmt::format("{}-01-01", i));
97         break;
98     }
99     case M_GENRE:
100         value.push_back(tag->genre().toCString(true));
101         break;
102     case M_DESCRIPTION:
103         value.push_back(tag->comment().toCString(true));
104         break;
105     case M_TRACKNUMBER: {
106         unsigned int i = tag->track();
107         if (i == 0)
108             return;
109         value.push_back(fmt::to_string(i));
110         item->setTrackNumber(i);
111         break;
112     }
113     case M_PARTNUMBER: {
114         auto list = file.properties()["DISCNUMBER"];
115         if (!list.isEmpty()) {
116             value.push_back(list[0].toCString(true));
117         } else {
118             list = file.properties()["TPOS"];
119             if (list.isEmpty())
120                 return;
121             value.push_back(list[0].toCString(true));
122         }
123         item->setPartNumber(stoiString(value[0]));
124         break;
125     }
126     default: {
127         auto it = propertyMap.find(field);
128         if (it == propertyMap.end())
129             return;
130         // we have to use file.properties() instead of tag->properties()
131         // because the latter returns incomplete properties
132         // https://mail.kde.org/pipermail/taglib-devel/2015-May/002729.html
133         auto list = file.properties()[it->second];
134         if (list.isEmpty())
135             return;
136         for (auto&& entry : list) {
137             for (auto&& val : entry.split(legacyEntrySeparator))
138                 value.push_back(val.toCString(true));
139         }
140     }
141     }
142 
143     if (!value.empty()) {
144         for (auto&& entry : value) {
145             trimStringInPlace(entry);
146             if (!entry.empty())
147                 item->addMetaData(field, sc->convert(entry));
148         }
149         // [log_debug("Setting metadata on item: {} = {}", field, sc->convert(value).c_str());]
150     }
151 }
152 
addSpecialFields(const TagLib::File & file,const TagLib::Tag * tag,const std::shared_ptr<CdsItem> & item) const153 void TagLibHandler::addSpecialFields(const TagLib::File& file, const TagLib::Tag* tag, const std::shared_ptr<CdsItem>& item) const
154 {
155     if (!tag || tag->isEmpty())
156         return;
157 
158     auto sc = StringConverter::i2i(config); // sure is sure
159 
160     for (auto&& [key, meta] : specialPropertyMap) {
161         auto list = file.properties()[key];
162         if (list.isEmpty())
163             return;
164         for (auto&& val : list) {
165             for (auto&& entrySeg : val.split(legacyEntrySeparator)) {
166                 std::string entry = entrySeg.toCString(true);
167                 trimStringInPlace(entry);
168                 if (!entry.empty())
169                     item->addMetaData(meta, sc->convert(entry));
170             }
171         }
172     }
173 }
174 
populateGenericTags(const std::shared_ptr<CdsItem> & item,const TagLib::File & file) const175 void TagLibHandler::populateGenericTags(const std::shared_ptr<CdsItem>& item, const TagLib::File& file) const
176 {
177     if (!file.tag())
178         return;
179 
180     const TagLib::Tag* tag = file.tag();
181     for (auto&& [field, key] : mt_keys)
182         addField(field, file, tag, item);
183 
184     addSpecialFields(file, tag, item);
185 
186     const TagLib::AudioProperties* audioProps = file.audioProperties();
187     if (!audioProps)
188         return;
189 
190     auto res = item->getResource(0);
191 
192     // UPnP bitrate is in bytes/second
193     int temp = audioProps->bitrate() * 1024 / 8; // kbit/second -> byte/second
194     if (temp > 0) {
195         res->addAttribute(R_BITRATE, fmt::to_string(temp));
196     }
197 
198     temp = audioProps->lengthInMilliseconds();
199     if (temp > 0) {
200         res->addAttribute(R_DURATION, millisecondsToHMSF(temp));
201     }
202 
203     temp = audioProps->sampleRate();
204     if (temp > 0) {
205         res->addAttribute(R_SAMPLEFREQUENCY, fmt::to_string(temp));
206     }
207 
208     temp = audioProps->channels();
209     if (temp > 0) {
210         res->addAttribute(R_NRAUDIOCHANNELS, fmt::to_string(temp));
211     }
212 }
213 
populateAuxTags(const std::shared_ptr<CdsItem> & item,const TagLib::PropertyMap & propertyMap,const std::unique_ptr<StringConverter> & sc) const214 void TagLibHandler::populateAuxTags(const std::shared_ptr<CdsItem>& item, const TagLib::PropertyMap& propertyMap, const std::unique_ptr<StringConverter>& sc) const
215 {
216     auto aux_tags_list = config->getArrayOption(CFG_IMPORT_LIBOPTS_ID3_AUXDATA_TAGS_LIST);
217     for (auto&& desiredTag : aux_tags_list) {
218         if (desiredTag.empty()) {
219             continue;
220         }
221 
222         if (propertyMap.contains(desiredTag.c_str())) {
223             auto&& property = propertyMap[desiredTag.c_str()];
224             if (property.isEmpty())
225                 continue;
226 
227             auto val = property.toString(entrySeparator);
228             if (!legacyEntrySeparator.empty())
229                 val = val.split(legacyEntrySeparator).toString(entrySeparator);
230             std::string value(val.toCString(true));
231             value = sc->convert(value);
232             log_debug("Adding auxdata: {} with value {}", desiredTag.c_str(), value.c_str());
233             item->setAuxData(desiredTag, value);
234         }
235     }
236 }
237 
238 /// \brief read metadata from file and add to object
239 /// \param obj Object to handle
fillMetadata(const std::shared_ptr<CdsObject> & obj)240 void TagLibHandler::fillMetadata(const std::shared_ptr<CdsObject>& obj)
241 {
242     auto item = std::dynamic_pointer_cast<CdsItem>(obj);
243     if (!item)
244         return;
245 
246     auto mappings = config->getDictionaryOption(CFG_IMPORT_MAPPINGS_MIMETYPE_TO_CONTENTTYPE_LIST);
247     std::string content_type = getValueOrDefault(mappings, item->getMimeType());
248 
249     auto fs = TagLib::FileStream(item->getLocation().c_str(), true); // true = Read only
250 
251     if (content_type == CONTENT_TYPE_MP3) {
252         extractMP3(&fs, item);
253     } else if (content_type == CONTENT_TYPE_FLAC) {
254         extractFLAC(&fs, item);
255     } else if (content_type == CONTENT_TYPE_MP4) {
256         extractMP4(&fs, item);
257     } else if (content_type == CONTENT_TYPE_OGG) {
258         extractOgg(&fs, item);
259     } else if (content_type == CONTENT_TYPE_APE) {
260         extractAPE(&fs, item);
261     } else if (content_type == CONTENT_TYPE_WMA) {
262         extractASF(&fs, item);
263     } else if (content_type == CONTENT_TYPE_WAVPACK) {
264         extractWavPack(&fs, item);
265     } else if (content_type == CONTENT_TYPE_AIFF) {
266         extractAiff(&fs, item);
267     } else {
268         log_warning("TagLibHandler {}: Does not handle the {} content type", item->getLocation().c_str(), content_type.c_str());
269     }
270     log_debug("TagLib handler done.");
271 }
272 
isValidArtworkContentType(std::string_view art_mimetype)273 bool TagLibHandler::isValidArtworkContentType(std::string_view art_mimetype)
274 {
275     // saw that simply "PNG" was used with some mp3's, so mimetype setting
276     // was probably invalid
277     return art_mimetype.find('/') != std::string_view::npos;
278 }
279 
getContentTypeFromByteVector(const TagLib::ByteVector & data) const280 std::string TagLibHandler::getContentTypeFromByteVector(const TagLib::ByteVector& data) const
281 {
282 #ifdef HAVE_MAGIC
283     auto art_mimetype = mime->bufferToMimeType(data.data(), data.size());
284     return art_mimetype.empty() ? MIMETYPE_DEFAULT : art_mimetype;
285 #else
286     return MIMETYPE_DEFAULT;
287 #endif
288 }
289 
addArtworkResource(const std::shared_ptr<CdsItem> & item,const std::string & art_mimetype)290 void TagLibHandler::addArtworkResource(const std::shared_ptr<CdsItem>& item, const std::string& art_mimetype)
291 {
292     // if we could not determine the mimetype, then there is no
293     // point to add the resource - it's probably garbage
294     log_debug("Found artwork of type {} in file {}", art_mimetype.c_str(), item->getLocation().c_str());
295 
296     if (art_mimetype != MIMETYPE_DEFAULT) {
297         auto resource = std::make_shared<CdsResource>(CH_ID3);
298         resource->addAttribute(R_PROTOCOLINFO, renderProtocolInfo(art_mimetype));
299         resource->addParameter(RESOURCE_CONTENT_TYPE, ID3_ALBUM_ART);
300         item->addResource(resource);
301     }
302 }
303 
304 /// \brief stream content of object or resource to client
305 /// \param obj Object to stream
306 /// \param resNum number of resource
307 /// \return iohandler to stream to client
serveContent(const std::shared_ptr<CdsObject> & obj,int resNum)308 std::unique_ptr<IOHandler> TagLibHandler::serveContent(const std::shared_ptr<CdsObject>& obj, int resNum)
309 {
310     auto item = std::dynamic_pointer_cast<CdsItem>(obj);
311     if (!item) // not streamable
312         return nullptr;
313 
314     auto mappings = config->getDictionaryOption(CFG_IMPORT_MAPPINGS_MIMETYPE_TO_CONTENTTYPE_LIST);
315     std::string content_type = getValueOrDefault(mappings, item->getMimeType());
316 
317     auto roStream = TagLib::FileStream(item->getLocation().c_str(), true); // Open read only
318 
319     if (content_type == CONTENT_TYPE_MP3) {
320         // stream album art from MP3 file
321         auto f = TagLib::MPEG::File(&roStream, TagLib::ID3v2::FrameFactory::instance());
322 
323         if (!f.isValid())
324             throw_std_runtime_error("Could not open file: {}", item->getLocation().c_str());
325 
326         if (!f.ID3v2Tag())
327             throw_std_runtime_error("resource has no album information");
328 
329         auto&& list = f.ID3v2Tag()->frameList("APIC");
330         if (list.isEmpty())
331             throw_std_runtime_error("resource has no album information");
332 
333         auto art = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(list.front());
334         if (!art) {
335             return nullptr;
336         }
337 
338         return std::make_unique<MemIOHandler>(art->picture().data(), art->picture().size());
339     }
340     if (content_type == CONTENT_TYPE_FLAC) {
341         // stream album art from FLAC file
342         auto f = TagLib::FLAC::File(&roStream, TagLib::ID3v2::FrameFactory::instance());
343 
344         if (!f.isValid())
345             throw_std_runtime_error("Could not open flac file: {}", item->getLocation().c_str());
346 
347         if (f.pictureList().isEmpty())
348             throw_std_runtime_error("flac resource has no picture information");
349 
350         TagLib::FLAC::Picture* pic = f.pictureList().front();
351         const TagLib::ByteVector& data = pic->data();
352 
353         return std::make_unique<MemIOHandler>(data.data(), data.size());
354     }
355     if (content_type == CONTENT_TYPE_MP4) {
356         // stream album art from MP4 file
357         auto f = TagLib::MP4::File(&roStream);
358 
359         if (!f.isValid()) {
360             throw_std_runtime_error("Could not open mp4 file: {}", item->getLocation().c_str());
361         }
362 
363         if (!f.hasMP4Tag()) {
364             throw_std_runtime_error("mp4 resource has no tag information");
365         }
366 
367         auto tag = f.tag();
368 
369         if (!tag->contains("covr")) {
370             throw_std_runtime_error("mp4 file has no 'covr' item");
371         }
372 
373         auto coverItem = tag->item("covr");
374         TagLib::MP4::CoverArtList coverArtList = coverItem.toCoverArtList();
375         if (coverArtList.isEmpty()) {
376             throw_std_runtime_error("mp4 resource has no picture information");
377         }
378 
379         const TagLib::MP4::CoverArt& coverArt = coverArtList.front();
380         const TagLib::ByteVector& data = coverArt.data();
381 
382         return std::make_unique<MemIOHandler>(data.data(), data.size());
383     }
384     if (content_type == CONTENT_TYPE_WMA) {
385         // stream album art from WMA file
386         auto f = TagLib::ASF::File(&roStream);
387 
388         if (!f.isValid())
389             throw_std_runtime_error("Could not open wma file: {}", item->getLocation().c_str());
390 
391         const TagLib::ASF::AttributeListMap& attrListMap = f.tag()->attributeListMap();
392         if (!attrListMap.contains("WM/Picture"))
393             throw_std_runtime_error("wma file has no picture information");
394 
395         const TagLib::ASF::AttributeList& attrList = attrListMap["WM/Picture"];
396         if (attrList.isEmpty())
397             throw_std_runtime_error("wma list has no picture information");
398 
399         const TagLib::ASF::Picture& wmpic = attrList[0].toPicture();
400         if (!wmpic.isValid())
401             throw_std_runtime_error("wma pic not valid");
402 
403         const TagLib::ByteVector& data = wmpic.picture();
404 
405         return std::make_unique<MemIOHandler>(data.data(), data.size());
406     }
407     if (content_type == CONTENT_TYPE_OGG) {
408         // stream album art from Ogg/Vorbis file
409         auto f = TagLib::Ogg::Vorbis::File(&roStream);
410 
411         if (!f.isValid() || !f.tag())
412             throw_std_runtime_error("Could not open vorbis file: {}", item->getLocation().c_str());
413 
414         const TagLib::List<TagLib::FLAC::Picture*> picList = f.tag()->pictureList();
415         if (picList.isEmpty())
416             throw_std_runtime_error("vorbis file has no picture information");
417 
418         const TagLib::FLAC::Picture* pic = picList.front();
419         const TagLib::ByteVector& data = pic->data();
420 
421         return std::make_unique<MemIOHandler>(data.data(), data.size());
422     }
423 
424     throw_std_runtime_error("Unsupported content_type: {}", content_type.c_str());
425 }
426 
extractMP3(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const427 void TagLibHandler::extractMP3(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
428 {
429     auto mp3 = TagLib::MPEG::File(roStream, TagLib::ID3v2::FrameFactory::instance());
430     if (!mp3.isValid()) {
431         log_info("TagLibHandler {}: does not appear to be a valid mp3 file", item->getLocation().c_str());
432         return;
433     }
434     populateGenericTags(item, mp3);
435 
436     if (!mp3.hasID3v2Tag()) {
437         log_debug("{}: has no IDv2 tags", item->getLocation().c_str());
438         return;
439     }
440 
441     auto sc = StringConverter::i2i(config);
442 
443     auto&& frameListMap = mp3.ID3v2Tag()->frameListMap();
444     // http://id3.org/id3v2.4.0-frames "4.2.6. User defined text information frame"
445     bool hasTXXXFrames = frameListMap.contains("TXXX");
446 
447     std::vector<std::string> aux_tags_list = config->getArrayOption(CFG_IMPORT_LIBOPTS_ID3_AUXDATA_TAGS_LIST);
448     for (auto&& desiredFrame : aux_tags_list) {
449         if (desiredFrame.empty()) {
450             continue;
451         }
452 
453         if (frameListMap.contains(desiredFrame.c_str())) {
454             auto&& frameList = frameListMap[desiredFrame.c_str()];
455             if (frameList.isEmpty())
456                 continue;
457 
458             std::vector<std::string> content;
459             for (auto&& frame : frameList) {
460                 auto textFrame = dynamic_cast<const TagLib::ID3v2::TextIdentificationFrame*>(frame);
461                 if (!textFrame)
462                     continue;
463                 for (auto&& field : textFrame->fieldList()) {
464                     if (legacyEntrySeparator.empty())
465                         content.push_back(sc->convert(field.toCString(true)));
466                     else
467                         for (auto&& val : field.split(legacyEntrySeparator))
468                             content.push_back(sc->convert(val.toCString(true)));
469                 }
470             }
471             if (!content.empty()) {
472                 log_debug("Adding auxdata: {} with value '{}'", desiredFrame, fmt::join(content, entrySeparator));
473                 item->setAuxData(desiredFrame, fmt::format("{}", fmt::join(content, entrySeparator)));
474             }
475         } else if (hasTXXXFrames && startswith(desiredFrame, "TXXX:")) {
476             auto&& frameList = frameListMap["TXXX"];
477             //log_debug("TXXX Frame list has {} elements", frameList.size());
478 
479             std::string desiredSubTag = desiredFrame.substr(5);
480             if (desiredSubTag.empty())
481                 continue;
482 
483             for (auto&& frame : frameList) {
484                 const auto textFrame = dynamic_cast<const TagLib::ID3v2::TextIdentificationFrame*>(frame);
485                 if (!textFrame)
486                     continue;
487                 std::vector<std::string> content;
488                 std::string subTag;
489                 for (auto&& field : textFrame->fieldList()) {
490                     if (subTag.empty()) {
491                         // first element is subTag name
492                         subTag = sc->convert(field.toCString(true));
493                     } else if (legacyEntrySeparator.empty()) {
494                         content.push_back(sc->convert(field.toCString(true)));
495                     } else {
496                         for (auto&& val : field.split(legacyEntrySeparator))
497                             content.push_back(sc->convert(val.toCString(true)));
498                     }
499                 }
500                 log_debug("TXXX Tag: {}", subTag.c_str());
501 
502                 if (desiredSubTag == subTag && !content.empty()) {
503                     if (content[0] == subTag)
504                         content.erase(content.begin()); // Avoid leading tag for options unknown to taglib
505 
506                     log_debug("Adding auxdata: '{}' with value '{}'", desiredFrame, fmt::join(content, entrySeparator));
507                     item->setAuxData(desiredFrame, fmt::format("{}", fmt::join(content, entrySeparator)));
508                     break;
509                 }
510             }
511         }
512     }
513 
514     auto&& apicFrameList = mp3.ID3v2Tag()->frameList("APIC");
515     if (!apicFrameList.isEmpty()) {
516         auto art = dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(apicFrameList.front());
517         if (!art) {
518             return;
519         }
520 
521         auto pic = art->picture();
522         std::string art_mimetype = sc->convert(art->mimeType().toCString(true));
523         if (!isValidArtworkContentType(art_mimetype)) {
524             art_mimetype = getContentTypeFromByteVector(pic);
525         }
526 
527         addArtworkResource(item, art_mimetype);
528     }
529 }
530 
extractOgg(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const531 void TagLibHandler::extractOgg(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
532 {
533     auto vorbis = TagLib::Ogg::Vorbis::File(roStream);
534 
535     if (!vorbis.isValid()) {
536         log_info("TagLibHandler {}: does not appear to be a valid ogg file", item->getLocation().c_str());
537         return;
538     }
539     populateGenericTags(item, vorbis);
540 
541     if (!vorbis.tag())
542         return;
543 
544     auto sc = StringConverter::i2i(config);
545     auto propertyMap = vorbis.properties();
546     populateAuxTags(item, propertyMap, sc);
547 
548     // Vorbis uses the FLAC binary picture structure...
549     // https://wiki.xiph.org/VorbisComment#Cover_art
550     // The unofficial COVERART field is not supported.
551     const TagLib::List<TagLib::FLAC::Picture*> picList = vorbis.tag()->pictureList();
552     if (picList.isEmpty())
553         return;
554 
555     const TagLib::FLAC::Picture* pic = picList.front();
556     const TagLib::ByteVector& data = pic->data();
557 
558     std::string art_mimetype = sc->convert(pic->mimeType().toCString(true));
559     if (!isValidArtworkContentType(art_mimetype)) {
560         art_mimetype = getContentTypeFromByteVector(data);
561     }
562     addArtworkResource(item, art_mimetype);
563 }
564 
extractASF(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const565 void TagLibHandler::extractASF(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
566 {
567     auto asf = TagLib::ASF::File(roStream);
568 
569     if (!asf.isValid()) {
570         log_info("TagLibHandler {}: does not appear to be a valid asf/wma file", item->getLocation().c_str());
571         return;
572     }
573     populateGenericTags(item, asf);
574 
575     auto sc = StringConverter::i2i(config);
576     auto propertyMap = asf.properties();
577     populateAuxTags(item, propertyMap, sc);
578 
579     auto audioProps = asf.audioProperties();
580     auto temp = audioProps->bitsPerSample();
581     auto res = item->getResource(0);
582     if (temp > 0) {
583         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
584     }
585 
586     const TagLib::ASF::AttributeListMap& attrListMap = asf.tag()->attributeListMap();
587     if (attrListMap.contains("WM/Picture")) {
588         const TagLib::ASF::AttributeList& attrList = attrListMap["WM/Picture"];
589         if (attrList.isEmpty())
590             return;
591 
592         const TagLib::ASF::Picture& wmpic = attrList[0].toPicture();
593         if (!wmpic.isValid())
594             return;
595 
596         std::string art_mimetype = sc->convert(wmpic.mimeType().toCString(true));
597         if (!isValidArtworkContentType(art_mimetype)) {
598             art_mimetype = getContentTypeFromByteVector(wmpic.picture());
599         }
600         addArtworkResource(item, art_mimetype);
601     }
602 }
603 
extractFLAC(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const604 void TagLibHandler::extractFLAC(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
605 {
606     auto flac = TagLib::FLAC::File(roStream, TagLib::ID3v2::FrameFactory::instance());
607 
608     if (!flac.isValid()) {
609         log_info("TagLibHandler {}: does not appear to be a valid flac file", item->getLocation().c_str());
610         return;
611     }
612     populateGenericTags(item, flac);
613 
614     auto sc = StringConverter::i2i(config);
615     auto propertyMap = flac.properties();
616     populateAuxTags(item, propertyMap, sc);
617 
618     auto audioProps = flac.audioProperties();
619     auto temp = audioProps->bitsPerSample();
620     auto res = item->getResource(0);
621     if (temp > 0) {
622         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
623     }
624 
625     if (flac.pictureList().isEmpty()) {
626         log_debug("TagLibHandler: flac resource has no picture information");
627         return;
628     }
629     const TagLib::FLAC::Picture* pic = flac.pictureList().front();
630     const TagLib::ByteVector& data = pic->data();
631 
632     std::string art_mimetype = sc->convert(pic->mimeType().toCString(true));
633     if (!isValidArtworkContentType(art_mimetype)) {
634         art_mimetype = getContentTypeFromByteVector(data);
635     }
636     addArtworkResource(item, art_mimetype);
637 }
638 
extractAPE(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const639 void TagLibHandler::extractAPE(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
640 {
641     auto ape = TagLib::APE::File(roStream);
642 
643     if (!ape.isValid()) {
644         log_info("TagLibHandler {}: does not appear to be a valid APE file", item->getLocation().c_str());
645         return;
646     }
647     populateGenericTags(item, ape);
648 
649     auto sc = StringConverter::i2i(config);
650     auto propertyMap = ape.properties();
651     populateAuxTags(item, propertyMap, sc);
652 
653     auto audioProps = ape.audioProperties();
654     auto temp = audioProps->bitsPerSample();
655     auto res = item->getResource(0);
656     if (temp > 0) {
657         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
658     }
659 }
660 
extractWavPack(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const661 void TagLibHandler::extractWavPack(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
662 {
663     auto wavpack = TagLib::WavPack::File(roStream);
664 
665     if (!wavpack.isValid()) {
666         log_info("TagLibHandler {}: does not appear to be a valid WavPack file", item->getLocation().c_str());
667         return;
668     }
669     populateGenericTags(item, wavpack);
670 
671     auto sc = StringConverter::i2i(config);
672     auto propertyMap = wavpack.properties();
673     populateAuxTags(item, propertyMap, sc);
674 
675     auto audioProps = wavpack.audioProperties();
676     auto temp = audioProps->bitsPerSample();
677     auto res = item->getResource(0);
678     if (temp > 0) {
679         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
680     }
681 }
682 
extractMP4(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const683 void TagLibHandler::extractMP4(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
684 {
685     auto mp4 = TagLib::MP4::File(roStream);
686 
687     if (!mp4.isValid()) {
688         log_info("TagLibHandler {}: does not appear to be a valid mp4 file", item->getLocation().c_str());
689         return;
690     }
691 
692     populateGenericTags(item, mp4);
693 
694     if (!mp4.hasMP4Tag()) {
695         log_debug("TagLibHandler {}: mp4 file has no tag information", item->getLocation().c_str());
696         return;
697     }
698 
699     auto sc = StringConverter::i2i(config);
700     auto propertyMap = mp4.tag()->properties();
701     populateAuxTags(item, propertyMap, sc);
702 
703     auto audioProps = mp4.audioProperties();
704     auto temp = audioProps->bitsPerSample();
705     auto res = item->getResource(0);
706     if (temp > 0) {
707         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
708     }
709 
710     if (mp4.tag()->contains("covr")) {
711         auto coverItem = mp4.tag()->item("covr");
712         TagLib::MP4::CoverArtList coverArtList = coverItem.toCoverArtList();
713         if (coverArtList.isEmpty()) {
714             log_info("TagLibHandler {}: mp4 file has no coverart",
715                 item->getLocation().c_str());
716             return;
717         }
718 
719         auto& coverArt = coverArtList.front();
720         auto art_mimetype = getContentTypeFromByteVector(coverArt.data());
721         if (!art_mimetype.empty()) {
722             addArtworkResource(item, art_mimetype);
723         }
724     } else {
725         log_debug("TagLibHandler {}: mp4 file has no 'covr' item",
726             item->getLocation().c_str());
727     }
728 }
729 
extractAiff(TagLib::IOStream * roStream,const std::shared_ptr<CdsItem> & item) const730 void TagLibHandler::extractAiff(TagLib::IOStream* roStream, const std::shared_ptr<CdsItem>& item) const
731 {
732     auto aiff = TagLib::RIFF::AIFF::File(roStream);
733 
734     if (!aiff.isValid()) {
735         log_info("TagLibHandler {}: does not appear to be a valid AIFF file", item->getLocation().c_str());
736         return;
737     }
738     populateGenericTags(item, aiff);
739 
740     auto sc = StringConverter::i2i(config);
741     auto propertyMap = aiff.properties();
742     populateAuxTags(item, propertyMap, sc);
743 
744     auto audioProps = aiff.audioProperties();
745     auto temp = audioProps->bitsPerSample();
746     auto res = item->getResource(0);
747     if (temp > 0) {
748         res->addAttribute(R_BITS_PER_SAMPLE, fmt::to_string(temp));
749     }
750 }
751 
752 #endif // HAVE_TAGLIB
753