+
+ ${tr:Lms.Settings.default-transcoding-output-bitrate}
- ${subsonic-transcode-bitrate class="form-control"}
+ ${subsonic-transcoding-output-bitrate class="form-control"}
kbps
- ${subsonic-transcode-bitrate-info class="help-block"}
+ ${subsonic-transcoding-output-bitrate-info class="help-block"}
diff --git a/conf/lms.conf b/conf/lms.conf
index 07e7943f..02985108 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -61,7 +61,10 @@ api-subsonic = true;
# Use this list to make the reported server version to 1.12.0 depending on the client's name
# Main usage is to make auto detections for the 'p' (password) parameter work
-api-subsonic-report-old-server-protocol = ("DSub");
+api-subsonic-old-server-protocol-clients = ("DSub");
+
+# List of clients for whom a default cover is served (as they do not have their own)
+api-subsonic-default-cover-clients = ("DSub", "substreamer");
# List of clients for whom open subsonic extensions and extra fields are disabled
api-open-subsonic-disabled-clients = ("DSub");
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js
index 2a345e78..b5dfdeb1 100644
--- a/docroot/js/mediaplayer.js
+++ b/docroot/js/mediaplayer.js
@@ -2,15 +2,15 @@
var LMS = LMS || {};
-// Keep in sync with MediaPlayer::TranscodeMode cpp
-const TranscodeMode = {
+// Keep in sync with MediaPlayer::TranscodingMode cpp
+const TranscodingMode = {
Never: 0,
Always: 1,
IfFormatNotSupported: 2,
}
const Mode = {
- Transcode: 1,
+ Transcoding: 1,
File: 2,
}
Object.freeze(Mode);
@@ -22,7 +22,7 @@ LMS.mediaplayer = function () {
let _trackId = null;
let _duration = 0;
let _audioNativeSrc;
- let _audioTranscodeSrc;
+ let _audioTranscodingSrc;
let _settings = {};
let _playedDuration = 0;
let _lastStartPlaying = null;
@@ -236,10 +236,10 @@ LMS.mediaplayer = function () {
let selectedOffset = parseInt(_elems.seek.value, 10);
switch (mode) {
- case Mode.Transcode:
+ case Mode.Transcoding:
_offset = selectedOffset;
_removeAudioSources();
- _addAudioSource(_audioTranscodeSrc + "&offset=" + _offset);
+ _addAudioSource(_audioTranscodingSrc + "&offset=" + _offset);
_elems.audio.load();
_elems.audio.currentTime = 0;
_playTrack();
@@ -270,7 +270,7 @@ LMS.mediaplayer = function () {
});
_elems.audio.addEventListener("canplay", function() {
- if (_getAudioMode() == Mode.Transcode) {
+ if (_getAudioMode() == Mode.Transcoding) {
_elems.transcodingActive.style.display = "inline";
}
else {
@@ -337,7 +337,7 @@ LMS.mediaplayer = function () {
let _getAudioMode = function() {
if (_elems.audio.currentSrc) {
if (_elems.audio.currentSrc.includes("format"))
- return Mode.Transcode;
+ return Mode.Transcoding;
else
return Mode.File;
}
@@ -353,19 +353,19 @@ LMS.mediaplayer = function () {
_offset = 0;
_duration = params.duration;
_audioNativeSrc = params.nativeResource;
- _audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format;
+ _audioTranscodingSrc = params.transcodingResource + "&bitrate=" + _settings.transcoding.bitrate + "&format=" + _settings.transcoding.format;
_elems.seek.max = _duration;
_removeAudioSources();
// ! order is important
- if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
+ if (_settings.transcoding.mode == TranscodingMode.Never || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{
_addAudioSource(_audioNativeSrc);
}
- if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
+ if (_settings.transcoding.mode == TranscodingMode.Always || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{
- _addAudioSource(_audioTranscodeSrc);
+ _addAudioSource(_audioTranscodingSrc);
}
_elems.audio.load();
diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt
index 4205e35c..fee1201e 100644
--- a/src/libs/av/CMakeLists.txt
+++ b/src/libs/av/CMakeLists.txt
@@ -1,9 +1,9 @@
add_library(lmsav SHARED
impl/AudioFile.cpp
+ impl/RawResourceHandlerCreator.cpp
impl/Transcoder.cpp
- impl/TranscodeResourceHandler.cpp
- impl/Types.cpp
+ impl/TranscodingResourceHandler.cpp
)
target_include_directories(lmsav INTERFACE
diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp
index fa730e72..346fe074 100644
--- a/src/libs/av/impl/AudioFile.cpp
+++ b/src/libs/av/impl/AudioFile.cpp
@@ -32,267 +32,295 @@ extern "C"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
-namespace Av {
-
-static std::string averror_to_string(int error)
+namespace Av
{
- std::array
buf = {0};
+ namespace
+ {
+ std::string averror_to_string(int error)
+ {
+ std::array buf = { 0 };
- if (::av_strerror(error, buf.data(), buf.size()) == 0)
- return &buf[0];
- else
- return "Unknown error";
-}
+ if (::av_strerror(error, buf.data(), buf.size()) == 0)
+ return &buf[0];
+ else
+ return "Unknown error";
+ }
-class AudioFileException : public Av::Exception
-{
- public:
- AudioFileException(int avError)
- : Av::Exception {"AudioFileException: " + averror_to_string(avError)}
- {}
-};
+ class AudioFileException : public Av::Exception
+ {
+ public:
+ AudioFileException(int avError)
+ : Av::Exception{ "AudioFileException: " + averror_to_string(avError) }
+ {}
+ };
-std::unique_ptr
-parseAudioFile(const std::filesystem::path& p)
-{
- return std::make_unique(p);
-}
+ void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
+ {
+ if (!dictionnary)
+ return;
-AudioFile::AudioFile(const std::filesystem::path& p)
-: _p {p}
-{
- int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)};
- if (error < 0)
- {
- LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
- throw AudioFileException {error};
- }
+ AVDictionaryEntry* tag = NULL;
+ while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ res[StringUtils::stringToUpper(tag->key)] = tag->value;
+ }
+ }
- error = avformat_find_stream_info(_context, nullptr);
- if (error < 0)
- {
- LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
- avformat_close_input(&_context);
- throw AudioFileException {error};
- }
-}
+ DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
+ {
+ switch (codec)
+ {
+ case AV_CODEC_ID_MP3: return DecodingCodec::MP3;
+ case AV_CODEC_ID_AAC: return DecodingCodec::AAC;
+ case AV_CODEC_ID_AC3: return DecodingCodec::AC3;
+ case AV_CODEC_ID_VORBIS: return DecodingCodec::VORBIS;
+ case AV_CODEC_ID_WMAV1: return DecodingCodec::WMAV1;
+ case AV_CODEC_ID_WMAV2: return DecodingCodec::WMAV2;
+ case AV_CODEC_ID_FLAC: return DecodingCodec::FLAC;
+ case AV_CODEC_ID_ALAC: return DecodingCodec::ALAC;
+ case AV_CODEC_ID_WAVPACK: return DecodingCodec::WAVPACK;
+ case AV_CODEC_ID_MUSEPACK7: return DecodingCodec::MUSEPACK7;
+ case AV_CODEC_ID_MUSEPACK8: return DecodingCodec::MUSEPACK8;
+ case AV_CODEC_ID_APE: return DecodingCodec::APE;
+ case AV_CODEC_ID_EAC3: return DecodingCodec::EAC3;
+ case AV_CODEC_ID_MP4ALS: return DecodingCodec::MP4ALS;
+ case AV_CODEC_ID_OPUS: return DecodingCodec::OPUS;
+ case AV_CODEC_ID_SHORTEN: return DecodingCodec::SHORTEN;
+ default:
+ return DecodingCodec::UNKNOWN;
+ }
+ }
+ }
-AudioFile::~AudioFile()
-{
- avformat_close_input(&_context);
-}
+ std::unique_ptr parseAudioFile(const std::filesystem::path& p)
+ {
+ return std::make_unique(p);
+ }
-const std::filesystem::path&
-AudioFile::getPath() const
-{
- return _p;
-}
+ AudioFile::AudioFile(const std::filesystem::path& p)
+ : _p{ p }
+ {
+ int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
+ if (error < 0)
+ {
+ LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
+ throw AudioFileException{ error };
+ }
-std::chrono::milliseconds
-AudioFile::getDuration() const
-{
- if (_context->duration == AV_NOPTS_VALUE)
- return std::chrono::milliseconds {0}; // TODO estimate
+ error = avformat_find_stream_info(_context, nullptr);
+ if (error < 0)
+ {
+ LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
+ avformat_close_input(&_context);
+ throw AudioFileException{ error };
+ }
+ }
- return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000};
-}
+ AudioFile::~AudioFile()
+ {
+ avformat_close_input(&_context);
+ }
-void
-getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
-{
- if (!dictionnary)
- return;
+ const std::filesystem::path& AudioFile::getPath() const
+ {
+ return _p;
+ }
- AVDictionaryEntry *tag = NULL;
- while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
- {
- res[StringUtils::stringToUpper(tag->key)] = tag->value;
- }
-}
+ ContainerInfo AudioFile::getContainerInfo() const
+ {
+ ContainerInfo info;
+ info.bitrate = _context->bit_rate;
+ info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1000 };
+ info.name = _context->iformat->name;
-AudioFile::MetadataMap
-AudioFile::getMetaData() const
-{
- MetadataMap res;
+ return info;
+ }
- getMetaDataFromDictionnary(_context->metadata, res);
+ AudioFile::MetadataMap AudioFile::getMetaData() const
+ {
+ MetadataMap res;
- // HACK for OGG files
- // If we did not find tags, search metadata in streams
- if (res.empty())
- {
- for (std::size_t i {}; i < _context->nb_streams; ++i)
- {
- getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
+ getMetaDataFromDictionnary(_context->metadata, res);
- if (!res.empty())
- break;
- }
- }
+ // HACK for OGG files
+ // If we did not find tags, search metadata in streams
+ if (res.empty())
+ {
+ for (std::size_t i{}; i < _context->nb_streams; ++i)
+ {
+ getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
- return res;
-}
+ if (!res.empty())
+ break;
+ }
+ }
-std::vector
-AudioFile::getStreamInfo() const
-{
- std::vector res;
+ return res;
+ }
- for (std::size_t i {}; i < _context->nb_streams; ++i)
- {
- std::optional streamInfo {getStreamInfo(i)};
- if (streamInfo)
- res.emplace_back(std::move(*streamInfo));
- }
+ std::vector AudioFile::getStreamInfo() const
+ {
+ std::vector res;
- return res;
-}
+ for (std::size_t i{}; i < _context->nb_streams; ++i)
+ {
+ std::optional streamInfo{ getStreamInfo(i) };
+ if (streamInfo)
+ res.emplace_back(std::move(*streamInfo));
+ }
-std::optional
-AudioFile::getBestStreamIndex() const
-{
- int res = ::av_find_best_stream(_context,
- AVMEDIA_TYPE_AUDIO,
- -1, // Auto
- -1, // Auto
- NULL,
- 0);
+ return res;
+ }
- if (res < 0)
- return std::nullopt;
+ std::optional AudioFile::getBestStreamIndex() const
+ {
+ int res = ::av_find_best_stream(_context,
+ AVMEDIA_TYPE_AUDIO,
+ -1, // Auto
+ -1, // Auto
+ NULL,
+ 0);
- return res;
-}
+ if (res < 0)
+ return std::nullopt;
-std::optional
-AudioFile::getBestStreamInfo() const
-{
- std::optional res;
+ return res;
+ }
- std::optional bestStreamIndex {getBestStreamIndex()};
- if (bestStreamIndex)
- res = getStreamInfo(*bestStreamIndex);
+ std::optional AudioFile::getBestStreamInfo() const
+ {
+ std::optional res;
- return res;
-}
+ std::optional bestStreamIndex{ getBestStreamIndex() };
+ if (bestStreamIndex)
+ res = getStreamInfo(*bestStreamIndex);
-bool
-AudioFile::hasAttachedPictures() const
-{
- for (std::size_t i = 0; i < _context->nb_streams; ++i)
- {
- if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
- return true;
- }
+ return res;
+ }
- return false;
-}
+ bool AudioFile::hasAttachedPictures() const
+ {
+ for (std::size_t i = 0; i < _context->nb_streams; ++i)
+ {
+ if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
+ return true;
+ }
-void
-AudioFile::visitAttachedPictures(std::function func) const
-{
- static const std::unordered_map codecMimeMap =
- {
- { AV_CODEC_ID_BMP, "image/x-bmp" },
- { AV_CODEC_ID_GIF, "image/gif" },
- { AV_CODEC_ID_MJPEG, "image/jpeg" },
- { AV_CODEC_ID_PNG, "image/png" },
- { AV_CODEC_ID_PNG, "image/x-png" },
- { AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
- };
+ return false;
+ }
- for (std::size_t i = 0; i < _context->nb_streams; ++i)
- {
- AVStream *avstream = _context->streams[i];
+ void AudioFile::visitAttachedPictures(std::function func) const
+ {
+ static const std::unordered_map codecMimeMap =
+ {
+ { AV_CODEC_ID_BMP, "image/x-bmp" },
+ { AV_CODEC_ID_GIF, "image/gif" },
+ { AV_CODEC_ID_MJPEG, "image/jpeg" },
+ { AV_CODEC_ID_PNG, "image/png" },
+ { AV_CODEC_ID_PNG, "image/x-png" },
+ { AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
+ };
- // Skip attached pics
- if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
- continue;
+ for (std::size_t i = 0; i < _context->nb_streams; ++i)
+ {
+ AVStream* avstream = _context->streams[i];
- if (avstream->codecpar == nullptr)
- {
- LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
- continue;
- }
+ // Skip attached pics
+ if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
+ continue;
- Picture picture;
+ if (avstream->codecpar == nullptr)
+ {
+ LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
+ continue;
+ }
- auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
- if (itMime != codecMimeMap.end())
- {
- picture.mimeType = itMime->second;
- }
- else
- {
- picture.mimeType = "application/octet-stream";
- LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
- }
+ Picture picture;
- const AVPacket& pkt {avstream->attached_pic};
+ auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
+ if (itMime != codecMimeMap.end())
+ {
+ picture.mimeType = itMime->second;
+ }
+ else
+ {
+ picture.mimeType = "application/octet-stream";
+ LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
+ }
- picture.data = reinterpret_cast(pkt.data);
- picture.dataSize = pkt.size;
+ const AVPacket& pkt{ avstream->attached_pic };
- func(picture);
- }
-}
+ picture.data = reinterpret_cast(pkt.data);
+ picture.dataSize = pkt.size;
-std::optional
-AudioFile::getStreamInfo(std::size_t streamIndex) const
-{
- std::optional res;
+ func(picture);
+ }
+ }
- AVStream* avstream { _context->streams[streamIndex]};
- assert(avstream);
+ std::optional AudioFile::getStreamInfo(std::size_t streamIndex) const
+ {
+ std::optional res;
- if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
- return res;
+ AVStream* avstream{ _context->streams[streamIndex] };
+ assert(avstream);
- if (!avstream->codecpar)
- {
- LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
- return res;
- }
+ if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
+ return res;
- if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
- return res;
+ if (!avstream->codecpar)
+ {
+ LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
+ return res;
+ }
- res.emplace();
- res->index = streamIndex;
- res->bitrate = static_cast(avstream->codecpar->bit_rate);
- res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
- assert(!res->codec.empty());
+ if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
+ return res;
- return res;
-}
+ res.emplace();
+ res->index = streamIndex;
+ res->bitrate = static_cast(avstream->codecpar->bit_rate);
+ res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
+ res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
+ assert(!res->codecName.empty()); // doc says it is never NULL
-std::optional
-guessAudioFileFormat(const std::filesystem::path& file)
-{
- const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)};
- if (!format || !format->name)
- return {};
+ return res;
+ }
- LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'";
+ std::string_view getMimeType(const std::filesystem::path& fileExtension)
+ {
+ // List should be sync with the demuxers shipped in the lms's docker version
+ // + the _audioFileExtensions in ScanSettings
+ static const std::unordered_map entries
+ {
+ {".mp3", "audio/mpeg"},
+ {".ogg", "audio/ogg"},
+ {".oga", "audio/ogg"},
+ {".opus", "audio/opus"},
+ {".aac", "audio/aac"},
+ {".alac", "audio/mp4"},
+ {".m4a", "audio/mp4"},
+ {".m4b", "audio/mp4"},
+ {".flac", "audio/flac"},
+ {".webm", "audio/webm"},
+ {".wav", "audio/x-wav"},
+ {".wma", "audio/x-ms-wma"},
+ {".ape", "audio/x-monkeys-audio"},
+ {".mpc", "audio/x-musepack"},
+ {".shn", "audio/x-shn"},
+ {".aif", "audio/x-aiff"},
+ {".aiff", "audio/x-aiff"},
+ {".m3u", "audio/x-mpegurl"},
+ {".pls", "audio/x-scpls"},
+ {".dsf", "audio/dsd"},
+ {".wv", "audio/x-wavpack"},
+ {".wvp", "audio/x-wavpack"},
+ {".mka", "audio/x-matroska"},
+ };
- auto formats {StringUtils::splitString(format->name, ",")};
- if (formats.size() > 1)
- LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'";
-
- std::vector mimeTypes;
- if (format->mime_type)
- mimeTypes = StringUtils::splitString(format->mime_type, ",");
-
- if (mimeTypes.empty())
- LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!";
- else if (mimeTypes.size() > 1)
- LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'";
-
- AudioFileFormat res;
- res.format = formats.front();
- res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front();
-
- return res;
-}
+ auto it{ entries.find(fileExtension) };
+ if (it == std::cend(entries))
+ return "";
+ return it->second;
+ }
} // namespace Av
diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp
index 823b7e00..46e8f830 100644
--- a/src/libs/av/impl/AudioFile.hpp
+++ b/src/libs/av/impl/AudioFile.hpp
@@ -17,7 +17,7 @@
* along with LMS. If not, see .
*/
-/* This file contains some classes in order to get info from file using the libavconv */
+ /* This file contains some classes in order to get info from file using the libavconv */
#pragma once
@@ -28,32 +28,30 @@ struct AVFormatContext;
namespace Av
{
- class AudioFile final : public IAudioFile
- {
- public:
- AudioFile(const std::filesystem::path& p);
- ~AudioFile();
+ class AudioFile final : public IAudioFile
+ {
+ public:
+ AudioFile(const std::filesystem::path& p);
+ ~AudioFile();
- AudioFile(const AudioFile&) = delete;
- AudioFile(AudioFile&&) = delete;
- AudioFile& operator=(const AudioFile&) = delete;
- AudioFile& operator=(AudioFile&&) = delete;
+ const std::filesystem::path& getPath() const override;
+ ContainerInfo getContainerInfo() const override;
+ MetadataMap getMetaData() const override;
+ std::vector getStreamInfo() const override;
+ std::optional getBestStreamInfo() const override;
+ std::optional getBestStreamIndex() const override;
+ bool hasAttachedPictures() const override;
+ void visitAttachedPictures(std::function func) const override;
- const std::filesystem::path& getPath() const override;
- std::chrono::milliseconds getDuration() const override;
- MetadataMap getMetaData() const override;
- std::vector getStreamInfo() const override;
- std::optional getBestStreamInfo() const override;
- std::optional getBestStreamIndex() const override;
- bool hasAttachedPictures() const override;
- void visitAttachedPictures(std::function func) const override;
+ private:
+ AudioFile(const AudioFile&) = delete;
+ AudioFile& operator=(const AudioFile&) = delete;
- private:
- std::optional getStreamInfo(std::size_t streamIndex) const;
+ std::optional getStreamInfo(std::size_t streamIndex) const;
- const std::filesystem::path _p;
- AVFormatContext* _context {};
- };
+ const std::filesystem::path _p;
+ AVFormatContext* _context{};
+ };
} // namespace Av
diff --git a/src/libs/av/impl/RawResourceHandlerCreator.cpp b/src/libs/av/impl/RawResourceHandlerCreator.cpp
new file mode 100644
index 00000000..04ea6f88
--- /dev/null
+++ b/src/libs/av/impl/RawResourceHandlerCreator.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "av/RawResourceHandlerCreator.hpp"
+
+#include "av/IAudioFile.hpp"
+#include "utils/FileResourceHandlerCreator.hpp"
+
+namespace Av
+{
+ std::unique_ptr createRawResourceHandler(const std::filesystem::path& path)
+ {
+ std::string_view mimeType{ Av::getMimeType(path.extension()) };
+ return createFileResourceHandler(path, mimeType.empty() ? "application/octet-stream" : mimeType);
+ }
+}
diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp
deleted file mode 100644
index 753213c4..00000000
--- a/src/libs/av/impl/TranscodeResourceHandler.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2020 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include "TranscodeResourceHandler.hpp"
-#include "utils/Logger.hpp"
-
-namespace Av
-{
- namespace
- {
- std::size_t
- doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
- {
- const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputFileParameters.duration).count()) / 1000};
- return estimatedContentLength;
- }
- }
-
- std::unique_ptr
- createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
- {
- return std::make_unique(inputFileParameters, transcodeParameters, estimateContentLength);
- }
-
- // TODO set some nice HTTP return code
-
- TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
- : _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt}
- , _transcoder {inputFileParameters, transcodeParameters}
- {
- if (_estimatedContentLength)
- LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
- else
- LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
- }
-
- Wt::Http::ResponseContinuation*
- TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
- {
- if (_estimatedContentLength)
- response.setContentLength(*_estimatedContentLength);
- response.setMimeType(_transcoder.getOutputMimeType());
- LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
-
- if (_bytesReadyCount > 0)
- {
- LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
-
- response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount);
- _totalServedByteCount += _bytesReadyCount;
- _bytesReadyCount = 0;
- }
-
- if (!_transcoder.finished())
- {
- Wt::Http::ResponseContinuation *continuation {response.createContinuation()};
- continuation->waitForMoreData();
- _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
- {
- LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
-
- assert(_bytesReadyCount == 0);
- _bytesReadyCount = nbBytesRead;
- continuation->haveMoreData();
- });
-
- return continuation;
- }
- else
- {
- // pad with 0 if necessary as duration may not be accurate
- if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
- {
- const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount};
-
- LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
-
- for (std::size_t i {}; i < padSize; ++i)
- response.out().put(0);
-
- _totalServedByteCount += padSize;
- }
-
- LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
- }
-
- return {};
- }
-}
-
diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp
index 5519021d..c4c76902 100644
--- a/src/libs/av/impl/Transcoder.cpp
+++ b/src/libs/av/impl/Transcoder.cpp
@@ -28,181 +28,191 @@
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
-namespace Av {
-
-#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - "
-
-static std::atomic globalId {};
-static std::filesystem::path ffmpegPath;
-
-void
-Transcoder::init()
+namespace Av::Transcoding
{
- ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
- if (!std::filesystem::exists(ffmpegPath))
- throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
-}
-Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
-: _debugId {globalId++}
-, _inputFileParameters {inputFileParameters}
-, _transcodeParameters {transcodeParameters}
-{
- start();
-}
+#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
-Transcoder::~Transcoder() = default;
+ static std::atomic globalId{};
+ static std::filesystem::path ffmpegPath;
-void
-Transcoder::start()
-{
- if (ffmpegPath.empty())
- init();
+ std::string_view formatToMimetype(OutputFormat format)
+ {
+ switch (format)
+ {
+ case OutputFormat::MP3: return "audio/mpeg";
+ case OutputFormat::OGG_OPUS: return "audio/opus";
+ case OutputFormat::MATROSKA_OPUS: return "audio/x-matroska";
+ case OutputFormat::OGG_VORBIS: return "audio/ogg";
+ case OutputFormat::WEBM_VORBIS: return "audio/webm";
+ }
- try
- {
- if (!std::filesystem::exists(_inputFileParameters.trackPath))
- throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
- else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
- throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
- }
- catch (const std::filesystem::filesystem_error& e)
- {
- throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
- }
+ throw Exception{ "Invalid encoding" };
+ }
- LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'";
+ void Transcoder::init()
+ {
+ ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
+ if (!std::filesystem::exists(ffmpegPath))
+ throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
+ }
- std::vector args;
+ Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
+ : _debugId{ globalId++ }
+ , _inputParameters{ inputParameters }
+ , _outputParameters{ outputParameters }
+ {
+ start();
+ }
- args.emplace_back(ffmpegPath.string());
+ Transcoder::~Transcoder() = default;
- // Make sure:
- // - we do not produce anything in the stderr output
- // - we do not rely on input
- // in order not to block the whole forked process
- args.emplace_back("-loglevel");
- args.emplace_back("quiet");
- args.emplace_back("-nostdin");
+ void Transcoder::start()
+ {
+ if (ffmpegPath.empty())
+ init();
- // input Offset
- {
- args.emplace_back("-ss");
+ try
+ {
+ if (!std::filesystem::exists(_inputParameters.trackPath))
+ throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
+ else if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
+ throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
+ }
+ catch (const std::filesystem::filesystem_error& e)
+ {
+ throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
+ }
- std::ostringstream oss;
- oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000});
- args.emplace_back(oss.str());
- }
+ LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
- // Input file
- args.emplace_back("-i");
- args.emplace_back(_inputFileParameters.trackPath.string());
+ std::vector args;
- // Stream mapping, if set
- if (_transcodeParameters.stream)
- {
- args.emplace_back("-map");
- args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
- }
+ args.emplace_back(ffmpegPath.string());
- if (_transcodeParameters.stripMetadata)
- {
- // Strip metadata
- args.emplace_back("-map_metadata");
- args.emplace_back("-1");
- }
+ // Make sure:
+ // - we do not produce anything in the stderr output
+ // - we do not rely on input
+ // in order not to block the whole forked process
+ args.emplace_back("-loglevel");
+ args.emplace_back("quiet");
+ args.emplace_back("-nostdin");
- // Skip video flows (including covers)
- args.emplace_back("-vn");
+ // input Offset
+ {
+ args.emplace_back("-ss");
- // Output bitrates
- args.emplace_back("-b:a");
- args.emplace_back(std::to_string(_transcodeParameters.bitrate));
+ std::ostringstream oss;
+ oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 });
+ args.emplace_back(oss.str());
+ }
- // Codecs and formats
- switch (_transcodeParameters.format)
- {
- case Format::MP3:
- args.emplace_back("-f");
- args.emplace_back("mp3");
- break;
+ // Input file
+ args.emplace_back("-i");
+ args.emplace_back(_inputParameters.trackPath.string());
- case Format::OGG_OPUS:
- args.emplace_back("-acodec");
- args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("ogg");
- break;
+ // Stream mapping, if set
+ if (_outputParameters.stream)
+ {
+ args.emplace_back("-map");
+ args.emplace_back("0:" + std::to_string(*_outputParameters.stream));
+ }
- case Format::MATROSKA_OPUS:
- args.emplace_back("-acodec");
- args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("matroska");
- break;
+ if (_outputParameters.stripMetadata)
+ {
+ // Strip metadata
+ args.emplace_back("-map_metadata");
+ args.emplace_back("-1");
+ }
- case Format::OGG_VORBIS:
- args.emplace_back("-acodec");
- args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("ogg");
- break;
+ // Skip video flows (including covers)
+ args.emplace_back("-vn");
- case Format::WEBM_VORBIS:
- args.emplace_back("-acodec");
- args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("webm");
- break;
+ // Output bitrates
+ args.emplace_back("-b:a");
+ args.emplace_back(std::to_string(_outputParameters.bitrate));
- default:
- throw Exception {"Unhandled format (" + std::to_string(static_cast(_transcodeParameters.format)) + ")"};
- }
+ // Codecs and formats
+ switch (_outputParameters.format)
+ {
+ case OutputFormat::MP3:
+ args.emplace_back("-f");
+ args.emplace_back("mp3");
+ break;
- _outputMimeType = formatToMimetype(_transcodeParameters.format);
+ case OutputFormat::OGG_OPUS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libopus");
+ args.emplace_back("-f");
+ args.emplace_back("ogg");
+ break;
- args.emplace_back("pipe:1");
+ case OutputFormat::MATROSKA_OPUS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libopus");
+ args.emplace_back("-f");
+ args.emplace_back("matroska");
+ break;
- LOG(DEBUG) << "Dumping args (" << args.size() << ")";
- for (const std::string& arg : args)
- LOG(DEBUG) << "Arg = '" << arg << "'";
+ case OutputFormat::OGG_VORBIS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libvorbis");
+ args.emplace_back("-f");
+ args.emplace_back("ogg");
+ break;
- // Caution: stdin must have been closed before
- try
- {
- _childProcess = Service::get()->spawnChildProcess(ffmpegPath, args);
- }
- catch (ChildProcessException& exception)
- {
- throw Exception {"Cannot execute '" + ffmpegPath.string() + "': " + exception.what()};
- }
-}
+ case OutputFormat::WEBM_VORBIS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libvorbis");
+ args.emplace_back("-f");
+ args.emplace_back("webm");
+ break;
-void
-Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
-{
- assert(_childProcess);
+ default:
+ throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParameters.format)) + ")" };
+ }
- return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
- {
- readCallback(nbBytesRead);
- });
-}
+ _outputMimeType = formatToMimetype(_outputParameters.format);
-std::size_t
-Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
-{
- assert(_childProcess);
+ args.emplace_back("pipe:1");
- return _childProcess->readSome(buffer, bufferSize);
-}
+ LOG(DEBUG) << "Dumping args (" << args.size() << ")";
+ for (const std::string& arg : args)
+ LOG(DEBUG) << "Arg = '" << arg << "'";
-bool
-Transcoder::finished() const
-{
- assert(_childProcess);
+ // Caution: stdin must have been closed before
+ try
+ {
+ _childProcess = Service::get()->spawnChildProcess(ffmpegPath, args);
+ }
+ catch (ChildProcessException& exception)
+ {
+ throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
+ }
+ }
- return _childProcess->finished();
-}
+ void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
+ {
+ assert(_childProcess);
-} // namespace Transcode
+ return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
+ {
+ readCallback(nbBytesRead);
+ });
+ }
+
+ std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
+ {
+ assert(_childProcess);
+
+ return _childProcess->readSome(buffer, bufferSize);
+ }
+
+ bool Transcoder::finished() const
+ {
+ assert(_childProcess);
+
+ return _childProcess->finished();
+ }
+
+} // namespace Av::Transcoding
diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp
index 0f6cf4c7..0bd3e1d2 100644
--- a/src/libs/av/impl/Transcoder.hpp
+++ b/src/libs/av/impl/Transcoder.hpp
@@ -22,47 +22,45 @@
#include
#include
-#include "av/TranscodeParameters.hpp"
+#include "av/TranscodingParameters.hpp"
#include "av/Types.hpp"
class IChildProcess;
-namespace Av
+namespace Av::Transcoding
{
- class Transcoder
- {
- public:
- Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters);
- ~Transcoder();
+ class Transcoder
+ {
+ public:
+ Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
+ ~Transcoder();
- Transcoder(const Transcoder&) = delete;
- Transcoder& operator=(const Transcoder&) = delete;
- Transcoder(Transcoder&&) = delete;
- Transcoder& operator=(Transcoder&&) = delete;
+ Transcoder(const Transcoder&) = delete;
+ Transcoder& operator=(const Transcoder&) = delete;
+ Transcoder(Transcoder&&) = delete;
+ Transcoder& operator=(Transcoder&&) = delete;
- // non blocking calls
- using ReadCallback = std::function;
- void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
- std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
+ // non blocking calls
+ using ReadCallback = std::function;
+ void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
+ std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
- const std::string& getOutputMimeType() const { return _outputMimeType; }
- const TranscodeParameters& getParameters() const { return _transcodeParameters; }
+ const std::string& getOutputMimeType() const { return _outputMimeType; }
+ const OutputParameters& getOutputParameters() const { return _outputParameters; }
- bool finished() const;
+ bool finished() const;
- private:
- static void init();
+ private:
+ static void init();
- void start();
+ void start();
- const std::size_t _debugId {};
- const InputFileParameters _inputFileParameters;
- const TranscodeParameters _transcodeParameters;
+ const std::size_t _debugId{};
+ const InputParameters _inputParameters;
+ const OutputParameters _outputParameters;
+ std::string _outputMimeType;
- std::unique_ptr _childProcess;
-
- std::string _outputMimeType;
- };
-
-} // namespace Av
+ std::unique_ptr _childProcess;
+ };
+} // namespace Av::Transcoding
diff --git a/src/libs/av/impl/TranscodingResourceHandler.cpp b/src/libs/av/impl/TranscodingResourceHandler.cpp
new file mode 100644
index 00000000..f5d2ca0f
--- /dev/null
+++ b/src/libs/av/impl/TranscodingResourceHandler.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2020 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "TranscodingResourceHandler.hpp"
+#include "utils/Logger.hpp"
+
+namespace Av::Transcoding
+{
+ namespace
+ {
+ std::size_t doEstimateContentLength(const InputParameters& inputParameters, const OutputParameters& outputParameters)
+ {
+ const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputParameters.duration).count()) / 1000 };
+ return estimatedContentLength;
+ }
+ }
+
+ std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
+ {
+ return std::make_unique(inputParameters, outputParameters, estimateContentLength);
+ }
+
+ // TODO set some nice HTTP return code
+
+ TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
+ : _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt }
+ , _transcoder{ inputParameters, outputParameters }
+ {
+ if (_estimatedContentLength)
+ LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
+ else
+ LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length";
+ }
+
+ Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
+ {
+ if (_estimatedContentLength)
+ response.setContentLength(*_estimatedContentLength);
+ response.setMimeType(_transcoder.getOutputMimeType());
+ LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
+
+ if (_bytesReadyCount > 0)
+ {
+ LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
+
+ response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount);
+ _totalServedByteCount += _bytesReadyCount;
+ _bytesReadyCount = 0;
+ }
+
+ if (!_transcoder.finished())
+ {
+ Wt::Http::ResponseContinuation* continuation{ response.createContinuation() };
+ continuation->waitForMoreData();
+ _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
+ {
+ LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
+
+ assert(_bytesReadyCount == 0);
+ _bytesReadyCount = nbBytesRead;
+ continuation->haveMoreData();
+ });
+
+ return continuation;
+ }
+ else
+ {
+ // pad with 0 if necessary as duration may not be accurate
+ if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
+ {
+ const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
+
+ LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes";
+
+ for (std::size_t i{}; i < padSize; ++i)
+ response.out().put(0);
+
+ _totalServedByteCount += padSize;
+ }
+
+ LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
+ }
+
+ return {};
+ }
+}
+
diff --git a/src/libs/av/impl/TranscodeResourceHandler.hpp b/src/libs/av/impl/TranscodingResourceHandler.hpp
similarity index 52%
rename from src/libs/av/impl/TranscodeResourceHandler.hpp
rename to src/libs/av/impl/TranscodingResourceHandler.hpp
index ff254b63..6821d42a 100644
--- a/src/libs/av/impl/TranscodeResourceHandler.hpp
+++ b/src/libs/av/impl/TranscodingResourceHandler.hpp
@@ -23,27 +23,27 @@
#include
#include
-#include "av/TranscodeParameters.hpp"
+#include "av/TranscodingParameters.hpp"
#include "utils/IResourceHandler.hpp"
#include "Transcoder.hpp"
-namespace Av
+namespace Av::Transcoding
{
- class TranscodeResourceHandler final : public IResourceHandler
- {
- public:
- TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
+ class TranscodingResourceHandler final : public IResourceHandler
+ {
+ public:
+ TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
- private:
- Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
- void abort() override {};
+ private:
+ Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
+ void abort() override {};
- static constexpr std::size_t _chunkSize {32768};
- std::optional _estimatedContentLength;
- std::array _buffer;
- std::size_t _bytesReadyCount {};
- std::size_t _totalServedByteCount {};
- Transcoder _transcoder;
- };
+ static constexpr std::size_t _chunkSize{ 262'144 };
+ std::optional _estimatedContentLength;
+ std::array _buffer;
+ std::size_t _bytesReadyCount{};
+ std::size_t _totalServedByteCount{};
+ Transcoder _transcoder;
+ };
}
diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp
index 3083056a..1ce66e57 100644
--- a/src/libs/av/include/av/IAudioFile.hpp
+++ b/src/libs/av/include/av/IAudioFile.hpp
@@ -17,7 +17,7 @@
* along with LMS. If not, see .
*/
-/* This file contains some classes in order to get info from file using the libavconv */
+ /* This file contains some classes in order to get info from file using the libavconv */
#pragma once
@@ -27,52 +27,84 @@
#include
#include
#include
+#include
#include
#include "Types.hpp"
namespace Av
{
- struct Picture
- {
- std::string mimeType;
- const std::byte* data {};
- std::size_t dataSize {};
- };
+ // List should be sync with the codecs shipped in the lms's docker version
+ enum class DecodingCodec
+ {
+ UNKNOWN,
+ MP3,
+ AAC,
+ AC3,
+ VORBIS,
+ WMAV1,
+ WMAV2,
+ FLAC, // Flac
+ ALAC, // Apple Lossless Audio Codec (ALAC)
+ WAVPACK, // WavPack
+ MUSEPACK7, // Musepack
+ MUSEPACK8,
+ APE, // // Monkey's Audio
+ EAC3, // Enhanced AC-3
+ MP4ALS, // MPEG-4 Audio Lossless Coding
+ OPUS, // Opus
+ SHORTEN, // Shorten (shn)
+ // TODO add PCM codecs
+ };
- struct StreamInfo
- {
- size_t index {};
- std::size_t bitrate {};
- std::string codec;
- };
+ struct Picture
+ {
+ std::string mimeType;
+ const std::byte* data{};
+ std::size_t dataSize{};
+ };
- class IAudioFile
- {
- public:
- virtual ~IAudioFile() = default;
+ struct ContainerInfo
+ {
+ std::size_t bitrate{};
+ std::string name{};
+ std::chrono::milliseconds duration{};
+ };
- using MetadataMap = std::unordered_map;
+ struct StreamInfo
+ {
+ size_t index{};
+ std::size_t bitrate{};
+ DecodingCodec codec;
+ std::string codecName;
+ };
- virtual const std::filesystem::path& getPath() const = 0;
- virtual std::chrono::milliseconds getDuration() const = 0;
- virtual MetadataMap getMetaData() const = 0;
- virtual std::vector getStreamInfo() const = 0;
- virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown
- virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown
- virtual bool hasAttachedPictures() const = 0;
- virtual void visitAttachedPictures(std::function func) const = 0;
- };
+ class IAudioFile
+ {
+ public:
+ virtual ~IAudioFile() = default;
- std::unique_ptr parseAudioFile(const std::filesystem::path& p);
+ using MetadataMap = std::unordered_map;
- struct AudioFileFormat
- {
- std::string mimeType;
- std::string format;
- };
+ virtual const std::filesystem::path& getPath() const = 0;
+ virtual ContainerInfo getContainerInfo() const = 0;
+ virtual MetadataMap getMetaData() const = 0;
+ virtual std::vector getStreamInfo() const = 0;
+ virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown
+ virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown
+ virtual bool hasAttachedPictures() const = 0;
+ virtual void visitAttachedPictures(std::function func) const = 0;
+ };
- std::optional guessAudioFileFormat(const std::filesystem::path& file);
+ std::unique_ptr parseAudioFile(const std::filesystem::path& p);
+
+ struct AudioFileFormat
+ {
+ std::string mimeType;
+ std::string format;
+ };
+
+ std::string_view getMimeType(const std::filesystem::path& fileExtension);
} // namespace Av
diff --git a/src/libs/av/impl/Types.cpp b/src/libs/av/include/av/RawResourceHandlerCreator.hpp
similarity index 60%
rename from src/libs/av/impl/Types.cpp
rename to src/libs/av/include/av/RawResourceHandlerCreator.hpp
index 713491bc..ed7b0e14 100644
--- a/src/libs/av/impl/Types.cpp
+++ b/src/libs/av/include/av/RawResourceHandlerCreator.hpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Emeric Poupon
+ * Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,25 +17,14 @@
* along with LMS. If not, see .
*/
-#include "av/Types.hpp"
+#pragma once
+
+#include
+#include
+
+#include "utils/IResourceHandler.hpp"
namespace Av
{
-
- std::string_view
- formatToMimetype(Format format)
- {
- switch (format)
- {
- case Format::MP3: return "audio/mpeg";
- case Format::OGG_OPUS: return "audio/opus";
- case Format::MATROSKA_OPUS: return "audio/x-matroska";
- case Format::OGG_VORBIS: return "audio/ogg";
- case Format::WEBM_VORBIS: return "audio/webm";
- }
-
- throw Exception {"Invalid encoding"};
- }
-
-}
-
+ std::unique_ptr createRawResourceHandler(const std::filesystem::path& path);
+}
\ No newline at end of file
diff --git a/src/libs/av/include/av/TranscodingParameters.hpp b/src/libs/av/include/av/TranscodingParameters.hpp
new file mode 100644
index 00000000..13d8f961
--- /dev/null
+++ b/src/libs/av/include/av/TranscodingParameters.hpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Types.hpp"
+
+namespace Av::Transcoding
+{
+ struct InputParameters
+ {
+ std::filesystem::path trackPath;
+ std::chrono::milliseconds duration; // used to estimate content length
+ };
+
+ enum class OutputFormat
+ {
+ MP3,
+ OGG_OPUS,
+ MATROSKA_OPUS,
+ OGG_VORBIS,
+ WEBM_VORBIS,
+ };
+
+ std::string_view toMimetype(OutputFormat format);
+
+ struct OutputParameters
+ {
+ OutputFormat format;
+ std::size_t bitrate{ 128000 };
+ std::optional stream; // Id of the stream to be transcoded (auto detect by default)
+ std::chrono::milliseconds offset{ 0 };
+ bool stripMetadata{ true };
+ };
+} // namespace Av::Transcoding
+
diff --git a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp
similarity index 75%
rename from src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp
rename to src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp
index 8b232d8a..16e0fcbf 100644
--- a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp
+++ b/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp
@@ -23,11 +23,10 @@
#include "utils/IResourceHandler.hpp"
-namespace Av
+namespace Av::Transcoding
{
- struct InputFileParameters;
- struct TranscodeParameters;
+ struct InputParameters;
+ struct OutputParameters;
- std::unique_ptr createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
+ std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
}
-
diff --git a/src/libs/av/include/av/Types.hpp b/src/libs/av/include/av/Types.hpp
index 166a19da..249529e3 100644
--- a/src/libs/av/include/av/Types.hpp
+++ b/src/libs/av/include/av/Types.hpp
@@ -19,27 +19,13 @@
#pragma once
-#include
-
#include "utils/Exception.hpp"
-namespace Av {
-
- class Exception : public LmsException
- {
- public:
- using LmsException::LmsException;
- };
-
- enum class Format
- {
- MP3,
- OGG_OPUS,
- MATROSKA_OPUS,
- OGG_VORBIS,
- WEBM_VORBIS,
- };
-
- std::string_view formatToMimetype(Format format);
+namespace Av
+{
+ class Exception : public LmsException
+ {
+ public:
+ using LmsException::LmsException;
+ };
}
-
diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp
index 8b3938f6..63347cc5 100644
--- a/src/libs/metadata/impl/AvFormatParser.cpp
+++ b/src/libs/metadata/impl/AvFormatParser.cpp
@@ -186,18 +186,9 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
{
const auto mediaFile {Av::parseAudioFile(p)};
- // Stream info
- {
- std::vector audioStreams;
-
- for (auto stream : mediaFile->getStreamInfo())
- {
- MetaData::AudioStream audioStream {static_cast(stream.bitrate)};
- track.audioStreams.emplace_back(audioStream);
- }
- }
-
- track.duration = mediaFile->getDuration();
+ Av::ContainerInfo info{ mediaFile->getContainerInfo() };
+ track.duration = info.duration;
+ track.bitrate = info.bitrate;
track.hasCover = mediaFile->hasAttachedPictures();
MetaData::Tags tags;
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp
index cfcaaeb7..1540142f 100644
--- a/src/libs/metadata/impl/TagLibParser.cpp
+++ b/src/libs/metadata/impl/TagLibParser.cpp
@@ -44,503 +44,479 @@
namespace MetaData
{
-
-// TODO use string_views here for values
-using TagMap = std::map>;
-
-template
-std::vector
-getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys)
-{
- std::vector res;
-
- for (std::string_view key : keys)
- {
- const auto itValues {tags.find(std::string {key})};
- if (itValues == std::cend(tags))
- continue;
-
- const std::vector& values {itValues->second};
- if (values.empty())
- continue;
-
- res.reserve(values.size());
-
- for (const auto& value : values)
- {
- std::optional val {StringUtils::readAs(value)};
- if (!val)
- continue;
-
- res.emplace_back(std::move(*val));
- }
-
- break;
- }
-
- return res;
-}
-
-template
-std::optional
-getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys)
-{
- std::optional res;
- std::vector values {getPropertyValuesFirstMatchAs(tags, keys)};
- if (!values.empty())
- res = std::move(values.front());
-
- return res;
-}
-
-template
-std::vector
-getPropertyValuesAs(const TagMap& tags, std::string_view key)
-{
- return getPropertyValuesFirstMatchAs(tags, {key});
-}
-
-template
-std::optional
-getPropertyValueAs(const TagMap& tags, std::string_view key)
-{
- return getPropertyValueFirstMatchAs(tags, {key});
-}
-
-static
-std::vector
-splitAndTrimString(std::string_view str, std::string_view delimiters)
-{
- std::vector strings {StringUtils::splitString(str, delimiters)};
- for (std::string_view& s : strings)
- s = StringUtils::stringTrim(s);
-
- return strings;
-}
-
-static
-std::vector
-getArtists(const TagMap& tags,
- std::initializer_list artistTagNames,
- std::initializer_list artistSortTagNames,
- std::initializer_list artistMBIDTagNames
- )
-{
- const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)};
- if (artistNames.empty())
- return {};
-
- std::vector artists;
- artists.reserve(artistNames.size());
- std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
- [&](std::string_view name) { return Artist {name}; });
-
- {
- const std::vector artistSortNames {getPropertyValuesFirstMatchAs(tags, artistSortTagNames)};
- if (artistSortNames.size() == artists.size())
- {
- for (std::size_t i {}; i < artistSortNames.size(); ++i)
- artists[i].sortName = artistSortNames[i];
- }
- }
-
- {
- const std::vector artistsMBID {getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames)};
-
- if (artistNames.size() == artistsMBID.size())
- {
- for (std::size_t i {}; i < artistsMBID.size(); ++i)
- artists[i].mbid = artistsMBID[i];
- }
- }
-
-
- return artists;
-}
-
-static
-PerformerContainer
-getPerformerArtists(const TagMap& tags,
- std::initializer_list artistTagNames)
-{
- PerformerContainer performers;
-
- // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
- // We may hit both styles for the same track
- // PERFORMER: artist (role)
- if (const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; !artistNames.empty())
- {
- for (std::string_view entry : artistNames)
- {
- Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)};
- StringUtils::capitalize(performer.role);
- performers[performer.role].push_back(std::move(performer.artist));
- }
- }
- // PERFORMER:role (MP3)
- for (const auto& [key, values] : tags)
- {
- if (key.find("PERFORMER:") == 0)
- {
- std::string performerStr {key};
- std::string role;
- if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
- {
- role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
- StringUtils::capitalize(role);
- }
-
- for (const auto& value : values)
- performers[role].push_back(Artist {value});
- }
- }
-
- return performers;
-}
-
-static
-std::optional
-getRelease(const TagMap& tags)
-{
- std::optional release;
-
- auto releaseName {getPropertyValueAs(tags, "ALBUM")};
- if (!releaseName)
- return release;
-
- release.emplace();
- release->name = std::move(*releaseName);
- release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or("");
- release->mbid = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"});
- release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
- release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL");
- if (!release->mediumCount)
- {
- // mediumCount may be encoded as "position/count"
- if (const auto value {getPropertyValueAs(tags, "DISCNUMBER")})
- {
- // Expecting 'Number/Total'
- const std::vector strings {StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- release->mediumCount = StringUtils::readAs(strings[1]);
- }
- }
-
- release->primaryType = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"});
- if (release->primaryType)
- {
- const auto secondaryTypes {getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})};
- release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes));
- }
-
- return release;
-}
-
-static
-std::optional
-getMedium(const TagMap& tags)
-{
- std::optional medium;
- medium.emplace();
-
- medium->type = getPropertyValueAs(tags, "MEDIA").value_or("");
- medium->name = getPropertyValueFirstMatchAs(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or("");
- medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL");
- if (!medium->trackCount)
- {
- // totalTracks may be encoded as "position/count"
- if (const auto value {getPropertyValueAs(tags, "TRACKNUMBER")})
- {
- // Expecting 'Number/Total'
- const std::vector strings {StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- medium->trackCount = StringUtils::readAs(strings[1]);
- }
- }
- // Expecting 'Number[/Total]'
- medium->position = getPropertyValueAs(tags, "DISCNUMBER");
- medium->release = getRelease(tags);
- medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN");
-
- if (medium->type.empty()
- && medium->name.empty()
- && !medium->trackCount
- && !medium->position
- && !medium->release
- && !medium->replayGain)
- {
- medium.reset();
- }
-
- return medium;
-}
-
-static
-TagLib::AudioProperties::ReadStyle
-readStyleToTagLibReadStyle(ParserReadStyle readStyle)
-{
- switch (readStyle)
- {
- case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
- case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
- case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
- }
-
- throw LmsException {"Cannot convert read style"};
-}
-
-TagLibParser::TagLibParser(ParserReadStyle readStyle)
- : _readStyle {readStyleToTagLibReadStyle(readStyle)}
-{
-}
-
-void
-TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug)
-{
- if (debug)
- std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
-
- if (tag.empty() || values.empty())
- return;
-
- std::string_view value {values.front()};
-
- if (tag == "TITLE")
- track.title = value;
- else if (tag == "MUSICBRAINZ_RELEASETRACKID"
- || tag == "MUSICBRAINZ RELEASE TRACK ID"
- || tag == "MUSICBRAINZ/RELEASE TRACK ID")
- {
- track.mbid = UUID::fromString(value);
- }
- else if (tag == "MUSICBRAINZ_TRACKID"
- || tag == "MUSICBRAINZ TRACK ID"
- || tag == "MUSICBRAINZ/TRACK ID")
- track.recordingMBID = UUID::fromString(value);
- else if (tag == "ACOUSTID_ID")
- track.acoustID = UUID::fromString(value);
- else if (tag == "TRACKNUMBER")
- {
- // Expecting 'Number/Total'
- track.position = StringUtils::readAs(value);
- }
- else if (tag == "DATE")
- {
- // Higher priority than YEAR
- if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid())
- track.date = date;
- }
- else if (tag == "YEAR" && !track.date.isValid())
- {
- // lower priority than DATE
- track.date = Utils::parseDate(value);
- }
- else if (tag == "ORIGINALDATE")
- {
- // Higher priority than ORIGINALYEAR
- if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid())
- track.originalDate = date;
- }
- else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
- {
- // Lower priority than ORIGINALDATE
- track.originalDate = Utils::parseDate(value);
- }
- else if (tag == "METADATA_BLOCK_PICTURE")
- track.hasCover = true;
- else if (tag == "COPYRIGHT")
- track.copyright = value;
- else if (tag == "COPYRIGHTURL")
- track.copyrightURL = value;
- else if (tag == "REPLAYGAIN_TRACK_GAIN")
- track.replayGain = StringUtils::readAs(value);
- else if (tag == "ARTIST")
- track.artistDisplayName = value;
- else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
- {
- std::set clusterNames;
- for (std::string_view valueList : values)
- {
- const std::vector splittedValues {splitAndTrimString(valueList, "/,;")};
- for (std::string_view value : splittedValues)
- clusterNames.insert(std::string {value});
- }
-
- if (!clusterNames.empty())
- track.tags[tag] = std::move(clusterNames);
- }
-}
-
-static
-TagMap
-constructTagMap(const TagLib::PropertyMap& properties)
-{
- TagMap tagMap;
-
- for (const auto& [propertyName, propertyValues] : properties)
- {
- std::vector& values {tagMap[propertyName.upper().to8Bit(true)]};
- for (const TagLib::String& propertyValue : propertyValues)
- {
- std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))};
- if (!trimedValue.empty())
- values.emplace_back(std::move(trimedValue));
- }
- }
-
- return tagMap;
-}
-
-static
-void
-mergeTagMaps(TagMap& dst, TagMap&& src)
-{
- for (auto&& [tag, values] : src)
- {
- if (dst.find(tag) == std::cend(dst))
- dst[tag] = std::move(values);
- }
-}
-
-std::optional
-TagLibParser::parse(const std::filesystem::path& p, bool debug)
-{
- TagLib::FileRef f {p.string().c_str(),
- true, // read audio properties
- _readStyle};
-
- if (f.isNull())
- {
- LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
- return std::nullopt;
- }
-
- if (!f.audioProperties())
- {
- LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
- return std::nullopt;
- }
-
- Track track;
-
- {
- const TagLib::AudioProperties *properties {f.audioProperties() };
-
- track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
-
- MetaData::AudioStream audioStream {static_cast(properties->bitrate() * 1000)};
- track.audioStreams = {audioStream};
- }
-
- TagMap tags {constructTagMap(f.file()->properties())};
-
- auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
- {
- if (!apeTag)
- return;
-
- mergeTagMaps(tags, constructTagMap(apeTag->properties()));
- };
-
- // Not that good embedded pictures handling
-
- // WMA
- if (TagLib::ASF::File* asfFile {dynamic_cast(f.file())})
- {
- const TagLib::ASF::Tag* tag {asfFile->tag()};
- if (tag)
- {
- if (tag->attributeListMap().contains("WM/Picture"))
- track.hasCover = true;
-
- for (const auto& [name, attributeList] : tag->attributeListMap())
- {
- std::string strName {StringUtils::stringToUpper(name.to8Bit(true))};
- if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
- continue;
-
- std::vector attributes;
- for (const auto& attribute : attributeList)
- {
- if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
- attributes.emplace_back(attribute.toString().to8Bit(true));
- }
-
- if (!attributes.empty())
- {
- if (debug)
- std::cout << "ASF property: '" << name << "'" << std::endl;
-
- tags.emplace(strName, std::move(attributes));
- }
- }
- }
- }
- // MP3
- else if (TagLib::MPEG::File* mp3File {dynamic_cast(f.file())})
- {
- if (mp3File->ID3v2Tag())
- {
- const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()};
-
- if (!frameListMap["APIC"].isEmpty())
- track.hasCover = true;
- if (!frameListMap["TSST"].isEmpty())
- tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)};
- }
-
- getAPETags(mp3File->APETag());
- }
- //MP4
- else if (TagLib::MP4::File* mp4File {dynamic_cast(f.file())})
- {
- TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")};
- TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
- if (!coverArtList.isEmpty())
- track.hasCover = true;
- }
- // MPC
- else if (TagLib::MPC::File* mpcFile {dynamic_cast(f.file())})
- {
- getAPETags(mpcFile->APETag());
- }
- // WavPack
- else if (TagLib::WavPack::File* wavPackFile {dynamic_cast(f.file())})
- {
- getAPETags(wavPackFile->APETag());
- }
- // FLAC
- else if (TagLib::FLAC::File* flacFile {dynamic_cast(f.file())})
- {
- if (!flacFile->pictureList().isEmpty())
- track.hasCover = true;
- }
- else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast(f.file())})
- {
- if (!vorbisFile->tag()->pictureList().isEmpty())
- track.hasCover = true;
- }
- else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast(f.file())})
- {
- if (!opusFile->tag()->pictureList().isEmpty())
- track.hasCover = true;
- }
-
- track.medium = getMedium(tags);
- track.artists = getArtists(tags, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
- track.conductorArtists = getArtists(tags, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
- track.composerArtists = getArtists(tags, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
- track.lyricistArtists = getArtists(tags, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
- track.mixerArtists = getArtists(tags, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
- track.producerArtists = getArtists(tags, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
- track.remixerArtists = getArtists(tags, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
- track.performerArtists = getPerformerArtists(tags, {"PERFORMERS", "PERFORMER"});
-
- for (const auto& [tag, values] : tags)
- processTag(track, tag, values, debug);
-
- return track;
-}
+ namespace
+ {
+ // TODO use string_views here for values
+ using TagMap = std::map>;
+
+ template
+ std::vector getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys)
+ {
+ std::vector res;
+
+ for (std::string_view key : keys)
+ {
+ const auto itValues{ tags.find(std::string {key}) };
+ if (itValues == std::cend(tags))
+ continue;
+
+ const std::vector& values{ itValues->second };
+ if (values.empty())
+ continue;
+
+ res.reserve(values.size());
+
+ for (const auto& value : values)
+ {
+ std::optional val{ StringUtils::readAs(value) };
+ if (!val)
+ continue;
+
+ res.emplace_back(std::move(*val));
+ }
+
+ break;
+ }
+
+ return res;
+ }
+
+ template
+ std::optional getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys)
+ {
+ std::optional res;
+ std::vector values{ getPropertyValuesFirstMatchAs(tags, keys) };
+ if (!values.empty())
+ res = std::move(values.front());
+
+ return res;
+ }
+
+ template
+ std::vector getPropertyValuesAs(const TagMap& tags, std::string_view key)
+ {
+ return getPropertyValuesFirstMatchAs(tags, { key });
+ }
+
+ template
+ std::optional getPropertyValueAs(const TagMap& tags, std::string_view key)
+ {
+ return getPropertyValueFirstMatchAs(tags, { key });
+ }
+
+ std::vector splitAndTrimString(std::string_view str, std::string_view delimiters)
+ {
+ std::vector strings{ StringUtils::splitString(str, delimiters) };
+ for (std::string_view& s : strings)
+ s = StringUtils::stringTrim(s);
+
+ return strings;
+ }
+
+ std::vector getArtists(const TagMap& tags,
+ std::initializer_list artistTagNames,
+ std::initializer_list artistSortTagNames,
+ std::initializer_list artistMBIDTagNames
+ )
+ {
+ const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) };
+ if (artistNames.empty())
+ return {};
+
+ std::vector artists;
+ artists.reserve(artistNames.size());
+ std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
+ [&](std::string_view name) { return Artist{ name }; });
+
+ {
+ const std::vector artistSortNames{ getPropertyValuesFirstMatchAs(tags, artistSortTagNames) };
+ if (artistSortNames.size() == artists.size())
+ {
+ for (std::size_t i{}; i < artistSortNames.size(); ++i)
+ artists[i].sortName = artistSortNames[i];
+ }
+ }
+
+ {
+ const std::vector artistsMBID{ getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames) };
+
+ if (artistNames.size() == artistsMBID.size())
+ {
+ for (std::size_t i{}; i < artistsMBID.size(); ++i)
+ artists[i].mbid = artistsMBID[i];
+ }
+ }
+
+
+ return artists;
+ }
+
+ PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list artistTagNames)
+ {
+ PerformerContainer performers;
+
+ // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
+ // We may hit both styles for the same track
+ // PERFORMER: artist (role)
+ if (const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; !artistNames.empty())
+ {
+ for (std::string_view entry : artistNames)
+ {
+ Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) };
+ StringUtils::capitalize(performer.role);
+ performers[performer.role].push_back(std::move(performer.artist));
+ }
+ }
+ // PERFORMER:role (MP3)
+ for (const auto& [key, values] : tags)
+ {
+ if (key.find("PERFORMER:") == 0)
+ {
+ std::string performerStr{ key };
+ std::string role;
+ if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos)
+ {
+ role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
+ StringUtils::capitalize(role);
+ }
+
+ for (const auto& value : values)
+ performers[role].push_back(Artist{ value });
+ }
+ }
+
+ return performers;
+ }
+
+ std::optional getRelease(const TagMap& tags)
+ {
+ std::optional release;
+
+ auto releaseName{ getPropertyValueAs(tags, "ALBUM") };
+ if (!releaseName)
+ return release;
+
+ release.emplace();
+ release->name = std::move(*releaseName);
+ release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or("");
+ release->mbid = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" });
+ release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" });
+ release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL");
+ if (!release->mediumCount)
+ {
+ // mediumCount may be encoded as "position/count"
+ if (const auto value{ getPropertyValueAs(tags, "DISCNUMBER") })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, "/") };
+ if (strings.size() == 2)
+ release->mediumCount = StringUtils::readAs(strings[1]);
+ }
+ }
+
+ release->primaryType = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
+ if (release->primaryType)
+ {
+ const auto secondaryTypes{ getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) };
+ release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes));
+ }
+
+ return release;
+ }
+
+ std::optional getMedium(const TagMap& tags)
+ {
+ std::optional medium;
+ medium.emplace();
+
+ medium->type = getPropertyValueAs(tags, "MEDIA").value_or("");
+ medium->name = getPropertyValueFirstMatchAs(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
+ medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL");
+ if (!medium->trackCount)
+ {
+ // totalTracks may be encoded as "position/count"
+ if (const auto value{ getPropertyValueAs(tags, "TRACKNUMBER") })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, "/") };
+ if (strings.size() == 2)
+ medium->trackCount = StringUtils::readAs(strings[1]);
+ }
+ }
+ // Expecting 'Number[/Total]'
+ medium->position = getPropertyValueAs(tags, "DISCNUMBER");
+ medium->release = getRelease(tags);
+ medium->replayGain = getPropertyValueAs(tags, "REPLAYGAIN_ALBUM_GAIN");
+
+ if (medium->type.empty()
+ && medium->name.empty()
+ && !medium->trackCount
+ && !medium->position
+ && !medium->release
+ && !medium->replayGain)
+ {
+ medium.reset();
+ }
+
+ return medium;
+ }
+
+ TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
+ {
+ switch (readStyle)
+ {
+ case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
+ case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
+ case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
+ }
+
+ throw LmsException{ "Cannot convert read style" };
+ }
+
+ TagMap constructTagMap(const TagLib::PropertyMap& properties)
+ {
+ TagMap tagMap;
+
+ for (const auto& [propertyName, propertyValues] : properties)
+ {
+ std::vector& values{ tagMap[propertyName.upper().to8Bit(true)] };
+ for (const TagLib::String& propertyValue : propertyValues)
+ {
+ std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) };
+ if (!trimedValue.empty())
+ values.emplace_back(std::move(trimedValue));
+ }
+ }
+
+ return tagMap;
+ }
+
+ void mergeTagMaps(TagMap& dst, TagMap&& src)
+ {
+ for (auto&& [tag, values] : src)
+ {
+ if (dst.find(tag) == std::cend(dst))
+ dst[tag] = std::move(values);
+ }
+ }
+
+ }
+
+ TagLibParser::TagLibParser(ParserReadStyle readStyle)
+ : _readStyle{ readStyleToTagLibReadStyle(readStyle) }
+ {
+ }
+
+ void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector& values, bool debug)
+ {
+ if (debug)
+ std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
+
+ if (tag.empty() || values.empty())
+ return;
+
+ std::string_view value{ values.front() };
+
+ if (tag == "TITLE")
+ track.title = value;
+ else if (tag == "MUSICBRAINZ_RELEASETRACKID"
+ || tag == "MUSICBRAINZ RELEASE TRACK ID"
+ || tag == "MUSICBRAINZ/RELEASE TRACK ID")
+ {
+ track.mbid = UUID::fromString(value);
+ }
+ else if (tag == "MUSICBRAINZ_TRACKID"
+ || tag == "MUSICBRAINZ TRACK ID"
+ || tag == "MUSICBRAINZ/TRACK ID")
+ track.recordingMBID = UUID::fromString(value);
+ else if (tag == "ACOUSTID_ID")
+ track.acoustID = UUID::fromString(value);
+ else if (tag == "TRACKNUMBER")
+ {
+ // Expecting 'Number/Total'
+ track.position = StringUtils::readAs(value);
+ }
+ else if (tag == "DATE")
+ {
+ // Higher priority than YEAR
+ if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
+ track.date = date;
+ }
+ else if (tag == "YEAR" && !track.date.isValid())
+ {
+ // lower priority than DATE
+ track.date = Utils::parseDate(value);
+ }
+ else if (tag == "ORIGINALDATE")
+ {
+ // Higher priority than ORIGINALYEAR
+ if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
+ track.originalDate = date;
+ }
+ else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
+ {
+ // Lower priority than ORIGINALDATE
+ track.originalDate = Utils::parseDate(value);
+ }
+ else if (tag == "METADATA_BLOCK_PICTURE")
+ track.hasCover = true;
+ else if (tag == "COPYRIGHT")
+ track.copyright = value;
+ else if (tag == "COPYRIGHTURL")
+ track.copyrightURL = value;
+ else if (tag == "REPLAYGAIN_TRACK_GAIN")
+ track.replayGain = StringUtils::readAs(value);
+ else if (tag == "ARTIST")
+ track.artistDisplayName = value;
+ else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
+ {
+ std::set clusterNames;
+ for (std::string_view valueList : values)
+ {
+ const std::vector splittedValues{ splitAndTrimString(valueList, "/,;") };
+ for (std::string_view value : splittedValues)
+ clusterNames.insert(std::string{ value });
+ }
+
+ if (!clusterNames.empty())
+ track.tags[tag] = std::move(clusterNames);
+ }
+ }
+
+ std::optional TagLibParser::parse(const std::filesystem::path& p, bool debug)
+ {
+ TagLib::FileRef f{ p.string().c_str(),
+ true, // read audio properties
+ _readStyle };
+
+ if (f.isNull())
+ {
+ LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
+ return std::nullopt;
+ }
+
+ Track track;
+
+ if (const TagLib::AudioProperties* properties{ f.audioProperties() })
+ {
+ track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
+ track.bitrate = static_cast(properties->bitrate() * 1000);
+ }
+ else
+ {
+ LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
+ return std::nullopt;
+ }
+
+ TagMap tags{ constructTagMap(f.file()->properties()) };
+
+ auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
+ {
+ if (!apeTag)
+ return;
+
+ mergeTagMaps(tags, constructTagMap(apeTag->properties()));
+ };
+
+ // Not that good embedded pictures handling
+
+ // WMA
+ if (TagLib::ASF::File * asfFile{ dynamic_cast(f.file()) })
+ {
+ const TagLib::ASF::Tag* tag{ asfFile->tag() };
+ if (tag)
+ {
+ if (tag->attributeListMap().contains("WM/Picture"))
+ track.hasCover = true;
+
+ for (const auto& [name, attributeList] : tag->attributeListMap())
+ {
+ std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) };
+ if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
+ continue;
+
+ std::vector attributes;
+ for (const auto& attribute : attributeList)
+ {
+ if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
+ attributes.emplace_back(attribute.toString().to8Bit(true));
+ }
+
+ if (!attributes.empty())
+ {
+ if (debug)
+ std::cout << "ASF property: '" << name << "'" << std::endl;
+
+ tags.emplace(strName, std::move(attributes));
+ }
+ }
+ }
+ }
+ // MP3
+ else if (TagLib::MPEG::File * mp3File{ dynamic_cast(f.file()) })
+ {
+ if (mp3File->ID3v2Tag())
+ {
+ const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() };
+
+ if (!frameListMap["APIC"].isEmpty())
+ track.hasCover = true;
+ if (!frameListMap["TSST"].isEmpty())
+ tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) };
+ }
+
+ getAPETags(mp3File->APETag());
+ }
+ //MP4
+ else if (TagLib::MP4::File * mp4File{ dynamic_cast(f.file()) })
+ {
+ TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") };
+ TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
+ if (!coverArtList.isEmpty())
+ track.hasCover = true;
+ }
+ // MPC
+ else if (TagLib::MPC::File * mpcFile{ dynamic_cast