Subsonic API: added bitrate support, ref #363

This commit is contained in:
emeric
2023-11-11 15:13:46 +01:00
parent afcb4d96ca
commit 1d21ba41b7
16 changed files with 1063 additions and 1053 deletions
+205 -211
View File
@@ -32,267 +32,261 @@ extern "C"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
namespace Av { namespace Av
static std::string averror_to_string(int error)
{ {
std::array<char, 128> buf = {0}; namespace
{
std::string averror_to_string(int error)
{
std::array<char, 128> buf = { 0 };
if (::av_strerror(error, buf.data(), buf.size()) == 0) if (::av_strerror(error, buf.data(), buf.size()) == 0)
return &buf[0]; return &buf[0];
else else
return "Unknown error"; return "Unknown error";
} }
class AudioFileException : public Av::Exception class AudioFileException : public Av::Exception
{ {
public: public:
AudioFileException(int avError) AudioFileException(int avError)
: Av::Exception {"AudioFileException: " + averror_to_string(avError)} : Av::Exception{ "AudioFileException: " + averror_to_string(avError) }
{} {}
}; };
std::unique_ptr<IAudioFile> void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
parseAudioFile(const std::filesystem::path& p) {
{ if (!dictionnary)
return std::make_unique<AudioFile>(p); return;
}
AudioFile::AudioFile(const std::filesystem::path& p) AVDictionaryEntry* tag = NULL;
: _p {p} while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
{ {
int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)}; res[StringUtils::stringToUpper(tag->key)] = tag->value;
if (error < 0) }
{ }
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); }
throw AudioFileException {error};
}
error = avformat_find_stream_info(_context, nullptr); std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
if (error < 0) {
{ return std::make_unique<AudioFile>(p);
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); }
avformat_close_input(&_context);
throw AudioFileException {error};
}
}
AudioFile::~AudioFile() AudioFile::AudioFile(const std::filesystem::path& p)
{ : _p{ p }
avformat_close_input(&_context); {
} 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& error = avformat_find_stream_info(_context, nullptr);
AudioFile::getPath() const if (error < 0)
{ {
return _p; 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::~AudioFile()
AudioFile::getDuration() const {
{ avformat_close_input(&_context);
if (_context->duration == AV_NOPTS_VALUE) }
return std::chrono::milliseconds {0}; // TODO estimate
return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000}; const std::filesystem::path& AudioFile::getPath() const
} {
return _p;
}
void std::chrono::milliseconds AudioFile::getDuration() const
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) {
{ if (_context->duration == AV_NOPTS_VALUE)
if (!dictionnary) return std::chrono::milliseconds{ 0 }; // TODO estimate
return;
AVDictionaryEntry *tag = NULL; return std::chrono::milliseconds{ _context->duration / AV_TIME_BASE * 1000 };
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) }
{
res[StringUtils::stringToUpper(tag->key)] = tag->value;
}
}
AudioFile::MetadataMap AudioFile::MetadataMap AudioFile::getMetaData() const
AudioFile::getMetaData() const {
{ MetadataMap res;
MetadataMap res;
getMetaDataFromDictionnary(_context->metadata, res); getMetaDataFromDictionnary(_context->metadata, res);
// HACK for OGG files // HACK for OGG files
// If we did not find tags, search metadata in streams // If we did not find tags, search metadata in streams
if (res.empty()) if (res.empty())
{ {
for (std::size_t i {}; i < _context->nb_streams; ++i) for (std::size_t i{}; i < _context->nb_streams; ++i)
{ {
getMetaDataFromDictionnary(_context->streams[i]->metadata, res); getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
if (!res.empty()) if (!res.empty())
break; break;
} }
} }
return res; return res;
} }
std::vector<StreamInfo> std::vector<StreamInfo> AudioFile::getStreamInfo() const
AudioFile::getStreamInfo() const {
{ std::vector<StreamInfo> res;
std::vector<StreamInfo> res;
for (std::size_t i {}; i < _context->nb_streams; ++i) for (std::size_t i{}; i < _context->nb_streams; ++i)
{ {
std::optional<StreamInfo> streamInfo {getStreamInfo(i)}; std::optional<StreamInfo> streamInfo{ getStreamInfo(i) };
if (streamInfo) if (streamInfo)
res.emplace_back(std::move(*streamInfo)); res.emplace_back(std::move(*streamInfo));
} }
return res; return res;
} }
std::optional<std::size_t> std::optional<std::size_t> AudioFile::getBestStreamIndex() const
AudioFile::getBestStreamIndex() const {
{ int res = ::av_find_best_stream(_context,
int res = ::av_find_best_stream(_context, AVMEDIA_TYPE_AUDIO,
AVMEDIA_TYPE_AUDIO, -1, // Auto
-1, // Auto -1, // Auto
-1, // Auto NULL,
NULL, 0);
0);
if (res < 0) if (res < 0)
return std::nullopt; return std::nullopt;
return res; return res;
} }
std::optional<StreamInfo> std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
AudioFile::getBestStreamInfo() const {
{ std::optional<StreamInfo> res;
std::optional<StreamInfo> res;
std::optional<std::size_t> bestStreamIndex {getBestStreamIndex()}; std::optional<std::size_t> bestStreamIndex{ getBestStreamIndex() };
if (bestStreamIndex) if (bestStreamIndex)
res = getStreamInfo(*bestStreamIndex); res = getStreamInfo(*bestStreamIndex);
return res; return res;
} }
bool bool AudioFile::hasAttachedPictures() const
AudioFile::hasAttachedPictures() const {
{ for (std::size_t i = 0; i < _context->nb_streams; ++i)
for (std::size_t i = 0; i < _context->nb_streams; ++i) {
{ if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) return true;
return true; }
}
return false; return false;
} }
void void AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const {
{ static const std::unordered_map<int, std::string> codecMimeMap =
static const std::unordered_map<int, std::string> codecMimeMap = {
{ { AV_CODEC_ID_BMP, "image/x-bmp" },
{ AV_CODEC_ID_BMP, "image/x-bmp" }, { AV_CODEC_ID_GIF, "image/gif" },
{ AV_CODEC_ID_GIF, "image/gif" }, { AV_CODEC_ID_MJPEG, "image/jpeg" },
{ AV_CODEC_ID_MJPEG, "image/jpeg" }, { AV_CODEC_ID_PNG, "image/png" },
{ AV_CODEC_ID_PNG, "image/png" }, { AV_CODEC_ID_PNG, "image/x-png" },
{ AV_CODEC_ID_PNG, "image/x-png" }, { AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, };
};
for (std::size_t i = 0; i < _context->nb_streams; ++i) for (std::size_t i = 0; i < _context->nb_streams; ++i)
{ {
AVStream *avstream = _context->streams[i]; AVStream* avstream = _context->streams[i];
// Skip attached pics // Skip attached pics
if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
continue; continue;
if (avstream->codecpar == nullptr) if (avstream->codecpar == nullptr)
{ {
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
continue; continue;
} }
Picture picture; Picture picture;
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end()) if (itMime != codecMimeMap.end())
{ {
picture.mimeType = itMime->second; picture.mimeType = itMime->second;
} }
else else
{ {
picture.mimeType = "application/octet-stream"; picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; 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<const std::byte*>(pkt.data); picture.data = reinterpret_cast<const std::byte*>(pkt.data);
picture.dataSize = pkt.size; picture.dataSize = pkt.size;
func(picture); func(picture);
} }
} }
std::optional<StreamInfo> std::optional<StreamInfo> AudioFile::getStreamInfo(std::size_t streamIndex) const
AudioFile::getStreamInfo(std::size_t streamIndex) const {
{ std::optional<StreamInfo> res;
std::optional<StreamInfo> res;
AVStream* avstream { _context->streams[streamIndex]}; AVStream* avstream{ _context->streams[streamIndex] };
assert(avstream); assert(avstream);
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
return res; return res;
if (!avstream->codecpar) if (!avstream->codecpar)
{ {
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set"; LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
return res; return res;
} }
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
return res; return res;
res.emplace(); res.emplace();
res->index = streamIndex; res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate); res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codec.empty()); assert(!res->codec.empty());
return res; return res;
} }
std::optional<AudioFileFormat> std::optional<AudioFileFormat> guessAudioFileFormat(const std::filesystem::path& file)
guessAudioFileFormat(const std::filesystem::path& file) {
{ const AVOutputFormat* format{ ::av_guess_format(NULL, file.string().c_str(), NULL) };
const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)}; if (!format || !format->name)
if (!format || !format->name) {
return {}; 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, ",")}; const auto formats{ StringUtils::splitString(format->name, ",") };
if (formats.size() > 1) if (formats.size() > 1)
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'"; LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'";
std::vector<std::string_view> mimeTypes; std::vector<std::string_view> mimeTypes;
if (format->mime_type) if (format->mime_type)
mimeTypes = StringUtils::splitString(format->mime_type, ","); mimeTypes = StringUtils::splitString(format->mime_type, ",");
if (mimeTypes.empty()) if (mimeTypes.empty())
LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!"; LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!";
else if (mimeTypes.size() > 1) else if (mimeTypes.size() > 1)
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'";
AudioFileFormat res; AudioFileFormat res;
res.format = formats.front(); res.format = formats.front();
res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front();
return res; return res;
} }
} // namespace Av } // namespace Av
+21 -23
View File
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* 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 #pragma once
@@ -28,32 +28,30 @@ struct AVFormatContext;
namespace Av namespace Av
{ {
class AudioFile final : public IAudioFile class AudioFile final : public IAudioFile
{ {
public: public:
AudioFile(const std::filesystem::path& p); AudioFile(const std::filesystem::path& p);
~AudioFile(); ~AudioFile();
AudioFile(const AudioFile&) = delete; const std::filesystem::path& getPath() const override;
AudioFile(AudioFile&&) = delete; std::chrono::milliseconds getDuration() const override;
AudioFile& operator=(const AudioFile&) = delete; MetadataMap getMetaData() const override;
AudioFile& operator=(AudioFile&&) = delete; std::vector<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
const std::filesystem::path& getPath() const override; private:
std::chrono::milliseconds getDuration() const override; AudioFile(const AudioFile&) = delete;
MetadataMap getMetaData() const override; AudioFile& operator=(const AudioFile&) = delete;
std::vector<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
private: std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p; const std::filesystem::path _p;
AVFormatContext* _context {}; AVFormatContext* _context{};
}; };
} // namespace Av } // namespace Av
+7 -7
View File
@@ -187,14 +187,14 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
const auto mediaFile {Av::parseAudioFile(p)}; const auto mediaFile {Av::parseAudioFile(p)};
// Stream info // Stream info
if (const auto stream{ mediaFile->getBestStreamInfo() })
{ {
std::vector<AudioStream> audioStreams; track.bitrate = stream->bitrate;
}
for (auto stream : mediaFile->getStreamInfo()) else
{ {
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)}; LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': cannot get best audio stream";
track.audioStreams.emplace_back(audioStream); return std::nullopt;
}
} }
track.duration = mediaFile->getDuration(); track.duration = mediaFile->getDuration();
+473 -497
View File
@@ -44,503 +44,479 @@
namespace MetaData namespace MetaData
{ {
namespace
// TODO use string_views here for values {
using TagMap = std::map<std::string, std::vector<std::string>>; // TODO use string_views here for values
using TagMap = std::map<std::string, std::vector<std::string>>;
template<typename T>
std::vector<T> template<typename T>
getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys) std::vector<T> getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
{ {
std::vector<T> res; std::vector<T> res;
for (std::string_view key : keys) for (std::string_view key : keys)
{ {
const auto itValues {tags.find(std::string {key})}; const auto itValues{ tags.find(std::string {key}) };
if (itValues == std::cend(tags)) if (itValues == std::cend(tags))
continue; continue;
const std::vector<std::string>& values {itValues->second}; const std::vector<std::string>& values{ itValues->second };
if (values.empty()) if (values.empty())
continue; continue;
res.reserve(values.size()); res.reserve(values.size());
for (const auto& value : values) for (const auto& value : values)
{ {
std::optional<T> val {StringUtils::readAs<T>(value)}; std::optional<T> val{ StringUtils::readAs<T>(value) };
if (!val) if (!val)
continue; continue;
res.emplace_back(std::move(*val)); res.emplace_back(std::move(*val));
} }
break; break;
} }
return res; return res;
} }
template <typename T> template <typename T>
std::optional<T> std::optional<T> getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys) {
{ std::optional<T> res;
std::optional<T> res; std::vector<T> values{ getPropertyValuesFirstMatchAs<T>(tags, keys) };
std::vector<T> values {getPropertyValuesFirstMatchAs<T>(tags, keys)}; if (!values.empty())
if (!values.empty()) res = std::move(values.front());
res = std::move(values.front());
return res;
return res; }
}
template <typename T>
template <typename T> std::vector<T> getPropertyValuesAs(const TagMap& tags, std::string_view key)
std::vector<T> {
getPropertyValuesAs(const TagMap& tags, std::string_view key) return getPropertyValuesFirstMatchAs<T>(tags, { key });
{ }
return getPropertyValuesFirstMatchAs<T>(tags, {key});
} template <typename T>
std::optional<T> getPropertyValueAs(const TagMap& tags, std::string_view key)
template <typename T> {
std::optional<T> return getPropertyValueFirstMatchAs<T>(tags, { key });
getPropertyValueAs(const TagMap& tags, std::string_view key) }
{
return getPropertyValueFirstMatchAs<T>(tags, {key}); std::vector<std::string_view> splitAndTrimString(std::string_view str, std::string_view delimiters)
} {
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
static for (std::string_view& s : strings)
std::vector<std::string_view> s = StringUtils::stringTrim(s);
splitAndTrimString(std::string_view str, std::string_view delimiters)
{ return strings;
std::vector<std::string_view> strings {StringUtils::splitString(str, delimiters)}; }
for (std::string_view& s : strings)
s = StringUtils::stringTrim(s); std::vector<Artist> getArtists(const TagMap& tags,
std::initializer_list<std::string_view> artistTagNames,
return strings; std::initializer_list<std::string_view> artistSortTagNames,
} std::initializer_list<std::string_view> artistMBIDTagNames
)
static {
std::vector<Artist> const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) };
getArtists(const TagMap& tags, if (artistNames.empty())
std::initializer_list<std::string_view> artistTagNames, return {};
std::initializer_list<std::string_view> artistSortTagNames,
std::initializer_list<std::string_view> artistMBIDTagNames std::vector<Artist> artists;
) artists.reserve(artistNames.size());
{ std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
const std::vector<std::string_view> artistNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames)}; [&](std::string_view name) { return Artist{ name }; });
if (artistNames.empty())
return {}; {
const std::vector<std::string_view> artistSortNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistSortTagNames) };
std::vector<Artist> artists; if (artistSortNames.size() == artists.size())
artists.reserve(artistNames.size()); {
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), for (std::size_t i{}; i < artistSortNames.size(); ++i)
[&](std::string_view name) { return Artist {name}; }); artists[i].sortName = artistSortNames[i];
}
{ }
const std::vector<std::string_view> artistSortNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistSortTagNames)};
if (artistSortNames.size() == artists.size()) {
{ const std::vector<UUID> artistsMBID{ getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames) };
for (std::size_t i {}; i < artistSortNames.size(); ++i)
artists[i].sortName = artistSortNames[i]; if (artistNames.size() == artistsMBID.size())
} {
} for (std::size_t i{}; i < artistsMBID.size(); ++i)
artists[i].mbid = artistsMBID[i];
{ }
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames)}; }
if (artistNames.size() == artistsMBID.size())
{ return artists;
for (std::size_t i {}; i < artistsMBID.size(); ++i) }
artists[i].mbid = artistsMBID[i];
} PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list<std::string_view> artistTagNames)
} {
PerformerContainer performers;
return artists; // 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)
static if (const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) }; !artistNames.empty())
PerformerContainer {
getPerformerArtists(const TagMap& tags, for (std::string_view entry : artistNames)
std::initializer_list<std::string_view> artistTagNames) {
{ Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) };
PerformerContainer performers; StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
// 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) // PERFORMER:role (MP3)
if (const std::vector<std::string_view> artistNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames)}; !artistNames.empty()) for (const auto& [key, values] : tags)
{ {
for (std::string_view entry : artistNames) if (key.find("PERFORMER:") == 0)
{ {
Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)}; std::string performerStr{ key };
StringUtils::capitalize(performer.role); std::string role;
performers[performer.role].push_back(std::move(performer.artist)); if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos)
} {
} role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
// PERFORMER:role (MP3) StringUtils::capitalize(role);
for (const auto& [key, values] : tags) }
{
if (key.find("PERFORMER:") == 0) for (const auto& value : values)
{ performers[role].push_back(Artist{ value });
std::string performerStr {key}; }
std::string role; }
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
{ return performers;
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); }
StringUtils::capitalize(role);
} std::optional<Release> getRelease(const TagMap& tags)
{
for (const auto& value : values) std::optional<Release> release;
performers[role].push_back(Artist {value});
} auto releaseName{ getPropertyValueAs<std::string>(tags, "ALBUM") };
} if (!releaseName)
return release;
return performers;
} release.emplace();
release->name = std::move(*releaseName);
static release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or("");
std::optional<Release> release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" });
getRelease(const TagMap& tags) release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" });
{ release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
std::optional<Release> release; if (!release->mediumCount)
{
auto releaseName {getPropertyValueAs<std::string>(tags, "ALBUM")}; // mediumCount may be encoded as "position/count"
if (!releaseName) if (const auto value{ getPropertyValueAs<std::string_view>(tags, "DISCNUMBER") })
return release; {
// Expecting 'Number/Total'
release.emplace(); const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
release->name = std::move(*releaseName); if (strings.size() == 2)
release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or(""); release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
release->mbid = getPropertyValueFirstMatchAs<UUID>(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<std::size_t>(tags, "DISCTOTAL");
if (!release->mediumCount) release->primaryType = getPropertyValueFirstMatchAs<MetaData::Release::PrimaryType>(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
{ if (release->primaryType)
// mediumCount may be encoded as "position/count" {
if (const auto value {getPropertyValueAs<std::string_view>(tags, "DISCNUMBER")}) const auto secondaryTypes{ getPropertyValuesFirstMatchAs<MetaData::Release::SecondaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) };
{ release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes));
// Expecting 'Number/Total' }
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2) return release;
release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]); }
}
} std::optional<Medium> getMedium(const TagMap& tags)
{
release->primaryType = getPropertyValueFirstMatchAs<MetaData::Release::PrimaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}); std::optional<Medium> medium;
if (release->primaryType) medium.emplace();
{
const auto secondaryTypes {getPropertyValuesFirstMatchAs<MetaData::Release::SecondaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})}; medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or("");
release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); medium->name = getPropertyValueFirstMatchAs<std::string>(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
} medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL");
if (!medium->trackCount)
return release; {
} // totalTracks may be encoded as "position/count"
if (const auto value{ getPropertyValueAs<std::string_view>(tags, "TRACKNUMBER") })
static {
std::optional<Medium> // Expecting 'Number/Total'
getMedium(const TagMap& tags) const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
{ if (strings.size() == 2)
std::optional<Medium> medium; medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
medium.emplace(); }
}
medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or(""); // Expecting 'Number[/Total]'
medium->name = getPropertyValueFirstMatchAs<std::string>(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or(""); medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER");
medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL"); medium->release = getRelease(tags);
if (!medium->trackCount) medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
{
// totalTracks may be encoded as "position/count" if (medium->type.empty()
if (const auto value {getPropertyValueAs<std::string_view>(tags, "TRACKNUMBER")}) && medium->name.empty()
{ && !medium->trackCount
// Expecting 'Number/Total' && !medium->position
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") }; && !medium->release
if (strings.size() == 2) && !medium->replayGain)
medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]); {
} medium.reset();
} }
// Expecting 'Number[/Total]'
medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER"); return medium;
medium->release = getRelease(tags); }
medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
if (medium->type.empty() {
&& medium->name.empty() switch (readStyle)
&& !medium->trackCount {
&& !medium->position case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
&& !medium->release case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
&& !medium->replayGain) case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
{ }
medium.reset();
} throw LmsException{ "Cannot convert read style" };
}
return medium;
} TagMap constructTagMap(const TagLib::PropertyMap& properties)
{
static TagMap tagMap;
TagLib::AudioProperties::ReadStyle
readStyleToTagLibReadStyle(ParserReadStyle readStyle) for (const auto& [propertyName, propertyValues] : properties)
{ {
switch (readStyle) std::vector<std::string>& values{ tagMap[propertyName.upper().to8Bit(true)] };
{ for (const TagLib::String& propertyValue : propertyValues)
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; {
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) };
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; if (!trimedValue.empty())
} values.emplace_back(std::move(trimedValue));
}
throw LmsException {"Cannot convert read style"}; }
}
return tagMap;
TagLibParser::TagLibParser(ParserReadStyle readStyle) }
: _readStyle {readStyleToTagLibReadStyle(readStyle)}
{ void mergeTagMaps(TagMap& dst, TagMap&& src)
} {
for (auto&& [tag, values] : src)
void {
TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug) if (dst.find(tag) == std::cend(dst))
{ dst[tag] = std::move(values);
if (debug) }
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; }
if (tag.empty() || values.empty()) }
return;
TagLibParser::TagLibParser(ParserReadStyle readStyle)
std::string_view value {values.front()}; : _readStyle{ readStyleToTagLibReadStyle(readStyle) }
{
if (tag == "TITLE") }
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID" void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|| tag == "MUSICBRAINZ RELEASE TRACK ID" {
|| tag == "MUSICBRAINZ/RELEASE TRACK ID") if (debug)
{ std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
track.mbid = UUID::fromString(value);
} if (tag.empty() || values.empty())
else if (tag == "MUSICBRAINZ_TRACKID" return;
|| tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID") std::string_view value{ values.front() };
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID") if (tag == "TITLE")
track.acoustID = UUID::fromString(value); track.title = value;
else if (tag == "TRACKNUMBER") else if (tag == "MUSICBRAINZ_RELEASETRACKID"
{ || tag == "MUSICBRAINZ RELEASE TRACK ID"
// Expecting 'Number/Total' || tag == "MUSICBRAINZ/RELEASE TRACK ID")
track.position = StringUtils::readAs<std::size_t>(value); {
} track.mbid = UUID::fromString(value);
else if (tag == "DATE") }
{ else if (tag == "MUSICBRAINZ_TRACKID"
// Higher priority than YEAR || tag == "MUSICBRAINZ TRACK ID"
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) || tag == "MUSICBRAINZ/TRACK ID")
track.date = date; track.recordingMBID = UUID::fromString(value);
} else if (tag == "ACOUSTID_ID")
else if (tag == "YEAR" && !track.date.isValid()) track.acoustID = UUID::fromString(value);
{ else if (tag == "TRACKNUMBER")
// lower priority than DATE {
track.date = Utils::parseDate(value); // Expecting 'Number/Total'
} track.position = StringUtils::readAs<std::size_t>(value);
else if (tag == "ORIGINALDATE") }
{ else if (tag == "DATE")
// Higher priority than ORIGINALYEAR {
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) // Higher priority than YEAR
track.originalDate = date; if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
} track.date = date;
else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) }
{ else if (tag == "YEAR" && !track.date.isValid())
// Lower priority than ORIGINALDATE {
track.originalDate = Utils::parseDate(value); // lower priority than DATE
} track.date = Utils::parseDate(value);
else if (tag == "METADATA_BLOCK_PICTURE") }
track.hasCover = true; else if (tag == "ORIGINALDATE")
else if (tag == "COPYRIGHT") {
track.copyright = value; // Higher priority than ORIGINALYEAR
else if (tag == "COPYRIGHTURL") if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
track.copyrightURL = value; track.originalDate = date;
else if (tag == "REPLAYGAIN_TRACK_GAIN") }
track.replayGain = StringUtils::readAs<float>(value); else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
else if (tag == "ARTIST") {
track.artistDisplayName = value; // Lower priority than ORIGINALDATE
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) track.originalDate = Utils::parseDate(value);
{ }
std::set<std::string> clusterNames; else if (tag == "METADATA_BLOCK_PICTURE")
for (std::string_view valueList : values) track.hasCover = true;
{ else if (tag == "COPYRIGHT")
const std::vector<std::string_view> splittedValues {splitAndTrimString(valueList, "/,;")}; track.copyright = value;
for (std::string_view value : splittedValues) else if (tag == "COPYRIGHTURL")
clusterNames.insert(std::string {value}); track.copyrightURL = value;
} else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.replayGain = StringUtils::readAs<float>(value);
if (!clusterNames.empty()) else if (tag == "ARTIST")
track.tags[tag] = std::move(clusterNames); track.artistDisplayName = value;
} else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
} {
std::set<std::string> clusterNames;
static for (std::string_view valueList : values)
TagMap {
constructTagMap(const TagLib::PropertyMap& properties) const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") };
{ for (std::string_view value : splittedValues)
TagMap tagMap; clusterNames.insert(std::string{ value });
}
for (const auto& [propertyName, propertyValues] : properties)
{ if (!clusterNames.empty())
std::vector<std::string>& values {tagMap[propertyName.upper().to8Bit(true)]}; track.tags[tag] = std::move(clusterNames);
for (const TagLib::String& propertyValue : propertyValues) }
{ }
std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))};
if (!trimedValue.empty()) std::optional<Track> TagLibParser::parse(const std::filesystem::path& p, bool debug)
values.emplace_back(std::move(trimedValue)); {
} TagLib::FileRef f{ p.string().c_str(),
} true, // read audio properties
_readStyle };
return tagMap;
} if (f.isNull())
{
static LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
void return std::nullopt;
mergeTagMaps(TagMap& dst, TagMap&& src) }
{
for (auto&& [tag, values] : src) Track track;
{
if (dst.find(tag) == std::cend(dst)) if (const TagLib::AudioProperties* properties{ f.audioProperties() })
dst[tag] = std::move(values); {
} track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
} track.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
}
std::optional<Track> else
TagLibParser::parse(const std::filesystem::path& p, bool debug) {
{ LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
TagLib::FileRef f {p.string().c_str(), return std::nullopt;
true, // read audio properties }
_readStyle};
TagMap tags{ constructTagMap(f.file()->properties()) };
if (f.isNull())
{ auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; {
return std::nullopt; if (!apeTag)
} return;
if (!f.audioProperties()) mergeTagMaps(tags, constructTagMap(apeTag->properties()));
{ };
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
return std::nullopt; // Not that good embedded pictures handling
}
// WMA
Track track; if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(f.file()) })
{
{ const TagLib::ASF::Tag* tag{ asfFile->tag() };
const TagLib::AudioProperties *properties {f.audioProperties() }; if (tag)
{
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()}; if (tag->attributeListMap().contains("WM/Picture"))
track.hasCover = true;
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
track.audioStreams = {audioStream}; for (const auto& [name, attributeList] : tag->attributeListMap())
} {
std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) };
TagMap tags {constructTagMap(f.file()->properties())}; if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
continue;
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{ std::vector<std::string> attributes;
if (!apeTag) for (const auto& attribute : attributeList)
return; {
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
mergeTagMaps(tags, constructTagMap(apeTag->properties())); attributes.emplace_back(attribute.toString().to8Bit(true));
}; }
// Not that good embedded pictures handling if (!attributes.empty())
{
// WMA if (debug)
if (TagLib::ASF::File* asfFile {dynamic_cast<TagLib::ASF::File*>(f.file())}) std::cout << "ASF property: '" << name << "'" << std::endl;
{
const TagLib::ASF::Tag* tag {asfFile->tag()}; tags.emplace(strName, std::move(attributes));
if (tag) }
{ }
if (tag->attributeListMap().contains("WM/Picture")) }
track.hasCover = true; }
// MP3
for (const auto& [name, attributeList] : tag->attributeListMap()) else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(f.file()) })
{ {
std::string strName {StringUtils::stringToUpper(name.to8Bit(true))}; if (mp3File->ID3v2Tag())
if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) {
continue; const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() };
std::vector<std::string> attributes; if (!frameListMap["APIC"].isEmpty())
for (const auto& attribute : attributeList) track.hasCover = true;
{ if (!frameListMap["TSST"].isEmpty())
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) };
attributes.emplace_back(attribute.toString().to8Bit(true)); }
}
getAPETags(mp3File->APETag());
if (!attributes.empty()) }
{ //MP4
if (debug) else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(f.file()) })
std::cout << "ASF property: '" << name << "'" << std::endl; {
TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") };
tags.emplace(strName, std::move(attributes)); TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
} if (!coverArtList.isEmpty())
} track.hasCover = true;
} }
} // MPC
// MP3 else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(f.file()) })
else if (TagLib::MPEG::File* mp3File {dynamic_cast<TagLib::MPEG::File*>(f.file())}) {
{ getAPETags(mpcFile->APETag());
if (mp3File->ID3v2Tag()) }
{ // WavPack
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()}; else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(f.file()) })
{
if (!frameListMap["APIC"].isEmpty()) getAPETags(wavPackFile->APETag());
track.hasCover = true; }
if (!frameListMap["TSST"].isEmpty()) // FLAC
tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)}; else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(f.file()) })
} {
if (!flacFile->pictureList().isEmpty())
getAPETags(mp3File->APETag()); track.hasCover = true;
} }
//MP4 else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(f.file()) })
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())}) {
{ if (!vorbisFile->tag()->pictureList().isEmpty())
TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")}; track.hasCover = true;
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; }
if (!coverArtList.isEmpty()) else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(f.file()) })
track.hasCover = true; {
} if (!opusFile->tag()->pictureList().isEmpty())
// MPC track.hasCover = true;
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())}) }
{
getAPETags(mpcFile->APETag()); track.medium = getMedium(tags);
} track.artists = getArtists(tags, { "ARTISTS", "ARTIST" }, { "ARTISTSORT" }, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" });
// WavPack track.conductorArtists = getArtists(tags, { "CONDUCTORS", "CONDUCTOR" }, { "CONDUCTORSSORT", "CONDUCTORSORT" }, {});
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())}) track.composerArtists = getArtists(tags, { "COMPOSERS", "COMPOSER" }, { "COMPOSERSSORT", "COMPOSERSORT" }, {});
{ track.lyricistArtists = getArtists(tags, { "LYRICISTS", "LYRICIST" }, { "LYRICISTSSORT", "LYRICISTSORT" }, {});
getAPETags(wavPackFile->APETag()); track.mixerArtists = getArtists(tags, { "MIXERS", "MIXER" }, { "MIXERSSORT", "MIXERSORT" }, {});
} track.producerArtists = getArtists(tags, { "PRODUCERS", "PRODUCER" }, { "PRODUCERSSORT", "PRODUCERSORT" }, {});
// FLAC track.remixerArtists = getArtists(tags, { "REMIXERS", "REMIXER", "ModifiedBy" }, { "REMIXERSSORT", "REMIXERSORT" }, {});
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())}) track.performerArtists = getPerformerArtists(tags, { "PERFORMERS", "PERFORMER" });
{
if (!flacFile->pictureList().isEmpty()) for (const auto& [tag, values] : tags)
track.hasCover = true; processTag(track, tag, values, debug);
}
else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast<TagLib::Ogg::Vorbis::File*>(f.file())}) return track;
{ }
if (!vorbisFile->tag()->pictureList().isEmpty())
track.hasCover = true;
}
else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast<TagLib::Ogg::Opus::File*>(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 } // namespace MetaData
+101 -106
View File
@@ -34,126 +34,121 @@
namespace MetaData namespace MetaData
{ {
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>; using Tags = std::map<std::string /* type */, std::set<std::string> /* 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 struct Artist
{ {
std::optional<UUID> mbid; std::optional<UUID> mbid;
std::string name; std::string name;
std::optional<std::string> sortName; std::optional<std::string> sortName;
Artist(std::string_view _name) : name {_name} {} Artist(std::string_view _name) : name{ _name } {}
Artist(std::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid {std::move(_mbid)}, name {_name}, sortName {std::move(_sortName)} {} Artist(std::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid{ std::move(_mbid) }, name{ _name }, sortName{ std::move(_sortName) } {}
}; };
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>; using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
struct Release struct Release
{ {
// see https://musicbrainz.org/doc/Release_Group/Type // see https://musicbrainz.org/doc/Release_Group/Type
enum class PrimaryType enum class PrimaryType
{ {
Album, Album,
Single, Single,
EP, EP,
Broadcast, Broadcast,
Other Other
}; };
enum class SecondaryType enum class SecondaryType
{ {
Compilation, Compilation,
Soundtrack, Soundtrack,
Spokenword, Spokenword,
Interview, Interview,
Audiobook, Audiobook,
AudioDrama, AudioDrama,
Live, Live,
Remix, Remix,
DJMix, DJMix,
Mixtape_Street, Mixtape_Street,
Demo, Demo,
}; };
std::optional<UUID> mbid; std::optional<UUID> mbid;
std::string name; std::string name;
std::string artistDisplayName; std::string artistDisplayName;
std::vector<Artist> artists; std::vector<Artist> artists;
std::optional<std::size_t> mediumCount; std::optional<std::size_t> mediumCount;
std::optional<PrimaryType> primaryType; std::optional<PrimaryType> primaryType;
EnumSet<SecondaryType> secondaryTypes; EnumSet<SecondaryType> secondaryTypes;
}; };
struct Medium struct Medium
{ {
std::string type; std::string type;
std::string name; std::string name;
std::optional<Release> release; std::optional<Release> release;
std::optional<std::size_t> position; // in release std::optional<std::size_t> position; // in release
std::optional<std::size_t> trackCount; std::optional<std::size_t> trackCount;
std::optional<float> replayGain; std::optional<float> replayGain;
}; };
struct AudioStream struct Track
{ {
unsigned bitRate; std::optional<UUID> mbid;
}; std::optional<UUID> recordingMBID;
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
Tags tags;
std::chrono::milliseconds duration{};
std::size_t bitrate{};
Wt::WDate date;
Wt::WDate originalDate;
bool hasCover{};
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> replayGain;
std::string artistDisplayName;
std::vector<Artist> artists;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
struct Track class IParser
{ {
std::optional<UUID> mbid; public:
std::optional<UUID> recordingMBID; virtual ~IParser() = default;
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
Tags tags;
std::chrono::milliseconds duration;
Wt::WDate date;
Wt::WDate originalDate;
bool hasCover {};
std::vector<AudioStream> audioStreams;
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> replayGain;
std::string artistDisplayName;
std::vector<Artist> artists;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
class IParser virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
{
public:
virtual ~IParser() = default;
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0; void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } protected:
std::set<std::string> _clusterTypeNames;
};
protected: enum class ParserType
std::set<std::string> _clusterTypeNames; {
}; TagLib,
AvFormat,
};
enum class ParserType enum class ParserReadStyle
{ {
TagLib, Fast,
AvFormat, Average,
}; Accurate,
};
enum class ParserReadStyle std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
{
Fast,
Average,
Accurate,
};
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
} // namespace MetaData } // namespace MetaData
@@ -234,6 +234,15 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
ScanSettings::get(session).modify()->incScanVersion(); 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) void doDbMigration(Session& session)
{ {
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; 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}, {41, migrateFromV41},
{42, migrateFromV42}, {42, migrateFromV42},
{43, migrateFromV43}, {43, migrateFromV43},
{44, migrateFromV44},
}; };
{ {
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 44 }; static constexpr Version LMS_DATABASE_VERSION{ 45 };
class VersionInfo class VersionInfo
{ {
public: public:
+12 -3
View File
@@ -387,9 +387,8 @@ namespace Database
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?") .where("r.id = ?").bind(getId())
.groupBy("copyright_url") .groupBy("copyright_url");
.bind(getId());
std::vector<std::string> values(copyrights.begin(), copyrights.end()); std::vector<std::string> values(copyrights.begin(), copyrights.end());
@@ -400,6 +399,16 @@ namespace Database
return values.front(); return values.front();
} }
std::size_t Release::getMeanBitrate() const
{
assert(session());
return session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t")
.where("release_id = ?").bind(getId())
.where("bitrate > 0")
.resultValue();
}
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
{ {
assert(session()); assert(session());
@@ -104,6 +104,7 @@ namespace Database
Wt::WDate getOriginalReleaseDate() const; Wt::WDate getOriginalReleaseDate() const;
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
std::size_t getMeanBitrate() const;
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
@@ -128,6 +128,7 @@ namespace Database {
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } void setPath(const std::filesystem::path& filePath) { _filePath = filePath; }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
void setDate(const Wt::WDate& date) { _date = date; } void setDate(const Wt::WDate& date) { _date = date; }
@@ -153,6 +154,7 @@ namespace Database {
std::string getName() const { return _name; } std::string getName() const { return _name; }
std::filesystem::path getPath() const { return _filePath; } std::filesystem::path getPath() const { return _filePath; }
std::chrono::milliseconds getDuration() const { return _duration; } std::chrono::milliseconds getDuration() const { return _duration; }
std::size_t getBitrate() const { return _bitrate; }
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
std::optional<int> getYear() const; std::optional<int> getYear() const;
std::optional<int> getOriginalYear() const; std::optional<int> 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, _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, _name, "name");
Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _date, "date"); Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _originalDate, "original_date"); Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _filePath, "file_path"); Wt::Dbo::field(a, _filePath, "file_path");
@@ -220,6 +223,7 @@ namespace Database {
std::string _discSubtitle; std::string _discSubtitle;
std::string _name; std::string _name;
std::chrono::duration<int, std::milli> _duration{}; std::chrono::duration<int, std::milli> _duration{};
int _bitrate; // in bps
Wt::WDate _date; Wt::WDate _date;
Wt::WDate _originalDate; Wt::WDate _originalDate;
std::string _filePath; std::string _filePath;
+40 -1
View File
@@ -547,7 +547,7 @@ TEST_F(DatabaseFixture, Release_releaseType)
} }
} }
TEST_F(DatabaseFixture, ReleaseSortOrder) TEST_F(DatabaseFixture, Release_sortMethod)
{ {
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} }; 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
}
@@ -405,22 +405,7 @@ namespace Scanner
} }
} }
// We estimate this is an audio file if: // We estimate this is an audio file if the duration is not null
// - 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;
}
if (trackInfo->duration == std::chrono::milliseconds::zero()) if (trackInfo->duration == std::chrono::milliseconds::zero())
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
@@ -513,6 +498,7 @@ namespace Scanner
track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration); track.modify()->setDuration(trackInfo->duration);
track.modify()->setBitrate(trackInfo->bitrate);
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
track.modify()->setTrackNumber(trackInfo->position); track.modify()->setTrackNumber(trackInfo->position);
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
@@ -148,6 +148,7 @@ namespace API::Subsonic
} }
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()); trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000));
trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("type", "music");
trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten()));
+8 -7
View File
@@ -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) 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()) }) if (const auto audioFile{ Av::parseAudioFile(track->getPath()) })
@@ -147,16 +147,17 @@ namespace UserInterface
{ {
releaseInfo->setCondition("if-has-codec", true); releaseInfo->setCondition("if-has-codec", true);
releaseInfo->bindString("codec", audioStream->codec); releaseInfo->bindString("codec", audioStream->codec);
if (audioStream->bitrate) break;
{
releaseInfo->setCondition("if-has-bitrate", true);
releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
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<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) }; Wt::WPushButton* okBtn{ releaseInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
okBtn->clicked().connect([=] okBtn->clicked().connect([=]
{ {
+176 -178
View File
@@ -43,219 +43,217 @@
#include "ModalManager.hpp" #include "ModalManager.hpp"
#include "Utils.hpp" #include "Utils.hpp"
using namespace Database;
namespace UserInterface::TrackListHelpers namespace UserInterface::TrackListHelpers
{ {
void using namespace Database;
showTrackInfoModal(Database::TrackId trackId, Filters& filters)
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; void showTrackInfoModal(Database::TrackId trackId, Filters& filters)
if (!track) {
return; auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
auto trackInfo {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-info"))}; const Database::Track::pointer track{ Track::find(LmsApp->getDbSession(), trackId) };
Wt::WWidget* trackInfoPtr {trackInfo.get()}; if (!track)
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr); return;
std::map<Wt::WString, std::set<ArtistId>> artistMap; auto trackInfo{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-info")) };
Wt::WWidget* trackInfoPtr{ trackInfo.get() };
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
auto addArtists = [&](TrackArtistLinkType linkType, const char* type) std::map<Wt::WString, std::set<ArtistId>> artistMap;
{
Artist::FindParameters params;
params.setTrack(trackId);
params.setLinkType(linkType);
const auto artistIds {Artist::findIds(LmsApp->getDbSession(), params)};
if (artistIds.results.empty())
return;
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};; auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
for (ArtistId artistId : artistIds.results) {
artistMap[typeStr].insert(artistId); Artist::FindParameters params;
}; params.setTrack(trackId);
params.setLinkType(linkType);
const auto artistIds{ Artist::findIds(LmsApp->getDbSession(), params) };
if (artistIds.results.empty())
return;
auto addPerformerArtists = [&] Wt::WString typeStr{ Wt::WString::trn(type, artistIds.results.size()) };;
{ for (ArtistId artistId : artistIds.results)
TrackArtistLink::FindParameters params; artistMap[typeStr].insert(artistId);
params.setTrack(trackId); };
params.setLinkType(TrackArtistLinkType::Performer);
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)};
if (links.results.empty())
return;
for (const TrackArtistLinkId linkId : links.results) auto addPerformerArtists = [&]
{ {
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)}; TrackArtistLink::FindParameters params;
if (!link) params.setTrack(trackId);
continue; params.setLinkType(TrackArtistLinkType::Performer);
const auto links{ TrackArtistLink::find(LmsApp->getDbSession(), params) };
if (links.results.empty())
return;
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId()); for (const TrackArtistLinkId linkId : links.results)
} {
}; const TrackArtistLink::pointer link{ TrackArtistLink::find(LmsApp->getDbSession(), linkId) };
if (!link)
continue;
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId());
addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); }
addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); };
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
if (auto itRolelessPerformers {artistMap.find("")}; itRolelessPerformers != std::cend(artistMap)) addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
{ addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
Wt::WString performersStr {Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size())}; addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
artistMap[performersStr] = std::move(itRolelessPerformers->second); addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
artistMap.erase(itRolelessPerformers); addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
} addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
if (!artistMap.empty()) if (auto itRolelessPerformers{ artistMap.find("") }; itRolelessPerformers != std::cend(artistMap))
{ {
trackInfo->setCondition("if-has-artist", true); Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size()) };
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")}; artistMap[performersStr] = std::move(itRolelessPerformers->second);
artistMap.erase(itRolelessPerformers);
}
for (const auto& [role, artistIds] : artistMap) if (!artistMap.empty())
{ {
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistAnchorList(std::vector (std::cbegin(artistIds), std::cend(artistIds)))}; trackInfo->setCondition("if-has-artist", true);
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))}; Wt::WContainerWidget* artistTable{ trackInfo->bindNew<Wt::WContainerWidget>("artist-table") };
artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
}
}
if (const auto audioFile {Av::parseAudioFile(track->getPath())}) for (const auto& [role, artistIds] : artistMap)
{ {
const std::optional<Av::StreamInfo> audioStream {audioFile->getBestStreamInfo()}; std::unique_ptr<Wt::WContainerWidget> artistContainer{ Utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
if (audioStream) auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
{ artistsEntry->bindString("type", role);
trackInfo->setCondition("if-has-codec", true); artistsEntry->bindWidget("artist-container", std::move(artistContainer));
trackInfo->bindString("codec", audioStream->codec); artistTable->addWidget(std::move(artistsEntry));
if (audioStream->bitrate) }
{ }
trackInfo->setCondition("if-has-bitrate", true);
trackInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
}
}
}
trackInfo->bindString("duration", Utils::durationToString(track->getDuration())); if (const auto audioFile{ Av::parseAudioFile(track->getPath()) })
{
const std::optional<Av::StreamInfo> audioStream{ audioFile->getBestStreamInfo() };
if (audioStream)
{
trackInfo->setCondition("if-has-codec", true);
trackInfo->bindString("codec", audioStream->codec);
}
}
Wt::WContainerWidget* clusterContainer {trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters))}; trackInfo->bindString("duration", Utils::durationToString(track->getDuration()));
if (clusterContainer->count() > 0) if (track->getBitrate())
trackInfo->setCondition("if-has-clusters", true); {
trackInfo->setCondition("if-has-bitrate", true);
trackInfo->bindString("bitrate", std::to_string(track->getBitrate() / 1000) + " kbps");
}
Wt::WPushButton* okBtn {trackInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok"))}; Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters)) };
okBtn->clicked().connect([=] if (clusterContainer->count() > 0)
{ trackInfo->setCondition("if-has-clusters", true);
LmsApp->getModalManager().dispose(trackInfoPtr);
});
LmsApp->getModalManager().show(std::move(trackInfo)); Wt::WPushButton* okBtn{ trackInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
} okBtn->clicked().connect([=]
{
LmsApp->getModalManager().dispose(trackInfoPtr);
});
std::unique_ptr<Wt::WWidget> LmsApp->getModalManager().show(std::move(trackInfo));
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters) }
{
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
auto* entryPtr {entry.get()};
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters)
{
auto entry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry")) };
auto* entryPtr{ entry.get() };
const Release::pointer release {track->getRelease()}; entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const TrackId trackId {track->getId()};
const auto artists {track->getArtistIds({TrackArtistLinkType::Artist})}; const Release::pointer release{ track->getRelease() };
if (!artists.empty()) const TrackId trackId{ track->getId() };
{
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
}
if (track->getRelease()) const auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
{ if (!artists.empty())
entry->setCondition("if-has-release", true); {
entry->bindWidget("release", Utils::createReleaseAnchor(track->getRelease())); entry->setCondition("if-has-artists", true);
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))}; entry->bindWidget("artists", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)}; entry->bindWidget("artists-md", Utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK }
anchor->setImage(std::move((cover)));
}
else
{
auto cover {Utils::createCover(trackId, CoverResource::Size::Small)};
cover->addStyleClass("Lms-cover-track"); // HACK
entry->bindWidget<Wt::WImage>("cover", std::move(cover));
}
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); if (track->getRelease())
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", Utils::createReleaseAnchor(track->getRelease()));
Wt::WAnchor* anchor{ entry->bindWidget("cover", Utils::createReleaseAnchor(release, false)) };
auto cover{ Utils::createCover(release->getId(), CoverResource::Size::Small) };
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
anchor->setImage(std::move((cover)));
}
else
{
auto cover{ Utils::createCover(trackId, CoverResource::Size::Small) };
cover->addStyleClass("Lms-cover-track"); // HACK
entry->bindWidget<Wt::WImage>("cover", std::move(cover));
}
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)}; entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
playBtn->clicked().connect([trackId, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::Play, {trackId});
});
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) playBtn->clicked().connect([trackId, &playQueueController]
->clicked().connect([trackId, &playQueueController] {
{ playQueueController.processCommand(PlayQueueController::Command::Play, { trackId });
playQueueController.processCommand(PlayQueueController::Command::Play, {trackId}); });
});
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
->clicked().connect([=, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::PlayNext, {trackId});
});
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
});
{ entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); }}; entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked().connect([trackId, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::Play, { trackId });
});
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
->clicked().connect([=, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::PlayNext, { trackId });
});
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=, &playQueueController]
{
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { trackId });
});
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; {
starBtn->clicked().connect([=] auto isStarred{ [=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
if (isStarred()) Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
{ starBtn->clicked().connect([=]
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId); {
starBtn->setText(Wt::WString::tr("Lms.Explore.star")); auto transaction{ LmsApp->getDbSession().createUniqueTransaction() };
}
else
{
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
}
});
}
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download")) if (isStarred())
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)}); {
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
}
else
{
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
}
});
}
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info")) entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
->clicked().connect([trackId, &filters] { showTrackInfoModal(trackId, filters); }); ->setLink(Wt::WLink{ std::make_unique<DownloadTrackResource>(trackId) });
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::TrackId loadedTrackId) entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
{ ->clicked().connect([trackId, &filters] { showTrackInfoModal(trackId, filters); });
entryPtr->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
});
if (auto trackIdLoaded {LmsApp->getMediaPlayer().getTrackLoaded()}) LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=](Database::TrackId loadedTrackId)
{ {
entryPtr->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId); entryPtr->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
} });
else
entry->removeStyleClass("Lms-entry-playing");
return entry; if (auto trackIdLoaded{ LmsApp->getMediaPlayer().getTrackLoaded() })
} {
entryPtr->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId);
}
else
entry->removeStyleClass("Lms-entry-playing");
return entry;
}
} // namespace UserInterface } // namespace UserInterface
+1 -3
View File
@@ -212,6 +212,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
} }
std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl; std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl;
std::cout << "Bitrate: " << track->bitrate << " bps" << std::endl;
if (track->position) if (track->position)
std::cout << "Position: " << *track->position << std::endl; std::cout << "Position: " << *track->position << std::endl;
@@ -224,9 +225,6 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl; std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl;
for (const auto& audioStream : track->audioStreams)
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
if (track->replayGain) if (track->replayGain)
std::cout << "Track replay gain: " << *track->replayGain << std::endl; std::cout << "Track replay gain: " << *track->replayGain << std::endl;