diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index fa730e72..40f4d38b 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -32,267 +32,261 @@ extern "C" #include "utils/Logger.hpp" #include "utils/String.hpp" -namespace Av { - -static std::string averror_to_string(int error) +namespace Av { - std::array buf = {0}; + namespace + { + std::string averror_to_string(int error) + { + std::array buf = { 0 }; - if (::av_strerror(error, buf.data(), buf.size()) == 0) - return &buf[0]; - else - return "Unknown error"; -} + if (::av_strerror(error, buf.data(), buf.size()) == 0) + return &buf[0]; + else + return "Unknown error"; + } -class AudioFileException : public Av::Exception -{ - public: - AudioFileException(int avError) - : Av::Exception {"AudioFileException: " + averror_to_string(avError)} - {} -}; + class AudioFileException : public Av::Exception + { + public: + AudioFileException(int avError) + : Av::Exception{ "AudioFileException: " + averror_to_string(avError) } + {} + }; -std::unique_ptr -parseAudioFile(const std::filesystem::path& p) -{ - return std::make_unique(p); -} + void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) + { + if (!dictionnary) + return; -AudioFile::AudioFile(const std::filesystem::path& p) -: _p {p} -{ - int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)}; - if (error < 0) - { - LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); - throw AudioFileException {error}; - } + AVDictionaryEntry* tag = NULL; + while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + res[StringUtils::stringToUpper(tag->key)] = tag->value; + } + } + } - error = avformat_find_stream_info(_context, nullptr); - if (error < 0) - { - LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); - avformat_close_input(&_context); - throw AudioFileException {error}; - } -} + std::unique_ptr parseAudioFile(const std::filesystem::path& p) + { + return std::make_unique(p); + } -AudioFile::~AudioFile() -{ - avformat_close_input(&_context); -} + AudioFile::AudioFile(const std::filesystem::path& p) + : _p{ p } + { + int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) }; + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); + throw AudioFileException{ error }; + } -const std::filesystem::path& -AudioFile::getPath() const -{ - return _p; -} + error = avformat_find_stream_info(_context, nullptr); + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); + avformat_close_input(&_context); + throw AudioFileException{ error }; + } + } -std::chrono::milliseconds -AudioFile::getDuration() const -{ - if (_context->duration == AV_NOPTS_VALUE) - return std::chrono::milliseconds {0}; // TODO estimate + AudioFile::~AudioFile() + { + avformat_close_input(&_context); + } - return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000}; -} + const std::filesystem::path& AudioFile::getPath() const + { + return _p; + } -void -getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) -{ - if (!dictionnary) - return; + std::chrono::milliseconds AudioFile::getDuration() const + { + if (_context->duration == AV_NOPTS_VALUE) + return std::chrono::milliseconds{ 0 }; // TODO estimate - AVDictionaryEntry *tag = NULL; - while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) - { - res[StringUtils::stringToUpper(tag->key)] = tag->value; - } -} + return std::chrono::milliseconds{ _context->duration / AV_TIME_BASE * 1000 }; + } -AudioFile::MetadataMap -AudioFile::getMetaData() const -{ - MetadataMap res; + AudioFile::MetadataMap AudioFile::getMetaData() const + { + MetadataMap res; - getMetaDataFromDictionnary(_context->metadata, res); + getMetaDataFromDictionnary(_context->metadata, res); - // HACK for OGG files - // If we did not find tags, search metadata in streams - if (res.empty()) - { - for (std::size_t i {}; i < _context->nb_streams; ++i) - { - getMetaDataFromDictionnary(_context->streams[i]->metadata, res); + // HACK for OGG files + // If we did not find tags, search metadata in streams + if (res.empty()) + { + for (std::size_t i{}; i < _context->nb_streams; ++i) + { + getMetaDataFromDictionnary(_context->streams[i]->metadata, res); - if (!res.empty()) - break; - } - } + if (!res.empty()) + break; + } + } - return res; -} + return res; + } -std::vector -AudioFile::getStreamInfo() const -{ - std::vector res; + std::vector AudioFile::getStreamInfo() const + { + std::vector res; - for (std::size_t i {}; i < _context->nb_streams; ++i) - { - std::optional streamInfo {getStreamInfo(i)}; - if (streamInfo) - res.emplace_back(std::move(*streamInfo)); - } + for (std::size_t i{}; i < _context->nb_streams; ++i) + { + std::optional streamInfo{ getStreamInfo(i) }; + if (streamInfo) + res.emplace_back(std::move(*streamInfo)); + } - return res; -} + return res; + } -std::optional -AudioFile::getBestStreamIndex() const -{ - int res = ::av_find_best_stream(_context, - AVMEDIA_TYPE_AUDIO, - -1, // Auto - -1, // Auto - NULL, - 0); + std::optional AudioFile::getBestStreamIndex() const + { + int res = ::av_find_best_stream(_context, + AVMEDIA_TYPE_AUDIO, + -1, // Auto + -1, // Auto + NULL, + 0); - if (res < 0) - return std::nullopt; + if (res < 0) + return std::nullopt; - return res; -} + return res; + } -std::optional -AudioFile::getBestStreamInfo() const -{ - std::optional res; + std::optional AudioFile::getBestStreamInfo() const + { + std::optional res; - std::optional bestStreamIndex {getBestStreamIndex()}; - if (bestStreamIndex) - res = getStreamInfo(*bestStreamIndex); + std::optional bestStreamIndex{ getBestStreamIndex() }; + if (bestStreamIndex) + res = getStreamInfo(*bestStreamIndex); - return res; -} + return res; + } -bool -AudioFile::hasAttachedPictures() const -{ - for (std::size_t i = 0; i < _context->nb_streams; ++i) - { - if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) - return true; - } + bool AudioFile::hasAttachedPictures() const + { + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) + return true; + } - return false; -} + return false; + } -void -AudioFile::visitAttachedPictures(std::function func) const -{ - static const std::unordered_map codecMimeMap = - { - { AV_CODEC_ID_BMP, "image/x-bmp" }, - { AV_CODEC_ID_GIF, "image/gif" }, - { AV_CODEC_ID_MJPEG, "image/jpeg" }, - { AV_CODEC_ID_PNG, "image/png" }, - { AV_CODEC_ID_PNG, "image/x-png" }, - { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, - }; + void AudioFile::visitAttachedPictures(std::function func) const + { + static const std::unordered_map codecMimeMap = + { + { AV_CODEC_ID_BMP, "image/x-bmp" }, + { AV_CODEC_ID_GIF, "image/gif" }, + { AV_CODEC_ID_MJPEG, "image/jpeg" }, + { AV_CODEC_ID_PNG, "image/png" }, + { AV_CODEC_ID_PNG, "image/x-png" }, + { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, + }; - for (std::size_t i = 0; i < _context->nb_streams; ++i) - { - AVStream *avstream = _context->streams[i]; + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + AVStream* avstream = _context->streams[i]; - // Skip attached pics - if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) - continue; + // Skip attached pics + if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) + continue; - if (avstream->codecpar == nullptr) - { - LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; - continue; - } + if (avstream->codecpar == nullptr) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; + continue; + } - Picture picture; + Picture picture; - auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); - if (itMime != codecMimeMap.end()) - { - picture.mimeType = itMime->second; - } - else - { - picture.mimeType = "application/octet-stream"; - LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; - } + auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); + if (itMime != codecMimeMap.end()) + { + picture.mimeType = itMime->second; + } + else + { + picture.mimeType = "application/octet-stream"; + LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; + } - const AVPacket& pkt {avstream->attached_pic}; + const AVPacket& pkt{ avstream->attached_pic }; - picture.data = reinterpret_cast(pkt.data); - picture.dataSize = pkt.size; + picture.data = reinterpret_cast(pkt.data); + picture.dataSize = pkt.size; - func(picture); - } -} + func(picture); + } + } -std::optional -AudioFile::getStreamInfo(std::size_t streamIndex) const -{ - std::optional res; + std::optional AudioFile::getStreamInfo(std::size_t streamIndex) const + { + std::optional res; - AVStream* avstream { _context->streams[streamIndex]}; - assert(avstream); + AVStream* avstream{ _context->streams[streamIndex] }; + assert(avstream); - if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) - return res; + if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) + return res; - if (!avstream->codecpar) - { - LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; - return res; - } + if (!avstream->codecpar) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; + return res; + } - if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) - return res; + if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) + return res; - res.emplace(); - res->index = streamIndex; - res->bitrate = static_cast(avstream->codecpar->bit_rate); - res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); - assert(!res->codec.empty()); + res.emplace(); + res->index = streamIndex; + res->bitrate = static_cast(avstream->codecpar->bit_rate); + res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); + assert(!res->codec.empty()); - return res; -} + return res; + } -std::optional -guessAudioFileFormat(const std::filesystem::path& file) -{ - const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)}; - if (!format || !format->name) - return {}; + std::optional guessAudioFileFormat(const std::filesystem::path& file) + { + const AVOutputFormat* format{ ::av_guess_format(NULL, file.string().c_str(), NULL) }; + if (!format || !format->name) + { + LMS_LOG(AV, INFO) << "File '" << file.string() << "': cannot guess file format!"; + return std::nullopt; + } - LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'"; + LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'"; - auto formats {StringUtils::splitString(format->name, ",")}; - if (formats.size() > 1) - LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'"; + const auto formats{ StringUtils::splitString(format->name, ",") }; + if (formats.size() > 1) + LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'"; - std::vector mimeTypes; - if (format->mime_type) - mimeTypes = StringUtils::splitString(format->mime_type, ","); + std::vector mimeTypes; + if (format->mime_type) + mimeTypes = StringUtils::splitString(format->mime_type, ","); - if (mimeTypes.empty()) - LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!"; - else if (mimeTypes.size() > 1) - LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; + if (mimeTypes.empty()) + LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!"; + else if (mimeTypes.size() > 1) + LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; - AudioFileFormat res; - res.format = formats.front(); - res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); + AudioFileFormat res; + res.format = formats.front(); + res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); - return res; -} + return res; + } } // namespace Av diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp index 823b7e00..bd3224dd 100644 --- a/src/libs/av/impl/AudioFile.hpp +++ b/src/libs/av/impl/AudioFile.hpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -/* This file contains some classes in order to get info from file using the libavconv */ + /* This file contains some classes in order to get info from file using the libavconv */ #pragma once @@ -28,32 +28,30 @@ struct AVFormatContext; namespace Av { - class AudioFile final : public IAudioFile - { - public: - AudioFile(const std::filesystem::path& p); - ~AudioFile(); + class AudioFile final : public IAudioFile + { + public: + AudioFile(const std::filesystem::path& p); + ~AudioFile(); - AudioFile(const AudioFile&) = delete; - AudioFile(AudioFile&&) = delete; - AudioFile& operator=(const AudioFile&) = delete; - AudioFile& operator=(AudioFile&&) = delete; + const std::filesystem::path& getPath() const override; + std::chrono::milliseconds getDuration() const override; + MetadataMap getMetaData() const override; + std::vector getStreamInfo() const override; + std::optional getBestStreamInfo() const override; + std::optional getBestStreamIndex() const override; + bool hasAttachedPictures() const override; + void visitAttachedPictures(std::function func) const override; - const std::filesystem::path& getPath() const override; - std::chrono::milliseconds getDuration() const override; - MetadataMap getMetaData() const override; - std::vector getStreamInfo() const override; - std::optional getBestStreamInfo() const override; - std::optional getBestStreamIndex() const override; - bool hasAttachedPictures() const override; - void visitAttachedPictures(std::function func) const override; + private: + AudioFile(const AudioFile&) = delete; + AudioFile& operator=(const AudioFile&) = delete; - private: - std::optional getStreamInfo(std::size_t streamIndex) const; + std::optional getStreamInfo(std::size_t streamIndex) const; - const std::filesystem::path _p; - AVFormatContext* _context {}; - }; + const std::filesystem::path _p; + AVFormatContext* _context{}; + }; } // namespace Av diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index 8b3938f6..6c8c6296 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -187,14 +187,14 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug) const auto mediaFile {Av::parseAudioFile(p)}; // Stream info + if (const auto stream{ mediaFile->getBestStreamInfo() }) { - std::vector audioStreams; - - for (auto stream : mediaFile->getStreamInfo()) - { - MetaData::AudioStream audioStream {static_cast(stream.bitrate)}; - track.audioStreams.emplace_back(audioStream); - } + track.bitrate = stream->bitrate; + } + else + { + LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': cannot get best audio stream"; + return std::nullopt; } track.duration = mediaFile->getDuration(); diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index cfcaaeb7..1540142f 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -44,503 +44,479 @@ namespace MetaData { - -// TODO use string_views here for values -using TagMap = std::map>; - -template -std::vector -getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys) -{ - std::vector res; - - for (std::string_view key : keys) - { - const auto itValues {tags.find(std::string {key})}; - if (itValues == std::cend(tags)) - continue; - - const std::vector& values {itValues->second}; - if (values.empty()) - continue; - - res.reserve(values.size()); - - for (const auto& value : values) - { - std::optional val {StringUtils::readAs(value)}; - if (!val) - continue; - - res.emplace_back(std::move(*val)); - } - - break; - } - - return res; -} - -template -std::optional -getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys) -{ - std::optional res; - std::vector values {getPropertyValuesFirstMatchAs(tags, keys)}; - if (!values.empty()) - res = std::move(values.front()); - - return res; -} - -template -std::vector -getPropertyValuesAs(const TagMap& tags, std::string_view key) -{ - return getPropertyValuesFirstMatchAs(tags, {key}); -} - -template -std::optional -getPropertyValueAs(const TagMap& tags, std::string_view key) -{ - return getPropertyValueFirstMatchAs(tags, {key}); -} - -static -std::vector -splitAndTrimString(std::string_view str, std::string_view delimiters) -{ - std::vector strings {StringUtils::splitString(str, delimiters)}; - for (std::string_view& s : strings) - s = StringUtils::stringTrim(s); - - return strings; -} - -static -std::vector -getArtists(const TagMap& tags, - std::initializer_list artistTagNames, - std::initializer_list artistSortTagNames, - std::initializer_list artistMBIDTagNames - ) -{ - const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; - if (artistNames.empty()) - return {}; - - std::vector artists; - artists.reserve(artistNames.size()); - std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), - [&](std::string_view name) { return Artist {name}; }); - - { - const std::vector artistSortNames {getPropertyValuesFirstMatchAs(tags, artistSortTagNames)}; - if (artistSortNames.size() == artists.size()) - { - for (std::size_t i {}; i < artistSortNames.size(); ++i) - artists[i].sortName = artistSortNames[i]; - } - } - - { - const std::vector artistsMBID {getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames)}; - - if (artistNames.size() == artistsMBID.size()) - { - for (std::size_t i {}; i < artistsMBID.size(); ++i) - artists[i].mbid = artistsMBID[i]; - } - } - - - return artists; -} - -static -PerformerContainer -getPerformerArtists(const TagMap& tags, - std::initializer_list artistTagNames) -{ - PerformerContainer performers; - - // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) - // We may hit both styles for the same track - // PERFORMER: artist (role) - if (const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; !artistNames.empty()) - { - for (std::string_view entry : artistNames) - { - Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)}; - StringUtils::capitalize(performer.role); - performers[performer.role].push_back(std::move(performer.artist)); - } - } - // PERFORMER:role (MP3) - for (const auto& [key, values] : tags) - { - if (key.find("PERFORMER:") == 0) - { - std::string performerStr {key}; - std::string role; - if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos) - { - role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); - StringUtils::capitalize(role); - } - - for (const auto& value : values) - performers[role].push_back(Artist {value}); - } - } - - return performers; -} - -static -std::optional -getRelease(const TagMap& tags) -{ - std::optional release; - - auto releaseName {getPropertyValueAs(tags, "ALBUM")}; - if (!releaseName) - return release; - - release.emplace(); - release->name = std::move(*releaseName); - release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or(""); - release->mbid = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"}); - release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}); - release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL"); - if (!release->mediumCount) - { - // mediumCount may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "DISCNUMBER")}) - { - // Expecting 'Number/Total' - const std::vector strings {StringUtils::splitString(*value, "/") }; - if (strings.size() == 2) - release->mediumCount = StringUtils::readAs(strings[1]); - } - } - - release->primaryType = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}); - if (release->primaryType) - { - const auto secondaryTypes {getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})}; - release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); - } - - return release; -} - -static -std::optional -getMedium(const TagMap& tags) -{ - std::optional medium; - medium.emplace(); - - medium->type = getPropertyValueAs(tags, "MEDIA").value_or(""); - medium->name = getPropertyValueFirstMatchAs(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or(""); - medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL"); - if (!medium->trackCount) - { - // totalTracks may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "TRACKNUMBER")}) - { - // Expecting 'Number/Total' - const std::vector strings {StringUtils::splitString(*value, "/") }; - if (strings.size() == 2) - medium->trackCount = StringUtils::readAs(strings[1]); - } - } - // Expecting 'Number[/Total]' - medium->position = getPropertyValueAs(tags, "DISCNUMBER"); - medium->release = getRelease(tags); - medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN"); - - if (medium->type.empty() - && medium->name.empty() - && !medium->trackCount - && !medium->position - && !medium->release - && !medium->replayGain) - { - medium.reset(); - } - - return medium; -} - -static -TagLib::AudioProperties::ReadStyle -readStyleToTagLibReadStyle(ParserReadStyle readStyle) -{ - switch (readStyle) - { - case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; - case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; - case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; - } - - throw LmsException {"Cannot convert read style"}; -} - -TagLibParser::TagLibParser(ParserReadStyle readStyle) - : _readStyle {readStyleToTagLibReadStyle(readStyle)} -{ -} - -void -TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug) -{ - if (debug) - std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; - - if (tag.empty() || values.empty()) - return; - - std::string_view value {values.front()}; - - if (tag == "TITLE") - track.title = value; - else if (tag == "MUSICBRAINZ_RELEASETRACKID" - || tag == "MUSICBRAINZ RELEASE TRACK ID" - || tag == "MUSICBRAINZ/RELEASE TRACK ID") - { - track.mbid = UUID::fromString(value); - } - else if (tag == "MUSICBRAINZ_TRACKID" - || tag == "MUSICBRAINZ TRACK ID" - || tag == "MUSICBRAINZ/TRACK ID") - track.recordingMBID = UUID::fromString(value); - else if (tag == "ACOUSTID_ID") - track.acoustID = UUID::fromString(value); - else if (tag == "TRACKNUMBER") - { - // Expecting 'Number/Total' - track.position = StringUtils::readAs(value); - } - else if (tag == "DATE") - { - // Higher priority than YEAR - if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) - track.date = date; - } - else if (tag == "YEAR" && !track.date.isValid()) - { - // lower priority than DATE - track.date = Utils::parseDate(value); - } - else if (tag == "ORIGINALDATE") - { - // Higher priority than ORIGINALYEAR - if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) - track.originalDate = date; - } - else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) - { - // Lower priority than ORIGINALDATE - track.originalDate = Utils::parseDate(value); - } - else if (tag == "METADATA_BLOCK_PICTURE") - track.hasCover = true; - else if (tag == "COPYRIGHT") - track.copyright = value; - else if (tag == "COPYRIGHTURL") - track.copyrightURL = value; - else if (tag == "REPLAYGAIN_TRACK_GAIN") - track.replayGain = StringUtils::readAs(value); - else if (tag == "ARTIST") - track.artistDisplayName = value; - else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) - { - std::set clusterNames; - for (std::string_view valueList : values) - { - const std::vector splittedValues {splitAndTrimString(valueList, "/,;")}; - for (std::string_view value : splittedValues) - clusterNames.insert(std::string {value}); - } - - if (!clusterNames.empty()) - track.tags[tag] = std::move(clusterNames); - } -} - -static -TagMap -constructTagMap(const TagLib::PropertyMap& properties) -{ - TagMap tagMap; - - for (const auto& [propertyName, propertyValues] : properties) - { - std::vector& values {tagMap[propertyName.upper().to8Bit(true)]}; - for (const TagLib::String& propertyValue : propertyValues) - { - std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))}; - if (!trimedValue.empty()) - values.emplace_back(std::move(trimedValue)); - } - } - - return tagMap; -} - -static -void -mergeTagMaps(TagMap& dst, TagMap&& src) -{ - for (auto&& [tag, values] : src) - { - if (dst.find(tag) == std::cend(dst)) - dst[tag] = std::move(values); - } -} - -std::optional -TagLibParser::parse(const std::filesystem::path& p, bool debug) -{ - TagLib::FileRef f {p.string().c_str(), - true, // read audio properties - _readStyle}; - - if (f.isNull()) - { - LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; - return std::nullopt; - } - - if (!f.audioProperties()) - { - LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties"; - return std::nullopt; - } - - Track track; - - { - const TagLib::AudioProperties *properties {f.audioProperties() }; - - track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()}; - - MetaData::AudioStream audioStream {static_cast(properties->bitrate() * 1000)}; - track.audioStreams = {audioStream}; - } - - TagMap tags {constructTagMap(f.file()->properties())}; - - auto getAPETags = [&](const TagLib::APE::Tag* apeTag) - { - if (!apeTag) - return; - - mergeTagMaps(tags, constructTagMap(apeTag->properties())); - }; - - // Not that good embedded pictures handling - - // WMA - if (TagLib::ASF::File* asfFile {dynamic_cast(f.file())}) - { - const TagLib::ASF::Tag* tag {asfFile->tag()}; - if (tag) - { - if (tag->attributeListMap().contains("WM/Picture")) - track.hasCover = true; - - for (const auto& [name, attributeList] : tag->attributeListMap()) - { - std::string strName {StringUtils::stringToUpper(name.to8Bit(true))}; - if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) - continue; - - std::vector attributes; - for (const auto& attribute : attributeList) - { - if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) - attributes.emplace_back(attribute.toString().to8Bit(true)); - } - - if (!attributes.empty()) - { - if (debug) - std::cout << "ASF property: '" << name << "'" << std::endl; - - tags.emplace(strName, std::move(attributes)); - } - } - } - } - // MP3 - else if (TagLib::MPEG::File* mp3File {dynamic_cast(f.file())}) - { - if (mp3File->ID3v2Tag()) - { - const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()}; - - if (!frameListMap["APIC"].isEmpty()) - track.hasCover = true; - if (!frameListMap["TSST"].isEmpty()) - tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)}; - } - - getAPETags(mp3File->APETag()); - } - //MP4 - else if (TagLib::MP4::File* mp4File {dynamic_cast(f.file())}) - { - TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")}; - TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; - if (!coverArtList.isEmpty()) - track.hasCover = true; - } - // MPC - else if (TagLib::MPC::File* mpcFile {dynamic_cast(f.file())}) - { - getAPETags(mpcFile->APETag()); - } - // WavPack - else if (TagLib::WavPack::File* wavPackFile {dynamic_cast(f.file())}) - { - getAPETags(wavPackFile->APETag()); - } - // FLAC - else if (TagLib::FLAC::File* flacFile {dynamic_cast(f.file())}) - { - if (!flacFile->pictureList().isEmpty()) - track.hasCover = true; - } - else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast(f.file())}) - { - if (!vorbisFile->tag()->pictureList().isEmpty()) - track.hasCover = true; - } - else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast(f.file())}) - { - if (!opusFile->tag()->pictureList().isEmpty()) - track.hasCover = true; - } - - track.medium = getMedium(tags); - track.artists = getArtists(tags, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"}); - track.conductorArtists = getArtists(tags, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {}); - track.composerArtists = getArtists(tags, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {}); - track.lyricistArtists = getArtists(tags, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {}); - track.mixerArtists = getArtists(tags, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {}); - track.producerArtists = getArtists(tags, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {}); - track.remixerArtists = getArtists(tags, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {}); - track.performerArtists = getPerformerArtists(tags, {"PERFORMERS", "PERFORMER"}); - - for (const auto& [tag, values] : tags) - processTag(track, tag, values, debug); - - return track; -} + namespace + { + // TODO use string_views here for values + using TagMap = std::map>; + + template + std::vector getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys) + { + std::vector res; + + for (std::string_view key : keys) + { + const auto itValues{ tags.find(std::string {key}) }; + if (itValues == std::cend(tags)) + continue; + + const std::vector& values{ itValues->second }; + if (values.empty()) + continue; + + res.reserve(values.size()); + + for (const auto& value : values) + { + std::optional val{ StringUtils::readAs(value) }; + if (!val) + continue; + + res.emplace_back(std::move(*val)); + } + + break; + } + + return res; + } + + template + std::optional getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys) + { + std::optional res; + std::vector values{ getPropertyValuesFirstMatchAs(tags, keys) }; + if (!values.empty()) + res = std::move(values.front()); + + return res; + } + + template + std::vector getPropertyValuesAs(const TagMap& tags, std::string_view key) + { + return getPropertyValuesFirstMatchAs(tags, { key }); + } + + template + std::optional getPropertyValueAs(const TagMap& tags, std::string_view key) + { + return getPropertyValueFirstMatchAs(tags, { key }); + } + + std::vector splitAndTrimString(std::string_view str, std::string_view delimiters) + { + std::vector strings{ StringUtils::splitString(str, delimiters) }; + for (std::string_view& s : strings) + s = StringUtils::stringTrim(s); + + return strings; + } + + std::vector getArtists(const TagMap& tags, + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames + ) + { + const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; + if (artistNames.empty()) + return {}; + + std::vector artists; + artists.reserve(artistNames.size()); + std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), + [&](std::string_view name) { return Artist{ name }; }); + + { + const std::vector artistSortNames{ getPropertyValuesFirstMatchAs(tags, artistSortTagNames) }; + if (artistSortNames.size() == artists.size()) + { + for (std::size_t i{}; i < artistSortNames.size(); ++i) + artists[i].sortName = artistSortNames[i]; + } + } + + { + const std::vector artistsMBID{ getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames) }; + + if (artistNames.size() == artistsMBID.size()) + { + for (std::size_t i{}; i < artistsMBID.size(); ++i) + artists[i].mbid = artistsMBID[i]; + } + } + + + return artists; + } + + PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list artistTagNames) + { + PerformerContainer performers; + + // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) + // We may hit both styles for the same track + // PERFORMER: artist (role) + if (const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; !artistNames.empty()) + { + for (std::string_view entry : artistNames) + { + Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) }; + StringUtils::capitalize(performer.role); + performers[performer.role].push_back(std::move(performer.artist)); + } + } + // PERFORMER:role (MP3) + for (const auto& [key, values] : tags) + { + if (key.find("PERFORMER:") == 0) + { + std::string performerStr{ key }; + std::string role; + if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos) + { + role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); + StringUtils::capitalize(role); + } + + for (const auto& value : values) + performers[role].push_back(Artist{ value }); + } + } + + return performers; + } + + std::optional getRelease(const TagMap& tags) + { + std::optional release; + + auto releaseName{ getPropertyValueAs(tags, "ALBUM") }; + if (!releaseName) + return release; + + release.emplace(); + release->name = std::move(*releaseName); + release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or(""); + release->mbid = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" }); + release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" }); + release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL"); + if (!release->mediumCount) + { + // mediumCount may be encoded as "position/count" + if (const auto value{ getPropertyValueAs(tags, "DISCNUMBER") }) + { + // Expecting 'Number/Total' + const std::vector strings{ StringUtils::splitString(*value, "/") }; + if (strings.size() == 2) + release->mediumCount = StringUtils::readAs(strings[1]); + } + } + + release->primaryType = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" }); + if (release->primaryType) + { + const auto secondaryTypes{ getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) }; + release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); + } + + return release; + } + + std::optional getMedium(const TagMap& tags) + { + std::optional medium; + medium.emplace(); + + medium->type = getPropertyValueAs(tags, "MEDIA").value_or(""); + medium->name = getPropertyValueFirstMatchAs(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or(""); + medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL"); + if (!medium->trackCount) + { + // totalTracks may be encoded as "position/count" + if (const auto value{ getPropertyValueAs(tags, "TRACKNUMBER") }) + { + // Expecting 'Number/Total' + const std::vector strings{ StringUtils::splitString(*value, "/") }; + if (strings.size() == 2) + medium->trackCount = StringUtils::readAs(strings[1]); + } + } + // Expecting 'Number[/Total]' + medium->position = getPropertyValueAs(tags, "DISCNUMBER"); + medium->release = getRelease(tags); + medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN"); + + if (medium->type.empty() + && medium->name.empty() + && !medium->trackCount + && !medium->position + && !medium->release + && !medium->replayGain) + { + medium.reset(); + } + + return medium; + } + + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle) + { + switch (readStyle) + { + case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; + case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; + case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; + } + + throw LmsException{ "Cannot convert read style" }; + } + + TagMap constructTagMap(const TagLib::PropertyMap& properties) + { + TagMap tagMap; + + for (const auto& [propertyName, propertyValues] : properties) + { + std::vector& values{ tagMap[propertyName.upper().to8Bit(true)] }; + for (const TagLib::String& propertyValue : propertyValues) + { + std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) }; + if (!trimedValue.empty()) + values.emplace_back(std::move(trimedValue)); + } + } + + return tagMap; + } + + void mergeTagMaps(TagMap& dst, TagMap&& src) + { + for (auto&& [tag, values] : src) + { + if (dst.find(tag) == std::cend(dst)) + dst[tag] = std::move(values); + } + } + + } + + TagLibParser::TagLibParser(ParserReadStyle readStyle) + : _readStyle{ readStyleToTagLibReadStyle(readStyle) } + { + } + + void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug) + { + if (debug) + std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; + + if (tag.empty() || values.empty()) + return; + + std::string_view value{ values.front() }; + + if (tag == "TITLE") + track.title = value; + else if (tag == "MUSICBRAINZ_RELEASETRACKID" + || tag == "MUSICBRAINZ RELEASE TRACK ID" + || tag == "MUSICBRAINZ/RELEASE TRACK ID") + { + track.mbid = UUID::fromString(value); + } + else if (tag == "MUSICBRAINZ_TRACKID" + || tag == "MUSICBRAINZ TRACK ID" + || tag == "MUSICBRAINZ/TRACK ID") + track.recordingMBID = UUID::fromString(value); + else if (tag == "ACOUSTID_ID") + track.acoustID = UUID::fromString(value); + else if (tag == "TRACKNUMBER") + { + // Expecting 'Number/Total' + track.position = StringUtils::readAs(value); + } + else if (tag == "DATE") + { + // Higher priority than YEAR + if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) + track.date = date; + } + else if (tag == "YEAR" && !track.date.isValid()) + { + // lower priority than DATE + track.date = Utils::parseDate(value); + } + else if (tag == "ORIGINALDATE") + { + // Higher priority than ORIGINALYEAR + if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) + track.originalDate = date; + } + else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) + { + // Lower priority than ORIGINALDATE + track.originalDate = Utils::parseDate(value); + } + else if (tag == "METADATA_BLOCK_PICTURE") + track.hasCover = true; + else if (tag == "COPYRIGHT") + track.copyright = value; + else if (tag == "COPYRIGHTURL") + track.copyrightURL = value; + else if (tag == "REPLAYGAIN_TRACK_GAIN") + track.replayGain = StringUtils::readAs(value); + else if (tag == "ARTIST") + track.artistDisplayName = value; + else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) + { + std::set clusterNames; + for (std::string_view valueList : values) + { + const std::vector splittedValues{ splitAndTrimString(valueList, "/,;") }; + for (std::string_view value : splittedValues) + clusterNames.insert(std::string{ value }); + } + + if (!clusterNames.empty()) + track.tags[tag] = std::move(clusterNames); + } + } + + std::optional TagLibParser::parse(const std::filesystem::path& p, bool debug) + { + TagLib::FileRef f{ p.string().c_str(), + true, // read audio properties + _readStyle }; + + if (f.isNull()) + { + LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; + return std::nullopt; + } + + Track track; + + if (const TagLib::AudioProperties* properties{ f.audioProperties() }) + { + track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; + track.bitrate = static_cast(properties->bitrate() * 1000); + } + else + { + LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties"; + return std::nullopt; + } + + TagMap tags{ constructTagMap(f.file()->properties()) }; + + auto getAPETags = [&](const TagLib::APE::Tag* apeTag) + { + if (!apeTag) + return; + + mergeTagMaps(tags, constructTagMap(apeTag->properties())); + }; + + // Not that good embedded pictures handling + + // WMA + if (TagLib::ASF::File * asfFile{ dynamic_cast(f.file()) }) + { + const TagLib::ASF::Tag* tag{ asfFile->tag() }; + if (tag) + { + if (tag->attributeListMap().contains("WM/Picture")) + track.hasCover = true; + + for (const auto& [name, attributeList] : tag->attributeListMap()) + { + std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) }; + if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) + continue; + + std::vector attributes; + for (const auto& attribute : attributeList) + { + if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) + attributes.emplace_back(attribute.toString().to8Bit(true)); + } + + if (!attributes.empty()) + { + if (debug) + std::cout << "ASF property: '" << name << "'" << std::endl; + + tags.emplace(strName, std::move(attributes)); + } + } + } + } + // MP3 + else if (TagLib::MPEG::File * mp3File{ dynamic_cast(f.file()) }) + { + if (mp3File->ID3v2Tag()) + { + const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() }; + + if (!frameListMap["APIC"].isEmpty()) + track.hasCover = true; + if (!frameListMap["TSST"].isEmpty()) + tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) }; + } + + getAPETags(mp3File->APETag()); + } + //MP4 + else if (TagLib::MP4::File * mp4File{ dynamic_cast(f.file()) }) + { + TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; + TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; + if (!coverArtList.isEmpty()) + track.hasCover = true; + } + // MPC + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(f.file()) }) + { + getAPETags(mpcFile->APETag()); + } + // WavPack + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(f.file()) }) + { + getAPETags(wavPackFile->APETag()); + } + // FLAC + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(f.file()) }) + { + if (!flacFile->pictureList().isEmpty()) + track.hasCover = true; + } + else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(f.file()) }) + { + if (!vorbisFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } + else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(f.file()) }) + { + if (!opusFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } + + track.medium = getMedium(tags); + track.artists = getArtists(tags, { "ARTISTS", "ARTIST" }, { "ARTISTSORT" }, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" }); + track.conductorArtists = getArtists(tags, { "CONDUCTORS", "CONDUCTOR" }, { "CONDUCTORSSORT", "CONDUCTORSORT" }, {}); + track.composerArtists = getArtists(tags, { "COMPOSERS", "COMPOSER" }, { "COMPOSERSSORT", "COMPOSERSORT" }, {}); + track.lyricistArtists = getArtists(tags, { "LYRICISTS", "LYRICIST" }, { "LYRICISTSSORT", "LYRICISTSORT" }, {}); + track.mixerArtists = getArtists(tags, { "MIXERS", "MIXER" }, { "MIXERSSORT", "MIXERSORT" }, {}); + track.producerArtists = getArtists(tags, { "PRODUCERS", "PRODUCER" }, { "PRODUCERSSORT", "PRODUCERSORT" }, {}); + track.remixerArtists = getArtists(tags, { "REMIXERS", "REMIXER", "ModifiedBy" }, { "REMIXERSSORT", "REMIXERSORT" }, {}); + track.performerArtists = getPerformerArtists(tags, { "PERFORMERS", "PERFORMER" }); + + for (const auto& [tag, values] : tags) + processTag(track, tag, values, debug); + + return track; + } } // namespace MetaData diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index bb2b0274..6db82619 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -34,126 +34,121 @@ namespace MetaData { - using Tags = std::map /* names */>; + using Tags = std::map /* names */>; - // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema + // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema - struct Artist - { - std::optional mbid; - std::string name; - std::optional sortName; + struct Artist + { + std::optional mbid; + std::string name; + std::optional sortName; - Artist(std::string_view _name) : name {_name} {} - Artist(std::optional _mbid, std::string_view _name, std::optional _sortName) : mbid {std::move(_mbid)}, name {_name}, sortName {std::move(_sortName)} {} - }; + Artist(std::string_view _name) : name{ _name } {} + Artist(std::optional _mbid, std::string_view _name, std::optional _sortName) : mbid{ std::move(_mbid) }, name{ _name }, sortName{ std::move(_sortName) } {} + }; - using PerformerContainer = std::map>; + using PerformerContainer = std::map>; - struct Release - { - // see https://musicbrainz.org/doc/Release_Group/Type - enum class PrimaryType - { - Album, - Single, - EP, - Broadcast, - Other - }; + struct Release + { + // see https://musicbrainz.org/doc/Release_Group/Type + enum class PrimaryType + { + Album, + Single, + EP, + Broadcast, + Other + }; - enum class SecondaryType - { - Compilation, - Soundtrack, - Spokenword, - Interview, - Audiobook, - AudioDrama, - Live, - Remix, - DJMix, - Mixtape_Street, - Demo, - }; + enum class SecondaryType + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + }; - std::optional mbid; - std::string name; - std::string artistDisplayName; - std::vector artists; - std::optional mediumCount; - std::optional primaryType; - EnumSet secondaryTypes; - }; + std::optional mbid; + std::string name; + std::string artistDisplayName; + std::vector artists; + std::optional mediumCount; + std::optional primaryType; + EnumSet secondaryTypes; + }; - struct Medium - { - std::string type; - std::string name; - std::optional release; - std::optional position; // in release - std::optional trackCount; - std::optional replayGain; - }; + struct Medium + { + std::string type; + std::string name; + std::optional release; + std::optional position; // in release + std::optional trackCount; + std::optional replayGain; + }; - struct AudioStream - { - unsigned bitRate; - }; + struct Track + { + std::optional mbid; + std::optional recordingMBID; + std::string title; + std::optional medium; + std::optional position; // in medium + Tags tags; + std::chrono::milliseconds duration{}; + std::size_t bitrate{}; + Wt::WDate date; + Wt::WDate originalDate; + bool hasCover{}; + std::optional acoustID; + std::string copyright; + std::string copyrightURL; + std::optional replayGain; + std::string artistDisplayName; + std::vector artists; + std::vector conductorArtists; + std::vector composerArtists; + std::vector lyricistArtists; + std::vector mixerArtists; + PerformerContainer performerArtists; + std::vector producerArtists; + std::vector remixerArtists; + }; - struct Track - { - std::optional mbid; - std::optional recordingMBID; - std::string title; - std::optional medium; - std::optional position; // in medium - Tags tags; - std::chrono::milliseconds duration; - Wt::WDate date; - Wt::WDate originalDate; - bool hasCover {}; - std::vector audioStreams; - std::optional acoustID; - std::string copyright; - std::string copyrightURL; - std::optional replayGain; - std::string artistDisplayName; - std::vector artists; - std::vector conductorArtists; - std::vector composerArtists; - std::vector lyricistArtists; - std::vector mixerArtists; - PerformerContainer performerArtists; - std::vector producerArtists; - std::vector remixerArtists; - }; + class IParser + { + public: + virtual ~IParser() = default; - class IParser - { - public: - virtual ~IParser() = default; + virtual std::optional parse(const std::filesystem::path& p, bool debug = false) = 0; - virtual std::optional parse(const std::filesystem::path& p, bool debug = false) = 0; + void setClusterTypeNames(const std::set& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } - void setClusterTypeNames(const std::set& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } + protected: + std::set _clusterTypeNames; + }; - protected: - std::set _clusterTypeNames; - }; + enum class ParserType + { + TagLib, + AvFormat, + }; - enum class ParserType - { - TagLib, - AvFormat, - }; - - enum class ParserReadStyle - { - Fast, - Average, - Accurate, - }; - std::unique_ptr createParser(ParserType parserType, ParserReadStyle parserReadStyle); + enum class ParserReadStyle + { + Fast, + Average, + Accurate, + }; + std::unique_ptr createParser(ParserType parserType, ParserReadStyle parserReadStyle); } // namespace MetaData diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index 06639393..044ebdbe 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -234,6 +234,15 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( ScanSettings::get(session).modify()->incScanVersion(); } + static void migrateFromV44(Session& session) + { + // add bitrate + session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -256,6 +265,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {41, migrateFromV41}, {42, migrateFromV42}, {43, migrateFromV43}, + {44, migrateFromV44}, }; { diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 1c671b51..bcebebea 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION{ 44 }; + static constexpr Version LMS_DATABASE_VERSION{ 45 }; class VersionInfo { public: diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 5ca1bfa4..f3091eb6 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -387,9 +387,8 @@ namespace Database Wt::Dbo::collection copyrights = session()->query ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") - .groupBy("copyright_url") - .bind(getId()); + .where("r.id = ?").bind(getId()) + .groupBy("copyright_url"); std::vector values(copyrights.begin(), copyrights.end()); @@ -400,6 +399,16 @@ namespace Database return values.front(); } + std::size_t Release::getMeanBitrate() const + { + assert(session()); + + return session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") + .where("release_id = ?").bind(getId()) + .where("bitrate > 0") + .resultValue(); + } + std::vector Release::getArtists(TrackArtistLinkType linkType) const { assert(session()); diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 2c943227..92bf6ffd 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -104,6 +104,7 @@ namespace Database Wt::WDate getOriginalReleaseDate() const; std::optional getCopyright() const; std::optional getCopyrightURL() const; + std::size_t getMeanBitrate() const; // Accessors const std::string& getName() const { return _name; } diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index 07de5a7c..3828672e 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -128,6 +128,7 @@ namespace Database { void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } + void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } void setDate(const Wt::WDate& date) { _date = date; } @@ -153,6 +154,7 @@ namespace Database { std::string getName() const { return _name; } std::filesystem::path getPath() const { return _filePath; } std::chrono::milliseconds getDuration() const { return _duration; } + std::size_t getBitrate() const { return _bitrate; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } std::optional getYear() const; std::optional getOriginalYear() const; @@ -186,6 +188,7 @@ namespace Database { Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _duration, "duration"); + Wt::Dbo::field(a, _bitrate, "bitrate"); Wt::Dbo::field(a, _date, "date"); Wt::Dbo::field(a, _originalDate, "original_date"); Wt::Dbo::field(a, _filePath, "file_path"); @@ -220,6 +223,7 @@ namespace Database { std::string _discSubtitle; std::string _name; std::chrono::duration _duration{}; + int _bitrate; // in bps Wt::WDate _date; Wt::WDate _originalDate; std::string _filePath; diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index a07a6e62..9d8e2825 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -547,7 +547,7 @@ TEST_F(DatabaseFixture, Release_releaseType) } } -TEST_F(DatabaseFixture, ReleaseSortOrder) +TEST_F(DatabaseFixture, Release_sortMethod) { ScopedRelease release1{ session, "MyRelease1" }; const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} }; @@ -616,3 +616,42 @@ TEST_F(DatabaseFixture, ReleaseSortOrder) } } + +TEST_F(DatabaseFixture, Release_meanBitrate) +{ + ScopedRelease release1{ session, "MyRelease1" }; + ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track2{ session, "MyTrack2" }; + ScopedTrack track3{ session, "MyTrack3" }; + + auto checkExpectedBitrate = [&](std::size_t bitrate) + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(release1->getMeanBitrate(), bitrate); + }; + + checkExpectedBitrate(0); + + { + auto transaction{ session.createUniqueTransaction() }; + track1.get().modify()->setBitrate(128); + track1.get().modify()->setRelease(release1.get()); + } + + checkExpectedBitrate(128); + + { + auto transaction{ session.createUniqueTransaction() }; + track2.get().modify()->setBitrate(256); + track2.get().modify()->setRelease(release1.get()); + } + checkExpectedBitrate(192); + + { + auto transaction{ session.createUniqueTransaction() }; + track3.get().modify()->setBitrate(0); + track3.get().modify()->setRelease(release1.get()); + } + checkExpectedBitrate(192); // 0 should not be taken into account + +} \ No newline at end of file diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index 77157b6b..74a33540 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -405,22 +405,7 @@ namespace Scanner } } - // We estimate this is an audio file if: - // - we found a least one audio stream - // - the duration is not null - if (trackInfo->audioStreams.empty()) - { - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)"; - - // If Track exists here, delete it! - if (track) - { - track.remove(); - stats.deletions++; - } - stats.errors.emplace_back(ScanError{ file, ScanErrorType::NoAudioTrack }); - return; - } + // We estimate this is an audio file if the duration is not null if (trackInfo->duration == std::chrono::milliseconds::zero()) { LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; @@ -513,6 +498,7 @@ namespace Scanner track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); track.modify()->setDuration(trackInfo->duration); + track.modify()->setBitrate(trackInfo->bitrate); track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); track.modify()->setTrackNumber(trackInfo->position); track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index ac86bfb9..edc091be 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -148,6 +148,7 @@ namespace API::Subsonic } trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); + trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 042e9c3c..6ed8bc64 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -137,7 +137,7 @@ namespace UserInterface } } - // TODO: save in DB and mean all this + // TODO: save in DB and aggregate all this for (const Track::pointer& track : Track::find(LmsApp->getDbSession(), Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 })).results) { if (const auto audioFile{ Av::parseAudioFile(track->getPath()) }) @@ -147,16 +147,17 @@ namespace UserInterface { releaseInfo->setCondition("if-has-codec", true); releaseInfo->bindString("codec", audioStream->codec); - if (audioStream->bitrate) - { - releaseInfo->setCondition("if-has-bitrate", true); - releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps"); - break; - } + break; } } } + if (std::size_t meanBitrate{ release->getMeanBitrate() }) + { + releaseInfo->setCondition("if-has-bitrate", true); + releaseInfo->bindString("bitrate", std::to_string(release->getMeanBitrate() / 1000) + " kbps"); + } + Wt::WPushButton* okBtn{ releaseInfo->bindNew("ok-btn", Wt::WString::tr("Lms.ok")) }; okBtn->clicked().connect([=] { diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 75cc4f68..c755a6af 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -43,219 +43,217 @@ #include "ModalManager.hpp" #include "Utils.hpp" -using namespace Database; - namespace UserInterface::TrackListHelpers { - void - showTrackInfoModal(Database::TrackId trackId, Filters& filters) - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + using namespace Database; - const Database::Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; - if (!track) - return; + void showTrackInfoModal(Database::TrackId trackId, Filters& filters) + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; - auto trackInfo {std::make_unique