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