Subsonic API: added bitrate support, ref #363
This commit is contained in:
@@ -32,9 +32,11 @@ 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)
|
namespace
|
||||||
|
{
|
||||||
|
std::string averror_to_string(int error)
|
||||||
{
|
{
|
||||||
std::array<char, 128> buf = { 0 };
|
std::array<char, 128> buf = { 0 };
|
||||||
|
|
||||||
@@ -52,8 +54,20 @@ class AudioFileException : public Av::Exception
|
|||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IAudioFile>
|
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||||
parseAudioFile(const std::filesystem::path& p)
|
{
|
||||||
|
if (!dictionnary)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AVDictionaryEntry* tag = NULL;
|
||||||
|
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||||
|
{
|
||||||
|
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
return std::make_unique<AudioFile>(p);
|
return std::make_unique<AudioFile>(p);
|
||||||
}
|
}
|
||||||
@@ -82,14 +96,12 @@ AudioFile::~AudioFile()
|
|||||||
avformat_close_input(&_context);
|
avformat_close_input(&_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path&
|
const std::filesystem::path& AudioFile::getPath() const
|
||||||
AudioFile::getPath() const
|
|
||||||
{
|
{
|
||||||
return _p;
|
return _p;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::chrono::milliseconds
|
std::chrono::milliseconds AudioFile::getDuration() const
|
||||||
AudioFile::getDuration() const
|
|
||||||
{
|
{
|
||||||
if (_context->duration == AV_NOPTS_VALUE)
|
if (_context->duration == AV_NOPTS_VALUE)
|
||||||
return std::chrono::milliseconds{ 0 }; // TODO estimate
|
return std::chrono::milliseconds{ 0 }; // TODO estimate
|
||||||
@@ -97,21 +109,7 @@ AudioFile::getDuration() const
|
|||||||
return std::chrono::milliseconds{ _context->duration / AV_TIME_BASE * 1000 };
|
return std::chrono::milliseconds{ _context->duration / AV_TIME_BASE * 1000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
AudioFile::MetadataMap AudioFile::getMetaData() const
|
||||||
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
|
||||||
{
|
|
||||||
if (!dictionnary)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AVDictionaryEntry *tag = NULL;
|
|
||||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
|
||||||
{
|
|
||||||
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioFile::MetadataMap
|
|
||||||
AudioFile::getMetaData() const
|
|
||||||
{
|
{
|
||||||
MetadataMap res;
|
MetadataMap res;
|
||||||
|
|
||||||
@@ -133,8 +131,7 @@ AudioFile::getMetaData() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<StreamInfo>
|
std::vector<StreamInfo> AudioFile::getStreamInfo() const
|
||||||
AudioFile::getStreamInfo() const
|
|
||||||
{
|
{
|
||||||
std::vector<StreamInfo> res;
|
std::vector<StreamInfo> res;
|
||||||
|
|
||||||
@@ -148,8 +145,7 @@ AudioFile::getStreamInfo() const
|
|||||||
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,
|
||||||
@@ -164,8 +160,7 @@ AudioFile::getBestStreamIndex() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<StreamInfo>
|
std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
|
||||||
AudioFile::getBestStreamInfo() const
|
|
||||||
{
|
{
|
||||||
std::optional<StreamInfo> res;
|
std::optional<StreamInfo> res;
|
||||||
|
|
||||||
@@ -176,8 +171,7 @@ AudioFile::getBestStreamInfo() const
|
|||||||
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)
|
||||||
{
|
{
|
||||||
@@ -188,8 +182,7 @@ AudioFile::hasAttachedPictures() const
|
|||||||
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 =
|
||||||
{
|
{
|
||||||
@@ -237,8 +230,7 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -266,16 +258,18 @@ AudioFile::getStreamInfo(std::size_t streamIndex) const
|
|||||||
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 << "'";
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ namespace Av
|
|||||||
AudioFile(const std::filesystem::path& p);
|
AudioFile(const std::filesystem::path& p);
|
||||||
~AudioFile();
|
~AudioFile();
|
||||||
|
|
||||||
AudioFile(const AudioFile&) = delete;
|
|
||||||
AudioFile(AudioFile&&) = delete;
|
|
||||||
AudioFile& operator=(const AudioFile&) = delete;
|
|
||||||
AudioFile& operator=(AudioFile&&) = delete;
|
|
||||||
|
|
||||||
const std::filesystem::path& getPath() const override;
|
const std::filesystem::path& getPath() const override;
|
||||||
std::chrono::milliseconds getDuration() const override;
|
std::chrono::milliseconds getDuration() const override;
|
||||||
MetadataMap getMetaData() const override;
|
MetadataMap getMetaData() const override;
|
||||||
@@ -49,6 +44,9 @@ namespace Av
|
|||||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AudioFile(const AudioFile&) = delete;
|
||||||
|
AudioFile& operator=(const AudioFile&) = delete;
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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())
|
|
||||||
{
|
|
||||||
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)};
|
|
||||||
track.audioStreams.emplace_back(audioStream);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': cannot get best audio stream";
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
track.duration = mediaFile->getDuration();
|
track.duration = mediaFile->getDuration();
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
namespace MetaData
|
namespace MetaData
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
// TODO use string_views here for values
|
// TODO use string_views here for values
|
||||||
using TagMap = std::map<std::string, std::vector<std::string>>;
|
using TagMap = std::map<std::string, std::vector<std::string>>;
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T>
|
std::vector<T> getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
|
||||||
getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
|
|
||||||
{
|
{
|
||||||
std::vector<T> res;
|
std::vector<T> res;
|
||||||
|
|
||||||
@@ -82,8 +82,7 @@ getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::str
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) };
|
||||||
@@ -94,22 +93,18 @@ getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
std::vector<T>
|
std::vector<T> getPropertyValuesAs(const TagMap& tags, std::string_view key)
|
||||||
getPropertyValuesAs(const TagMap& tags, std::string_view key)
|
|
||||||
{
|
{
|
||||||
return getPropertyValuesFirstMatchAs<T>(tags, { key });
|
return getPropertyValuesFirstMatchAs<T>(tags, { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
std::optional<T>
|
std::optional<T> getPropertyValueAs(const TagMap& tags, std::string_view key)
|
||||||
getPropertyValueAs(const TagMap& tags, std::string_view key)
|
|
||||||
{
|
{
|
||||||
return getPropertyValueFirstMatchAs<T>(tags, { key });
|
return getPropertyValueFirstMatchAs<T>(tags, { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::vector<std::string_view> splitAndTrimString(std::string_view str, std::string_view delimiters)
|
||||||
std::vector<std::string_view>
|
|
||||||
splitAndTrimString(std::string_view str, std::string_view delimiters)
|
|
||||||
{
|
{
|
||||||
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
|
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
|
||||||
for (std::string_view& s : strings)
|
for (std::string_view& s : strings)
|
||||||
@@ -118,9 +113,7 @@ splitAndTrimString(std::string_view str, std::string_view delimiters)
|
|||||||
return strings;
|
return strings;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::vector<Artist> getArtists(const TagMap& tags,
|
||||||
std::vector<Artist>
|
|
||||||
getArtists(const TagMap& tags,
|
|
||||||
std::initializer_list<std::string_view> artistTagNames,
|
std::initializer_list<std::string_view> artistTagNames,
|
||||||
std::initializer_list<std::string_view> artistSortTagNames,
|
std::initializer_list<std::string_view> artistSortTagNames,
|
||||||
std::initializer_list<std::string_view> artistMBIDTagNames
|
std::initializer_list<std::string_view> artistMBIDTagNames
|
||||||
@@ -158,10 +151,7 @@ getArtists(const TagMap& tags,
|
|||||||
return artists;
|
return artists;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list<std::string_view> artistTagNames)
|
||||||
PerformerContainer
|
|
||||||
getPerformerArtists(const TagMap& tags,
|
|
||||||
std::initializer_list<std::string_view> artistTagNames)
|
|
||||||
{
|
{
|
||||||
PerformerContainer performers;
|
PerformerContainer performers;
|
||||||
|
|
||||||
@@ -198,9 +188,7 @@ getPerformerArtists(const TagMap& tags,
|
|||||||
return performers;
|
return performers;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Release> getRelease(const TagMap& tags)
|
||||||
std::optional<Release>
|
|
||||||
getRelease(const TagMap& tags)
|
|
||||||
{
|
{
|
||||||
std::optional<Release> release;
|
std::optional<Release> release;
|
||||||
|
|
||||||
@@ -236,9 +224,7 @@ getRelease(const TagMap& tags)
|
|||||||
return release;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Medium> getMedium(const TagMap& tags)
|
||||||
std::optional<Medium>
|
|
||||||
getMedium(const TagMap& tags)
|
|
||||||
{
|
{
|
||||||
std::optional<Medium> medium;
|
std::optional<Medium> medium;
|
||||||
medium.emplace();
|
medium.emplace();
|
||||||
@@ -275,9 +261,7 @@ getMedium(const TagMap& tags)
|
|||||||
return medium;
|
return medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||||
TagLib::AudioProperties::ReadStyle
|
|
||||||
readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
|
||||||
{
|
{
|
||||||
switch (readStyle)
|
switch (readStyle)
|
||||||
{
|
{
|
||||||
@@ -289,13 +273,41 @@ readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
|||||||
throw LmsException{ "Cannot convert read style" };
|
throw LmsException{ "Cannot convert read style" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagMap constructTagMap(const TagLib::PropertyMap& properties)
|
||||||
|
{
|
||||||
|
TagMap tagMap;
|
||||||
|
|
||||||
|
for (const auto& [propertyName, propertyValues] : properties)
|
||||||
|
{
|
||||||
|
std::vector<std::string>& 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)
|
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
||||||
: _readStyle{ readStyleToTagLibReadStyle(readStyle) }
|
: _readStyle{ readStyleToTagLibReadStyle(readStyle) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|
||||||
TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|
|
||||||
{
|
{
|
||||||
if (debug)
|
if (debug)
|
||||||
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
|
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
|
||||||
@@ -371,39 +383,7 @@ TagLibParser::processTag(Track& track, const std::string& tag, const std::vector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Track> TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||||
TagMap
|
|
||||||
constructTagMap(const TagLib::PropertyMap& properties)
|
|
||||||
{
|
|
||||||
TagMap tagMap;
|
|
||||||
|
|
||||||
for (const auto& [propertyName, propertyValues] : properties)
|
|
||||||
{
|
|
||||||
std::vector<std::string>& 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<Track>
|
|
||||||
TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|
||||||
{
|
{
|
||||||
TagLib::FileRef f{ p.string().c_str(),
|
TagLib::FileRef f{ p.string().c_str(),
|
||||||
true, // read audio properties
|
true, // read audio properties
|
||||||
@@ -415,23 +395,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!f.audioProperties())
|
Track track;
|
||||||
|
|
||||||
|
if (const TagLib::AudioProperties* properties{ f.audioProperties() })
|
||||||
|
{
|
||||||
|
track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||||
|
track.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Track track;
|
|
||||||
|
|
||||||
{
|
|
||||||
const TagLib::AudioProperties *properties {f.audioProperties() };
|
|
||||||
|
|
||||||
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
|
|
||||||
|
|
||||||
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
|
||||||
track.audioStreams = {audioStream};
|
|
||||||
}
|
|
||||||
|
|
||||||
TagMap tags{ constructTagMap(f.file()->properties()) };
|
TagMap tags{ constructTagMap(f.file()->properties()) };
|
||||||
|
|
||||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
||||||
|
|||||||
@@ -96,11 +96,6 @@ namespace MetaData
|
|||||||
std::optional<float> replayGain;
|
std::optional<float> replayGain;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AudioStream
|
|
||||||
{
|
|
||||||
unsigned bitRate;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Track
|
struct Track
|
||||||
{
|
{
|
||||||
std::optional<UUID> mbid;
|
std::optional<UUID> mbid;
|
||||||
@@ -109,11 +104,11 @@ namespace MetaData
|
|||||||
std::optional<Medium> medium;
|
std::optional<Medium> medium;
|
||||||
std::optional<std::size_t> position; // in medium
|
std::optional<std::size_t> position; // in medium
|
||||||
Tags tags;
|
Tags tags;
|
||||||
std::chrono::milliseconds duration;
|
std::chrono::milliseconds duration{};
|
||||||
|
std::size_t bitrate{};
|
||||||
Wt::WDate date;
|
Wt::WDate date;
|
||||||
Wt::WDate originalDate;
|
Wt::WDate originalDate;
|
||||||
bool hasCover{};
|
bool hasCover{};
|
||||||
std::vector<AudioStream> audioStreams;
|
|
||||||
std::optional<UUID> acoustID;
|
std::optional<UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +147,15 @@ 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)
|
|
||||||
{
|
|
||||||
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<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
|
Wt::WPushButton* okBtn{ releaseInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
|
||||||
|
|||||||
@@ -43,12 +43,11 @@
|
|||||||
#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)
|
|
||||||
|
void showTrackInfoModal(Database::TrackId trackId, Filters& filters)
|
||||||
{
|
{
|
||||||
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
|
|
||||||
@@ -132,15 +131,15 @@ namespace UserInterface::TrackListHelpers
|
|||||||
{
|
{
|
||||||
trackInfo->setCondition("if-has-codec", true);
|
trackInfo->setCondition("if-has-codec", true);
|
||||||
trackInfo->bindString("codec", audioStream->codec);
|
trackInfo->bindString("codec", audioStream->codec);
|
||||||
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()));
|
trackInfo->bindString("duration", Utils::durationToString(track->getDuration()));
|
||||||
|
if (track->getBitrate())
|
||||||
|
{
|
||||||
|
trackInfo->setCondition("if-has-bitrate", true);
|
||||||
|
trackInfo->bindString("bitrate", std::to_string(track->getBitrate() / 1000) + " kbps");
|
||||||
|
}
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters)) };
|
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters)) };
|
||||||
if (clusterContainer->count() > 0)
|
if (clusterContainer->count() > 0)
|
||||||
@@ -155,8 +154,7 @@ namespace UserInterface::TrackListHelpers
|
|||||||
LmsApp->getModalManager().show(std::move(trackInfo));
|
LmsApp->getModalManager().show(std::move(trackInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WWidget>
|
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters)
|
||||||
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 entry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry")) };
|
||||||
auto* entryPtr{ entry.get() };
|
auto* entryPtr{ entry.get() };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user