Merge branch 'develop' for release v3.73.0
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
|
||||
project(lms VERSION 3.72.1)
|
||||
project(lms VERSION 3.73.0)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
|
||||
|
||||
|
||||
+5
-4
@@ -67,11 +67,12 @@ RUN \
|
||||
--enable-libopenjpeg \
|
||||
--enable-libopus \
|
||||
--enable-libvorbis \
|
||||
--enable-zlib \
|
||||
--disable-everything \
|
||||
--enable-decoder=aac*,ac3*,alac,als,dsd*,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \
|
||||
--enable-encoder=libmp3lame,libopus,libvorbis \
|
||||
--enable-demuxer=aac,aiff,asf,dsf,flac,ipod,ogg,matroska,mov,mp3,mp4,wav,wv,webm \
|
||||
--enable-muxer=ogg,matroska,mp3,webm \
|
||||
--enable-decoder=aac*,ac3*,alac,als,ape,asf,dsd*,flac,libopus,pcm*,libvorbis,mp3*,mpc7,mpc8,shorten,tta,wavpack,wma*,libopenjpg,png \
|
||||
--enable-encoder=flac,libmp3lame,libopus,libvorbis \
|
||||
--enable-demuxer=aac,aiff,ape,asf,dsf,flac,m4a,mp3,mov,mpc,mpc8,ogg,shn,tta,wav,wv \
|
||||
--enable-muxer=flac,mp3,ogg \
|
||||
--enable-protocol=file,pipe \
|
||||
--enable-filter=aresample \
|
||||
--enable-lto \
|
||||
|
||||
@@ -52,7 +52,7 @@ The folder must follow a structure defined by Kodi, as detailed [here](https://k
|
||||
__Note__: If no name is provided in the `artist.nfo` file, the name of the containing folder is used.
|
||||
|
||||
### Filtering
|
||||
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
|
||||
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, `codec`, and by music library. More tags, including custom ones, can be added in the database administration settings.
|
||||
|
||||
__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_.
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
|
||||
${</if-has-mbid>}
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,11 +371,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">Never</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">User Interface</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -370,11 +370,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">Nunca</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate de transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Formato de transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">Interfaz de usuario</message>
|
||||
|
||||
<!--Wt-->
|
||||
|
||||
@@ -370,11 +370,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">Jamais</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">Interface utilisateur</message>
|
||||
|
||||
<!--Wt-->
|
||||
|
||||
@@ -370,11 +370,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">Mai</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">Interfaccia utente</message>
|
||||
|
||||
<!--Wt-->
|
||||
|
||||
@@ -396,11 +396,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">Nigdy</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">Interfejs użytkownika</message>
|
||||
|
||||
<!--Wt-->
|
||||
|
||||
@@ -367,11 +367,9 @@
|
||||
<message id="Lms.Settings.transcoding-mode.never">从不</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">转码格式</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
|
||||
|
||||
<!--Wt-->
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${play class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
|
||||
${</if-has-mbid>}
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
<li>${release-info class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -109,7 +111,9 @@
|
||||
<li>${play-next class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
|
||||
</ul>
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
${<if-has-delete>}
|
||||
<li>${delete class="dropdown-item"}</li>
|
||||
${</if-has-delete>}
|
||||
|
||||
@@ -59,7 +59,9 @@
|
||||
<li>${play-next class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
${<if-has-download>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
${</if-has-download>}
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
|
||||
</ul>
|
||||
|
||||
+6
-3
@@ -113,9 +113,6 @@ artist-image-file-names = ("artist");
|
||||
# Note: files named after the disc itself are always searched before the names in this list.
|
||||
medium-image-file-names = ("discsubtitle");
|
||||
|
||||
# Playqueue max entry count
|
||||
playqueue-max-entry-count = 1000;
|
||||
|
||||
# Internal tracing for profiling purposes. Enable only if necessary, as it incurs some runtime overhead!
|
||||
# Possible values are "disabled", "overview" or "detailed".
|
||||
# If enabled, data has to be dumped in the tracing view located in the admin menu
|
||||
@@ -140,3 +137,9 @@ podcast-auto-download-episodes = true;
|
||||
|
||||
# Max age in days for auto-downloaded episodes before deletion
|
||||
podcast-auto-download-episodes-max-age-days = 30;
|
||||
|
||||
# Playqueue max entry count
|
||||
ui-playqueue-max-entry-count = 1000;
|
||||
|
||||
# Allow downloads
|
||||
ui-allow-downloads = true;
|
||||
@@ -4,17 +4,17 @@ pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
add_library(lmsaudio STATIC
|
||||
impl/ffmpeg/AudioFile.cpp
|
||||
impl/ffmpeg/AudioFileInfo.cpp
|
||||
impl/ffmpeg/AudioFileInfoParser.cpp
|
||||
impl/ffmpeg/ImageReader.cpp
|
||||
impl/ffmpeg/TagReader.cpp
|
||||
impl/ffmpeg/Transcoder.cpp
|
||||
impl/ffmpeg/Utils.cpp
|
||||
impl/taglib/AudioFileInfo.cpp
|
||||
impl/taglib/AudioFileInfoParser.cpp
|
||||
impl/taglib/ImageReader.cpp
|
||||
impl/taglib/TagReader.cpp
|
||||
impl/taglib/Utils.cpp
|
||||
impl/AudioTypes.cpp
|
||||
impl/ImageReader.cpp
|
||||
impl/ParseAudioFileInfo.cpp
|
||||
impl/AudioFileInfoParser.cpp
|
||||
impl/TagReader.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
#include "ffmpeg/AudioFileInfoParser.hpp"
|
||||
#include "taglib/AudioFileInfoParser.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend)
|
||||
{
|
||||
switch (backend)
|
||||
{
|
||||
case AudioFileInfoParserBackend::TagLib:
|
||||
return std::make_unique<taglib::AudioFileInfoParser>();
|
||||
case AudioFileInfoParserBackend::FFmpeg:
|
||||
return std::make_unique<ffmpeg::AudioFileInfoParser>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString containerTypeToString(ContainerType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ContainerType::AIFF:
|
||||
return "AIFF";
|
||||
case ContainerType::APE:
|
||||
return "APE";
|
||||
case ContainerType::ASF:
|
||||
return "ASF";
|
||||
case ContainerType::DSF:
|
||||
return "DSF";
|
||||
case ContainerType::FLAC:
|
||||
return "FLAC";
|
||||
case ContainerType::MP4:
|
||||
return "MP4";
|
||||
case ContainerType::MPC:
|
||||
return "MPC";
|
||||
case ContainerType::MPEG:
|
||||
return "MPEG";
|
||||
case ContainerType::Ogg:
|
||||
return "Ogg";
|
||||
case ContainerType::Shorten:
|
||||
return "Shorten";
|
||||
case ContainerType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case ContainerType::WAV:
|
||||
return "WAV";
|
||||
case ContainerType::WavPack:
|
||||
return "WavPack";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
core::LiteralString codecTypeToString(CodecType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CodecType::AAC:
|
||||
return "AAC";
|
||||
case CodecType::ALAC:
|
||||
return "ALAC";
|
||||
case CodecType::APE:
|
||||
return "APE";
|
||||
case CodecType::DSD:
|
||||
return "DSD";
|
||||
case CodecType::FLAC:
|
||||
return "FLAC";
|
||||
case CodecType::MP3:
|
||||
return "MP3";
|
||||
case CodecType::MP4ALS:
|
||||
return "MP4ALS";
|
||||
case CodecType::MPC7:
|
||||
return "MPC7";
|
||||
case CodecType::MPC8:
|
||||
return "MPC8";
|
||||
case CodecType::Opus:
|
||||
return "Opus";
|
||||
case CodecType::PCM:
|
||||
return "PCM";
|
||||
case CodecType::Shorten:
|
||||
return "Shorten";
|
||||
case CodecType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case CodecType::Vorbis:
|
||||
return "Vorbis";
|
||||
case CodecType::WavPack:
|
||||
return "WavPack";
|
||||
case CodecType::WMA1:
|
||||
return "WMA1";
|
||||
case CodecType::WMA2:
|
||||
return "WMA2";
|
||||
case CodecType::WMA9Pro:
|
||||
return "WMA9Pro";
|
||||
case CodecType::WMA9Lossless:
|
||||
return "WMA9Lossless";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "ffmpeg/AudioFileInfo.hpp"
|
||||
#include "ffmpeg/Utils.hpp"
|
||||
#include "taglib/AudioFileInfo.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
|
||||
{
|
||||
switch (parserOptions.parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
|
||||
{
|
||||
switch (parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return taglib::utils::getSupportedExtensions();
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return ffmpeg::utils::getSupportedExtensions();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
@@ -20,21 +20,22 @@
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <unordered_map>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#define __STDC_CONSTANT_MACROS
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/log.h>
|
||||
}
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
@@ -50,115 +51,169 @@ namespace lms::audio::ffmpeg
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
class AudioFileException : public AudioFileParsingException
|
||||
class AvException : public Exception
|
||||
{
|
||||
public:
|
||||
AudioFileException(int avError)
|
||||
: AudioFileParsingException{ averror_to_string(avError) }
|
||||
AvException(int avError)
|
||||
: Exception{ averror_to_string(avError) }
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||
void extractMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||
{
|
||||
if (!dictionnary)
|
||||
return;
|
||||
|
||||
AVDictionaryEntry* tag = NULL;
|
||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
{
|
||||
const AVDictionaryEntry* tag{ NULL };
|
||||
while ((tag = av_dict_iterate(dictionnary, tag)))
|
||||
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
|
||||
std::optional<core::media::Container> avdemuxerToContainerType(std::string_view name)
|
||||
{
|
||||
if (name == "aiff")
|
||||
return ContainerType::AIFF;
|
||||
return core::media::Container::AIFF;
|
||||
if (name == "ape")
|
||||
return ContainerType::APE;
|
||||
return core::media::Container::APE;
|
||||
if (name.starts_with("asf"))
|
||||
return ContainerType::ASF;
|
||||
return core::media::Container::ASF;
|
||||
if (name == "dsf")
|
||||
return ContainerType::DSF;
|
||||
return core::media::Container::DSF;
|
||||
if (name == "flac")
|
||||
return ContainerType::FLAC;
|
||||
return core::media::Container::FLAC;
|
||||
if (name.find("mp4") != std::string_view::npos)
|
||||
return ContainerType::MP4;
|
||||
return core::media::Container::MP4;
|
||||
if (name.starts_with("mpc"))
|
||||
return ContainerType::MPC;
|
||||
return core::media::Container::MPC;
|
||||
if (name == "mp3")
|
||||
return ContainerType::MPEG;
|
||||
return core::media::Container::MPEG;
|
||||
if (name == "ogg")
|
||||
return ContainerType::Ogg;
|
||||
return core::media::Container::Ogg;
|
||||
if (name == "shn")
|
||||
return ContainerType::Shorten;
|
||||
return core::media::Container::Shorten;
|
||||
if (name == "tta")
|
||||
return ContainerType::TrueAudio;
|
||||
return core::media::Container::TrueAudio;
|
||||
if (name == "wav")
|
||||
return ContainerType::WAV;
|
||||
return core::media::Container::WAV;
|
||||
if (name == "wv")
|
||||
return ContainerType::WavPack;
|
||||
return core::media::Container::WavPack;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
|
||||
std::optional<core::media::Codec> avcodecToCodecType(AVCodecID codec)
|
||||
{
|
||||
switch (codec)
|
||||
{
|
||||
case AV_CODEC_ID_MP3:
|
||||
return CodecType::MP3;
|
||||
case AV_CODEC_ID_AAC:
|
||||
return CodecType::AAC;
|
||||
case AV_CODEC_ID_VORBIS:
|
||||
return CodecType::Vorbis;
|
||||
case AV_CODEC_ID_WMAV1:
|
||||
return CodecType::WMA1;
|
||||
case AV_CODEC_ID_WMAV2:
|
||||
return CodecType::WMA2;
|
||||
case AV_CODEC_ID_WMAPRO:
|
||||
return CodecType::WMA9Pro;
|
||||
case AV_CODEC_ID_WMALOSSLESS:
|
||||
return CodecType::WMA9Lossless;
|
||||
case AV_CODEC_ID_FLAC:
|
||||
return CodecType::FLAC;
|
||||
return core::media::Codec::AAC;
|
||||
case AV_CODEC_ID_AC3:
|
||||
return core::media::Codec::AC3;
|
||||
case AV_CODEC_ID_ALAC:
|
||||
return CodecType::ALAC;
|
||||
case AV_CODEC_ID_WAVPACK:
|
||||
return CodecType::WavPack;
|
||||
case AV_CODEC_ID_MUSEPACK7:
|
||||
return CodecType::MPC7;
|
||||
case AV_CODEC_ID_MUSEPACK8:
|
||||
return CodecType::MPC8;
|
||||
return core::media::Codec::ALAC;
|
||||
case AV_CODEC_ID_APE:
|
||||
return CodecType::APE;
|
||||
case AV_CODEC_ID_MP4ALS:
|
||||
return CodecType::MP4ALS;
|
||||
case AV_CODEC_ID_OPUS:
|
||||
return CodecType::Opus;
|
||||
case AV_CODEC_ID_SHORTEN:
|
||||
return CodecType::Shorten;
|
||||
return core::media::Codec::APE;
|
||||
case AV_CODEC_ID_DSD_LSBF:
|
||||
case AV_CODEC_ID_DSD_LSBF_PLANAR:
|
||||
case AV_CODEC_ID_DSD_MSBF:
|
||||
case AV_CODEC_ID_DSD_MSBF_PLANAR:
|
||||
return CodecType::DSD;
|
||||
return core::media::Codec::DSD;
|
||||
case AV_CODEC_ID_EAC3:
|
||||
return core::media::Codec::EAC3;
|
||||
case AV_CODEC_ID_FLAC:
|
||||
return core::media::Codec::FLAC;
|
||||
case AV_CODEC_ID_MP3:
|
||||
return core::media::Codec::MP3;
|
||||
case AV_CODEC_ID_MP4ALS:
|
||||
return core::media::Codec::MP4ALS;
|
||||
case AV_CODEC_ID_MUSEPACK7:
|
||||
return core::media::Codec::MPC7;
|
||||
case AV_CODEC_ID_MUSEPACK8:
|
||||
return core::media::Codec::MPC8;
|
||||
case AV_CODEC_ID_OPUS:
|
||||
return core::media::Codec::Opus;
|
||||
case AV_CODEC_ID_SHORTEN:
|
||||
return core::media::Codec::Shorten;
|
||||
case AV_CODEC_ID_VORBIS:
|
||||
return core::media::Codec::Vorbis;
|
||||
case AV_CODEC_ID_WAVPACK:
|
||||
return core::media::Codec::WavPack;
|
||||
case AV_CODEC_ID_WMALOSSLESS:
|
||||
return core::media::Codec::WMA9Lossless;
|
||||
case AV_CODEC_ID_WMAPRO:
|
||||
return core::media::Codec::WMA9Pro;
|
||||
case AV_CODEC_ID_WMAV1:
|
||||
return core::media::Codec::WMA1;
|
||||
case AV_CODEC_ID_WMAV2:
|
||||
return core::media::Codec::WMA2;
|
||||
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
core::LiteralString avLogLevelToStr(int level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case AV_LOG_TRACE:
|
||||
return "trace";
|
||||
case AV_LOG_DEBUG:
|
||||
return "debug";
|
||||
case AV_LOG_VERBOSE:
|
||||
return "verbose";
|
||||
case AV_LOG_INFO:
|
||||
return "info";
|
||||
case AV_LOG_WARNING:
|
||||
return "warning";
|
||||
case AV_LOG_ERROR:
|
||||
return "error";
|
||||
case AV_LOG_FATAL:
|
||||
return "fatal";
|
||||
case AV_LOG_PANIC:
|
||||
return "panic";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void avLogCallback(void*, int level, const char* fmt, va_list vl)
|
||||
{
|
||||
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
return;
|
||||
|
||||
if (level > AV_LOG_WARNING)
|
||||
return;
|
||||
|
||||
std::array<char, 256> buffer{ 0 };
|
||||
std::vsnprintf(buffer.data(), buffer.size(), fmt, vl);
|
||||
|
||||
LMS_LOG(AUDIO, DEBUG, "FFmpeg [" << avLogLevelToStr(level) << "] " << buffer.data());
|
||||
}
|
||||
|
||||
class AvInitializer
|
||||
{
|
||||
public:
|
||||
AvInitializer()
|
||||
{
|
||||
::av_log_set_callback(avLogCallback);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
AudioFile::AudioFile(const std::filesystem::path& p)
|
||||
: _p{ p }
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "FFmpegParseFile");
|
||||
|
||||
static AvInitializer init;
|
||||
|
||||
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
||||
throw AudioFileException{ error };
|
||||
throw AvException{ error };
|
||||
}
|
||||
|
||||
error = avformat_find_stream_info(_context, nullptr);
|
||||
@@ -166,7 +221,7 @@ namespace lms::audio::ffmpeg
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
|
||||
avformat_close_input(&_context);
|
||||
throw AudioFileException{ error };
|
||||
throw AvException{ error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,17 +242,18 @@ namespace lms::audio::ffmpeg
|
||||
info.container = avdemuxerToContainerType(_context->iformat->name);
|
||||
info.containerName = _context->iformat->name;
|
||||
|
||||
info.bitrate = _context->bit_rate;
|
||||
if (_context->bit_rate > 0)
|
||||
info.bitrate = _context->bit_rate;
|
||||
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
AudioFile::MetadataMap AudioFile::getMetaData() const
|
||||
AudioFile::MetadataMap AudioFile::extractMetaData() const
|
||||
{
|
||||
MetadataMap res;
|
||||
|
||||
getMetaDataFromDictionnary(_context->metadata, res);
|
||||
extractMetaDataFromDictionnary(_context->metadata, res);
|
||||
|
||||
// HACK for OGG files
|
||||
// If we did not find tags, search metadata in streams
|
||||
@@ -205,7 +261,7 @@ namespace lms::audio::ffmpeg
|
||||
{
|
||||
for (std::size_t i{}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
|
||||
extractMetaDataFromDictionnary(_context->streams[i]->metadata, res);
|
||||
|
||||
if (!res.empty())
|
||||
break;
|
||||
@@ -266,7 +322,7 @@ namespace lms::audio::ffmpeg
|
||||
return false;
|
||||
}
|
||||
|
||||
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
|
||||
void AudioFile::visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const
|
||||
{
|
||||
static const std::unordered_map<int, std::string> codecMimeMap{
|
||||
{ AV_CODEC_ID_BMP, "image/bmp" },
|
||||
@@ -286,14 +342,14 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
if (avstream->codecpar == nullptr)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
|
||||
LMS_LOG(AUDIO, WARNING, "Skipping stream " << i << " since no codecpar is set");
|
||||
continue;
|
||||
}
|
||||
|
||||
MetadataMap metadata;
|
||||
getMetaDataFromDictionnary(avstream->metadata, metadata);
|
||||
extractMetaDataFromDictionnary(avstream->metadata, metadata);
|
||||
|
||||
Picture picture;
|
||||
PictureView picture;
|
||||
|
||||
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
||||
if (itMime != codecMimeMap.end())
|
||||
@@ -303,10 +359,10 @@ namespace lms::audio::ffmpeg
|
||||
else
|
||||
{
|
||||
picture.mimeType = "application/octet-stream";
|
||||
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
|
||||
LMS_LOG(AUDIO, WARNING, "AVCodecID" << avstream->codecpar->codec_id << " (" << ::avcodec_get_name(avstream->codecpar->codec_id) << ") not handled in mime type conversion");
|
||||
}
|
||||
|
||||
const AVPacket& pkt{ avstream->attached_pic };
|
||||
const ::AVPacket& pkt{ avstream->attached_pic };
|
||||
|
||||
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
|
||||
func(picture, metadata);
|
||||
@@ -325,7 +381,7 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
if (!avstream->codecpar)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
|
||||
LMS_LOG(AUDIO, WARNING, "Skipping stream " << streamIndex << " since no codecpar is set");
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
@@ -36,7 +37,7 @@ extern "C"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
struct Picture
|
||||
struct PictureView
|
||||
{
|
||||
std::string mimeType;
|
||||
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||
@@ -44,17 +45,17 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
struct ContainerInfo
|
||||
{
|
||||
std::optional<ContainerType> container;
|
||||
std::optional<core::media::Container> container;
|
||||
std::string containerName;
|
||||
|
||||
std::size_t bitrate{};
|
||||
std::optional<std::size_t> bitrate;
|
||||
std::chrono::milliseconds duration{};
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t index{};
|
||||
std::optional<CodecType> codec;
|
||||
std::optional<core::media::Codec> codec;
|
||||
std::string codecName;
|
||||
|
||||
std::optional<size_t> bitrate;
|
||||
@@ -75,12 +76,12 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
const std::filesystem::path& getPath() const;
|
||||
ContainerInfo getContainerInfo() const;
|
||||
MetadataMap getMetaData() const;
|
||||
MetadataMap extractMetaData() const;
|
||||
std::vector<StreamInfo> getStreamInfo() const;
|
||||
std::optional<StreamInfo> getBestStreamInfo() const;
|
||||
std::optional<std::size_t> getBestStreamIndex() const;
|
||||
bool hasAttachedPictures() const;
|
||||
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
|
||||
void visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const;
|
||||
|
||||
private:
|
||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "audio/AudioProperties.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
@@ -30,65 +35,108 @@ namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const AudioFile& audioFile)
|
||||
std::optional<AudioProperties> computeAudioProperties(const AudioFile& audioFile)
|
||||
{
|
||||
AudioProperties audioProperties;
|
||||
std::optional<AudioProperties> audioProperties;
|
||||
|
||||
const auto containerInfo{ audioFile.getContainerInfo() };
|
||||
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
|
||||
if (!bestStreamInfo)
|
||||
throw AudioFileParsingException{ "Cannot find best audio stream" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot find best audio stream in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
if (containerInfo.duration.count() == 0)
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
if (!containerInfo.container)
|
||||
throw AudioFileParsingException{ "Unhandled container type '" + containerInfo.containerName + "'" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled container '" << containerInfo.containerName << "' in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
if (!bestStreamInfo->codec)
|
||||
throw AudioFileParsingException{ "Unhandled codec type '" + bestStreamInfo->codecName + "'" };
|
||||
|
||||
if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine bitrate" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled codec '" << bestStreamInfo->codecName << "' in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine channel count" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
if (!bestStreamInfo->sampleRate || *bestStreamInfo->sampleRate == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine sample rate" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << audioFile.getPath());
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
audioProperties.container = *containerInfo.container;
|
||||
audioProperties.duration = containerInfo.duration;
|
||||
audioProperties.codec = *bestStreamInfo->codec;
|
||||
audioProperties.bitrate = *bestStreamInfo->bitrate;
|
||||
audioProperties.channelCount = *bestStreamInfo->channelCount;
|
||||
audioProperties.sampleRate = *bestStreamInfo->sampleRate;
|
||||
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
|
||||
audioProperties.emplace();
|
||||
|
||||
audioProperties->container = *containerInfo.container;
|
||||
audioProperties->duration = containerInfo.duration;
|
||||
audioProperties->codec = *bestStreamInfo->codec;
|
||||
|
||||
if (bestStreamInfo->bitrate)
|
||||
audioProperties->bitrate = *bestStreamInfo->bitrate;
|
||||
else if (containerInfo.bitrate)
|
||||
audioProperties->bitrate = *containerInfo.bitrate;
|
||||
else // Fallback on a bitrate based on duration/size
|
||||
{
|
||||
std::error_code ec;
|
||||
const auto fileSize{ std::filesystem::file_size(audioFile.getPath(), ec) };
|
||||
if (ec)
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine file size for " << audioFile.getPath() << ": " << ec.message());
|
||||
audioProperties.reset();
|
||||
return audioProperties;
|
||||
}
|
||||
|
||||
audioProperties->bitrate = static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::duration<double>>(containerInfo.duration).count() / fileSize);
|
||||
LMS_LOG(AUDIO, DEBUG, "Estimated bitrate from duration/size: " << audioProperties->bitrate << " bps in " << audioFile.getPath());
|
||||
}
|
||||
assert(audioProperties->bitrate > 0);
|
||||
|
||||
audioProperties->channelCount = *bestStreamInfo->channelCount;
|
||||
audioProperties->sampleRate = *bestStreamInfo->sampleRate;
|
||||
audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample;
|
||||
|
||||
return audioProperties;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
|
||||
: _audioFile{ std::make_unique<AudioFile>(filePath) }
|
||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
|
||||
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
|
||||
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
|
||||
, _audioProperties{ computeAudioProperties(*_audioFile) }
|
||||
{
|
||||
if (parseOptions.readTags)
|
||||
_tagReader = std::make_unique<TagReader>(*_audioFile, parseOptions.enableExtraDebugLogs);
|
||||
|
||||
if (parseOptions.readImages)
|
||||
_imageReader = std::make_unique<ImageReader>(*_audioFile);
|
||||
}
|
||||
|
||||
AudioFileInfo::~AudioFileInfo() = default;
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
const AudioProperties* AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return *_audioProperties;
|
||||
return _audioProperties.has_value() ? &_audioProperties.value() : nullptr;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
const IImageReader* AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return *_imageReader;
|
||||
return _imageReader.get();
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
const ITagReader* AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return *_tagReader;
|
||||
return _tagReader.get();
|
||||
}
|
||||
|
||||
} // namespace lms::audio::ffmpeg
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/AudioProperties.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
@@ -33,18 +35,19 @@ namespace lms::audio::ffmpeg
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
|
||||
~AudioFileInfo();
|
||||
AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
|
||||
~AudioFileInfo() override;
|
||||
|
||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||
|
||||
private:
|
||||
const AudioProperties& getAudioProperties() const override;
|
||||
const IImageReader& getImageReader() const override;
|
||||
const ITagReader& getTagReader() const override;
|
||||
const AudioProperties* getAudioProperties() const override;
|
||||
const IImageReader* getImageReader() const override;
|
||||
const ITagReader* getTagReader() const override;
|
||||
|
||||
std::unique_ptr<AudioFile> _audioFile;
|
||||
std::unique_ptr<AudioProperties> _audioProperties;
|
||||
const std::optional<AudioProperties> _audioProperties;
|
||||
std::unique_ptr<TagReader> _tagReader;
|
||||
std::unique_ptr<ImageReader> _imageReader;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfoParser.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
|
||||
{
|
||||
return std::make_unique<AudioFileInfo>(p, parseOptions);
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
|
||||
{
|
||||
return utils::getSupportedExtensions();
|
||||
}
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AudioFileInfoParser : public IAudioFileInfoParser
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
|
||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -40,17 +40,16 @@ namespace lms::audio::ffmpeg
|
||||
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
|
||||
} };
|
||||
|
||||
_audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
|
||||
_audioFile.visitAttachedPictures([&](const PictureView& picture, const AudioFile::MetadataMap& metaData) {
|
||||
Image image;
|
||||
image.data = picture.data;
|
||||
image.mimeType = picture.mimeType;
|
||||
if (metaDataHasKeyword(metaData, "front"))
|
||||
image.type = Image::Type::FrontCover;
|
||||
image.type = core::media::ImageType::FrontCover;
|
||||
else if (metaDataHasKeyword(metaData, "back"))
|
||||
image.type = Image::Type::BackCover;
|
||||
image.type = core::media::ImageType::BackCover;
|
||||
|
||||
visitor(image);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lms::audio::ffmpeg
|
||||
|
||||
@@ -144,8 +144,7 @@ namespace lms::audio::ffmpeg
|
||||
} // namespace
|
||||
|
||||
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
|
||||
: _audioFile{ audioFile }
|
||||
, _metaDataMap{ audioFile.getMetaData() }
|
||||
: _metaDataMap{ audioFile.extractMetaData() }
|
||||
{
|
||||
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace lms::audio::ffmpeg
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||
|
||||
const AudioFile& _audioFile;
|
||||
AudioFile::MetadataMap _metaDataMap;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
|
||||
#include "core/IChildProcessManager.hpp"
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "core/media/MimeType.hpp"
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/TranscodeTypes.hpp"
|
||||
@@ -42,18 +43,29 @@ namespace lms::audio::ffmpeg
|
||||
{
|
||||
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||
|
||||
static std::atomic<size_t> globalId{};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
void Transcoder::init()
|
||||
namespace
|
||||
{
|
||||
ffmpegPath = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(ffmpegPath))
|
||||
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
||||
}
|
||||
class FFmpegPath
|
||||
{
|
||||
public:
|
||||
FFmpegPath()
|
||||
{
|
||||
path = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(path))
|
||||
throw Exception{ "File '" + path.string() + "' does not exist!" };
|
||||
}
|
||||
|
||||
const std::filesystem::path& get() const { return path; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::atomic<std::size_t> Transcoder::_nextDebugId;
|
||||
|
||||
Transcoder::Transcoder(const TranscodeParameters& parameters)
|
||||
: _debugId{ globalId++ }
|
||||
: _debugId{ _nextDebugId++ }
|
||||
, _inputParams{ parameters.inputParameters }
|
||||
, _outputParams{ parameters.outputParameters }
|
||||
{
|
||||
@@ -64,8 +76,7 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
void Transcoder::start()
|
||||
{
|
||||
if (ffmpegPath.empty())
|
||||
init();
|
||||
static FFmpegPath ffmpegPath;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -76,15 +87,16 @@ namespace lms::audio::ffmpeg
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
// TODO store/raise e.code()
|
||||
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
|
||||
throw IOFileException{ _inputParams.filePath, "Failed to check if file exists", e.code() };
|
||||
}
|
||||
|
||||
LOG(INFO, "Transcoding file " << _inputParams.filePath);
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
args.emplace_back(ffmpegPath.string());
|
||||
args.emplace_back(ffmpegPath.get().string());
|
||||
|
||||
// TODO some codecs have restrictions, take them into account (channel count, sample rate, etc.)
|
||||
|
||||
// Make sure:
|
||||
// - we do not produce anything in the stderr output
|
||||
@@ -114,7 +126,7 @@ namespace lms::audio::ffmpeg
|
||||
args.emplace_back("-1");
|
||||
}
|
||||
|
||||
// Skip video flows (including covers)
|
||||
// Skip video flows (including covers!)
|
||||
args.emplace_back("-vn");
|
||||
|
||||
// Output bitrates
|
||||
@@ -124,63 +136,88 @@ namespace lms::audio::ffmpeg
|
||||
args.emplace_back(std::to_string(*_outputParams.bitrate));
|
||||
}
|
||||
|
||||
// -ac doc says: "For output streams it is set by default to the number of input audio channels".
|
||||
// But it looks like specifying it is required in case the original layout is not compatible with the output codec selected
|
||||
{
|
||||
args.emplace_back("-ac");
|
||||
args.emplace_back(std::to_string(_outputParams.channelCount ? *_outputParams.channelCount : _inputParams.audioProperties.channelCount));
|
||||
}
|
||||
|
||||
if (_outputParams.sampleRate)
|
||||
{
|
||||
args.emplace_back("-ar");
|
||||
args.emplace_back(std::to_string(*_outputParams.sampleRate));
|
||||
}
|
||||
|
||||
if (_outputParams.bitsPerSample)
|
||||
{
|
||||
args.emplace_back("-sample_fmt");
|
||||
args.emplace_back("s" + std::to_string(*_outputParams.bitsPerSample));
|
||||
}
|
||||
|
||||
// Codecs and formats
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.format)
|
||||
args.emplace_back("-f");
|
||||
|
||||
switch (_outputParams.format->container)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
args.emplace_back("-f");
|
||||
case core::media::Container::FLAC:
|
||||
args.emplace_back("flac");
|
||||
break;
|
||||
case core::media::Container::Ogg:
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
case core::media::Container::MPEG:
|
||||
args.emplace_back("mp3");
|
||||
break;
|
||||
|
||||
case OutputFormat::OGG_OPUS:
|
||||
args.emplace_back("-acodec");
|
||||
default:
|
||||
throw Exception{ "Unsupported container type " + std::string{ core::media::containerToString(_outputParams.format->container).str() } };
|
||||
}
|
||||
|
||||
args.emplace_back("-acodec");
|
||||
|
||||
switch (_outputParams.format->codec)
|
||||
{
|
||||
case core::media::Codec::MP3:
|
||||
args.emplace_back("libmp3lame");
|
||||
break;
|
||||
|
||||
case core::media::Codec::Opus:
|
||||
args.emplace_back("libopus");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
|
||||
case OutputFormat::MATROSKA_OPUS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libopus");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("matroska");
|
||||
break;
|
||||
|
||||
case OutputFormat::OGG_VORBIS:
|
||||
args.emplace_back("-acodec");
|
||||
case core::media::Codec::Vorbis:
|
||||
args.emplace_back("libvorbis");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libvorbis");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("webm");
|
||||
case core::media::Codec::FLAC:
|
||||
args.emplace_back("flac");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
|
||||
throw Exception{ "Unhandled codec type " + std::string{ core::media::getCodecDesc(_outputParams.format->codec).name.str() } };
|
||||
}
|
||||
}
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
|
||||
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||
for (const std::string& arg : args)
|
||||
LOG(DEBUG, "Arg = '" << arg << "'");
|
||||
if (core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||
for (const std::string& arg : args)
|
||||
LOG(DEBUG, "Arg = '" << arg << "'");
|
||||
}
|
||||
|
||||
// Caution: stdin must have been closed before
|
||||
try
|
||||
{
|
||||
_childProcess = core::Service<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
|
||||
_childProcess = core::Service<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath.get(), args);
|
||||
}
|
||||
catch (core::ChildProcessException& exception)
|
||||
{
|
||||
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
|
||||
throw Exception{ "Cannot execute '" + ffmpegPath.get().string() + "': " + exception.what() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,25 +239,10 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
std::string_view Transcoder::getOutputMimeType() const
|
||||
{
|
||||
// TODO: use input mime type
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.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";
|
||||
}
|
||||
}
|
||||
return core::media::getMimeType(_outputParams.format->container, _outputParams.format->codec).str();
|
||||
|
||||
return "application/octet-stream"; // default, should not happen
|
||||
return core::media::getMimeType(_inputParams.audioProperties.container, _inputParams.audioProperties.codec).str();
|
||||
}
|
||||
|
||||
bool Transcoder::finished() const
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "audio/ITranscoder.hpp"
|
||||
|
||||
namespace lms::core
|
||||
@@ -47,7 +49,8 @@ namespace lms::audio::ffmpeg
|
||||
static void init();
|
||||
void start();
|
||||
|
||||
const std::size_t _debugId{};
|
||||
static std::atomic<std::size_t> _nextDebugId;
|
||||
const std::size_t _debugId;
|
||||
const TranscodeInputParameters _inputParams;
|
||||
const TranscodeOutputParameters _outputParams;
|
||||
std::unique_ptr<core::IChildProcess> _childProcess;
|
||||
|
||||
@@ -43,8 +43,11 @@
|
||||
#include <taglib/shortenfile.h>
|
||||
#endif
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "audio/AudioProperties.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
#include "ImageReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
@@ -54,60 +57,78 @@ namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const ::TagLib::File& file)
|
||||
std::optional<AudioProperties> computeAudioProperties(const ::TagLib::File& file, const std::filesystem::path& filePath)
|
||||
{
|
||||
assert(file.audioProperties());
|
||||
|
||||
AudioProperties audioProperties;
|
||||
|
||||
{
|
||||
const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
|
||||
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
|
||||
if (!properties)
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine audio properties in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Common properties
|
||||
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
|
||||
audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||
if (audioProperties.bitrate == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine bitrate" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
|
||||
audioProperties.channelCount = static_cast<std::size_t>(properties->channels());
|
||||
if (audioProperties.channelCount == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine channel count" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
|
||||
audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||
if (audioProperties.duration == decltype(audioProperties.duration)::zero())
|
||||
throw AudioFileParsingException{ "Cannot determine duration" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
|
||||
audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
||||
if (audioProperties.sampleRate == 0)
|
||||
throw AudioFileParsingException{ "Cannot determine sample rate" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Guess container from the file type
|
||||
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::APE;
|
||||
audioProperties.codec = CodecType::APE; // TODO version?
|
||||
audioProperties.container = core::media::Container::APE;
|
||||
audioProperties.codec = core::media::Codec::APE; // TODO version?
|
||||
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::ASF;
|
||||
audioProperties.container = core::media::Container::ASF;
|
||||
|
||||
switch (asfFile->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::ASF::Properties::Codec::WMA1:
|
||||
audioProperties.codec = CodecType::WMA1;
|
||||
audioProperties.codec = core::media::Codec::WMA1;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA2:
|
||||
audioProperties.codec = CodecType::WMA2;
|
||||
audioProperties.codec = core::media::Codec::WMA2;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
|
||||
audioProperties.codec = CodecType::WMA9Lossless;
|
||||
audioProperties.codec = core::media::Codec::WMA9Lossless;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
|
||||
audioProperties.codec = CodecType::WMA9Pro;
|
||||
audioProperties.codec = core::media::Codec::WMA9Pro;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::Unknown:
|
||||
throw AudioFileParsingException{ "Unhandled ASF codec" };
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled ASF codec in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
|
||||
@@ -115,110 +136,116 @@ namespace lms::audio::taglib
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* dsfFile{ dynamic_cast<const ::TagLib::DSF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::DSF;
|
||||
audioProperties.codec = CodecType::DSD;
|
||||
audioProperties.container = core::media::Container::DSF;
|
||||
audioProperties.codec = core::media::Codec::DSD;
|
||||
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* flacFile{ dynamic_cast<const ::TagLib::FLAC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::FLAC;
|
||||
audioProperties.codec = CodecType::FLAC;
|
||||
audioProperties.container = core::media::Container::FLAC;
|
||||
audioProperties.codec = core::media::Codec::FLAC;
|
||||
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MP4;
|
||||
audioProperties.container = core::media::Container::MP4;
|
||||
switch (mp4File->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::MP4::Properties::Codec::AAC:
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
audioProperties.codec = core::media::Codec::AAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::ALAC:
|
||||
audioProperties.codec = CodecType::ALAC;
|
||||
audioProperties.codec = core::media::Codec::ALAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::Unknown:
|
||||
throw AudioFileParsingException{ "Unhandled MP4 codec" };
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled MP4 codec in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MPC;
|
||||
audioProperties.container = core::media::Container::MPC;
|
||||
|
||||
switch (mpcFile->audioProperties()->mpcVersion())
|
||||
{
|
||||
case 7:
|
||||
audioProperties.codec = CodecType::MPC7;
|
||||
audioProperties.codec = core::media::Codec::MPC7;
|
||||
break;
|
||||
case 8:
|
||||
audioProperties.codec = CodecType::MPC8;
|
||||
audioProperties.codec = core::media::Codec::MPC8;
|
||||
break;
|
||||
default:
|
||||
throw AudioFileParsingException{ "Unhandled MPC codec" };
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
|
||||
{
|
||||
const auto& properties{ *mpegFile->audioProperties() };
|
||||
|
||||
audioProperties.container = ContainerType::MPEG;
|
||||
audioProperties.container = core::media::Container::MPEG;
|
||||
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
|
||||
&& mpegFile->audioProperties()->layer() == 3)
|
||||
audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
|
||||
audioProperties.codec = core::media::Codec::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
|
||||
#if LMS_TAGLIB_HAS_ADTS
|
||||
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
audioProperties.codec = core::media::Codec::AAC;
|
||||
#endif
|
||||
else
|
||||
throw AudioFileParsingException{ "Unhandled MPEG codec" };
|
||||
{
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled MPEG codec in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Opus;
|
||||
audioProperties.container = core::media::Container::Ogg;
|
||||
audioProperties.codec = core::media::Codec::Opus;
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Vorbis;
|
||||
audioProperties.container = core::media::Container::Ogg;
|
||||
audioProperties.codec = core::media::Codec::Vorbis;
|
||||
}
|
||||
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::AIFF;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.container = core::media::Container::AIFF;
|
||||
audioProperties.codec = core::media::Codec::PCM;
|
||||
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WAV;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.container = core::media::Container::WAV;
|
||||
audioProperties.codec = core::media::Codec::PCM;
|
||||
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#if LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::Shorten;
|
||||
audioProperties.codec = CodecType::Shorten;
|
||||
audioProperties.container = core::media::Container::Shorten;
|
||||
audioProperties.codec = core::media::Codec::Shorten;
|
||||
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::TrueAudio;
|
||||
audioProperties.codec = CodecType::TrueAudio;
|
||||
audioProperties.container = core::media::Container::TrueAudio;
|
||||
audioProperties.codec = core::media::Codec::TrueAudio;
|
||||
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WavPack;
|
||||
audioProperties.codec = CodecType::WavPack;
|
||||
audioProperties.container = core::media::Container::WavPack;
|
||||
audioProperties.codec = core::media::Codec::WavPack;
|
||||
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw AudioFileParsingException{ "Unhandled file type" };
|
||||
LMS_LOG(AUDIO, DEBUG, "Unhandled file type in " << filePath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0)
|
||||
@@ -228,30 +255,32 @@ namespace lms::audio::taglib
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
|
||||
: _filePath{ filePath }
|
||||
, _file{ utils::parseFile(filePath, readStyle) }
|
||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_file)) }
|
||||
, _tagReader{ std::make_unique<TagReader>(*_file, enableExtraDebugLogs) }
|
||||
, _imageReader{ std::make_unique<ImageReader>(*_file) }
|
||||
, _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
|
||||
, _audioProperties{ computeAudioProperties(*_file, filePath) }
|
||||
{
|
||||
if (parseOptions.readTags)
|
||||
_tagReader = std::make_unique<TagReader>(*_file, parseOptions.enableExtraDebugLogs);
|
||||
|
||||
if (parseOptions.readImages)
|
||||
_imageReader = std::make_unique<ImageReader>(*_file);
|
||||
}
|
||||
|
||||
AudioFileInfo::~AudioFileInfo() = default;
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
const AudioProperties* AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return *_audioProperties;
|
||||
return _audioProperties.has_value() ? &_audioProperties.value() : nullptr;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
const IImageReader* AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return *_imageReader;
|
||||
return _imageReader.get();
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
const ITagReader* AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return *_tagReader;
|
||||
return _tagReader.get();
|
||||
}
|
||||
|
||||
} // namespace lms::audio::taglib
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "audio/AudioProperties.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
@@ -37,20 +41,20 @@ namespace lms::audio::taglib
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
|
||||
AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
|
||||
~AudioFileInfo() override;
|
||||
|
||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||
|
||||
private:
|
||||
const AudioProperties& getAudioProperties() const override;
|
||||
const IImageReader& getImageReader() const override;
|
||||
const ITagReader& getTagReader() const override;
|
||||
const AudioProperties* getAudioProperties() const override;
|
||||
const IImageReader* getImageReader() const override;
|
||||
const ITagReader* getTagReader() const override;
|
||||
|
||||
const std::filesystem::path _filePath;
|
||||
std::unique_ptr<::TagLib::File> _file;
|
||||
std::unique_ptr<AudioProperties> _audioProperties;
|
||||
std::optional<AudioProperties> _audioProperties;
|
||||
std::unique_ptr<TagReader> _tagReader;
|
||||
std::unique_ptr<ImageReader> _imageReader;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfoParser.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
|
||||
{
|
||||
return std::make_unique<AudioFileInfo>(p, parseOptions);
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
|
||||
{
|
||||
return utils::getSupportedExtensions();
|
||||
}
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class AudioFileInfoParser : public IAudioFileInfoParser
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
|
||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||
};
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -44,157 +44,157 @@ namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
||||
core::media::ImageType imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
|
||||
return Image::Type::Other;
|
||||
return core::media::ImageType::Other;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
return core::media::ImageType::FileIcon;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
return core::media::ImageType::OtherFileIcon;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
return core::media::ImageType::FrontCover;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
return core::media::ImageType::BackCover;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
return core::media::ImageType::LeafletPage;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
|
||||
return Image::Type::Media;
|
||||
return core::media::ImageType::Media;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
return core::media::ImageType::LeadArtist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
return core::media::ImageType::Artist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
return core::media::ImageType::Conductor;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
|
||||
return Image::Type::Band;
|
||||
return core::media::ImageType::Band;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
return core::media::ImageType::Composer;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
return core::media::ImageType::Lyricist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
return core::media::ImageType::RecordingLocation;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
return core::media::ImageType::DuringRecording;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
return core::media::ImageType::DuringPerformance;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
return core::media::ImageType::MovieScreenCapture;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
return core::media::ImageType::ColouredFish;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
return core::media::ImageType::Illustration;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
return core::media::ImageType::BandLogo;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
return core::media::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
return core::media::ImageType::Unknown;
|
||||
}
|
||||
|
||||
Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
|
||||
core::media::ImageType imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::ASF::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
return core::media::ImageType::Other;
|
||||
case ::TagLib::ASF::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
return core::media::ImageType::FileIcon;
|
||||
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
return core::media::ImageType::OtherFileIcon;
|
||||
case ::TagLib::ASF::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
return core::media::ImageType::FrontCover;
|
||||
case ::TagLib::ASF::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
return core::media::ImageType::BackCover;
|
||||
case ::TagLib::ASF::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
return core::media::ImageType::LeafletPage;
|
||||
case ::TagLib::ASF::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
return core::media::ImageType::Media;
|
||||
case ::TagLib::ASF::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
return core::media::ImageType::LeadArtist;
|
||||
case ::TagLib::ASF::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
return core::media::ImageType::Artist;
|
||||
case ::TagLib::ASF::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
return core::media::ImageType::Conductor;
|
||||
case ::TagLib::ASF::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
return core::media::ImageType::Band;
|
||||
case ::TagLib::ASF::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
return core::media::ImageType::Composer;
|
||||
case ::TagLib::ASF::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
return core::media::ImageType::Lyricist;
|
||||
case ::TagLib::ASF::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
return core::media::ImageType::RecordingLocation;
|
||||
case ::TagLib::ASF::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
return core::media::ImageType::DuringRecording;
|
||||
case ::TagLib::ASF::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
return core::media::ImageType::DuringPerformance;
|
||||
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
return core::media::ImageType::MovieScreenCapture;
|
||||
case ::TagLib::ASF::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
return core::media::ImageType::ColouredFish;
|
||||
case ::TagLib::ASF::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
return core::media::ImageType::Illustration;
|
||||
case ::TagLib::ASF::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
return core::media::ImageType::BandLogo;
|
||||
case ::TagLib::ASF::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
return core::media::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
return core::media::ImageType::Unknown;
|
||||
}
|
||||
|
||||
Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
|
||||
core::media::ImageType imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::FLAC::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
return core::media::ImageType::Other;
|
||||
case ::TagLib::FLAC::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
return core::media::ImageType::FileIcon;
|
||||
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
return core::media::ImageType::OtherFileIcon;
|
||||
case ::TagLib::FLAC::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
return core::media::ImageType::FrontCover;
|
||||
case ::TagLib::FLAC::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
return core::media::ImageType::BackCover;
|
||||
case ::TagLib::FLAC::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
return core::media::ImageType::LeafletPage;
|
||||
case ::TagLib::FLAC::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
return core::media::ImageType::Media;
|
||||
case ::TagLib::FLAC::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
return core::media::ImageType::LeadArtist;
|
||||
case ::TagLib::FLAC::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
return core::media::ImageType::Artist;
|
||||
case ::TagLib::FLAC::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
return core::media::ImageType::Conductor;
|
||||
case ::TagLib::FLAC::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
return core::media::ImageType::Band;
|
||||
case ::TagLib::FLAC::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
return core::media::ImageType::Composer;
|
||||
case ::TagLib::FLAC::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
return core::media::ImageType::Lyricist;
|
||||
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
return core::media::ImageType::RecordingLocation;
|
||||
case ::TagLib::FLAC::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
return core::media::ImageType::DuringRecording;
|
||||
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
return core::media::ImageType::DuringPerformance;
|
||||
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
return core::media::ImageType::MovieScreenCapture;
|
||||
case ::TagLib::FLAC::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
return core::media::ImageType::ColouredFish;
|
||||
case ::TagLib::FLAC::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
return core::media::ImageType::Illustration;
|
||||
case ::TagLib::FLAC::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
return core::media::ImageType::BandLogo;
|
||||
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
return core::media::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
return core::media::ImageType::Unknown;
|
||||
}
|
||||
|
||||
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
|
||||
@@ -217,14 +217,14 @@ namespace lms::audio::taglib
|
||||
}
|
||||
|
||||
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
|
||||
core::media::ImageType imageTypeFromAPEPictureType(std::string_view pictureType)
|
||||
{
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
|
||||
return Image::Type::FrontCover;
|
||||
return core::media::ImageType::FrontCover;
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
|
||||
return Image::Type::BackCover;
|
||||
return core::media::ImageType::BackCover;
|
||||
|
||||
return Image::Type::Unknown;
|
||||
return core::media::ImageType::Unknown;
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
|
||||
@@ -294,7 +294,7 @@ namespace lms::audio::taglib
|
||||
image.data = pictureData;
|
||||
|
||||
// By convention, consider the first cover art as the front cover
|
||||
image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown;
|
||||
image.type = firstCover ? core::media::ImageType::FrontCover : core::media::ImageType::Unknown;
|
||||
firstCover = false;
|
||||
|
||||
visitor(image);
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
@@ -89,15 +89,15 @@ namespace lms::audio::taglib::utils
|
||||
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||
}
|
||||
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserOptions::AudioPropertiesReadStyle::Fast:
|
||||
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast:
|
||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserOptions::AudioPropertiesReadStyle::Average:
|
||||
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average:
|
||||
return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserOptions::AudioPropertiesReadStyle::Accurate:
|
||||
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Accurate:
|
||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace lms::audio::taglib::utils
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fopen failed", ec };
|
||||
throw IOFileException{ p, "fopen failed", ec };
|
||||
}
|
||||
|
||||
int fd{ ::fileno(file) };
|
||||
@@ -119,7 +119,7 @@ namespace lms::audio::taglib::utils
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fileno failed", ec };
|
||||
throw IOFileException{ p, "fileno failed", ec };
|
||||
}
|
||||
|
||||
return TagLib::FileStream{ fd, true };
|
||||
@@ -236,7 +236,7 @@ namespace lms::audio::taglib::utils
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||
|
||||
@@ -252,10 +252,7 @@ namespace lms::audio::taglib::utils
|
||||
}
|
||||
|
||||
if (!file)
|
||||
throw AudioFileParsingException{ "Parsing failed" };
|
||||
|
||||
if (!file->audioProperties())
|
||||
throw AudioFileParsingException{ "No audio properties" };
|
||||
throw Exception{ "Parsing failed" };
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IAudioFileInfoParser.hpp"
|
||||
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
|
||||
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle);
|
||||
} // namespace lms::audio::taglib::utils
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
struct AudioProperties
|
||||
{
|
||||
core::media::Container container;
|
||||
core::media::Codec codec;
|
||||
std::chrono::milliseconds duration;
|
||||
unsigned bitrate;
|
||||
unsigned channelCount;
|
||||
unsigned sampleRate;
|
||||
std::optional<unsigned> bitsPerSample;
|
||||
};
|
||||
} // namespace lms::audio
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
@@ -28,4 +31,23 @@ namespace lms::audio
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
class IOFileException : public Exception
|
||||
{
|
||||
public:
|
||||
IOFileException(const std::filesystem::path& path, std::string_view message, std::error_code err)
|
||||
: Exception{ "File '" + path.string() + "': " + std::string{ message } + ": " + err.message() }
|
||||
, _path{ path }
|
||||
, _err{ err }
|
||||
{
|
||||
}
|
||||
|
||||
const std::filesystem::path& getPath() const { return _path; }
|
||||
std::error_code getErrorCode() const { return _err; }
|
||||
|
||||
private:
|
||||
std::filesystem::path _path;
|
||||
std::error_code _err;
|
||||
};
|
||||
|
||||
} // namespace lms::audio
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,14 +19,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
class AudioProperties;
|
||||
struct AudioProperties;
|
||||
class IImageReader;
|
||||
class ITagReader;
|
||||
|
||||
@@ -35,52 +30,8 @@ namespace lms::audio
|
||||
public:
|
||||
virtual ~IAudioFileInfo() = default;
|
||||
|
||||
virtual const AudioProperties& getAudioProperties() const = 0;
|
||||
virtual const IImageReader& getImageReader() const = 0;
|
||||
virtual const ITagReader& getTagReader() const = 0;
|
||||
virtual const AudioProperties* getAudioProperties() const = 0; // may be null even if requested, due to backend limitation
|
||||
virtual const IImageReader* getImageReader() const = 0;
|
||||
virtual const ITagReader* getTagReader() const = 0;
|
||||
};
|
||||
|
||||
class AudioFileParsingException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class IOException : public Exception
|
||||
{
|
||||
public:
|
||||
IOException(std::string_view message, std::error_code err)
|
||||
: Exception{ std::string{ message } + ": " + err.message() }
|
||||
, _err{ err }
|
||||
{
|
||||
}
|
||||
|
||||
std::error_code getErrorCode() const { return _err; }
|
||||
|
||||
private:
|
||||
std::error_code _err;
|
||||
};
|
||||
|
||||
struct ParserOptions
|
||||
{
|
||||
enum class Parser
|
||||
{
|
||||
TagLib,
|
||||
FFmpeg,
|
||||
};
|
||||
|
||||
enum class AudioPropertiesReadStyle
|
||||
{
|
||||
Fast,
|
||||
Average,
|
||||
Accurate,
|
||||
};
|
||||
|
||||
Parser parser{ Parser::TagLib };
|
||||
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
|
||||
bool enableExtraDebugLogs{};
|
||||
};
|
||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
|
||||
} // namespace lms::audio
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
class IAudioFileInfo;
|
||||
|
||||
struct AudioFileInfoParseOptions
|
||||
{
|
||||
enum class AudioPropertiesReadStyle
|
||||
{
|
||||
Fast, // fastest
|
||||
Average, // normal
|
||||
Accurate, // full scan
|
||||
};
|
||||
|
||||
AudioPropertiesReadStyle audioPropertiesReadStyle{ AudioPropertiesReadStyle::Average };
|
||||
bool readTags{ true };
|
||||
bool readImages{ true };
|
||||
|
||||
bool enableExtraDebugLogs{};
|
||||
};
|
||||
|
||||
class IAudioFileInfoParser
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioFileInfoParser() = default;
|
||||
|
||||
virtual std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const = 0;
|
||||
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||
};
|
||||
|
||||
enum class AudioFileInfoParserBackend
|
||||
{
|
||||
TagLib,
|
||||
FFmpeg,
|
||||
};
|
||||
static inline constexpr AudioFileInfoParserBackend defaultAudioFileInfoParserBackend{ AudioFileInfoParserBackend::TagLib };
|
||||
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend = defaultAudioFileInfoParserBackend);
|
||||
} // namespace lms::audio
|
||||
@@ -23,69 +23,18 @@
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/media/ImageType.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
struct Image
|
||||
{
|
||||
// See TagLib types (based on ID3v2 APIC types)
|
||||
enum class Type
|
||||
{
|
||||
// No information
|
||||
Unknown,
|
||||
// A type not enumerated below
|
||||
Other,
|
||||
// 32x32 PNG image that should be used as the file icon
|
||||
FileIcon,
|
||||
// File icon of a different size or format
|
||||
OtherFileIcon,
|
||||
// Front cover image of the album
|
||||
FrontCover,
|
||||
// Back cover image of the album
|
||||
BackCover,
|
||||
// Inside leaflet page of the album
|
||||
LeafletPage,
|
||||
// Image from the album itself
|
||||
Media,
|
||||
// Picture of the lead artist or soloist
|
||||
LeadArtist,
|
||||
// Picture of the artist or performer
|
||||
Artist,
|
||||
// Picture of the conductor
|
||||
Conductor,
|
||||
// Picture of the band or orchestra
|
||||
Band,
|
||||
// Picture of the composer
|
||||
Composer,
|
||||
// Picture of the lyricist or text writer
|
||||
Lyricist,
|
||||
// Picture of the recording location or studio
|
||||
RecordingLocation,
|
||||
// Picture of the artists during recording
|
||||
DuringRecording,
|
||||
// Picture of the artists during performance
|
||||
DuringPerformance,
|
||||
// Picture from a movie or video related to the track
|
||||
MovieScreenCapture,
|
||||
// Picture of a large, coloured fish
|
||||
ColouredFish,
|
||||
// Illustration related to the track
|
||||
Illustration,
|
||||
// Logo of the band or performer
|
||||
BandLogo,
|
||||
// Logo of the publisher (record company)
|
||||
PublisherLogo
|
||||
};
|
||||
|
||||
Type type{ Type::Unknown };
|
||||
core::media::ImageType type{ core::media::ImageType::Unknown };
|
||||
std::string mimeType{ "application/octet-stream" };
|
||||
std::string description;
|
||||
std::span<const std::byte> data;
|
||||
};
|
||||
|
||||
core::LiteralString imageTypeToString(Image::Type type);
|
||||
|
||||
class IImageReader
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -23,28 +23,30 @@
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
#include "audio/AudioProperties.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
// TODO deprecate?
|
||||
enum class OutputFormat
|
||||
{
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
struct TranscodeInputParameters
|
||||
{
|
||||
std::filesystem::path filePath;
|
||||
std::chrono::milliseconds duration{}; // Duration of the audio file
|
||||
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
|
||||
AudioProperties audioProperties; // properties of the audio file
|
||||
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
|
||||
};
|
||||
|
||||
struct TranscodeOutputFormat
|
||||
{
|
||||
core::media::Container container;
|
||||
core::media::Codec codec;
|
||||
};
|
||||
|
||||
struct TranscodeOutputParameters
|
||||
{
|
||||
std::optional<OutputFormat> format;
|
||||
// Not setting a value here means to use the same value as the input
|
||||
std::optional<TranscodeOutputFormat> format;
|
||||
std::optional<unsigned> bitrate;
|
||||
std::optional<unsigned> bitsPerSample;
|
||||
std::optional<unsigned> channelCount;
|
||||
|
||||
@@ -13,6 +13,10 @@ configure_file(
|
||||
add_library(lmscore STATIC
|
||||
impl/http/Client.cpp
|
||||
impl/http/SendQueue.cpp
|
||||
impl/media/Codec.cpp
|
||||
impl/media/Container.cpp
|
||||
impl/media/ImageType.cpp
|
||||
impl/media/MimeType.cpp
|
||||
impl/ArchiveZipper.cpp
|
||||
impl/ChildProcess.cpp
|
||||
impl/ChildProcessManager.cpp
|
||||
|
||||
@@ -144,33 +144,33 @@ namespace lms::core::logging
|
||||
{
|
||||
case core::logging::Severity::DEBUG:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::DEBUG });
|
||||
addOutputStream(*_logFileStream, core::logging::Severity::DEBUG);
|
||||
else
|
||||
addOutputStream(std::cout, { core::logging::Severity::DEBUG });
|
||||
addOutputStream(std::cout, core::logging::Severity::DEBUG);
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::INFO:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::INFO });
|
||||
addOutputStream(*_logFileStream, core::logging::Severity::INFO);
|
||||
else
|
||||
addOutputStream(std::cout, { core::logging::Severity::INFO });
|
||||
addOutputStream(std::cout, core::logging::Severity::INFO);
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::WARNING:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::WARNING });
|
||||
addOutputStream(*_logFileStream, core::logging::Severity::WARNING);
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::WARNING });
|
||||
addOutputStream(std::cerr, core::logging::Severity::WARNING);
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::ERROR:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::ERROR });
|
||||
addOutputStream(*_logFileStream, core::logging::Severity::ERROR);
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::ERROR });
|
||||
addOutputStream(std::cerr, core::logging::Severity::ERROR);
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::FATAL:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::FATAL });
|
||||
addOutputStream(*_logFileStream, core::logging::Severity::FATAL);
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::FATAL });
|
||||
addOutputStream(std::cerr, core::logging::Severity::FATAL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -206,5 +206,4 @@ namespace lms::core::logging
|
||||
std::unique_lock lock{ outputStream->mutex };
|
||||
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
|
||||
}
|
||||
|
||||
} // namespace lms::core::logging
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
namespace lms::core::stringUtils
|
||||
{
|
||||
namespace details
|
||||
namespace detail
|
||||
{
|
||||
constexpr std::pair<char, std::string_view> jsEscapeChars[]{
|
||||
{ '\\', "\\\\" },
|
||||
@@ -203,7 +203,7 @@ namespace lms::core::stringUtils
|
||||
|
||||
return std::distance(std::cbegin(months), str) + 1;
|
||||
}
|
||||
} // namespace details
|
||||
} // namespace detail
|
||||
|
||||
template<>
|
||||
std::optional<std::string> readAs(std::string_view str)
|
||||
@@ -240,32 +240,32 @@ namespace lms::core::stringUtils
|
||||
|
||||
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string_view> separators)
|
||||
{
|
||||
return details::splitString(str, separators);
|
||||
return detail::splitString(str, separators);
|
||||
}
|
||||
|
||||
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string> separators)
|
||||
{
|
||||
return details::splitString(str, separators);
|
||||
return detail::splitString(str, separators);
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, delimiter);
|
||||
return detail::joinStrings(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, delimiter);
|
||||
return detail::joinStrings(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string> strings, char delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
return detail::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string_view> strings, char delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
return detail::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
}
|
||||
|
||||
std::string escapeAndJoinStrings(std::span<const std::string_view> strings, char delimiter, char escapeChar)
|
||||
@@ -434,32 +434,32 @@ namespace lms::core::stringUtils
|
||||
|
||||
std::string jsEscape(std::string_view str)
|
||||
{
|
||||
return details::escape(str, details::jsEscapeChars);
|
||||
return detail::escape(str, detail::jsEscapeChars);
|
||||
}
|
||||
|
||||
void writeJSEscapedString(std::ostream& os, std::string_view str)
|
||||
{
|
||||
details::writeEscapedString(os, str, details::jsEscapeChars);
|
||||
detail::writeEscapedString(os, str, detail::jsEscapeChars);
|
||||
}
|
||||
|
||||
std::string jsonEscape(std::string_view str)
|
||||
{
|
||||
return details::escape(str, details::jsonEscapeChars);
|
||||
return detail::escape(str, detail::jsonEscapeChars);
|
||||
}
|
||||
|
||||
void writeJsonEscapedString(std::ostream& os, std::string_view str)
|
||||
{
|
||||
details::writeEscapedString(os, str, details::jsonEscapeChars);
|
||||
detail::writeEscapedString(os, str, detail::jsonEscapeChars);
|
||||
}
|
||||
|
||||
std::string xmlEscape(std::string_view str)
|
||||
{
|
||||
return details::escape(str, details::xmlEscapeChars);
|
||||
return detail::escape(str, detail::xmlEscapeChars);
|
||||
}
|
||||
|
||||
void writeXmlEscapedString(std::ostream& os, std::string_view str)
|
||||
{
|
||||
details::writeEscapedString(os, str, details::xmlEscapeChars);
|
||||
detail::writeEscapedString(os, str, detail::xmlEscapeChars);
|
||||
}
|
||||
|
||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
@@ -611,11 +611,11 @@ namespace lms::core::stringUtils
|
||||
timeStr += ":00";
|
||||
|
||||
// Normalize zone
|
||||
const std::optional<std::chrono::minutes> offset{ details::getRFC822ZoneOffset(zoneStr) };
|
||||
const std::optional<std::chrono::minutes> offset{ detail::getRFC822ZoneOffset(zoneStr) };
|
||||
if (!offset)
|
||||
return {};
|
||||
|
||||
std::optional<unsigned> month{ details::getRFC822Month(monthStr) };
|
||||
std::optional<unsigned> month{ detail::getRFC822Month(monthStr) };
|
||||
if (!month)
|
||||
return {};
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::array codecDescs{
|
||||
CodecDesc{ Codec::AAC, "AAC", "Advanced Audio Coding", false },
|
||||
CodecDesc{ Codec::AC3, "AC3", "Dolby AC-3", false },
|
||||
CodecDesc{ Codec::ALAC, "ALAC", "Apple Lossless Audio Codec", true },
|
||||
CodecDesc{ Codec::APE, "APE", "Monkey's Audio", true },
|
||||
CodecDesc{ Codec::DSD, "DSD", "Direct Stream Digital", true },
|
||||
CodecDesc{ Codec::EAC3, "E-AC3", "Dolby Digital Plus", false },
|
||||
CodecDesc{ Codec::FLAC, "FLAC", "Free Lossless Audio Codec", true },
|
||||
CodecDesc{ Codec::MP3, "MP3", "MPEG-3", false },
|
||||
CodecDesc{ Codec::MP4ALS, "MP4ALS", "MPEG-4 Audio Lossless Coding", true },
|
||||
CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false },
|
||||
CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false },
|
||||
CodecDesc{ Codec::Opus, "Opus", "Opus", false },
|
||||
CodecDesc{ Codec::PCM, "PCM", "Pulse-code modulation", false },
|
||||
CodecDesc{ Codec::Shorten, "Shorten", "Shorten", true },
|
||||
CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true },
|
||||
CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false },
|
||||
CodecDesc{ Codec::WavPack, "WavPack", "WavPack", true },
|
||||
CodecDesc{ Codec::WMA1, "WMA1", "Windows Media Audio", false },
|
||||
CodecDesc{ Codec::WMA2, "WMA2", "Windows Media Audio 2", false },
|
||||
CodecDesc{ Codec::WMA9Pro, "WMA9Pro", "Windows Media Audio Professional", false },
|
||||
CodecDesc{ Codec::WMA9Lossless, "WMA9Lossless", "Windows Media Audio Lossless", true },
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void visitCodecs(const std::function<void(const CodecDesc&)>& visitor)
|
||||
{
|
||||
std::for_each(std::cbegin(codecDescs), std::cend(codecDescs), visitor);
|
||||
}
|
||||
|
||||
const CodecDesc& getCodecDesc(Codec type)
|
||||
{
|
||||
const auto it{ std::find_if(codecDescs.begin(), codecDescs.end(), [type](const auto& desc) { return desc.type == type; }) };
|
||||
if (it == codecDescs.end()) [[unlikely]]
|
||||
throw LmsException{ "Unknown codec type" };
|
||||
|
||||
return *it;
|
||||
}
|
||||
} // namespace lms::core::media
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
core::LiteralString containerToString(Container type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Container::AIFF:
|
||||
return "AIFF";
|
||||
case Container::APE:
|
||||
return "APE";
|
||||
case Container::ASF:
|
||||
return "ASF";
|
||||
case Container::DSF:
|
||||
return "DSF";
|
||||
case Container::FLAC:
|
||||
return "FLAC";
|
||||
case Container::MP4:
|
||||
return "MP4";
|
||||
case Container::MPC:
|
||||
return "MPC";
|
||||
case Container::MPEG:
|
||||
return "MPEG";
|
||||
case Container::Ogg:
|
||||
return "Ogg";
|
||||
case Container::Shorten:
|
||||
return "Shorten";
|
||||
case Container::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case Container::WAV:
|
||||
return "WAV";
|
||||
case Container::WavPack:
|
||||
return "WavPack";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
} // namespace lms::core::media
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,61 +17,60 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "core/media/ImageType.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
namespace lms::core::media
|
||||
{
|
||||
core::LiteralString imageTypeToString(Image::Type type)
|
||||
core::LiteralString imageTypeToString(ImageType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Image::Type::Other:
|
||||
case ImageType::Other:
|
||||
return "Other";
|
||||
case Image::Type::FileIcon:
|
||||
case ImageType::FileIcon:
|
||||
return "FileIcon";
|
||||
case Image::Type::OtherFileIcon:
|
||||
case ImageType::OtherFileIcon:
|
||||
return "OtherFileIcon";
|
||||
case Image::Type::FrontCover:
|
||||
case ImageType::FrontCover:
|
||||
return "FrontCover";
|
||||
case Image::Type::BackCover:
|
||||
case ImageType::BackCover:
|
||||
return "BackCover";
|
||||
case Image::Type::LeafletPage:
|
||||
case ImageType::LeafletPage:
|
||||
return "LeafletPage";
|
||||
case Image::Type::Media:
|
||||
case ImageType::Media:
|
||||
return "Media";
|
||||
case Image::Type::LeadArtist:
|
||||
case ImageType::LeadArtist:
|
||||
return "LeadArtist";
|
||||
case Image::Type::Artist:
|
||||
case ImageType::Artist:
|
||||
return "Artist";
|
||||
case Image::Type::Conductor:
|
||||
case ImageType::Conductor:
|
||||
return "Conductor";
|
||||
case Image::Type::Band:
|
||||
case ImageType::Band:
|
||||
return "Band";
|
||||
case Image::Type::Composer:
|
||||
case ImageType::Composer:
|
||||
return "Composer";
|
||||
case Image::Type::Lyricist:
|
||||
case ImageType::Lyricist:
|
||||
return "Lyricist";
|
||||
case Image::Type::RecordingLocation:
|
||||
case ImageType::RecordingLocation:
|
||||
return "RecordingLocation";
|
||||
case Image::Type::DuringRecording:
|
||||
case ImageType::DuringRecording:
|
||||
return "DuringRecording";
|
||||
case Image::Type::DuringPerformance:
|
||||
case ImageType::DuringPerformance:
|
||||
return "DuringPerformance";
|
||||
case Image::Type::MovieScreenCapture:
|
||||
case ImageType::MovieScreenCapture:
|
||||
return "MovieScreenCapture";
|
||||
case Image::Type::ColouredFish:
|
||||
case ImageType::ColouredFish:
|
||||
return "ColouredFish";
|
||||
case Image::Type::Illustration:
|
||||
case ImageType::Illustration:
|
||||
return "Illustration";
|
||||
case Image::Type::BandLogo:
|
||||
case ImageType::BandLogo:
|
||||
return "BandLogo";
|
||||
case Image::Type::PublisherLogo:
|
||||
case ImageType::PublisherLogo:
|
||||
return "PublisherLogo";
|
||||
case Image::Type::Unknown:
|
||||
case ImageType::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
} // namespace lms::core::media
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/media/MimeType.hpp"
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
core::LiteralString getMimeType(Container container, Codec codec)
|
||||
{
|
||||
switch (container)
|
||||
{
|
||||
case Container::AIFF:
|
||||
return "audio/x-aiff";
|
||||
case Container::APE:
|
||||
return "audio/x-monkeys-audio";
|
||||
case Container::ASF:
|
||||
return "audio/x-ms-wma";
|
||||
case Container::DSF:
|
||||
return "audio/x-dsd-dsf";
|
||||
case Container::FLAC:
|
||||
return "audio/flac";
|
||||
case Container::MP4:
|
||||
switch (codec)
|
||||
{
|
||||
case Codec::AAC:
|
||||
return "audio/mp4; codecs=\"mp4a.40.2\"";
|
||||
case Codec::ALAC:
|
||||
return "audio/mp4; codecs=\"alac\"";
|
||||
case Codec::MP4ALS:
|
||||
return "audio/mp4; codecs=\"mp4als\"";
|
||||
default:
|
||||
return "audio/mp4";
|
||||
}
|
||||
|
||||
case Container::MPC:
|
||||
return "audio/x-musepack";
|
||||
case Container::MPEG:
|
||||
return "audio/mpeg";
|
||||
case Container::Ogg:
|
||||
switch (codec)
|
||||
{
|
||||
case Codec::Opus:
|
||||
return "audio/opus";
|
||||
case Codec::Vorbis:
|
||||
return "audio/ogg; codecs=\"vorbis\"";
|
||||
case Codec::FLAC:
|
||||
return "audio/ogg; codecs=\"flac\"";
|
||||
default:
|
||||
return "audio/ogg";
|
||||
}
|
||||
|
||||
case Container::Shorten:
|
||||
return "audio/x-shn";
|
||||
case Container::TrueAudio:
|
||||
return "audio/x-tta";
|
||||
case Container::WAV:
|
||||
return "audio/wav";
|
||||
case Container::WavPack:
|
||||
return "audio/x-wavpack";
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
} // namespace lms::core::media
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <bit>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
@@ -51,6 +52,11 @@ namespace lms::core
|
||||
assign(begin, end);
|
||||
}
|
||||
|
||||
constexpr std::size_t size() const
|
||||
{
|
||||
return std::popcount(_bitfield);
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
constexpr void assign(It begin, It end)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
namespace details
|
||||
namespace detail
|
||||
{
|
||||
template<int... Is>
|
||||
struct Seq
|
||||
@@ -45,11 +45,11 @@ namespace lms::core
|
||||
{
|
||||
auto l = { (f(std::get<Is>(t)), 0)... };
|
||||
}
|
||||
} // namespace details
|
||||
} // namespace detail
|
||||
|
||||
template<typename... Ts, typename Func>
|
||||
void forEachTypeInTuple(std::tuple<Ts...> const& t, Func f)
|
||||
{
|
||||
details::forEachTypeInTuple(t, f, details::GenSeq<sizeof...(Ts)>());
|
||||
detail::forEachTypeInTuple(t, f, detail::GenSeq<sizeof...(Ts)>());
|
||||
}
|
||||
} // namespace lms::core
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -49,3 +50,15 @@ namespace lms::core::stringUtils
|
||||
std::optional<UUID>
|
||||
readAs(std::string_view str);
|
||||
}
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<>
|
||||
struct hash<lms::core::UUID>
|
||||
{
|
||||
size_t operator()(const lms::core::UUID& str) const
|
||||
{
|
||||
return hash<std::string_view>{}(str.getAsString());
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
+13
-34
@@ -19,38 +19,20 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
namespace lms::core::media
|
||||
{
|
||||
enum class ContainerType
|
||||
{
|
||||
AIFF,
|
||||
APE, // Monkey's Audio
|
||||
ASF, // Advanced Systems Format
|
||||
DSF,
|
||||
FLAC,
|
||||
MP4,
|
||||
MPC, // Musepack
|
||||
MPEG,
|
||||
Ogg,
|
||||
Shorten,
|
||||
TrueAudio,
|
||||
WAV,
|
||||
WavPack,
|
||||
};
|
||||
|
||||
core::LiteralString containerTypeToString(ContainerType type);
|
||||
|
||||
enum class CodecType
|
||||
enum class Codec
|
||||
{
|
||||
AAC,
|
||||
AC3,
|
||||
ALAC, // Apple Lossless Audio Codec (ALAC)
|
||||
APE, // Monkey's Audio
|
||||
DSD, // DSD
|
||||
EAC3,
|
||||
FLAC, // Flac
|
||||
MP3,
|
||||
MP4ALS, // MPEG-4 Audio Lossless Coding
|
||||
@@ -68,16 +50,13 @@ namespace lms::audio
|
||||
WMA9Lossless,
|
||||
};
|
||||
|
||||
core::LiteralString codecTypeToString(CodecType type);
|
||||
|
||||
struct AudioProperties
|
||||
struct CodecDesc
|
||||
{
|
||||
ContainerType container;
|
||||
CodecType codec;
|
||||
std::chrono::milliseconds duration;
|
||||
unsigned bitrate;
|
||||
unsigned channelCount;
|
||||
unsigned sampleRate;
|
||||
std::optional<unsigned> bitsPerSample;
|
||||
Codec type;
|
||||
core::LiteralString name;
|
||||
core::LiteralString longName;
|
||||
bool isLossless;
|
||||
};
|
||||
} // namespace lms::audio
|
||||
void visitCodecs(const std::function<void(const CodecDesc&)>& visitor);
|
||||
const CodecDesc& getCodecDesc(Codec type);
|
||||
} // namespace lms::core::media
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
enum class Container
|
||||
{
|
||||
AIFF,
|
||||
APE, // Monkey's Audio
|
||||
ASF, // Advanced Systems Format
|
||||
DSF,
|
||||
FLAC,
|
||||
MP4,
|
||||
MPC, // Musepack
|
||||
MPEG,
|
||||
Ogg,
|
||||
Shorten,
|
||||
TrueAudio,
|
||||
WAV,
|
||||
WavPack,
|
||||
};
|
||||
|
||||
core::LiteralString containerToString(Container type);
|
||||
} // namespace lms::core::media
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
// Based on ID3v2 APIC types
|
||||
enum class ImageType
|
||||
{
|
||||
// No information
|
||||
Unknown,
|
||||
// A type not enumerated below
|
||||
Other,
|
||||
// 32x32 PNG image that should be used as the file icon
|
||||
FileIcon,
|
||||
// File icon of a different size or format
|
||||
OtherFileIcon,
|
||||
// Front cover image of the album
|
||||
FrontCover,
|
||||
// Back cover image of the album
|
||||
BackCover,
|
||||
// Inside leaflet page of the album
|
||||
LeafletPage,
|
||||
// Image from the album itself
|
||||
Media,
|
||||
// Picture of the lead artist or soloist
|
||||
LeadArtist,
|
||||
// Picture of the artist or performer
|
||||
Artist,
|
||||
// Picture of the conductor
|
||||
Conductor,
|
||||
// Picture of the band or orchestra
|
||||
Band,
|
||||
// Picture of the composer
|
||||
Composer,
|
||||
// Picture of the lyricist or text writer
|
||||
Lyricist,
|
||||
// Picture of the recording location or studio
|
||||
RecordingLocation,
|
||||
// Picture of the artists during recording
|
||||
DuringRecording,
|
||||
// Picture of the artists during performance
|
||||
DuringPerformance,
|
||||
// Picture from a movie or video related to the track
|
||||
MovieScreenCapture,
|
||||
// Picture of a large, coloured fish
|
||||
ColouredFish,
|
||||
// Illustration related to the track
|
||||
Illustration,
|
||||
// Logo of the band or performer
|
||||
BandLogo,
|
||||
// Logo of the publisher (record company)
|
||||
PublisherLogo
|
||||
};
|
||||
|
||||
core::LiteralString imageTypeToString(ImageType type);
|
||||
} // namespace lms::core::media
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
namespace lms::core::media
|
||||
{
|
||||
core::LiteralString getMimeType(Container container, Codec codec);
|
||||
}
|
||||
@@ -31,9 +31,19 @@ namespace lms::core
|
||||
Two,
|
||||
};
|
||||
|
||||
{
|
||||
constexpr EnumSet<Foo> test;
|
||||
|
||||
static_assert(test.size() == 0);
|
||||
static_assert(test.empty());
|
||||
static_assert(!test.contains(Foo::One));
|
||||
static_assert(!test.contains(Foo::Two));
|
||||
}
|
||||
|
||||
{
|
||||
constexpr EnumSet<Foo> test{ Foo::One };
|
||||
|
||||
static_assert(test.size() == 1);
|
||||
static_assert(!test.empty());
|
||||
static_assert(test.contains(Foo::One));
|
||||
static_assert(!test.contains(Foo::Two));
|
||||
@@ -49,6 +59,8 @@ namespace lms::core
|
||||
constexpr EnumSet<Foo> test{ Foo::One, Foo::Two };
|
||||
constexpr auto bitfield{ test.getBitfield() };
|
||||
|
||||
static_assert(test.size() == 2);
|
||||
|
||||
EnumSet<Foo> test2;
|
||||
EXPECT_FALSE(test2.contains(Foo::One));
|
||||
EXPECT_FALSE(test2.contains(Foo::Two));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
add_library(lmsdatabase STATIC
|
||||
impl/objects/detail/Types.cpp
|
||||
impl/objects/Artist.cpp
|
||||
impl/objects/ArtistInfo.cpp
|
||||
impl/objects/Artwork.cpp
|
||||
@@ -29,6 +30,7 @@ add_library(lmsdatabase STATIC
|
||||
impl/objects/TrackEmbeddedImage.cpp
|
||||
impl/objects/TrackEmbeddedImageLink.cpp
|
||||
impl/objects/TrackLyrics.cpp
|
||||
impl/objects/Types.cpp
|
||||
impl/objects/UIState.cpp
|
||||
impl/objects/User.cpp
|
||||
impl/Db.cpp
|
||||
@@ -39,7 +41,6 @@ add_library(lmsdatabase STATIC
|
||||
impl/Session.cpp
|
||||
impl/SqlQuery.cpp
|
||||
impl/Transaction.cpp
|
||||
impl/Types.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/ScanSettings.hpp"
|
||||
|
||||
@@ -34,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 100 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 102 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -1581,6 +1582,105 @@ FROM track)");
|
||||
constraint "fk_podcast_episode_podcast" foreign key ("podcast_id") references "podcast" ("id") on delete cascade deferrable initially deferred))");
|
||||
}
|
||||
|
||||
void migrateFromV100(Session& session)
|
||||
{
|
||||
// Add container and codec to track table + made bits_per_sample optional
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"absolute_file_path" text not null,
|
||||
"file_size" bigint not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"duration" integer,
|
||||
"container" integer not null,
|
||||
"codec" integer not null,
|
||||
"bitrate" integer not null,
|
||||
"channel_count" integer not null,
|
||||
"sample_rate" integer not null,
|
||||
"bits_per_sample" integer,
|
||||
"replay_gain" real,
|
||||
"track_number" integer,
|
||||
"name" text not null,
|
||||
"date" text,
|
||||
"original_date" text,
|
||||
"mbid" text not null,
|
||||
"recording_mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"artist_display_name" text not null,
|
||||
"comment" text not null,
|
||||
"advisory" integer not null,
|
||||
"medium_id" bigint,
|
||||
"release_id" bigint,
|
||||
"media_library_id" bigint,
|
||||
"directory_id" bigint,
|
||||
"preferred_artwork_id" bigint,
|
||||
"preferred_media_artwork_id" bigint,
|
||||
constraint "fk_track_medium" foreign key ("medium_id") references "medium" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
|
||||
constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred,
|
||||
constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))");
|
||||
|
||||
// Copy data from track to track_backup
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
scan_version,
|
||||
absolute_file_path,
|
||||
file_size,
|
||||
file_last_write,
|
||||
file_added,
|
||||
duration,
|
||||
0 as container,
|
||||
0 as codec,
|
||||
bitrate,
|
||||
channel_count,
|
||||
sample_rate,
|
||||
CASE WHEN bits_per_sample = 0 THEN NULL ELSE bits_per_sample END as bits_per_sample,
|
||||
replay_gain,
|
||||
track_number,
|
||||
name,
|
||||
date,
|
||||
original_date,
|
||||
mbid,
|
||||
recording_mbid,
|
||||
copyright,
|
||||
copyright_url,
|
||||
artist_display_name,
|
||||
comment,
|
||||
advisory,
|
||||
medium_id,
|
||||
release_id,
|
||||
media_library_id,
|
||||
directory_id,
|
||||
preferred_artwork_id,
|
||||
preferred_media_artwork_id
|
||||
FROM track)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scan rescan all audio files
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV101(Session& session)
|
||||
{
|
||||
// Add audio properties for podcast episodes
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD container INTEGER NOT NULL DEFAULT(0)"); // 0 means unknown
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD codec INTEGER NOT NULL DEFAULT(0)"); // 0 means unknown
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bitrate INTEGER NOT NULL DEFAULT(0)");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD channel_count INTEGER NOT NULL DEFAULT(0)");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bits_per_sample INTEGER");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1657,6 +1757,8 @@ FROM track)");
|
||||
{ 97, migrateFromV97 },
|
||||
{ 98, migrateFromV98 },
|
||||
{ 99, migrateFromV99 },
|
||||
{ 100, migrateFromV100 },
|
||||
{ 101, migrateFromV101 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace lms::db::utils
|
||||
|
||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||
|
||||
namespace details
|
||||
namespace detail
|
||||
{
|
||||
template<typename Query>
|
||||
void recordQueryPlanIfNeeded(const Query& query)
|
||||
@@ -50,7 +50,7 @@ namespace lms::db::utils
|
||||
if (IQueryPlanRecorder * recorder{ core::Service<IQueryPlanRecorder>::get() })
|
||||
static_cast<QueryPlanRecorder*>(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString());
|
||||
}
|
||||
} // namespace details
|
||||
} // namespace detail
|
||||
|
||||
template<typename Query>
|
||||
void applyRange(Query& query, std::optional<Range> range)
|
||||
@@ -100,7 +100,7 @@ namespace lms::db::utils
|
||||
template<typename Query, typename UnaryFunc>
|
||||
void forEachQueryResult(const Query& query, UnaryFunc&& func)
|
||||
{
|
||||
details::recordQueryPlanIfNeeded(query);
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace lms::db::utils
|
||||
template<typename T, typename Query>
|
||||
std::vector<T> fetchQueryResults(const Query& query)
|
||||
{
|
||||
details::recordQueryPlanIfNeeded(query);
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace lms::db::utils
|
||||
template<typename Query>
|
||||
std::vector<typename QueryResultType<Query>::type> fetchQueryResults(const Query& query)
|
||||
{
|
||||
details::recordQueryPlanIfNeeded(query);
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace lms::db::utils
|
||||
template<typename Query>
|
||||
auto fetchQuerySingleResult(const Query& query)
|
||||
{
|
||||
details::recordQueryPlanIfNeeded(query);
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
|
||||
return query.resultValue();
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "objects/detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
@@ -56,6 +57,7 @@ namespace lms::db
|
||||
|| params.track.isValid()
|
||||
|| params.release.isValid()
|
||||
|| params.filters.clusters.size() == 1
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
@@ -67,6 +69,7 @@ namespace lms::db
|
||||
|| params.sortMethod == ArtistSortMethod::AddedDesc
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.release.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
@@ -79,6 +82,9 @@ namespace lms::db
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
|
||||
|
||||
if (params.filters.mediaLibrary.isValid())
|
||||
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "objects/detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Listen)
|
||||
@@ -49,11 +50,15 @@ namespace lms::db
|
||||
assert(!params.artist.isValid()); // poor check
|
||||
|
||||
if (params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
|
||||
|
||||
if (params.filters.mediaLibrary.isValid())
|
||||
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "database/objects/Podcast.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/PathTraits.hpp"
|
||||
|
||||
@@ -98,6 +99,16 @@ namespace lms::db
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
std::optional<core::media::Container> PodcastEpisode::getContainer() const
|
||||
{
|
||||
return detail::getMediaContainerType(_container);
|
||||
}
|
||||
|
||||
std::optional<core::media::Codec> PodcastEpisode::getCodec() const
|
||||
{
|
||||
return detail::getMediaCodecType(_codec);
|
||||
}
|
||||
|
||||
ObjectPtr<Artwork> PodcastEpisode::getArtwork() const
|
||||
{
|
||||
return _artwork;
|
||||
@@ -108,6 +119,16 @@ namespace lms::db
|
||||
return _artwork.id();
|
||||
}
|
||||
|
||||
void PodcastEpisode::setContainer(core::media::Container container)
|
||||
{
|
||||
_container = detail::getDbContainer(container);
|
||||
}
|
||||
|
||||
void PodcastEpisode::setCodec(core::media::Codec codec)
|
||||
{
|
||||
_codec = detail::getDbCodec(codec);
|
||||
}
|
||||
|
||||
void PodcastEpisode::setArtwork(ObjectPtr<Artwork> artwork)
|
||||
{
|
||||
_artwork = getDboPtr(artwork);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "detail/Types.hpp"
|
||||
#include "traits/EnumSetTraits.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/PartialDateTimeTraits.hpp"
|
||||
@@ -74,6 +75,7 @@ namespace lms::db
|
||||
|| params.artist.isValid()
|
||||
|| params.filters.clusters.size() == 1
|
||||
|| params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.directory.isValid()
|
||||
|| params.parentDirectory.isValid())
|
||||
{
|
||||
@@ -217,6 +219,9 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value()));
|
||||
|
||||
if (params.releaseGroupMBID)
|
||||
query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString());
|
||||
|
||||
@@ -679,6 +684,22 @@ namespace lms::db
|
||||
return utils::fetchQuerySingleResult(session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0"));
|
||||
}
|
||||
|
||||
std::vector<core::media::Codec> Release::getCodecs() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// Get the codec ordered by frequency
|
||||
auto query{ session()->query<detail::Codec>("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") };
|
||||
|
||||
std::vector<core::media::Codec> res;
|
||||
utils::forEachQueryResult(query, [&](detail::Codec codec) {
|
||||
if (const auto mediaCodecType{ detail::getMediaCodecType(codec) })
|
||||
res.push_back(*mediaCodecType);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
@@ -41,6 +42,7 @@
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "objects/detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/PartialDateTimeTraits.hpp"
|
||||
#include "traits/PathTraits.hpp"
|
||||
@@ -195,6 +197,9 @@ namespace lms::db
|
||||
query.where("t_e_i_l.track_embedded_image_id = ?").bind(params.embeddedImageId);
|
||||
}
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case TrackSortMethod::None:
|
||||
@@ -523,6 +528,16 @@ namespace lms::db
|
||||
_absoluteFilePath = filePath;
|
||||
}
|
||||
|
||||
void Track::setContainer(core::media::Container container)
|
||||
{
|
||||
_container = detail::getDbContainer(container);
|
||||
}
|
||||
|
||||
void Track::setCodec(core::media::Codec codec)
|
||||
{
|
||||
_codec = detail::getDbCodec(codec);
|
||||
}
|
||||
|
||||
void Track::setName(std::string_view name)
|
||||
{
|
||||
_name = std::string{ name, 0, _maxNameLength };
|
||||
@@ -603,6 +618,16 @@ namespace lms::db
|
||||
_preferredMediaArtwork = getDboPtr(artwork);
|
||||
}
|
||||
|
||||
std::optional<core::media::Container> Track::getContainer() const
|
||||
{
|
||||
return detail::getMediaContainerType(_container);
|
||||
}
|
||||
|
||||
std::optional<core::media::Codec> Track::getCodec() const
|
||||
{
|
||||
return detail::getMediaCodecType(_codec);
|
||||
}
|
||||
|
||||
std::optional<int> Track::getYear() const
|
||||
{
|
||||
return _date.getYear();
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "detail/Types.hpp"
|
||||
#include "objects/detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/ImageHashTypeTraits.hpp"
|
||||
|
||||
@@ -83,7 +85,7 @@ namespace lms::db
|
||||
}
|
||||
|
||||
if (params.imageType.has_value())
|
||||
query.where("t_e_i_l.type = ?").bind(params.imageType.value());
|
||||
query.where("t_e_i_l.type = ?").bind(detail::getDbImageType(params.imageType.value()));
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/ImageHashTypeTraits.hpp"
|
||||
|
||||
@@ -77,4 +77,14 @@ namespace lms::db
|
||||
{
|
||||
return _image;
|
||||
}
|
||||
|
||||
core::media::ImageType TrackEmbeddedImageLink::getType() const
|
||||
{
|
||||
return detail::getMediaImageType(_type);
|
||||
}
|
||||
|
||||
void TrackEmbeddedImageLink::setType(core::media::ImageType type)
|
||||
{
|
||||
_type = detail::getDbImageType(type);
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "objects/detail/Types.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
@@ -49,6 +50,7 @@ namespace lms::db
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
||||
|
||||
if (!params.filters.clusters.empty()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
@@ -61,11 +63,15 @@ namespace lms::db
|
||||
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_l_e.track_id");
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
|
||||
|
||||
if (params.filters.mediaLibrary.isValid())
|
||||
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -54,7 +54,7 @@ namespace lms::db
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static const std::set<Bitrate> allowedAudioBitrates{
|
||||
static const std::unordered_set<Bitrate> allowedAudioBitrates{
|
||||
64000,
|
||||
96000,
|
||||
128000,
|
||||
@@ -72,5 +72,4 @@ namespace lms::db
|
||||
{
|
||||
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace lms::db::detail
|
||||
{
|
||||
std::optional<core::media::Container> getMediaContainerType(db::detail::Container container)
|
||||
{
|
||||
switch (container)
|
||||
{
|
||||
case Container::AIFF:
|
||||
return core::media::Container::AIFF;
|
||||
case Container::APE:
|
||||
return core::media::Container::APE;
|
||||
case Container::ASF:
|
||||
return core::media::Container::ASF;
|
||||
case Container::DSF:
|
||||
return core::media::Container::DSF;
|
||||
case Container::FLAC:
|
||||
return core::media::Container::FLAC;
|
||||
case Container::MP4:
|
||||
return core::media::Container::MP4;
|
||||
case Container::MPC:
|
||||
return core::media::Container::MPC;
|
||||
case Container::MPEG:
|
||||
return core::media::Container::MPEG;
|
||||
case Container::Ogg:
|
||||
return core::media::Container::Ogg;
|
||||
case Container::Shorten:
|
||||
return core::media::Container::Shorten;
|
||||
case Container::TrueAudio:
|
||||
return core::media::Container::TrueAudio;
|
||||
case Container::WAV:
|
||||
return core::media::Container::WAV;
|
||||
case Container::WavPack:
|
||||
return core::media::Container::WavPack;
|
||||
|
||||
case Container::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
db::detail::Container getDbContainer(core::media::Container container)
|
||||
{
|
||||
switch (container)
|
||||
{
|
||||
case core::media::Container::AIFF:
|
||||
return Container::AIFF;
|
||||
case core::media::Container::APE:
|
||||
return Container::APE;
|
||||
case core::media::Container::ASF:
|
||||
return Container::ASF;
|
||||
case core::media::Container::DSF:
|
||||
return Container::DSF;
|
||||
case core::media::Container::FLAC:
|
||||
return Container::FLAC;
|
||||
case core::media::Container::MP4:
|
||||
return Container::MP4;
|
||||
case core::media::Container::MPC:
|
||||
return Container::MPC;
|
||||
case core::media::Container::MPEG:
|
||||
return Container::MPEG;
|
||||
case core::media::Container::Ogg:
|
||||
return Container::Ogg;
|
||||
case core::media::Container::Shorten:
|
||||
return Container::Shorten;
|
||||
case core::media::Container::TrueAudio:
|
||||
return Container::TrueAudio;
|
||||
case core::media::Container::WAV:
|
||||
return Container::WAV;
|
||||
case core::media::Container::WavPack:
|
||||
return Container::WavPack;
|
||||
}
|
||||
|
||||
return Container::Unknown;
|
||||
}
|
||||
|
||||
std::optional<core::media::Codec> getMediaCodecType(db::detail::Codec codec)
|
||||
{
|
||||
switch (codec)
|
||||
{
|
||||
case Codec::AAC:
|
||||
return core::media::Codec::AAC;
|
||||
case Codec::AC3:
|
||||
return core::media::Codec::AC3;
|
||||
case Codec::ALAC:
|
||||
return core::media::Codec::ALAC;
|
||||
case Codec::APE:
|
||||
return core::media::Codec::APE;
|
||||
case Codec::DSD:
|
||||
return core::media::Codec::DSD;
|
||||
case Codec::EAC3:
|
||||
return core::media::Codec::EAC3;
|
||||
case Codec::FLAC:
|
||||
return core::media::Codec::FLAC;
|
||||
case Codec::MP3:
|
||||
return core::media::Codec::MP3;
|
||||
case Codec::MP4ALS:
|
||||
return core::media::Codec::MP4ALS;
|
||||
case Codec::MPC7:
|
||||
return core::media::Codec::MPC7;
|
||||
case Codec::MPC8:
|
||||
return core::media::Codec::MPC8;
|
||||
case Codec::Opus:
|
||||
return core::media::Codec::Opus;
|
||||
case Codec::PCM:
|
||||
return core::media::Codec::PCM;
|
||||
case Codec::Shorten:
|
||||
return core::media::Codec::Shorten;
|
||||
case Codec::TrueAudio:
|
||||
return core::media::Codec::TrueAudio;
|
||||
case Codec::Vorbis:
|
||||
return core::media::Codec::Vorbis;
|
||||
case Codec::WavPack:
|
||||
return core::media::Codec::WavPack;
|
||||
case Codec::WMA1:
|
||||
return core::media::Codec::WMA1;
|
||||
case Codec::WMA2:
|
||||
return core::media::Codec::WMA2;
|
||||
case Codec::WMA9Pro:
|
||||
return core::media::Codec::WMA9Pro;
|
||||
case Codec::WMA9Lossless:
|
||||
return core::media::Codec::WMA9Lossless;
|
||||
|
||||
case Codec::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
db::detail::Codec getDbCodec(core::media::Codec codec)
|
||||
{
|
||||
switch (codec)
|
||||
{
|
||||
case core::media::Codec::AAC:
|
||||
return Codec::AAC;
|
||||
case core::media::Codec::AC3:
|
||||
return Codec::AC3;
|
||||
case core::media::Codec::ALAC:
|
||||
return Codec::ALAC;
|
||||
case core::media::Codec::APE:
|
||||
return Codec::APE;
|
||||
case core::media::Codec::DSD:
|
||||
return Codec::DSD;
|
||||
case core::media::Codec::EAC3:
|
||||
return Codec::EAC3;
|
||||
case core::media::Codec::FLAC:
|
||||
return Codec::FLAC;
|
||||
case core::media::Codec::MP3:
|
||||
return Codec::MP3;
|
||||
case core::media::Codec::MP4ALS:
|
||||
return Codec::MP4ALS;
|
||||
case core::media::Codec::MPC7:
|
||||
return Codec::MPC7;
|
||||
case core::media::Codec::MPC8:
|
||||
return Codec::MPC8;
|
||||
case core::media::Codec::Opus:
|
||||
return Codec::Opus;
|
||||
case core::media::Codec::PCM:
|
||||
return Codec::PCM;
|
||||
case core::media::Codec::Shorten:
|
||||
return Codec::Shorten;
|
||||
case core::media::Codec::TrueAudio:
|
||||
return Codec::TrueAudio;
|
||||
case core::media::Codec::Vorbis:
|
||||
return Codec::Vorbis;
|
||||
case core::media::Codec::WavPack:
|
||||
return Codec::WavPack;
|
||||
case core::media::Codec::WMA1:
|
||||
return Codec::WMA1;
|
||||
case core::media::Codec::WMA2:
|
||||
return Codec::WMA2;
|
||||
case core::media::Codec::WMA9Pro:
|
||||
return Codec::WMA9Pro;
|
||||
case core::media::Codec::WMA9Lossless:
|
||||
return Codec::WMA9Lossless;
|
||||
}
|
||||
|
||||
return Codec::Unknown;
|
||||
}
|
||||
|
||||
core::media::ImageType getMediaImageType(db::detail::ImageType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case db::detail::ImageType::Unknown:
|
||||
return core::media::ImageType::Unknown;
|
||||
case db::detail::ImageType::Other:
|
||||
return core::media::ImageType::Other;
|
||||
case db::detail::ImageType::FileIcon:
|
||||
return core::media::ImageType::FileIcon;
|
||||
case db::detail::ImageType::OtherFileIcon:
|
||||
return core::media::ImageType::OtherFileIcon;
|
||||
case db::detail::ImageType::FrontCover:
|
||||
return core::media::ImageType::FrontCover;
|
||||
case db::detail::ImageType::BackCover:
|
||||
return core::media::ImageType::BackCover;
|
||||
case db::detail::ImageType::LeafletPage:
|
||||
return core::media::ImageType::LeafletPage;
|
||||
case db::detail::ImageType::Media:
|
||||
return core::media::ImageType::Media;
|
||||
case db::detail::ImageType::LeadArtist:
|
||||
return core::media::ImageType::LeadArtist;
|
||||
case db::detail::ImageType::Artist:
|
||||
return core::media::ImageType::Artist;
|
||||
case db::detail::ImageType::Conductor:
|
||||
return core::media::ImageType::Conductor;
|
||||
case db::detail::ImageType::Band:
|
||||
return core::media::ImageType::Band;
|
||||
case db::detail::ImageType::Composer:
|
||||
return core::media::ImageType::Composer;
|
||||
case db::detail::ImageType::Lyricist:
|
||||
return core::media::ImageType::Lyricist;
|
||||
case db::detail::ImageType::RecordingLocation:
|
||||
return core::media::ImageType::RecordingLocation;
|
||||
case db::detail::ImageType::DuringRecording:
|
||||
return core::media::ImageType::DuringRecording;
|
||||
case db::detail::ImageType::DuringPerformance:
|
||||
return core::media::ImageType::DuringPerformance;
|
||||
case db::detail::ImageType::MovieScreenCapture:
|
||||
return core::media::ImageType::MovieScreenCapture;
|
||||
case db::detail::ImageType::ColouredFish:
|
||||
return core::media::ImageType::ColouredFish;
|
||||
case db::detail::ImageType::Illustration:
|
||||
return core::media::ImageType::Illustration;
|
||||
case db::detail::ImageType::BandLogo:
|
||||
return core::media::ImageType::BandLogo;
|
||||
case db::detail::ImageType::PublisherLogo:
|
||||
return core::media::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return core::media::ImageType::Unknown;
|
||||
}
|
||||
|
||||
db::detail::ImageType getDbImageType(core::media::ImageType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case core::media::ImageType::Unknown:
|
||||
return db::detail::ImageType::Unknown;
|
||||
case core::media::ImageType::Other:
|
||||
return db::detail::ImageType::Other;
|
||||
case core::media::ImageType::FileIcon:
|
||||
return db::detail::ImageType::FileIcon;
|
||||
case core::media::ImageType::OtherFileIcon:
|
||||
return db::detail::ImageType::OtherFileIcon;
|
||||
case core::media::ImageType::FrontCover:
|
||||
return db::detail::ImageType::FrontCover;
|
||||
case core::media::ImageType::BackCover:
|
||||
return db::detail::ImageType::BackCover;
|
||||
case core::media::ImageType::LeafletPage:
|
||||
return db::detail::ImageType::LeafletPage;
|
||||
case core::media::ImageType::Media:
|
||||
return db::detail::ImageType::Media;
|
||||
case core::media::ImageType::LeadArtist:
|
||||
return db::detail::ImageType::LeadArtist;
|
||||
case core::media::ImageType::Artist:
|
||||
return db::detail::ImageType::Artist;
|
||||
case core::media::ImageType::Conductor:
|
||||
return db::detail::ImageType::Conductor;
|
||||
case core::media::ImageType::Band:
|
||||
return db::detail::ImageType::Band;
|
||||
case core::media::ImageType::Composer:
|
||||
return db::detail::ImageType::Composer;
|
||||
case core::media::ImageType::Lyricist:
|
||||
return db::detail::ImageType::Lyricist;
|
||||
case core::media::ImageType::RecordingLocation:
|
||||
return db::detail::ImageType::RecordingLocation;
|
||||
case core::media::ImageType::DuringRecording:
|
||||
return db::detail::ImageType::DuringRecording;
|
||||
case core::media::ImageType::DuringPerformance:
|
||||
return db::detail::ImageType::DuringPerformance;
|
||||
case core::media::ImageType::MovieScreenCapture:
|
||||
return db::detail::ImageType::MovieScreenCapture;
|
||||
case core::media::ImageType::ColouredFish:
|
||||
return db::detail::ImageType::ColouredFish;
|
||||
case core::media::ImageType::Illustration:
|
||||
return db::detail::ImageType::Illustration;
|
||||
case core::media::ImageType::BandLogo:
|
||||
return db::detail::ImageType::BandLogo;
|
||||
case core::media::ImageType::PublisherLogo:
|
||||
return db::detail::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return db::detail::ImageType::Unknown;
|
||||
}
|
||||
} // namespace lms::db::detail
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
#include "core/media/ImageType.hpp"
|
||||
|
||||
#include "database/objects/detail/Types.hpp"
|
||||
|
||||
namespace lms::db::detail
|
||||
{
|
||||
std::optional<core::media::Container> getMediaContainerType(db::detail::Container container);
|
||||
db::detail::Container getDbContainer(core::media::Container container);
|
||||
|
||||
std::optional<core::media::Codec> getMediaCodecType(db::detail::Codec codec);
|
||||
db::detail::Codec getDbCodec(core::media::Codec codec);
|
||||
|
||||
core::media::ImageType getMediaImageType(db::detail::ImageType type);
|
||||
db::detail::ImageType getDbImageType(core::media::ImageType type);
|
||||
} // namespace lms::db::detail
|
||||
@@ -21,14 +21,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/TaggedType.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -113,243 +109,4 @@ namespace lms::db
|
||||
|
||||
std::size_t getTotalFileCount() const { return trackCount + imageCount + trackLyricsCount + playListCount + artistInfoCount; }
|
||||
};
|
||||
|
||||
struct YearRange
|
||||
{
|
||||
int begin{};
|
||||
int end{};
|
||||
};
|
||||
|
||||
struct FileInfo
|
||||
{
|
||||
Wt::WDateTime lastWrittenTime;
|
||||
std::size_t scanVersion{};
|
||||
};
|
||||
|
||||
enum class ArtistSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Name,
|
||||
SortName,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ClusterSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class DirectorySortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
|
||||
|
||||
enum class LabelSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class MediumSortMethod
|
||||
{
|
||||
None,
|
||||
PositionAsc,
|
||||
};
|
||||
|
||||
enum class PodcastEpisodeSortMode
|
||||
{
|
||||
None,
|
||||
PubDateAsc,
|
||||
PubDateDesc,
|
||||
};
|
||||
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Name,
|
||||
SortName,
|
||||
ArtistNameThenName,
|
||||
DateAsc,
|
||||
DateDesc,
|
||||
OriginalDate,
|
||||
OriginalDateDesc,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ReleaseTypeSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkSortMethod
|
||||
{
|
||||
None,
|
||||
OriginalDateDesc,
|
||||
};
|
||||
|
||||
enum class TrackEmbeddedImageSortMethod
|
||||
{
|
||||
None,
|
||||
SizeDesc,
|
||||
TrackNumberThenSizeDesc,
|
||||
DiscNumberThenTrackNumberThenSizeDesc,
|
||||
TrackListIndexAscThenSizeDesc,
|
||||
};
|
||||
|
||||
enum class TrackListSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
LastModifiedDesc,
|
||||
};
|
||||
|
||||
enum class TrackSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
AbsoluteFilePath,
|
||||
Name,
|
||||
DateDescAndRelease,
|
||||
OriginalDateDescAndRelease,
|
||||
Release, // order by disc/track number
|
||||
TrackList, // order by asc order in tracklist
|
||||
TrackNumber,
|
||||
};
|
||||
|
||||
enum class TrackLyricsSortMethod
|
||||
{
|
||||
None,
|
||||
ExternalFirst,
|
||||
EmbeddedFirst,
|
||||
};
|
||||
|
||||
enum class ImageType
|
||||
{
|
||||
Unknown = 0,
|
||||
Other = 1,
|
||||
FileIcon = 2,
|
||||
OtherFileIcon = 3,
|
||||
FrontCover = 4,
|
||||
BackCover = 5,
|
||||
LeafletPage = 6,
|
||||
Media = 7,
|
||||
LeadArtist = 8,
|
||||
Artist = 9,
|
||||
Conductor = 10,
|
||||
Band = 11,
|
||||
Composer = 12,
|
||||
Lyricist = 13,
|
||||
RecordingLocation = 14,
|
||||
DuringRecording = 15,
|
||||
DuringPerformance = 16,
|
||||
MovieScreenCapture = 17,
|
||||
ColouredFish = 18,
|
||||
Illustration = 19,
|
||||
BandLogo = 20,
|
||||
PublisherLogo = 21
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkType
|
||||
{
|
||||
Artist = 0, // regular track artist
|
||||
Arranger = 1,
|
||||
Composer = 2,
|
||||
Conductor = 3,
|
||||
Lyricist = 4,
|
||||
Mixer = 5,
|
||||
Performer = 6,
|
||||
Producer = 7,
|
||||
ReleaseArtist = 8,
|
||||
Remixer = 9,
|
||||
Writer = 10,
|
||||
};
|
||||
|
||||
core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type);
|
||||
|
||||
// User selectable transcoding output formats
|
||||
enum class TranscodingOutputFormat
|
||||
{
|
||||
MP3 = 1,
|
||||
OGG_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
MATROSKA_OPUS = 5,
|
||||
};
|
||||
|
||||
using Bitrate = std::uint32_t;
|
||||
// Do not remove values!
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||
|
||||
using Rating = int;
|
||||
|
||||
enum class ScrobblingBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class FeedbackBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class SyncState
|
||||
{
|
||||
PendingAdd = 0,
|
||||
Synchronized = 1,
|
||||
PendingRemove = 2,
|
||||
};
|
||||
|
||||
enum class UserType
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
enum class UITheme
|
||||
{
|
||||
Light = 0,
|
||||
Dark = 1,
|
||||
};
|
||||
|
||||
enum class SubsonicArtistListMode
|
||||
{
|
||||
AllArtists = 0,
|
||||
ReleaseArtists = 1,
|
||||
TrackArtists = 2,
|
||||
};
|
||||
|
||||
enum class TrackListType
|
||||
{
|
||||
PlayList = 0, // user controlled playlists
|
||||
Internal = 1, // internal usage (current playqueue, history, ...)
|
||||
};
|
||||
|
||||
enum class Advisory
|
||||
{
|
||||
UnSet = 0,
|
||||
Unknown = 1,
|
||||
Clean = 2,
|
||||
Explicit = 3,
|
||||
};
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/ClusterId.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <Wt/Dbo/Field.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
|
||||
#include "database/objects/ClusterId.hpp"
|
||||
#include "database/objects/LabelId.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
@@ -31,10 +34,11 @@ namespace lms::db
|
||||
{
|
||||
struct Filters
|
||||
{
|
||||
MediaLibraryId mediaLibrary; // tracks that belongs to this library
|
||||
std::vector<ClusterId> clusters; // tracks that belong to all these clusters
|
||||
LabelId label; // tracks which release has this label
|
||||
ReleaseTypeId releaseType; // tracks which release has this type
|
||||
MediaLibraryId mediaLibrary; // tracks that belongs to this library
|
||||
std::vector<ClusterId> clusters; // tracks that belong to *all* these clusters
|
||||
LabelId label; // tracks which release has this label
|
||||
ReleaseTypeId releaseType; // tracks which release has this type
|
||||
std::optional<core::media::Codec> codec; // tracks that match this codec
|
||||
|
||||
Filters& setClusters(std::span<const ClusterId> _clusters)
|
||||
{
|
||||
@@ -56,5 +60,10 @@ namespace lms::db
|
||||
releaseType = _releaseType;
|
||||
return *this;
|
||||
}
|
||||
Filters& setCodec(std::optional<core::media::Codec> _codec)
|
||||
{
|
||||
codec = _codec;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "database/objects/ListenId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/ArtworkId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace lms::db
|
||||
// Get tracks, ordered by position
|
||||
std::vector<TrackId> getTrackIds() const;
|
||||
void visitTracks(const std::function<void(const ObjectPtr<Track>& track)>& visitor) const;
|
||||
const Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; }
|
||||
Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; }
|
||||
bool isEmpty() const { return _tracks.empty(); }
|
||||
|
||||
// Modifiers
|
||||
void setCurrentIndex(std::size_t index) { _currentIndex = index; }
|
||||
|
||||
@@ -29,11 +29,16 @@
|
||||
#include <Wt/Dbo/collection.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtworkId.hpp"
|
||||
#include "database/objects/PodcastEpisodeId.hpp"
|
||||
#include "database/objects/PodcastId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/detail/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -90,6 +95,15 @@ namespace lms::db
|
||||
ManualDownloadState getManualDownloadState() const { return _manualDownloadState; }
|
||||
const std::filesystem::path& getAudioRelativeFilePath() const { return _audioRelativeFilePath; }
|
||||
|
||||
// Audio properties
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
std::optional<core::media::Container> getContainer() const;
|
||||
std::optional<core::media::Codec> getCodec() const;
|
||||
std::size_t getBitrate() const { return _bitrate; }
|
||||
std::size_t getChannelCount() const { return _channelCount; }
|
||||
std::size_t getSampleRate() const { return _sampleRate; }
|
||||
std::optional<std::size_t> getBitsPerSample() const { return _bitsPerSample; }
|
||||
|
||||
std::string_view getTitle() const { return _title; }
|
||||
std::string_view getLink() const { return _link; }
|
||||
std::string_view getDescription() const { return _description; }
|
||||
@@ -103,7 +117,6 @@ namespace lms::db
|
||||
std::string_view getSubtitle() const { return _subtitle; }
|
||||
std::string_view getSummary() const { return _summary; }
|
||||
bool isExplicit() const { return _explicit; }
|
||||
std::chrono::duration<int, std::milli> getDuration() const { return _duration; }
|
||||
ObjectPtr<Podcast> getPodcast() const { return _podcast; }
|
||||
PodcastId getPodcastId() const { return _podcast.id(); }
|
||||
ObjectPtr<Artwork> getArtwork() const;
|
||||
@@ -113,6 +126,15 @@ namespace lms::db
|
||||
void setManualDownloadState(ManualDownloadState state) { _manualDownloadState = state; }
|
||||
void setAudioRelativeFilePath(const std::filesystem::path& relativeFilePath) { _audioRelativeFilePath = relativeFilePath; }
|
||||
|
||||
// Audio properties
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setContainer(core::media::Container container);
|
||||
void setCodec(core::media::Codec codec);
|
||||
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
|
||||
void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; }
|
||||
void setSampleRate(std::size_t sampleRate) { _sampleRate = sampleRate; }
|
||||
void setBitsPerSample(std::optional<std::size_t> bitsPerSample) { _bitsPerSample = bitsPerSample; }
|
||||
|
||||
void setTitle(std::string_view title) { _title = title; }
|
||||
void setLink(std::string_view link) { _link = link; }
|
||||
void setDescription(std::string_view description) { _description = description; }
|
||||
@@ -135,6 +157,14 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _manualDownloadState, "manual_download_state");
|
||||
Wt::Dbo::field(a, _audioRelativeFilePath, "audio_relative_file_path");
|
||||
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _container, "container");
|
||||
Wt::Dbo::field(a, _codec, "codec");
|
||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||
Wt::Dbo::field(a, _channelCount, "channel_count");
|
||||
Wt::Dbo::field(a, _sampleRate, "sample_rate");
|
||||
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
|
||||
|
||||
Wt::Dbo::field(a, _title, "title");
|
||||
Wt::Dbo::field(a, _link, "link");
|
||||
Wt::Dbo::field(a, _description, "description");
|
||||
@@ -148,7 +178,6 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _subtitle, "subtitle");
|
||||
Wt::Dbo::field(a, _summary, "summary");
|
||||
Wt::Dbo::field(a, _explicit, "explicit");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _artwork, "artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::belongsTo(a, _podcast, "podcast", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -162,6 +191,15 @@ namespace lms::db
|
||||
ManualDownloadState _manualDownloadState{ ManualDownloadState::None };
|
||||
std::filesystem::path _audioRelativeFilePath; // relative to cache dir, only set if downloaded
|
||||
|
||||
// Audio properties
|
||||
std::chrono::duration<int, std::milli> _duration{ 0 };
|
||||
detail::Container _container{ detail::Container::Unknown };
|
||||
detail::Codec _codec{ detail::Codec::Unknown };
|
||||
int _bitrate{}; // in bps
|
||||
int _channelCount{};
|
||||
int _sampleRate{};
|
||||
std::optional<int> _bitsPerSample;
|
||||
|
||||
std::string _url;
|
||||
std::string _title;
|
||||
std::string _link;
|
||||
@@ -178,7 +216,6 @@ namespace lms::db
|
||||
std::string _subtitle;
|
||||
std::string _summary;
|
||||
bool _explicit{};
|
||||
std::chrono::duration<int, std::milli> _duration{ 0 };
|
||||
|
||||
Wt::Dbo::ptr<Artwork> _artwork;
|
||||
Wt::Dbo::ptr<Podcast> _podcast;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/RatedArtistId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/RatedReleaseId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/RatedTrackId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "core/media/Codec.hpp"
|
||||
|
||||
#include "database/IdRange.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
@@ -43,6 +45,7 @@
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/ReleaseTypeId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
@@ -277,6 +280,7 @@ namespace lms::db
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::size_t getMeanBitrate() const;
|
||||
std::vector<core::media::Codec> getCodecs() const;
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/StarredArtistId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/StarredReleaseId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/StarredTrackId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "core/media/Codec.hpp"
|
||||
#include "core/media/Container.hpp"
|
||||
|
||||
#include "database/IdRange.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
@@ -48,7 +51,9 @@
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/TrackListId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
#include "database/objects/detail/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -216,19 +221,28 @@ namespace lms::db
|
||||
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||
static void updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||
|
||||
// Accessors
|
||||
// Setters
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
void setName(std::string_view name);
|
||||
|
||||
// File properties
|
||||
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||
void setLastWriteTime(const Wt::WDateTime& time) { _fileLastWrite = time; }
|
||||
void setAddedTime(const Wt::WDateTime& time) { _fileAdded = time; }
|
||||
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
|
||||
void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; }
|
||||
|
||||
// Audio properties
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setContainer(core::media::Container container);
|
||||
void setCodec(core::media::Codec codec);
|
||||
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
|
||||
void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; }
|
||||
void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; }
|
||||
void setSampleRate(std::size_t sampleRate) { _sampleRate = sampleRate; }
|
||||
void setBitsPerSample(std::optional<std::size_t> bitsPerSample) { _bitsPerSample = bitsPerSample; }
|
||||
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
|
||||
|
||||
// Metadata
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
void setName(std::string_view name);
|
||||
void setDate(const core::PartialDateTime& date) { _date = date; }
|
||||
void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; }
|
||||
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||
@@ -236,7 +250,6 @@ namespace lms::db
|
||||
void setCopyright(std::string_view copyright);
|
||||
void setCopyrightURL(std::string_view copyrightURL);
|
||||
void setAdvisory(Advisory advisory) { _advisory = advisory; }
|
||||
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
|
||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
void clearArtistLinks();
|
||||
@@ -254,32 +267,41 @@ namespace lms::db
|
||||
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||
void setPreferredMediaArtwork(ObjectPtr<Artwork> artwork);
|
||||
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
std::string getName() const { return _name; }
|
||||
|
||||
// File properties
|
||||
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||
long long getFileSize() const { return _fileSize; }
|
||||
std::size_t getBitrate() const { return _bitrate; }
|
||||
std::size_t getBitsPerSample() const { return _bitsPerSample; }
|
||||
std::size_t getChannelCount() const { return _channelCount; }
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
std::size_t getSampleRate() const { return _sampleRate; }
|
||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
|
||||
|
||||
// Audio properties
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
std::optional<core::media::Container> getContainer() const;
|
||||
std::optional<core::media::Codec> getCodec() const;
|
||||
std::size_t getBitrate() const { return _bitrate; }
|
||||
std::size_t getChannelCount() const { return _channelCount; }
|
||||
std::size_t getSampleRate() const { return _sampleRate; }
|
||||
std::optional<std::size_t> getBitsPerSample() const { return _bitsPerSample; }
|
||||
std::optional<float> getReplayGain() const { return _replayGain; }
|
||||
|
||||
// Metadata
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
std::string getName() const { return _name; }
|
||||
const core::PartialDateTime& getDate() const { return _date; }
|
||||
std::optional<int> getYear() const;
|
||||
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
|
||||
std::optional<int> getOriginalYear() const;
|
||||
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
|
||||
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
|
||||
bool hasLyrics() const;
|
||||
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
|
||||
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
Advisory getAdvisory() const { return _advisory; }
|
||||
std::optional<float> getReplayGain() const { return _replayGain; }
|
||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
Advisory getAdvisory() const { return _advisory; }
|
||||
|
||||
// no artistLinkTypes means get all
|
||||
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||
@@ -304,27 +326,32 @@ namespace lms::db
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
|
||||
Wt::Dbo::field(a, _channelCount, "channel_count");
|
||||
Wt::Dbo::field(a, _sampleRate, "sample_rate");
|
||||
Wt::Dbo::field(a, _date, "date");
|
||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||
|
||||
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
||||
Wt::Dbo::field(a, _fileSize, "file_size");
|
||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||
Wt::Dbo::field(a, _fileAdded, "file_added");
|
||||
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _container, "container");
|
||||
Wt::Dbo::field(a, _codec, "codec");
|
||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||
Wt::Dbo::field(a, _channelCount, "channel_count");
|
||||
Wt::Dbo::field(a, _sampleRate, "sample_rate");
|
||||
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
|
||||
Wt::Dbo::field(a, _replayGain, "replay_gain");
|
||||
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _date, "date");
|
||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||
Wt::Dbo::field(a, _trackMBID, "mbid");
|
||||
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _advisory, "advisory");
|
||||
Wt::Dbo::field(a, _replayGain, "replay_gain");
|
||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
|
||||
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table?
|
||||
Wt::Dbo::field(a, _advisory, "advisory");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _medium, "medium", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -347,27 +374,36 @@ namespace lms::db
|
||||
static constexpr std::size_t _maxCopyrightURLLength{ 512 };
|
||||
|
||||
int _scanVersion{};
|
||||
std::optional<int> _trackNumber;
|
||||
std::string _name;
|
||||
int _bitrate{}; // in bps
|
||||
int _bitsPerSample{};
|
||||
int _channelCount{};
|
||||
std::chrono::duration<int, std::milli> _duration{};
|
||||
int _sampleRate{};
|
||||
core::PartialDateTime _date;
|
||||
core::PartialDateTime _originalDate;
|
||||
|
||||
// File information
|
||||
std::filesystem::path _absoluteFilePath; // full path
|
||||
long long _fileSize{};
|
||||
Wt::WDateTime _fileLastWrite;
|
||||
Wt::WDateTime _fileAdded;
|
||||
|
||||
// Audio properties
|
||||
std::chrono::duration<int, std::milli> _duration{};
|
||||
detail::Container _container{ detail::Container::Unknown };
|
||||
detail::Codec _codec{ detail::Codec::Unknown };
|
||||
int _bitrate{}; // in bps
|
||||
int _channelCount{};
|
||||
int _sampleRate{};
|
||||
std::optional<int> _bitsPerSample;
|
||||
std::optional<float> _replayGain;
|
||||
|
||||
// Metadata
|
||||
std::optional<int> _trackNumber;
|
||||
std::string _name;
|
||||
core::PartialDateTime _date;
|
||||
core::PartialDateTime _originalDate;
|
||||
std::string _trackMBID;
|
||||
std::string _recordingMBID;
|
||||
std::string _copyright;
|
||||
std::string _copyrightURL;
|
||||
Advisory _advisory{ Advisory::UnSet };
|
||||
std::optional<float> _replayGain;
|
||||
std::string _artistDisplayName;
|
||||
std::string _comment;
|
||||
Advisory _advisory{ Advisory::UnSet };
|
||||
|
||||
Wt::Dbo::ptr<Medium> _medium;
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
#include <Wt/Dbo/Field.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "core/media/ImageType.hpp"
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
@@ -33,6 +35,7 @@
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/TrackListId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -52,7 +55,7 @@ namespace lms::db
|
||||
ReleaseId release;
|
||||
MediumId medium;
|
||||
TrackListId trackList;
|
||||
std::optional<ImageType> imageType;
|
||||
std::optional<core::media::ImageType> imageType;
|
||||
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
@@ -80,7 +83,7 @@ namespace lms::db
|
||||
trackList = _trackList;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setImageType(std::optional<ImageType> _imageType)
|
||||
FindParameters& setImageType(std::optional<core::media::ImageType> _imageType)
|
||||
{
|
||||
imageType = _imageType;
|
||||
return *this;
|
||||
|
||||
@@ -24,10 +24,13 @@
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
|
||||
#include "core/media/ImageType.hpp"
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLinkId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/detail/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -49,12 +52,12 @@ namespace lms::db
|
||||
ObjectPtr<Track> getTrack() const;
|
||||
ObjectPtr<TrackEmbeddedImage> getImage() const;
|
||||
std::size_t getIndex() const { return _index; }
|
||||
ImageType getType() const { return _type; }
|
||||
core::media::ImageType getType() const;
|
||||
std::string_view getDescription() const { return _description; }
|
||||
|
||||
// setters
|
||||
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
|
||||
void setType(ImageType type) { _type = type; }
|
||||
void setType(core::media::ImageType type);
|
||||
void setDescription(std::string_view description) { _description = description; }
|
||||
|
||||
template<class Action>
|
||||
@@ -75,7 +78,7 @@ namespace lms::db
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
|
||||
|
||||
int _index{}; // index within the track
|
||||
ImageType _type{ ImageType::Unknown };
|
||||
detail::ImageType _type{ detail::ImageType::Unknown };
|
||||
std::string _description;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/Filters.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/TrackListId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackLyricsId)
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/TaggedType.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
// For enums that have explicit values, never change enum values as they are stored in database.
|
||||
// => only add new values or remove unused values, do not recycle values.
|
||||
|
||||
struct YearRange
|
||||
{
|
||||
int begin{};
|
||||
int end{};
|
||||
};
|
||||
|
||||
struct FileInfo
|
||||
{
|
||||
Wt::WDateTime lastWrittenTime;
|
||||
std::size_t scanVersion{};
|
||||
};
|
||||
|
||||
enum class ArtistSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Name,
|
||||
SortName,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ClusterSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class DirectorySortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
|
||||
|
||||
enum class LabelSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class MediumSortMethod
|
||||
{
|
||||
None,
|
||||
PositionAsc,
|
||||
};
|
||||
|
||||
enum class PodcastEpisodeSortMode
|
||||
{
|
||||
None,
|
||||
PubDateAsc,
|
||||
PubDateDesc,
|
||||
};
|
||||
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Name,
|
||||
SortName,
|
||||
ArtistNameThenName,
|
||||
DateAsc,
|
||||
DateDesc,
|
||||
OriginalDate,
|
||||
OriginalDateDesc,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class ReleaseTypeSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkSortMethod
|
||||
{
|
||||
None,
|
||||
OriginalDateDesc,
|
||||
};
|
||||
|
||||
enum class TrackEmbeddedImageSortMethod
|
||||
{
|
||||
None,
|
||||
SizeDesc,
|
||||
TrackNumberThenSizeDesc,
|
||||
DiscNumberThenTrackNumberThenSizeDesc,
|
||||
TrackListIndexAscThenSizeDesc,
|
||||
};
|
||||
|
||||
enum class TrackListSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
LastModifiedDesc,
|
||||
};
|
||||
|
||||
enum class TrackSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Random,
|
||||
LastWrittenDesc,
|
||||
AddedDesc,
|
||||
StarredDateDesc,
|
||||
AbsoluteFilePath,
|
||||
Name,
|
||||
DateDescAndRelease,
|
||||
OriginalDateDescAndRelease,
|
||||
Release, // order by disc/track number
|
||||
TrackList, // order by asc order in tracklist
|
||||
TrackNumber,
|
||||
};
|
||||
|
||||
enum class TrackLyricsSortMethod
|
||||
{
|
||||
None,
|
||||
ExternalFirst,
|
||||
EmbeddedFirst,
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkType
|
||||
{
|
||||
Artist = 0, // regular track artist
|
||||
Arranger = 1,
|
||||
Composer = 2,
|
||||
Conductor = 3,
|
||||
Lyricist = 4,
|
||||
Mixer = 5,
|
||||
Performer = 6,
|
||||
Producer = 7,
|
||||
ReleaseArtist = 8,
|
||||
Remixer = 9,
|
||||
Writer = 10,
|
||||
};
|
||||
|
||||
core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type);
|
||||
|
||||
// User selectable transcoding output formats
|
||||
enum class TranscodingOutputFormat
|
||||
{
|
||||
MP3 = 1,
|
||||
OGG_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
// WEBM_VORBIS = 4,
|
||||
// MATROSKA_OPUS = 5,
|
||||
};
|
||||
|
||||
using Bitrate = std::uint32_t;
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||
|
||||
using Rating = int;
|
||||
|
||||
enum class ScrobblingBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class FeedbackBackend
|
||||
{
|
||||
Internal = 0,
|
||||
ListenBrainz = 1,
|
||||
};
|
||||
|
||||
enum class SyncState
|
||||
{
|
||||
PendingAdd = 0,
|
||||
Synchronized = 1,
|
||||
PendingRemove = 2,
|
||||
};
|
||||
|
||||
enum class UserType
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
enum class UITheme
|
||||
{
|
||||
Light = 0,
|
||||
Dark = 1,
|
||||
};
|
||||
|
||||
enum class SubsonicArtistListMode
|
||||
{
|
||||
AllArtists = 0,
|
||||
ReleaseArtists = 1,
|
||||
TrackArtists = 2,
|
||||
};
|
||||
|
||||
enum class TrackListType
|
||||
{
|
||||
PlayList = 0, // user controlled playlists
|
||||
Internal = 1, // internal usage (current playqueue, history, ...)
|
||||
};
|
||||
|
||||
enum class Advisory
|
||||
{
|
||||
UnSet = 0,
|
||||
Unknown = 1,
|
||||
Clean = 2,
|
||||
Explicit = 3,
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -28,8 +28,10 @@
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace lms::db::detail
|
||||
{
|
||||
// For enums that have explicit values, never change enum values as they are stored in database.
|
||||
// => only add new values or remove unused values, do not recycle values.
|
||||
enum class ImageType
|
||||
{
|
||||
Unknown = 0,
|
||||
Other = 1,
|
||||
FileIcon = 2,
|
||||
OtherFileIcon = 3,
|
||||
FrontCover = 4,
|
||||
BackCover = 5,
|
||||
LeafletPage = 6,
|
||||
Media = 7,
|
||||
LeadArtist = 8,
|
||||
Artist = 9,
|
||||
Conductor = 10,
|
||||
Band = 11,
|
||||
Composer = 12,
|
||||
Lyricist = 13,
|
||||
RecordingLocation = 14,
|
||||
DuringRecording = 15,
|
||||
DuringPerformance = 16,
|
||||
MovieScreenCapture = 17,
|
||||
ColouredFish = 18,
|
||||
Illustration = 19,
|
||||
BandLogo = 20,
|
||||
PublisherLogo = 21
|
||||
};
|
||||
|
||||
enum class Container
|
||||
{
|
||||
Unknown = 0,
|
||||
|
||||
AIFF = 1,
|
||||
APE = 2,
|
||||
ASF = 3,
|
||||
DSF = 4,
|
||||
FLAC = 5,
|
||||
MP4 = 6,
|
||||
MPC = 7,
|
||||
MPEG = 8,
|
||||
Ogg = 9,
|
||||
Shorten = 10,
|
||||
TrueAudio = 11,
|
||||
WAV = 12,
|
||||
WavPack = 13,
|
||||
};
|
||||
|
||||
enum class Codec
|
||||
{
|
||||
Unknown = 0,
|
||||
|
||||
AAC = 1,
|
||||
AC3 = 2,
|
||||
ALAC = 3,
|
||||
APE = 4,
|
||||
DSD = 5,
|
||||
EAC3 = 6,
|
||||
FLAC = 7,
|
||||
MP3 = 8,
|
||||
MP4ALS = 9,
|
||||
MPC7 = 10,
|
||||
MPC8 = 11,
|
||||
Opus = 12,
|
||||
PCM = 13,
|
||||
Shorten = 14,
|
||||
TrueAudio = 15,
|
||||
Vorbis = 16,
|
||||
WavPack = 17,
|
||||
WMA1 = 18,
|
||||
WMA2 = 19,
|
||||
WMA9Pro = 20,
|
||||
WMA9Lossless = 21,
|
||||
};
|
||||
} // namespace lms::db::detail
|
||||
@@ -518,6 +518,34 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByCodec)
|
||||
{
|
||||
ScopedArtist artist1{ session, "A" };
|
||||
ScopedArtist artist2{ session, "B" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
track1.get().modify()->setCodec(core::media::Codec::MP3);
|
||||
track2.get().modify()->setCodec(core::media::Codec::FLAC);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Artist::FindParameters params;
|
||||
params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC));
|
||||
|
||||
const auto artists{ Artist::find(session, params) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front()->getId(), artist2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByName)
|
||||
{
|
||||
ScopedArtist artist{ session, "AAA" };
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/ArtistInfo.hpp"
|
||||
#include "database/objects/AuthToken.hpp"
|
||||
|
||||
@@ -1214,6 +1214,74 @@ namespace lms::db::tests
|
||||
checkExpectedBitrate(192); // 0 should not be taken into account
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_codec)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
ScopedTrack track4{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
ASSERT_EQ(release1->getCodecs().size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setCodec(core::media::Codec::FLAC);
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
|
||||
track2.get().modify()->setCodec(core::media::Codec::MP3);
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
|
||||
track3.get().modify()->setCodec(core::media::Codec::MP3);
|
||||
track3.get().modify()->setRelease(release1.get());
|
||||
|
||||
track4.get().modify()->setCodec(core::media::Codec::AAC);
|
||||
track4.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release1->getCodecs(), (std::vector<core::media::Codec>{ core::media::Codec::MP3, core::media::Codec::FLAC, core::media::Codec::AAC }));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_findBycodec)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedTrack track1{ session };
|
||||
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
ASSERT_EQ(release1->getCodecs().size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setCodec(core::media::Codec::FLAC);
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
|
||||
track2.get().modify()->setCodec(core::media::Codec::MP3);
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setFilters(Filters{}.setCodec(core::media::Codec::MP3));
|
||||
|
||||
auto releases{ Release::findIds(session, params) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_trackCount)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user