diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ebbd18d..3952fe39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) -project(lms VERSION 3.70.0) +project(lms VERSION 3.72.0) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/) @@ -22,7 +22,7 @@ find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) find_package(Filesystem REQUIRED) -find_package(Boost REQUIRED COMPONENTS system program_options iostreams) +find_package(Boost REQUIRED COMPONENTS program_options iostreams) find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) find_package(Pugixml CONFIG REQUIRED) diff --git a/Dockerfile-release b/Dockerfile-release index 7bcf39f7..1bf40d82 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -39,7 +39,7 @@ ARG BUILD_PACKAGES=" \ RUN apk add --no-cache --update ${BUILD_PACKAGES} # ffmpeg -ARG FFMPEG_VERSION=6.1.2 +ARG FFMPEG_VERSION=6.1.3 RUN \ DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \ curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \ @@ -61,7 +61,6 @@ RUN \ --enable-shared \ --disable-static \ --enable-gpl \ - --enable-small \ --enable-version3 \ --enable-nonfree \ --enable-libmp3lame \ @@ -80,7 +79,7 @@ RUN \ make -j$(nproc) install # WT -ARG WT_VERSION=4.11.4 +ARG WT_VERSION=4.12.1 ARG WT_DEBUG=OFF RUN \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ diff --git a/README.md b/README.md index b069afa2..d4344ff7 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ __Note__: If no name is provided in the `artist.nfo` file, the name of the conta ### 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. -__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_. +__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_. ### Multiple artists _LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved. diff --git a/SUBSONIC.md b/SUBSONIC.md index c6a4fe76..025367b7 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -1,5 +1,5 @@ # Subsonic API -The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_ and _Ultrasonic_. +The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_, [_Tempus_](https://github.com/eddyizm/tempus) and _Ultrasonic_. Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists. Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder. diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index bc594855..ca3f3bdb 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -184,7 +184,6 @@ class LMSMediaPlayer { let source = this.#audioCtx.createMediaElementSource(this.#elems.audio); source.connect(this.#gainNode); this.#gainNode.connect(this.#audioCtx.destination); - this.#audioCtx.resume(); // not sure of this if ("mediaSession" in navigator) { navigator.mediaSession.setActionHandler("play", () => { @@ -268,6 +267,10 @@ class LMSMediaPlayer { #playPause() { this.#initAudioCtx(); + if (this.#audioCtx.state === "suspended") { + this.#audioCtx.resume().catch(err => console.warn(err)); + } + if (this.#elems.audio.paused && this.#elems.audio.children.length > 0) { this.#playTrack(); } @@ -355,6 +358,8 @@ class LMSMediaPlayer { if (!mode) return; + let wasPlaying = !this.#elems.audio.paused; + switch (mode) { case LMSMediaPlayer.#Mode.Transcoding: this.#offset = seekTime; @@ -362,15 +367,16 @@ class LMSMediaPlayer { this.#addAudioSource(this.#audioTranscodingSrc + "&offset=" + this.#offset); this.#elems.audio.load(); this.#elems.audio.currentTime = 0; - this.#playTrack(); break; case LMSMediaPlayer.#Mode.File: this.#elems.audio.currentTime = seekTime; - this.#playTrack(); break; } + if (wasPlaying) + this.#playTrack(); + this.#updateMediaSessionState(); } diff --git a/src/libs/CMakeLists.txt b/src/libs/CMakeLists.txt index 72a280ca..9a52d2f5 100644 --- a/src/libs/CMakeLists.txt +++ b/src/libs/CMakeLists.txt @@ -1,8 +1,7 @@ -add_subdirectory(av) +add_subdirectory(audio) add_subdirectory(core) add_subdirectory(database) add_subdirectory(image) -add_subdirectory(metadata) add_subdirectory(services) add_subdirectory(som) add_subdirectory(subsonic) diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt new file mode 100644 index 00000000..48f071bb --- /dev/null +++ b/src/libs/audio/CMakeLists.txt @@ -0,0 +1,40 @@ +pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) +pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) + +add_library(lmsaudio STATIC + impl/ffmpeg/AudioFile.cpp + impl/ffmpeg/AudioFileInfo.cpp + impl/ffmpeg/ImageReader.cpp + impl/ffmpeg/TagReader.cpp + impl/ffmpeg/Transcoder.cpp + impl/ffmpeg/Utils.cpp + impl/taglib/AudioFileInfo.cpp + impl/taglib/ImageReader.cpp + impl/taglib/TagReader.cpp + impl/taglib/Utils.cpp + impl/AudioTypes.cpp + impl/ImageReader.cpp + impl/ParseAudioFileInfo.cpp + impl/TagReader.cpp + ) + +target_include_directories(lmsaudio INTERFACE + include + ) + +target_include_directories(lmsaudio PRIVATE + include + ${AVCODEC_INCLUDE_DIR} + ${AVFORMAT_INCLUDE_DIR} + ${AVUTIL_INCLUDE_DIR} + ) + +target_link_libraries(lmsaudio PUBLIC + lmscore + std::filesystem + ) + +target_link_libraries(lmsaudio PRIVATE + PkgConfig::LIBAV + PkgConfig::Taglib + ) diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/audio/impl/AudioTypes.cpp new file mode 100644 index 00000000..d0a5930b --- /dev/null +++ b/src/libs/audio/impl/AudioTypes.cpp @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/audio/impl/ImageReader.cpp b/src/libs/audio/impl/ImageReader.cpp new file mode 100644 index 00000000..7b314875 --- /dev/null +++ b/src/libs/audio/impl/ImageReader.cpp @@ -0,0 +1,77 @@ +/* + * 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 . + */ + +#include "audio/IImageReader.hpp" + +namespace lms::audio +{ + core::LiteralString imageTypeToString(Image::Type type) + { + switch (type) + { + case Image::Type::Other: + return "Other"; + case Image::Type::FileIcon: + return "FileIcon"; + case Image::Type::OtherFileIcon: + return "OtherFileIcon"; + case Image::Type::FrontCover: + return "FrontCover"; + case Image::Type::BackCover: + return "BackCover"; + case Image::Type::LeafletPage: + return "LeafletPage"; + case Image::Type::Media: + return "Media"; + case Image::Type::LeadArtist: + return "LeadArtist"; + case Image::Type::Artist: + return "Artist"; + case Image::Type::Conductor: + return "Conductor"; + case Image::Type::Band: + return "Band"; + case Image::Type::Composer: + return "Composer"; + case Image::Type::Lyricist: + return "Lyricist"; + case Image::Type::RecordingLocation: + return "RecordingLocation"; + case Image::Type::DuringRecording: + return "DuringRecording"; + case Image::Type::DuringPerformance: + return "DuringPerformance"; + case Image::Type::MovieScreenCapture: + return "MovieScreenCapture"; + case Image::Type::ColouredFish: + return "ColouredFish"; + case Image::Type::Illustration: + return "Illustration"; + case Image::Type::BandLogo: + return "BandLogo"; + case Image::Type::PublisherLogo: + return "PublisherLogo"; + case Image::Type::Unknown: + break; + } + + return "Unknown"; + } + +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/impl/ParseAudioFileInfo.cpp b/src/libs/audio/impl/ParseAudioFileInfo.cpp new file mode 100644 index 00000000..321d46a7 --- /dev/null +++ b/src/libs/audio/impl/ParseAudioFileInfo.cpp @@ -0,0 +1,57 @@ +/* + * 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 . + */ + +#include + +#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 parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions) + { + switch (parserOptions.parser) + { + case ParserOptions::Parser::TagLib: + return std::make_unique(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs); + case ParserOptions::Parser::FFmpeg: + return std::make_unique(p, parserOptions.enableExtraDebugLogs); + } + + return {}; + } + + std::span 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 \ No newline at end of file diff --git a/src/libs/audio/impl/TagReader.cpp b/src/libs/audio/impl/TagReader.cpp new file mode 100644 index 00000000..39ef83bd --- /dev/null +++ b/src/libs/audio/impl/TagReader.cpp @@ -0,0 +1,277 @@ +/* + * 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 . + */ + +#include "audio/ITagReader.hpp" + +namespace lms::audio +{ + core::LiteralString tagTypeToString(TagType type) + { + switch (type) + { + case TagType::AcoustID: + return "AcoustID"; + case TagType::AcoustIDFingerprint: + return "AcoustIDFingerprint"; + case TagType::Advisory: + return "Advisory"; + case TagType::Album: + return "Album"; + case TagType::AlbumArtist: + return "AlbumArtist"; + case TagType::AlbumArtists: + return "AlbumArtists"; + case TagType::AlbumArtistSortOrder: + return "AlbumArtistSortOrder"; + case TagType::AlbumArtistsSortOrder: + return "AlbumArtistsSortOrder"; + case TagType::AlbumComment: + return "AlbumComment"; + case TagType::AlbumSortOrder: + return "AlbumSortOrder"; + case TagType::Arranger: + return "Arranger"; + case TagType::Artist: + return "Artist"; + case TagType::ArtistSortOrder: + return "ArtistSortOrder"; + case TagType::Artists: + return "Artists"; + case TagType::ArtistsSortOrder: + return "ArtistsSortOrder"; + case TagType::ASIN: + return "ASIN"; + case TagType::Barcode: + return "Barcode"; + case TagType::BPM: + return "BPM"; + case TagType::CatalogNumber: + return "CatalogNumber"; + case TagType::Comment: + return "Comment"; + case TagType::Compilation: + return "Compilation"; + case TagType::Composer: + return "Composer"; + case TagType::ComposerSortOrder: + return "ComposerSortOrder"; + case TagType::Composers: + return "Composers"; + case TagType::ComposersSortOrder: + return "ComposersSortOrder"; + case TagType::Conductor: + return "Conductor"; + case TagType::ConductorSortOrder: + return "ConductorSortOrder"; + case TagType::Conductors: + return "Conductors"; + case TagType::ConductorsSortOrder: + return "ConductorsSortOrder"; + case TagType::Copyright: + return "Copyright"; + case TagType::CopyrightURL: + return "CopyrightURL"; + case TagType::Date: + return "Date"; + case TagType::Director: + return "Director"; + case TagType::DiscNumber: + return "DiscNumber"; + case TagType::DiscSubtitle: + return "DiscSubtitle"; + case TagType::EncodedBy: + return "EncodedBy"; + case TagType::EncoderSettings: + return "EncoderSettings"; + case TagType::EncodingTime: + return "EncodingTime"; + case TagType::Engineer: + return "Engineer"; + case TagType::GaplessPlayback: + return "GaplessPlayback"; + case TagType::Genre: + return "Genre"; + case TagType::Grouping: + return "Grouping"; + case TagType::InitialKey: + return "InitialKey"; + case TagType::ISRC: + return "ISRC"; + case TagType::Language: + return "Language"; + case TagType::Lyricist: + return "Lyricist"; + case TagType::LyricistSortOrder: + return "LyricistSortOrder"; + case TagType::Lyricists: + return "Lyricists"; + case TagType::LyricistsSortOrder: + return "LyricistsSortOrder"; + case TagType::Media: + return "Media"; + case TagType::MixDJ: + return "MixDJ"; + case TagType::Mixer: + return "Mixer"; + case TagType::MixerSortOrder: + return "MixerSortOrder"; + case TagType::Mixers: + return "Mixers"; + case TagType::MixersSortOrder: + return "MixersSortOrder"; + case TagType::Movement: + return "Movement"; + case TagType::MovementCount: + return "MovementCount"; + case TagType::MovementNumber: + return "MovementNumber"; + case TagType::Mood: + return "Mood"; + case TagType::MusicBrainzArtistID: + return "MusicBrainzArtistID"; + case TagType::MusicBrainzArrangerID: + return "MusicBrainzArrangerID"; + case TagType::MusicBrainzComposerID: + return "MusicBrainzComposerID"; + case TagType::MusicBrainzConductorID: + return "MusicBrainzConductorID"; + case TagType::MusicBrainzDirectorID: + return "MusicBrainzDirectorID"; + case TagType::MusicBrainzDiscID: + return "MusicBrainzDiscID"; + case TagType::MusicBrainzLyricistID: + return "MusicBrainzLyricistID"; + case TagType::MusicBrainzProducerID: + return "MusicBrainzProducerID"; + case TagType::MusicBrainzOriginalArtistID: + return "MusicBrainzOriginalArtistID"; + case TagType::MusicBrainzRecordingID: + return "MusicBrainzRecordingID"; + case TagType::MusicBrainzRemixerID: + return "MusicBrainzRemixerID"; + case TagType::MusicBrainzWorkID: + return "MusicBrainzWorkID"; + case TagType::Remixer: + return "Remixer"; + case TagType::Script: + return "Script"; + case TagType::ShowName: + return "ShowName"; + case TagType::ShowNameSortOrder: + return "ShowNameSortOrder"; + case TagType::ShowWorkAndMovement: + return "ShowWorkAndMovement"; + case TagType::Subtitle: + return "Subtitle"; + case TagType::TrackNumber: + return "TrackNumber"; + case TagType::TrackTitle: + return "TrackTitle"; + case TagType::TrackTitleSortOrder: + return "TrackTitleSortOrder"; + case TagType::WorkTitle: + return "WorkTitle"; + case TagType::Writer: + return "Writer"; + case TagType::License: + return "License"; + case TagType::MusicBrainzOriginalReleaseID: + return "MusicBrainzOriginalReleaseID"; + case TagType::MusicBrainzMixerID: + return "MusicBrainzMixerID"; + case TagType::MusicBrainzReleaseArtistID: + return "MusicBrainzReleaseArtistID"; + case TagType::MusicBrainzReleaseGroupID: + return "MusicBrainzReleaseGroupID"; + case TagType::MusicBrainzReleaseID: + return "MusicBrainzReleaseID"; + case TagType::MusicBrainzTrackID: + return "MusicBrainzTrackID"; + case TagType::MusicIPFingerprint: + return "MusicIPFingerprint"; + case TagType::MusicIPPUID: + return "MusicIPPUID"; + case TagType::OriginalAlbum: + return "OriginalAlbum"; + case TagType::OriginalArtist: + return "OriginalArtist"; + case TagType::OriginalFilename: + return "OriginalFilename"; + case TagType::OriginalReleaseDate: + return "OriginalReleaseDate"; + case TagType::OriginalReleaseYear: + return "OriginalReleaseYear"; + case TagType::Podcast: + return "Podcast"; + case TagType::PodcastURL: + return "PodcastURL"; + case TagType::Producer: + return "Producer"; + case TagType::ProducerSortOrder: + return "ProducerSortOrder"; + case TagType::Producers: + return "Producers"; + case TagType::ProducersSortOrder: + return "ProducersSortOrder"; + case TagType::Rating: + return "Rating"; + case TagType::RecordLabel: + return "RecordLabel"; + case TagType::ReleaseCountry: + return "ReleaseCountry"; + case TagType::ReleaseDate: + return "ReleaseDate"; + case TagType::ReleaseStatus: + return "ReleaseStatus"; + case TagType::ReleaseType: + return "ReleaseType"; + case TagType::RemixerSortOrder: + return "RemixerSortOrder"; + case TagType::Remixers: + return "Remixers"; + case TagType::RemixersSortOrder: + return "RemixersSortOrder"; + case TagType::ReplayGainAlbumGain: + return "ReplayGainAlbumGain"; + case TagType::ReplayGainAlbumPeak: + return "ReplayGainAlbumPeak"; + case TagType::ReplayGainAlbumRange: + return "ReplayGainAlbumRange"; + case TagType::ReplayGainReferenceLoudness: + return "ReplayGainReferenceLoudness"; + case TagType::ReplayGainTrackGain: + return "ReplayGainTrackGain"; + case TagType::ReplayGainTrackPeak: + return "ReplayGainTrackPeak"; + case TagType::ReplayGainTrackRange: + return "ReplayGainTrackRange"; + case TagType::TotalDiscs: + return "TotalDiscs"; + case TagType::TotalTracks: + return "TotalTracks"; + case TagType::Website: + return "Website"; + + case TagType::Count: + break; + } + + return ""; + } +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp similarity index 66% rename from src/libs/av/impl/AudioFile.cpp rename to src/libs/audio/impl/ffmpeg/AudioFile.cpp index 8c3ee0d2..ad1861a7 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -19,6 +19,9 @@ #include "AudioFile.hpp" +#include +#include + extern "C" { #define __STDC_CONSTANT_MACROS @@ -27,15 +30,13 @@ extern "C" #include } -#include -#include - #include "core/ILogger.hpp" #include "core/String.hpp" -#include "av/Exception.hpp" +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" -namespace lms::av +namespace lms::audio::ffmpeg { namespace { @@ -49,11 +50,11 @@ namespace lms::av return "Unknown error"; } - class AudioFileException : public Exception + class AudioFileException : public AudioFileParsingException { public: AudioFileException(int avError) - : Exception{ "AudioFileException: " + averror_to_string(avError) } + : AudioFileParsingException{ averror_to_string(avError) } { } }; @@ -70,76 +71,100 @@ namespace lms::av } } - DecodingCodec avcodecToDecodingCodec(AVCodecID codec) + std::optional avdemuxerToContainerType(std::string_view name) + { + if (name == "aiff") + return ContainerType::AIFF; + if (name == "ape") + return ContainerType::APE; + if (name.starts_with("asf")) + return ContainerType::ASF; + if (name == "dsf") + return ContainerType::DSF; + if (name == "flac") + return ContainerType::FLAC; + if (name.find("mp4") != std::string_view::npos) + return ContainerType::MP4; + if (name.starts_with("mpc")) + return ContainerType::MPC; + if (name == "mp3") + return ContainerType::MPEG; + if (name == "ogg") + return ContainerType::Ogg; + if (name == "shn") + return ContainerType::Shorten; + if (name == "tta") + return ContainerType::TrueAudio; + if (name == "wav") + return ContainerType::WAV; + if (name == "wv") + return ContainerType::WavPack; + + return std::nullopt; + } + + std::optional avcodecToCodecType(AVCodecID codec) { switch (codec) { case AV_CODEC_ID_MP3: - return DecodingCodec::MP3; + return CodecType::MP3; case AV_CODEC_ID_AAC: - return DecodingCodec::AAC; - case AV_CODEC_ID_AC3: - return DecodingCodec::AC3; + return CodecType::AAC; case AV_CODEC_ID_VORBIS: - return DecodingCodec::VORBIS; + return CodecType::Vorbis; case AV_CODEC_ID_WMAV1: - return DecodingCodec::WMAV1; + return CodecType::WMA1; case AV_CODEC_ID_WMAV2: - return DecodingCodec::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 DecodingCodec::FLAC; + return CodecType::FLAC; case AV_CODEC_ID_ALAC: - return DecodingCodec::ALAC; + return CodecType::ALAC; case AV_CODEC_ID_WAVPACK: - return DecodingCodec::WAVPACK; + return CodecType::WavPack; case AV_CODEC_ID_MUSEPACK7: - return DecodingCodec::MUSEPACK7; + return CodecType::MPC7; case AV_CODEC_ID_MUSEPACK8: - return DecodingCodec::MUSEPACK8; + return CodecType::MPC8; case AV_CODEC_ID_APE: - return DecodingCodec::APE; - case AV_CODEC_ID_EAC3: - return DecodingCodec::EAC3; + return CodecType::APE; case AV_CODEC_ID_MP4ALS: - return DecodingCodec::MP4ALS; + return CodecType::MP4ALS; case AV_CODEC_ID_OPUS: - return DecodingCodec::OPUS; + return CodecType::Opus; case AV_CODEC_ID_SHORTEN: - return DecodingCodec::SHORTEN; + return CodecType::Shorten; case AV_CODEC_ID_DSD_LSBF: - return DecodingCodec::DSD_LSBF; case AV_CODEC_ID_DSD_LSBF_PLANAR: - return DecodingCodec::DSD_LSBF_PLANAR; case AV_CODEC_ID_DSD_MSBF: - return DecodingCodec::DSD_MSBF; case AV_CODEC_ID_DSD_MSBF_PLANAR: - return DecodingCodec::DSD_MSBF_PLANAR; + return CodecType::DSD; default: - return DecodingCodec::UNKNOWN; + return std::nullopt; } } } // namespace - std::unique_ptr parseAudioFile(const std::filesystem::path& p) - { - return std::make_unique(p); - } - AudioFile::AudioFile(const std::filesystem::path& p) : _p{ p } { int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) }; if (error < 0) { - LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); + LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); throw AudioFileException{ error }; } error = avformat_find_stream_info(_context, nullptr); if (error < 0) { - LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error)); + LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error)); avformat_close_input(&_context); throw AudioFileException{ error }; } @@ -158,9 +183,12 @@ namespace lms::av ContainerInfo AudioFile::getContainerInfo() const { ContainerInfo info; + + info.container = avdemuxerToContainerType(_context->iformat->name); + info.containerName = _context->iformat->name; + info.bitrate = _context->bit_rate; info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 }; - info.name = _context->iformat->name; return info; } @@ -258,7 +286,7 @@ namespace lms::av if (avstream->codecpar == nullptr) { - LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set"); + LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set"); continue; } @@ -275,7 +303,7 @@ namespace lms::av else { picture.mimeType = "application/octet-stream"; - LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"); + LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"); } const AVPacket& pkt{ avstream->attached_pic }; @@ -297,7 +325,7 @@ namespace lms::av if (!avstream->codecpar) { - LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set"); + LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set"); return res; } @@ -305,19 +333,29 @@ namespace lms::av return res; res.emplace(); + res->index = streamIndex; - res->bitrate = static_cast(avstream->codecpar->bit_rate); - res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100) - res->channelCount = static_cast(avstream->codecpar->channels); -#else - res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); -#endif - res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); + res->codec = avcodecToCodecType(avstream->codecpar->codec_id); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); + + if (avstream->codecpar->bit_rate) + res->bitrate = static_cast(avstream->codecpar->bit_rate); + if (avstream->codecpar->bits_per_coded_sample) + res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); + else if (avstream->codecpar->bits_per_raw_sample) + res->bitsPerSample = static_cast(avstream->codecpar->bits_per_raw_sample); + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100) + if (avstream->codecpar->channels) + res->channelCount = static_cast(avstream->codecpar->channels); +#else + if (avstream->codecpar->ch_layout.nb_channels) + res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); +#endif assert(!res->codecName.empty()); // doc says it is never NULL - res->sampleRate = static_cast(avstream->codecpar->sample_rate); + if (avstream->codecpar->sample_rate) + res->sampleRate = static_cast(avstream->codecpar->sample_rate); return res; } -} // namespace lms::av +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp new file mode 100644 index 00000000..2af7fb1b --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "audio/AudioTypes.hpp" + +extern "C" +{ + struct AVFormatContext; +} + +namespace lms::audio::ffmpeg +{ + struct Picture + { + std::string mimeType; + std::span data; // valid as long as IAudioFile exists + }; + + struct ContainerInfo + { + std::optional container; + std::string containerName; + + std::size_t bitrate{}; + std::chrono::milliseconds duration{}; + }; + + struct StreamInfo + { + size_t index{}; + std::optional codec; + std::string codecName; + + std::optional bitrate; + std::optional bitsPerSample; + std::optional channelCount; + std::optional sampleRate; + }; + + class AudioFile + { + public: + AudioFile(const std::filesystem::path& p); + ~AudioFile(); + AudioFile(const AudioFile&) = delete; + AudioFile& operator=(const AudioFile&) = delete; + + using MetadataMap = std::unordered_map; + + const std::filesystem::path& getPath() const; + ContainerInfo getContainerInfo() const; + MetadataMap getMetaData() const; + std::vector getStreamInfo() const; + std::optional getBestStreamInfo() const; + std::optional getBestStreamIndex() const; + bool hasAttachedPictures() const; + void visitAttachedPictures(std::function func) const; + + private: + std::optional getStreamInfo(std::size_t streamIndex) const; + + const std::filesystem::path _p; + AVFormatContext* _context{}; + }; +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp new file mode 100644 index 00000000..fe0793f9 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp @@ -0,0 +1,94 @@ +/* + * 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 . + */ + +#include "AudioFileInfo.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" + +#include "AudioFile.hpp" +#include "ImageReader.hpp" +#include "TagReader.hpp" + +namespace lms::audio::ffmpeg +{ + namespace + { + AudioProperties computeAudioProperties(const AudioFile& audioFile) + { + AudioProperties audioProperties; + + const auto containerInfo{ audioFile.getContainerInfo() }; + const auto bestStreamInfo{ audioFile.getBestStreamInfo() }; + if (!bestStreamInfo) + throw AudioFileParsingException{ "Cannot find best audio stream" }; + + if (!containerInfo.container) + throw AudioFileParsingException{ "Unhandled container type '" + containerInfo.containerName + "'" }; + + if (!bestStreamInfo->codec) + throw AudioFileParsingException{ "Unhandled codec type '" + bestStreamInfo->codecName + "'" }; + + if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0) + throw AudioFileParsingException{ "Cannot determine bitrate" }; + + if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0) + throw AudioFileParsingException{ "Cannot determine channel count" }; + + if (!bestStreamInfo->sampleRate || *bestStreamInfo->sampleRate == 0) + throw AudioFileParsingException{ "Cannot determine sample rate" }; + + 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; + + return audioProperties; + } + } // namespace + + AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs) + : _audioFile{ std::make_unique(filePath) } + , _audioProperties{ std::make_unique(computeAudioProperties(*_audioFile)) } + , _tagReader{ std::make_unique(*_audioFile, enableExtraDebugLogs) } + , _imageReader{ std::make_unique(*_audioFile) } + { + } + + AudioFileInfo::~AudioFileInfo() = default; + + const AudioProperties& AudioFileInfo::getAudioProperties() const + { + return *_audioProperties; + } + + const IImageReader& AudioFileInfo::getImageReader() const + { + return *_imageReader; + } + + const ITagReader& AudioFileInfo::getTagReader() const + { + return *_tagReader; + } + +} // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp new file mode 100644 index 00000000..e8c806a7 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" + +namespace lms::audio::ffmpeg +{ + class AudioFile; + class TagReader; + class ImageReader; + + class AudioFileInfo final : public IAudioFileInfo + { + public: + AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs); + ~AudioFileInfo(); + 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; + + std::unique_ptr _audioFile; + std::unique_ptr _audioProperties; + std::unique_ptr _tagReader; + std::unique_ptr _imageReader; + }; +} // namespace lms::audio::ffmpeg diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp similarity index 61% rename from src/libs/metadata/impl/avformat/AvFormatImageReader.cpp rename to src/libs/audio/impl/ffmpeg/ImageReader.cpp index 8a420534..ad935d1e 100644 --- a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp +++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp @@ -17,35 +17,30 @@ * along with LMS. If not, see . */ -#include "AvFormatImageReader.hpp" +#include "ImageReader.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" -#include "metadata/Exception.hpp" +#include -namespace lms::metadata::avformat +#include "core/String.hpp" + +#include "AudioFile.hpp" + +namespace lms::audio::ffmpeg { - AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p) + ImageReader::ImageReader(const AudioFile& audioFile) + : _audioFile{ audioFile } { - try - { - _audioFile = av::parseAudioFile(p); - } - catch (av::Exception& e) - { - throw AudioFileParsingException{ e.what() }; - } } - AvFormatImageReader::~AvFormatImageReader() = default; + ImageReader::~ImageReader() = default; - void AvFormatImageReader::visitImages(ImageVisitor visitor) const + void ImageReader::visitImages(const ImageVisitor& visitor) const { - auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) { + auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) { return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); }); } }; - _audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) { + _audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) { Image image; image.data = picture.data; image.mimeType = picture.mimeType; @@ -58,4 +53,4 @@ namespace lms::metadata::avformat }); } -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.hpp b/src/libs/audio/impl/ffmpeg/ImageReader.hpp new file mode 100644 index 00000000..27a24b30 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/ImageReader.hpp @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +#pragma once + +#include "audio/IImageReader.hpp" + +namespace lms::audio::ffmpeg +{ + class AudioFile; + + class ImageReader : public IImageReader + { + public: + ImageReader(const AudioFile& audioFile); + ~ImageReader() override; + + ImageReader(const ImageReader&) = delete; + ImageReader& operator=(const ImageReader&) = delete; + + private: + void visitImages(const ImageVisitor& visitor) const override; + + const AudioFile& _audioFile; + }; +} // namespace lms::audio::ffmpeg diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp similarity index 86% rename from src/libs/metadata/impl/avformat/AvFormatTagReader.cpp rename to src/libs/audio/impl/ffmpeg/TagReader.cpp index fb7981c1..fa5d9e4f 100644 --- a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp @@ -17,15 +17,12 @@ * along with LMS. If not, see . */ -#include "AvFormatTagReader.hpp" +#include "TagReader.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" #include "core/ILogger.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" -namespace lms::metadata::avformat +namespace lms::audio::ffmpeg { namespace { @@ -146,41 +143,20 @@ namespace lms::metadata::avformat }; } // namespace - AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug) + TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs) + : _audioFile{ audioFile } + , _metaDataMap{ audioFile.getMetaData() } { - try + if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { - const auto audioFile{ av::parseAudioFile(p) }; - - _audioProperties.duration = audioFile->getContainerInfo().duration; - - const auto bestAudioStream{ audioFile->getBestStreamInfo() }; - if (bestAudioStream) - { - _audioProperties.bitrate = bestAudioStream->bitrate; - _audioProperties.bitsPerSample = bestAudioStream->bitsPerSample; - _audioProperties.channelCount = bestAudioStream->channelCount; - _audioProperties.sampleRate = bestAudioStream->sampleRate; - } - - _containerInfo = audioFile->getContainerInfo(); - _metaDataMap = audioFile->getMetaData(); - - if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) - { - for (const auto& [key, value] : _metaDataMap) - LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'"); - } - } - catch (av::Exception& e) - { - throw AudioFileParsingException{ e.what() }; + for (const auto& [key, value] : _metaDataMap) + LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'"); } } - AvFormatTagReader::~AvFormatTagReader() = default; + TagReader::~TagReader() = default; - void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ avFormatTagMapping.find(tag) }; if (itTagNames == std::cend(avFormatTagMapping)) @@ -200,7 +176,7 @@ namespace lms::metadata::avformat } } - void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const + void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const { auto itValues{ _metaDataMap.find(std::string{ key }) }; if (itValues == std::cend(_metaDataMap)) @@ -209,14 +185,14 @@ namespace lms::metadata::avformat visitor(itValues->second); } - void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const + void TagReader::visitPerformerTags(PerformerVisitor visitor) const { visitTagValues("PERFORMER", [&](std::string_view value) { visitor("", value); }); } - void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const + void TagReader::visitLyricsTags(LyricsVisitor visitor) const { // MPEG files: need to visit LYRICS-language entries for (const auto& [tag, value] : _metaDataMap) @@ -234,4 +210,4 @@ namespace lms::metadata::avformat visitor("", value); }); } -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp b/src/libs/audio/impl/ffmpeg/TagReader.hpp similarity index 61% rename from src/libs/metadata/impl/avformat/AvFormatTagReader.hpp rename to src/libs/audio/impl/ffmpeg/TagReader.hpp index 44b38c38..25e7bd7f 100644 --- a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.hpp @@ -19,31 +19,27 @@ #pragma once -#include +#include "audio/ITagReader.hpp" -#include "av/IAudioFile.hpp" +#include "AudioFile.hpp" -#include "ITagReader.hpp" - -namespace lms::metadata::avformat +namespace lms::audio::ffmpeg { - class AvFormatTagReader : public ITagReader + class TagReader : public ITagReader { public: - AvFormatTagReader(const std::filesystem::path& path, bool debug); - ~AvFormatTagReader() override; - AvFormatTagReader(const AvFormatTagReader&) = delete; - AvFormatTagReader& operator=(const AvFormatTagReader&) = delete; + TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs); + ~TagReader() override; + TagReader(const TagReader&) = delete; + TagReader& operator=(const TagReader&) = delete; private: void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - const AudioProperties& getAudioProperties() const override { return _audioProperties; } - AudioProperties _audioProperties; - av::IAudioFile::MetadataMap _metaDataMap; - av::ContainerInfo _containerInfo; + const AudioFile& _audioFile; + AudioFile::MetadataMap _metaDataMap; }; -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp similarity index 58% rename from src/libs/av/impl/Transcoder.cpp rename to src/libs/audio/impl/ffmpeg/Transcoder.cpp index 8dc4d59d..cd6ea9d5 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -27,17 +27,21 @@ #include "core/ILogger.hpp" #include "core/Service.hpp" -#include "av/Exception.hpp" +#include "audio/Exception.hpp" +#include "audio/TranscodeTypes.hpp" -namespace lms::av +namespace lms::audio +{ + std::unique_ptr createTranscoder(const TranscodeParameters& parameters) + { + return std::make_unique(parameters); + } +} // namespace lms::audio + +namespace lms::audio::ffmpeg { #define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message) - std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters) - { - return std::make_unique(inputParameters, outputParameters); - } - static std::atomic globalId{}; static std::filesystem::path ffmpegPath; @@ -48,10 +52,10 @@ namespace lms::av throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; } - Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams) + Transcoder::Transcoder(const TranscodeParameters& parameters) : _debugId{ globalId++ } - , _inputParams{ inputParams } - , _outputParams{ outputParams } + , _inputParams{ parameters.inputParameters } + , _outputParams{ parameters.outputParameters } { start(); } @@ -65,18 +69,18 @@ namespace lms::av try { - if (!std::filesystem::exists(_inputParams.file)) - throw Exception{ "File " + _inputParams.file.string() + " does not exist!" }; - if (!std::filesystem::is_regular_file(_inputParams.file)) - throw Exception{ "File " + _inputParams.file.string() + " is not regular!" }; + if (!std::filesystem::exists(_inputParams.filePath)) + throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" }; + if (!std::filesystem::is_regular_file(_inputParams.filePath)) + throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" }; } catch (const std::filesystem::filesystem_error& e) { // TODO store/raise e.code() - throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() }; + throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() }; } - LOG(INFO, "Transcoding file " << _inputParams.file); + LOG(INFO, "Transcoding file " << _inputParams.filePath); std::vector args; @@ -101,14 +105,7 @@ namespace lms::av // Input file args.emplace_back("-i"); - args.emplace_back(_inputParams.file.string()); - - // Stream mapping, if set - if (_inputParams.streamIndex) - { - args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex)); - } + args.emplace_back(_inputParams.filePath.string()); if (_outputParams.stripMetadata) { @@ -121,47 +118,53 @@ namespace lms::av args.emplace_back("-vn"); // Output bitrates - args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_outputParams.bitrate)); + if (_outputParams.bitrate) + { + args.emplace_back("-b:a"); + args.emplace_back(std::to_string(*_outputParams.bitrate)); + } // Codecs and formats - switch (_outputParams.format) + if (_outputParams.format) { - case OutputFormat::MP3: - args.emplace_back("-f"); - args.emplace_back("mp3"); - break; + switch (*_outputParams.format) + { + case OutputFormat::MP3: + args.emplace_back("-f"); + args.emplace_back("mp3"); + break; - case OutputFormat::OGG_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case OutputFormat::OGG_OPUS: + args.emplace_back("-acodec"); + 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::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"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case OutputFormat::OGG_VORBIS: + args.emplace_back("-acodec"); + 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"); - break; + case OutputFormat::WEBM_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("webm"); + break; - default: - throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParams.format)) + ")" }; + default: + throw Exception{ "Unhandled format (" + std::to_string(static_cast(*_outputParams.format)) + ")" }; + } } args.emplace_back("pipe:1"); @@ -199,18 +202,22 @@ namespace lms::av std::string_view Transcoder::getOutputMimeType() const { - switch (_outputParams.format) + // TODO: use input mime type + if (_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"; + 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 "application/octet-stream"; // default, should not happen @@ -222,5 +229,4 @@ namespace lms::av return _childProcess->finished(); } - -} // namespace lms::av +} // namespace lms::audio::ffmpeg diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/audio/impl/ffmpeg/Transcoder.hpp similarity index 79% rename from src/libs/av/impl/Transcoder.hpp rename to src/libs/audio/impl/ffmpeg/Transcoder.hpp index 685d18b0..715c6d4f 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.hpp @@ -19,19 +19,19 @@ #pragma once -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" namespace lms::core { class IChildProcess; } -namespace lms::av +namespace lms::audio::ffmpeg { class Transcoder : public ITranscoder { public: - Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); + Transcoder(const TranscodeParameters& parameters); ~Transcoder() override; Transcoder(const Transcoder&) = delete; Transcoder& operator=(const Transcoder&) = delete; @@ -41,15 +41,15 @@ namespace lms::av std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override; std::string_view getOutputMimeType() const override; - const OutputParameters& getOutputParameters() const override { return _outputParams; } + const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; } bool finished() const override; static void init(); void start(); const std::size_t _debugId{}; - const InputParameters _inputParams; - const OutputParameters _outputParams; + const TranscodeInputParameters _inputParams; + const TranscodeOutputParameters _outputParams; std::unique_ptr _childProcess; }; -} // namespace lms::av \ No newline at end of file +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/Utils.cpp b/src/libs/audio/impl/ffmpeg/Utils.cpp similarity index 89% rename from src/libs/metadata/impl/avformat/Utils.cpp rename to src/libs/audio/impl/ffmpeg/Utils.cpp index 83055d2f..a3ab243c 100644 --- a/src/libs/metadata/impl/avformat/Utils.cpp +++ b/src/libs/audio/impl/ffmpeg/Utils.cpp @@ -19,11 +19,11 @@ #include "Utils.hpp" -namespace lms::metadata::avformat::utils +namespace lms::audio::ffmpeg::utils { std::span getSupportedExtensions() { - // TODO: use av capability to retrieve supported formats + // TODO: list demuxers to retrieve supported formats static const std::array fileExtensions{ ".aac", ".alac", @@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils }; return fileExtensions; } -} // namespace lms::metadata::avformat::utils \ No newline at end of file +} // namespace lms::audio::ffmpeg::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/Utils.hpp b/src/libs/audio/impl/ffmpeg/Utils.hpp similarity index 90% rename from src/libs/metadata/impl/avformat/Utils.hpp rename to src/libs/audio/impl/ffmpeg/Utils.hpp index b4f655fa..35d7be48 100644 --- a/src/libs/metadata/impl/avformat/Utils.hpp +++ b/src/libs/audio/impl/ffmpeg/Utils.hpp @@ -22,7 +22,7 @@ #include #include -namespace lms::metadata::avformat::utils +namespace lms::audio::ffmpeg::utils { std::span getSupportedExtensions(); -} // namespace lms::metadata::avformat::utils \ No newline at end of file +} // namespace lms::audio::ffmpeg::utils \ No newline at end of file diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp new file mode 100644 index 00000000..6cc81dea --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp @@ -0,0 +1,257 @@ +/* + * 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 . + */ + +#include "AudioFileInfo.hpp" + +#include + +#include "TagLibDefs.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if LMS_TAGLIB_HAS_DSF + #include +#endif +#if LMS_TAGLIB_HAS_SHORTEN + #include +#endif + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" + +#include "ImageReader.hpp" +#include "TagReader.hpp" +#include "Utils.hpp" + +namespace lms::audio::taglib +{ + namespace + { + AudioProperties computeAudioProperties(const ::TagLib::File& file) + { + assert(file.audioProperties()); + + AudioProperties audioProperties; + + { + const ::TagLib::AudioProperties& properties{ *file.audioProperties() }; + + // Common properties + audioProperties.bitrate = static_cast(properties.bitrate() * 1000); + if (audioProperties.bitrate == 0) + throw AudioFileParsingException{ "Cannot determine bitrate" }; + + audioProperties.channelCount = static_cast(properties.channels()); + if (audioProperties.channelCount == 0) + throw AudioFileParsingException{ "Cannot determine channel count" }; + + audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() }; + if (audioProperties.duration == decltype(audioProperties.duration)::zero()) + throw AudioFileParsingException{ "Cannot determine duration" }; + + audioProperties.sampleRate = static_cast(properties.sampleRate()); + if (audioProperties.sampleRate == 0) + throw AudioFileParsingException{ "Cannot determine sample rate" }; + } + + // Guess container from the file type + if (const auto* apeFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::APE; + audioProperties.codec = CodecType::APE; // TODO version? + audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample(); + } + else if (const auto* asfFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::ASF; + + switch (asfFile->audioProperties()->codec()) + { + case ::TagLib::ASF::Properties::Codec::WMA1: + audioProperties.codec = CodecType::WMA1; + break; + case ::TagLib::ASF::Properties::Codec::WMA2: + audioProperties.codec = CodecType::WMA2; + break; + case ::TagLib::ASF::Properties::Codec::WMA9Lossless: + audioProperties.codec = CodecType::WMA9Lossless; + break; + case ::TagLib::ASF::Properties::Codec::WMA9Pro: + audioProperties.codec = CodecType::WMA9Pro; + break; + case ::TagLib::ASF::Properties::Codec::Unknown: + throw AudioFileParsingException{ "Unhandled ASF codec" }; + } + + audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample(); + } +#if LMS_TAGLIB_HAS_DSF + else if (const auto* dsfFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::DSF; + audioProperties.codec = CodecType::DSD; + audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample(); + } +#endif // LMS_TAGLIB_HAS_DSF + else if (const auto* flacFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::FLAC; + audioProperties.codec = CodecType::FLAC; + audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample(); + } + else if (const auto* mp4File{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::MP4; + switch (mp4File->audioProperties()->codec()) + { + case ::TagLib::MP4::Properties::Codec::AAC: + audioProperties.codec = CodecType::AAC; + break; + case ::TagLib::MP4::Properties::Codec::ALAC: + audioProperties.codec = CodecType::ALAC; + break; + case ::TagLib::MP4::Properties::Codec::Unknown: + throw AudioFileParsingException{ "Unhandled MP4 codec" }; + } + + audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample(); + } + else if (const auto* mpcFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::MPC; + + switch (mpcFile->audioProperties()->mpcVersion()) + { + case 7: + audioProperties.codec = CodecType::MPC7; + break; + case 8: + audioProperties.codec = CodecType::MPC8; + break; + default: + throw AudioFileParsingException{ "Unhandled MPC codec" }; + } + } + else if (const auto* mpegFile{ dynamic_cast(&file) }) + { + const auto& properties{ *mpegFile->audioProperties() }; + + audioProperties.container = ContainerType::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 +#if LMS_TAGLIB_HAS_ADTS + else if (mpegFile->audioProperties()->isADTS()) // likely AAC + audioProperties.codec = CodecType::AAC; +#endif + else + throw AudioFileParsingException{ "Unhandled MPEG codec" }; + } + else if (dynamic_cast(&file)) + { + audioProperties.container = ContainerType::Ogg; + audioProperties.codec = CodecType::Opus; + } + else if (dynamic_cast(&file)) + { + audioProperties.container = ContainerType::Ogg; + audioProperties.codec = CodecType::Vorbis; + } + else if (const auto* aiffFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::AIFF; + audioProperties.codec = CodecType::PCM; + audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample(); + } + else if (const auto* wavFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::WAV; + audioProperties.codec = CodecType::PCM; + audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample(); + } +#if LMS_TAGLIB_HAS_SHORTEN + else if (const auto* shortenFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::Shorten; + audioProperties.codec = CodecType::Shorten; + audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample(); + } +#endif // LMS_TAGLIB_HAS_SHORTEN + else if (const auto* trueAudioFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::TrueAudio; + audioProperties.codec = CodecType::TrueAudio; + audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample(); + } + else if (const auto* wavPackFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::WavPack; + audioProperties.codec = CodecType::WavPack; + audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample(); + } + else + { + throw AudioFileParsingException{ "Unhandled file type" }; + } + + if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0) + audioProperties.bitsPerSample.reset(); + + return audioProperties; + } + } // namespace + + AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs) + : _filePath{ filePath } + , _file{ utils::parseFile(filePath, readStyle) } + , _audioProperties{ std::make_unique(computeAudioProperties(*_file)) } + , _tagReader{ std::make_unique(*_file, enableExtraDebugLogs) } + , _imageReader{ std::make_unique(*_file) } + { + } + + AudioFileInfo::~AudioFileInfo() = default; + + const AudioProperties& AudioFileInfo::getAudioProperties() const + { + return *_audioProperties; + } + + const IImageReader& AudioFileInfo::getImageReader() const + { + return *_imageReader; + } + + const ITagReader& AudioFileInfo::getTagReader() const + { + return *_tagReader; + } + +} // namespace lms::audio::taglib diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp new file mode 100644 index 00000000..4cd85452 --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" +#include "audio/ITagReader.hpp" + +namespace TagLib +{ + class File; +} + +namespace lms::audio::taglib +{ + class ImageReader; + class TagReader; + + class AudioFileInfo final : public IAudioFileInfo + { + public: + AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs); + ~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 std::filesystem::path _filePath; + std::unique_ptr<::TagLib::File> _file; + std::unique_ptr _audioProperties; + std::unique_ptr _tagReader; + std::unique_ptr _imageReader; + }; +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp b/src/libs/audio/impl/taglib/ImageReader.cpp similarity index 55% rename from src/libs/metadata/impl/taglib/TagLibImageReader.cpp rename to src/libs/audio/impl/taglib/ImageReader.cpp index 927da6c4..adad0a24 100644 --- a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp +++ b/src/libs/audio/impl/taglib/ImageReader.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TagLibImageReader.hpp" +#include "ImageReader.hpp" #include "TagLibDefs.hpp" @@ -33,16 +33,14 @@ #include #include #include +#include #include #include #include -#include "core/ILogger.hpp" -#include "metadata/Exception.hpp" +#include "core/String.hpp" -#include "taglib/Utils.hpp" - -namespace lms::metadata::taglib +namespace lms::audio::taglib { namespace { @@ -50,47 +48,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::ID3v2::AttachedPictureFrame::Type::Other: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other: return Image::Type::Other; - case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: return Image::Type::BackCover; - case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::ID3v2::AttachedPictureFrame::Type::Media: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media: return Image::Type::Media; - case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::ID3v2::AttachedPictureFrame::Type::Artist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist: return Image::Type::Artist; - case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: return Image::Type::Conductor; - case TagLib::ID3v2::AttachedPictureFrame::Type::Band: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band: return Image::Type::Band; - case TagLib::ID3v2::AttachedPictureFrame::Type::Composer: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer: return Image::Type::Composer; - case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: return Image::Type::Illustration; - case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -101,47 +99,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::ASF::Picture::Type::Other: + case ::TagLib::ASF::Picture::Type::Other: return Image::Type::Other; - case TagLib::ASF::Picture::Type::FileIcon: + case ::TagLib::ASF::Picture::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::ASF::Picture::Type::OtherFileIcon: + case ::TagLib::ASF::Picture::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::ASF::Picture::Type::FrontCover: + case ::TagLib::ASF::Picture::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::ASF::Picture::Type::BackCover: + case ::TagLib::ASF::Picture::Type::BackCover: return Image::Type::BackCover; - case TagLib::ASF::Picture::Type::LeafletPage: + case ::TagLib::ASF::Picture::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::ASF::Picture::Type::Media: + case ::TagLib::ASF::Picture::Type::Media: return Image::Type::Media; - case TagLib::ASF::Picture::Type::LeadArtist: + case ::TagLib::ASF::Picture::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::ASF::Picture::Type::Artist: + case ::TagLib::ASF::Picture::Type::Artist: return Image::Type::Artist; - case TagLib::ASF::Picture::Type::Conductor: + case ::TagLib::ASF::Picture::Type::Conductor: return Image::Type::Conductor; - case TagLib::ASF::Picture::Type::Band: + case ::TagLib::ASF::Picture::Type::Band: return Image::Type::Band; - case TagLib::ASF::Picture::Type::Composer: + case ::TagLib::ASF::Picture::Type::Composer: return Image::Type::Composer; - case TagLib::ASF::Picture::Type::Lyricist: + case ::TagLib::ASF::Picture::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::ASF::Picture::Type::RecordingLocation: + case ::TagLib::ASF::Picture::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::ASF::Picture::Type::DuringRecording: + case ::TagLib::ASF::Picture::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::ASF::Picture::Type::DuringPerformance: + case ::TagLib::ASF::Picture::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::ASF::Picture::Type::MovieScreenCapture: + case ::TagLib::ASF::Picture::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::ASF::Picture::Type::ColouredFish: + case ::TagLib::ASF::Picture::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::ASF::Picture::Type::Illustration: + case ::TagLib::ASF::Picture::Type::Illustration: return Image::Type::Illustration; - case TagLib::ASF::Picture::Type::BandLogo: + case ::TagLib::ASF::Picture::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::ASF::Picture::Type::PublisherLogo: + case ::TagLib::ASF::Picture::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -152,47 +150,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::FLAC::Picture::Type::Other: + case ::TagLib::FLAC::Picture::Type::Other: return Image::Type::Other; - case TagLib::FLAC::Picture::Type::FileIcon: + case ::TagLib::FLAC::Picture::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::FLAC::Picture::Type::OtherFileIcon: + case ::TagLib::FLAC::Picture::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::FLAC::Picture::Type::FrontCover: + case ::TagLib::FLAC::Picture::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::FLAC::Picture::Type::BackCover: + case ::TagLib::FLAC::Picture::Type::BackCover: return Image::Type::BackCover; - case TagLib::FLAC::Picture::Type::LeafletPage: + case ::TagLib::FLAC::Picture::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::FLAC::Picture::Type::Media: + case ::TagLib::FLAC::Picture::Type::Media: return Image::Type::Media; - case TagLib::FLAC::Picture::Type::LeadArtist: + case ::TagLib::FLAC::Picture::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::FLAC::Picture::Type::Artist: + case ::TagLib::FLAC::Picture::Type::Artist: return Image::Type::Artist; - case TagLib::FLAC::Picture::Type::Conductor: + case ::TagLib::FLAC::Picture::Type::Conductor: return Image::Type::Conductor; - case TagLib::FLAC::Picture::Type::Band: + case ::TagLib::FLAC::Picture::Type::Band: return Image::Type::Band; - case TagLib::FLAC::Picture::Type::Composer: + case ::TagLib::FLAC::Picture::Type::Composer: return Image::Type::Composer; - case TagLib::FLAC::Picture::Type::Lyricist: + case ::TagLib::FLAC::Picture::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::FLAC::Picture::Type::RecordingLocation: + case ::TagLib::FLAC::Picture::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::FLAC::Picture::Type::DuringRecording: + case ::TagLib::FLAC::Picture::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::FLAC::Picture::Type::DuringPerformance: + case ::TagLib::FLAC::Picture::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::FLAC::Picture::Type::MovieScreenCapture: + case ::TagLib::FLAC::Picture::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::FLAC::Picture::Type::ColouredFish: + case ::TagLib::FLAC::Picture::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::FLAC::Picture::Type::Illustration: + case ::TagLib::FLAC::Picture::Type::Illustration: return Image::Type::Illustration; - case TagLib::FLAC::Picture::Type::BandLogo: + case ::TagLib::FLAC::Picture::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::FLAC::Picture::Type::PublisherLogo: + case ::TagLib::FLAC::Picture::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -203,44 +201,44 @@ namespace lms::metadata::taglib { switch (format) { - case TagLib::MP4::CoverArt::Format::BMP: + case ::TagLib::MP4::CoverArt::Format::BMP: return "image/bmp"; - case TagLib::MP4::CoverArt::Format::GIF: + case ::TagLib::MP4::CoverArt::Format::GIF: return "image/gif"; - case TagLib::MP4::CoverArt::Format::JPEG: + case ::TagLib::MP4::CoverArt::Format::JPEG: return "image/jpeg"; - case TagLib::MP4::CoverArt::Format::PNG: + case ::TagLib::MP4::CoverArt::Format::PNG: return "image/png"; - case TagLib::MP4::CoverArt::Format::Unknown: + case ::TagLib::MP4::CoverArt::Format::Unknown: return "application/octet-stream"; } return "application/octet-stream"; } -#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES Image::Type imageTypeFromAPEPictureType(std::string_view pictureType) { if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front")) return Image::Type::FrontCover; - else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) + if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) return Image::Type::BackCover; return Image::Type::Unknown; } -#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES - void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor) + void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor) { const auto& frameListMap{ id3v2Tags.frameListMap() }; - for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"]) { - const auto* attachedPictureFrame{ dynamic_cast(frame) }; + const auto* attachedPictureFrame{ dynamic_cast(frame) }; if (!attachedPictureFrame) continue; - TagLib::ByteVector picture{ attachedPictureFrame->picture() }; + ::TagLib::ByteVector picture{ attachedPictureFrame->picture() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -252,15 +250,15 @@ namespace lms::metadata::taglib } } - void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor) + void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor) { - for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture")) + for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture")) { - TagLib::ASF::Picture asfPicture{ attribute.toPicture() }; + ::TagLib::ASF::Picture asfPicture{ attribute.toPicture() }; if (!asfPicture.isValid()) continue; - TagLib::ByteVector picture{ asfPicture.picture() }; + ::TagLib::ByteVector picture{ asfPicture.picture() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -273,22 +271,22 @@ namespace lms::metadata::taglib } } - void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor) + void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor) { - const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") }; + const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") }; if (!coverItem.isValid()) return; -#if TAGLIB_HAS_MP4_ITEM_TYPE - if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList) +#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE + if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList) return; #endif - TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; + ::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; bool firstCover{ true }; for (TagLib::MP4::CoverArt& coverArt : coverArtList) { - TagLib::ByteVector picture{ coverArt.data() }; + ::TagLib::ByteVector picture{ coverArt.data() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -303,11 +301,11 @@ namespace lms::metadata::taglib } } - void visitFLACImages(const TagLib::List pictureList, TagLibImageReader::ImageVisitor visitor) + void visitFLACImages(const ::TagLib::List& pictureList, const ImageReader::ImageVisitor& visitor) { for (TagLib::FLAC::Picture* flacPicture : pictureList) { - TagLib::ByteVector picture{ flacPicture->data() }; + ::TagLib::ByteVector picture{ flacPicture->data() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -320,14 +318,14 @@ namespace lms::metadata::taglib } } - void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor) + void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor) { -#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES - const TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") }; - for (const TagLib::VariantMap& pictureProperty : pictureProperties) +#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES + const ::TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") }; + for (const ::TagLib::VariantMap& pictureProperty : pictureProperties) { Image image; - TagLib::ByteVector picture; + ::TagLib::ByteVector picture; if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend()) image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true)); @@ -344,81 +342,77 @@ namespace lms::metadata::taglib if (!image.data.empty()) visitor(image); } - -#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES } } // namespace - TagLibImageReader::TagLibImageReader(const std::filesystem::path& p) - : _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) } + ImageReader::ImageReader(::TagLib::File& file) + : _file{ file } { - if (!_file) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); - throw AudioFileParsingException{}; - } } - void TagLibImageReader::visitImages(ImageVisitor visitor) const + ImageReader::~ImageReader() = default; + + void ImageReader::visitImages(const ImageVisitor& visitor) const { // MP3 - if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) + if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) }) { if (mp3File->hasID3v2Tag()) - visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*mp3File->ID3v2Tag(), visitor); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) + else if (const TagLib::MP4::File * mp4File{ dynamic_cast(&_file) }) { - visitMP4Images(*mp4File, std::move(visitor)); + visitMP4Images(*mp4File, visitor); } // WMA - else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::ASF::File * asfFile{ dynamic_cast(&_file) }) { - if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) - visitASFImages(*tag, std::move(visitor)); + if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() }) + visitASFImages(*tag, visitor); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) }) { if (flacFile->hasID3v2Tag()) // usage discouraged - visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*flacFile->ID3v2Tag(), visitor); else - visitFLACImages(flacFile->pictureList(), std::move(visitor)); + visitFLACImages(flacFile->pictureList(), visitor); } // Ogg vorbis - else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(&_file) }) { - visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor)); + visitFLACImages(vorbisFile->tag()->pictureList(), visitor); } // Ogg Opus - else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(&_file) }) { - visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor)); + visitFLACImages(opusFile->tag()->pictureList(), visitor); } // Aiff - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) }) { if (aiffFile->hasID3v2Tag()) - visitID3V2Images(*aiffFile->tag(), std::move(visitor)); + visitID3V2Images(*aiffFile->tag(), visitor); } // Wav - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) }) { if (wavFile->hasID3v2Tag()) - visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*wavFile->ID3v2Tag(), visitor); } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) }) { if (mpcFile->hasAPETag()) - visitAPEImages(*mpcFile->APETag(), std::move(visitor)); + visitAPEImages(*mpcFile->APETag(), visitor); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) }) { if (wavPackFile->hasAPETag()) - visitAPEImages(*wavPackFile->APETag(), std::move(visitor)); + visitAPEImages(*wavPackFile->APETag(), visitor); } } -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp b/src/libs/audio/impl/taglib/ImageReader.hpp similarity index 57% rename from src/libs/metadata/impl/avformat/AvFormatImageReader.hpp rename to src/libs/audio/impl/taglib/ImageReader.hpp index 0658b8a7..b4adb32c 100644 --- a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp +++ b/src/libs/audio/impl/taglib/ImageReader.hpp @@ -19,27 +19,26 @@ #pragma once -#include "IImageReader.hpp" +#include "audio/IImageReader.hpp" -namespace lms::av +namespace TagLib { - class IAudioFile; + class File; } -namespace lms::metadata::avformat +namespace lms::audio::taglib { - class AvFormatImageReader : public IImageReader + class ImageReader : public IImageReader { public: - AvFormatImageReader(const std::filesystem::path& p); - ~AvFormatImageReader() override; - - AvFormatImageReader(const AvFormatImageReader&) = delete; - AvFormatImageReader& operator=(const AvFormatImageReader&) = delete; + ImageReader(TagLib::File& _file); + ~ImageReader() override; + ImageReader(const ImageReader&) = delete; + ImageReader& operator=(const ImageReader&) = delete; private: - void visitImages(ImageVisitor visitor) const override; + void visitImages(const ImageVisitor& visitor) const override; - std::unique_ptr _audioFile; + TagLib::File& _file; }; -} // namespace lms::metadata::avformat +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/TagLibDefs.hpp b/src/libs/audio/impl/taglib/TagLibDefs.hpp similarity index 70% rename from src/libs/metadata/impl/taglib/TagLibDefs.hpp rename to src/libs/audio/impl/taglib/TagLibDefs.hpp index cbcbbf6d..580c72bf 100644 --- a/src/libs/metadata/impl/taglib/TagLibDefs.hpp +++ b/src/libs/audio/impl/taglib/TagLibDefs.hpp @@ -22,15 +22,21 @@ #include #if (TAGLIB_MAJOR_VERSION >= 2) - #define TAGLIB_HAS_DSF 1 + #define LMS_TAGLIB_HAS_DSF 1 + #define LMS_TAGLIB_HAS_ADTS 1 #endif -// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1 +// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1 #if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1)) - #define TAGLIB_HAS_MP4_ITEM_TYPE 1 + #define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1 #endif -// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2 +// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2 #if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2)) - #define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1 + #define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1 +#endif + +// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1 +#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1)) + #define LMS_TAGLIB_HAS_SHORTEN 1 #endif diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp b/src/libs/audio/impl/taglib/TagReader.cpp similarity index 72% rename from src/libs/metadata/impl/taglib/TagLibTagReader.cpp rename to src/libs/audio/impl/taglib/TagReader.cpp index b2fd0db8..a905f7b5 100644 --- a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp +++ b/src/libs/audio/impl/taglib/TagReader.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TagLibTagReader.hpp" +#include "TagReader.hpp" #include @@ -25,7 +25,6 @@ #include #include -#include #include #include #include @@ -38,33 +37,25 @@ #include #include #include +#include #include #include #include #include #include #include -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF #include #include #endif #include "core/ILogger.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" -#include "Utils.hpp" - -namespace lms::metadata::taglib +namespace lms::audio::taglib { namespace { - class TagParsingFailedException : public Exception - { - public: - using Exception::Exception; - }; - // Mapping to internal taglib names and/or common alternative custom names const std::unordered_map> tagLibTagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, @@ -181,7 +172,7 @@ namespace lms::metadata::taglib { TagType::Writer, { "WRITER" } }, }; - void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src) + void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src) { for (auto&& [tag, values] : src) { @@ -190,49 +181,36 @@ namespace lms::metadata::taglib } } - void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file) + void dedupTagValues(TagLib::PropertyMap& propertyMap) { for (auto& [key, values] : propertyMap) { if (values.size() <= 1) continue; - TagLib::StringList newList; - for (const TagLib::String& value : values) + ::TagLib::StringList newList; + for (const ::TagLib::String& value : values) { - if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; })) + if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; })) newList.append(value); } if (values != newList) { - LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)"); + LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)"); values = newList; } } } } // namespace - TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug) - : _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) } + TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs) + : _file{ file } { - if (!_file) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); - throw AudioFileParsingException{ "Parsing failed" }; - } + _propertyMap = _file.properties(); - if (!_file->audioProperties()) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties"); - throw AudioFileNoAudioPropertiesException{}; - } - - computeAudioProperties(); - - _propertyMap = _file->properties(); - - if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) + enableExtraDebugLogs &= core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG); + if (enableExtraDebugLogs) { for (const auto& [key, values] : _propertyMap) { @@ -245,7 +223,7 @@ namespace lms::metadata::taglib } // Some tags may not be known by TagLib - auto getAPETags = [&](const TagLib::APE::Tag* apeTag) { + auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) { if (!apeTag) return; @@ -254,7 +232,7 @@ namespace lms::metadata::taglib auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) { // Dedup values for some tags that may be written in both a standard tag and in a custom tag - dedupTagValues(_propertyMap, p); + dedupTagValues(_propertyMap); const auto& frameListMap{ id3v2Tags.frameListMap() }; @@ -264,26 +242,30 @@ namespace lms::metadata::taglib // consider each frame hold a different set of lyrics // Synchronized lyrics frames - for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"]) { - const auto* lyricsFrame{ dynamic_cast(frame) }; + const auto* lyricsFrame{ dynamic_cast(frame) }; if (!lyricsFrame) continue; // TODO log or assert? const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() }; std::string lyrics; - for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText()) + for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText()) { std::chrono::milliseconds timestamp{}; switch (lyricsFrame->timestampFormat()) { - case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds: + case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds: timestamp = std::chrono::milliseconds{ synchedText.time }; break; - case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames: - timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 }; + case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames: + { + const ::TagLib::AudioProperties* properties{ file.audioProperties() }; + if (properties && properties->sampleRate()) + timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() }; + } break; - case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown: + case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown: break; } @@ -298,9 +280,9 @@ namespace lms::metadata::taglib } // Unsynchronized lyrics frames - for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"]) { - const auto* lyricsFrame{ dynamic_cast(frame) }; + const auto* lyricsFrame{ dynamic_cast(frame) }; if (!lyricsFrame) continue; // TODO log or assert? @@ -310,9 +292,9 @@ namespace lms::metadata::taglib }; // WMA - if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) + if (const ::TagLib::ASF::File * asfFile{ dynamic_cast(&_file) }) { - if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) + if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() }) { for (const auto& [name, attributeList] : tag->attributeListMap()) { @@ -320,19 +302,19 @@ namespace lms::metadata::taglib continue; const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) }; - if (debug) + if (enableExtraDebugLogs) { for (const auto& attribute : attributeList) - LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "" }) << "'"); + LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "" }) << "'"); } if (strName.find("WM/") == 0 || _propertyMap.contains(strName)) continue; - TagLib::StringList strAttributes; - for (const TagLib::ASF::Attribute& attribute : attributeList) + ::TagLib::StringList strAttributes; + for (const ::TagLib::ASF::Attribute& attribute : attributeList) { - if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) + if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType) strAttributes.append(attribute.toString()); } @@ -356,23 +338,23 @@ namespace lms::metadata::taglib } } // MP3 - else if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) + else if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) }) { if (mp3File->hasID3v2Tag()) - processID3v2Tags(*mp3File->ID3v2Tag()); + processID3v2Tags(*mp3File->ID3v2Tag(false)); getAPETags(mp3File->APETag()); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) + else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast(&_file) }) { // Taglib does not expose rtng in properties - if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) + if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) { -#if TAGLIB_HAS_MP4_ITEM_TYPE - if (rtngItem.type() == TagLib::MP4::Item::Type::Byte) +#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE + if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte) #endif - _propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) }; + _propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) }; } if (!_propertyMap.contains("ORIGINALDATE")) @@ -386,7 +368,7 @@ namespace lms::metadata::taglib auto itOrigDateTag{ tags.find(origDateString) }; if (itOrigDateTag != std::cend(tags)) { - const TagLib::StringList dates{ itOrigDateTag->second.toStringList() }; + const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() }; if (!dates.isEmpty()) { _propertyMap["ORIGINALDATE"] = dates.front(); @@ -397,68 +379,36 @@ namespace lms::metadata::taglib } } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) + else if (::TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) }) { getAPETags(mpcFile->APETag()); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) }) { getAPETags(wavPackFile->APETag()); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) }) { if (flacFile->hasID3v2Tag()) // discouraged usage processID3v2Tags(*flacFile->ID3v2Tag()); } - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) }) { if (aiffFile->hasID3v2Tag()) processID3v2Tags(*aiffFile->tag()); } - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) }) { if (wavFile->hasID3v2Tag()) processID3v2Tags(*wavFile->ID3v2Tag()); } } - TagLibTagReader::~TagLibTagReader() = default; + TagReader::~TagReader() = default; - void TagLibTagReader::computeAudioProperties() - { - const TagLib::AudioProperties* properties{ _file->audioProperties() }; - - // Common properties - _audioProperties.bitrate = static_cast(properties->bitrate() * 1000); - _audioProperties.channelCount = static_cast(_file->audioProperties()->channels()); - _audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; - _audioProperties.sampleRate = static_cast(properties->sampleRate()); - - if (const auto* apeProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = apeProperties->bitsPerSample(); - if (const auto* asfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = asfProperties->bitsPerSample(); - else if (const auto* flacProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = flacProperties->bitsPerSample(); - else if (const auto* mp4Properties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = mp4Properties->bitsPerSample(); - else if (const auto* wavePackProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = wavePackProperties->bitsPerSample(); - else if (const auto* aiffProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = aiffProperties->bitsPerSample(); - else if (const auto* wavProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = wavProperties->bitsPerSample(); -#if TAGLIB_HAS_DSF - else if (const auto* dsfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); - else if (const auto* dsfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); -#endif - } - - void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ tagLibTagMapping.find(tag) }; if (itTagNames == std::cend(tagLibTagMapping)) @@ -478,19 +428,19 @@ namespace lms::metadata::taglib } } - void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const { - TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 }; + ::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 }; auto itValues{ _propertyMap.find(key) }; if (itValues == std::cend(_propertyMap)) return; - for (const TagLib::String& value : itValues->second) + for (const ::TagLib::String& value : itValues->second) visitor(value.to8Bit(true)); } - void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const + void TagReader::visitPerformerTags(PerformerVisitor visitor) const { visitTagValues("PERFORMER", [&](std::string_view value) { visitor("", value); @@ -505,7 +455,7 @@ namespace lms::metadata::taglib assert(rolePos != std::string::npos); std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) }; - for (const TagLib::String& value : values) + for (const ::TagLib::String& value : values) { const std::string name{ value.to8Bit(true) }; visitor(role, name); @@ -514,7 +464,7 @@ namespace lms::metadata::taglib } } - void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const + void TagReader::visitLyricsTags(LyricsVisitor visitor) const { if (!_id3v2Lyrics.empty()) { @@ -529,4 +479,4 @@ namespace lms::metadata::taglib }); } } -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp b/src/libs/audio/impl/taglib/TagReader.hpp similarity index 61% rename from src/libs/metadata/impl/taglib/TagLibTagReader.hpp rename to src/libs/audio/impl/taglib/TagReader.hpp index 07034a38..7bb98e84 100644 --- a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp +++ b/src/libs/audio/impl/taglib/TagReader.hpp @@ -19,38 +19,37 @@ #pragma once -#include #include -#include #include -#include #include -#include "ITagReader.hpp" +#include "audio/ITagReader.hpp" -namespace lms::metadata::taglib +namespace TagLib { - class TagLibTagReader : public ITagReader + class File; +} + +namespace lms::audio::taglib +{ + class TagReader : public ITagReader { public: - TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug); - ~TagLibTagReader() override; - TagLibTagReader(const TagLibTagReader&) = delete; - TagLibTagReader& operator=(const TagLibTagReader&) = delete; + TagReader(::TagLib::File& file, bool enableExtraDebugLogs); + ~TagReader() override; + + TagReader(const TagReader&) = delete; + TagReader& operator=(const TagReader&) = delete; private: - void computeAudioProperties(); void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - const AudioProperties& getAudioProperties() const override { return _audioProperties; } - - std::unique_ptr _file; - AudioProperties _audioProperties; - TagLib::PropertyMap _propertyMap; // case-insensitive keys + ::TagLib::File& _file; + ::TagLib::PropertyMap _propertyMap; // case-insensitive keys std::multimap _id3v2Lyrics; }; -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/Utils.cpp b/src/libs/audio/impl/taglib/Utils.cpp similarity index 85% rename from src/libs/metadata/impl/taglib/Utils.cpp rename to src/libs/audio/impl/taglib/Utils.cpp index 2d01b6e2..aebdaf05 100644 --- a/src/libs/metadata/impl/taglib/Utils.cpp +++ b/src/libs/audio/impl/taglib/Utils.cpp @@ -39,40 +39,65 @@ #include #include #include -#if TAGLIB_HAS_DSF - #include +#if LMS_TAGLIB_HAS_DSF #include #endif #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" +#include "core/String.hpp" -namespace lms::metadata::taglib::utils +#include "audio/IAudioFileInfo.hpp" + +namespace lms::audio::taglib::utils { std::span getSupportedExtensions() { static const std::vector supportedExtensions{ - ".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus", - ".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p", - ".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc", + ".mp3", + ".mp2", + ".ogg", + ".oga", + ".flac", + ".spx", + ".opus", + ".mpc", + ".wv", + ".ape", + ".tta", + ".m4a", + ".m4r", + ".m4b", + ".m4p", + ".3g2", + ".m4v", + ".wma", + ".asf", + ".aif", + ".aiff", + ".afc", + ".aifc", ".wav", -#if TAGLIB_HAS_DSF - ".dsf", ".dff", ".dsdiff" +#if LMS_TAGLIB_HAS_ADTS + ".aac", +#endif +#if LMS_TAGLIB_HAS_DSF + ".dsf", #endif }; return std::span{ supportedExtensions }; } - TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle) + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle) { switch (readStyle) { - case ParserReadStyle::Fast: + case ParserOptions::AudioPropertiesReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; - case ParserReadStyle::Average: + case ParserOptions::AudioPropertiesReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; - case ParserReadStyle::Accurate: + case ParserOptions::AudioPropertiesReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; } @@ -100,8 +125,9 @@ namespace lms::metadata::taglib::utils return TagLib::FileStream{ fd, true }; } - std::unique_ptr parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties) + std::unique_ptr parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) { + constexpr bool readAudioProperties{ true }; std::unique_ptr file; if (extension.empty()) @@ -149,11 +175,9 @@ namespace lms::metadata::taglib::utils file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); else if (ext == "WAV") file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF else if (ext == "DSF") file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); - else if (ext == "DFF" || ext == "DSDIFF") - file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #endif if (file && !file->isValid()) @@ -162,8 +186,9 @@ namespace lms::metadata::taglib::utils return file; } - std::unique_ptr parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties) + std::unique_ptr parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) { + constexpr bool readAudioProperties{ true }; std::unique_ptr file; if (TagLib::MPEG::File::isSupported(stream)) @@ -200,11 +225,9 @@ namespace lms::metadata::taglib::utils file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); else if (TagLib::RIFF::WAV::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF else if (TagLib::DSF::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); - else if (TagLib::DSDIFF::File::isSupported(stream)) - file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #endif if (file && !file->isValid()) @@ -213,20 +236,27 @@ namespace lms::metadata::taglib::utils return file; } - std::unique_ptr parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties) + std::unique_ptr parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle) { LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile"); + const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) }; TagLib::FileStream fileStream{ createFileStream(p) }; - std::unique_ptr file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) }; + std::unique_ptr file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) }; if (!file) { LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension"); - file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value()); + file = parseFileByContent(&fileStream, tagLibReadStyle); if (!file) LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content"); } + if (!file) + throw AudioFileParsingException{ "Parsing failed" }; + + if (!file->audioProperties()) + throw AudioFileParsingException{ "No audio properties" }; + return file; } -} // namespace lms::metadata::taglib::utils \ No newline at end of file +} // namespace lms::audio::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/taglib/TagLibImageReader.hpp b/src/libs/audio/impl/taglib/Utils.hpp similarity index 66% rename from src/libs/metadata/impl/taglib/TagLibImageReader.hpp rename to src/libs/audio/impl/taglib/Utils.hpp index c0c5bea4..f79e96bf 100644 --- a/src/libs/metadata/impl/taglib/TagLibImageReader.hpp +++ b/src/libs/audio/impl/taglib/Utils.hpp @@ -19,19 +19,16 @@ #pragma once -#include "IImageReader.hpp" +#include +#include +#include #include -namespace lms::metadata::taglib +#include "audio/IAudioFileInfo.hpp" + +namespace lms::audio::taglib::utils { - class TagLibImageReader : public IImageReader - { - public: - TagLibImageReader(const std::filesystem::path& p); - - void visitImages(ImageVisitor visitor) const override; - - std::unique_ptr _file; - }; -} // namespace lms::metadata::taglib + std::span getSupportedExtensions(); + std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle); +} // namespace lms::audio::taglib::utils \ No newline at end of file diff --git a/src/libs/audio/include/audio/AudioTypes.hpp b/src/libs/audio/include/audio/AudioTypes.hpp new file mode 100644 index 00000000..84478949 --- /dev/null +++ b/src/libs/audio/include/audio/AudioTypes.hpp @@ -0,0 +1,83 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "core/LiteralString.hpp" + +namespace lms::audio +{ + 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 + { + AAC, + ALAC, // Apple Lossless Audio Codec (ALAC) + APE, // Monkey's Audio + DSD, // DSD + FLAC, // Flac + MP3, + MP4ALS, // MPEG-4 Audio Lossless Coding + MPC7, // Musepack + MPC8, // Musepack + Opus, // Opus + PCM, + Shorten, // Shorten (shn) + TrueAudio, + Vorbis, + WavPack, // WavPack + WMA1, + WMA2, + WMA9Pro, + WMA9Lossless, + }; + + core::LiteralString codecTypeToString(CodecType type); + + struct AudioProperties + { + ContainerType container; + CodecType codec; + std::chrono::milliseconds duration; + unsigned bitrate; + unsigned channelCount; + unsigned sampleRate; + std::optional bitsPerSample; + }; +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/av/include/av/Exception.hpp b/src/libs/audio/include/audio/Exception.hpp similarity index 94% rename from src/libs/av/include/av/Exception.hpp rename to src/libs/audio/include/audio/Exception.hpp index baf7809b..13f13437 100644 --- a/src/libs/av/include/av/Exception.hpp +++ b/src/libs/audio/include/audio/Exception.hpp @@ -21,11 +21,11 @@ #include "core/Exception.hpp" -namespace lms::av +namespace lms::audio { class Exception : public core::LmsException { public: using LmsException::LmsException; }; -} // namespace lms::av +} // namespace lms::audio diff --git a/src/libs/metadata/include/metadata/Exception.hpp b/src/libs/audio/include/audio/IAudioFileInfo.hpp similarity index 51% rename from src/libs/metadata/include/metadata/Exception.hpp rename to src/libs/audio/include/audio/IAudioFileInfo.hpp index 2acdda72..b417e78b 100644 --- a/src/libs/metadata/include/metadata/Exception.hpp +++ b/src/libs/audio/include/audio/IAudioFileInfo.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Emeric Poupon + * Copyright (C) 2015 Emeric Poupon * * This file is part of LMS. * @@ -19,16 +19,31 @@ #pragma once -#include +#include +#include -#include "core/Exception.hpp" +#include "audio/Exception.hpp" -namespace lms::metadata +namespace lms::audio { - class Exception : public core::LmsException + class AudioProperties; + class IImageReader; + class ITagReader; + + class IAudioFileInfo { public: - using LmsException::LmsException; + virtual ~IAudioFileInfo() = default; + + virtual const AudioProperties& getAudioProperties() const = 0; + virtual const IImageReader& getImageReader() const = 0; + virtual const ITagReader& getTagReader() const = 0; + }; + + class AudioFileParsingException : public Exception + { + public: + using Exception::Exception; }; class IOException : public Exception @@ -46,16 +61,26 @@ namespace lms::metadata std::error_code _err; }; - class AudioFileParsingException : public Exception + struct ParserOptions { - public: - using Exception::Exception; - }; + enum class Parser + { + TagLib, + FFmpeg, + }; - class AudioFileNoAudioPropertiesException : public AudioFileParsingException - { - public: - using AudioFileParsingException::AudioFileParsingException; - }; + enum class AudioPropertiesReadStyle + { + Fast, + Average, + Accurate, + }; -} // namespace lms::metadata \ No newline at end of file + Parser parser{ Parser::TagLib }; + AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average }; + bool enableExtraDebugLogs{}; + }; + std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{}); + + std::span getSupportedExtensions(ParserOptions::ParserOptions::Parser parser); +} // namespace lms::audio diff --git a/src/libs/audio/include/audio/IImageReader.hpp b/src/libs/audio/include/audio/IImageReader.hpp new file mode 100644 index 00000000..0063151c --- /dev/null +++ b/src/libs/audio/include/audio/IImageReader.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +#include "core/LiteralString.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 }; + std::string mimeType{ "application/octet-stream" }; + std::string description; + std::span data; + }; + + core::LiteralString imageTypeToString(Image::Type type); + + class IImageReader + { + public: + virtual ~IImageReader() = default; + + using ImageVisitor = std::function; + virtual void visitImages(const ImageVisitor& visitor) const = 0; + }; +} // namespace lms::audio diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/audio/include/audio/ITagReader.hpp similarity index 95% rename from src/libs/metadata/impl/ITagReader.hpp rename to src/libs/audio/include/audio/ITagReader.hpp index fe5f76a0..57493f43 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/audio/include/audio/ITagReader.hpp @@ -20,10 +20,11 @@ #pragma once #include +#include -#include "metadata/Types.hpp" +#include "core/LiteralString.hpp" -namespace lms::metadata +namespace lms::audio { // prefer using picard internal names // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html @@ -152,8 +153,12 @@ namespace lms::metadata Website, WorkTitle, Writer, + + Count, // Special value used to count the number of tags }; + core::LiteralString tagTypeToString(TagType type); + class ITagReader { public: @@ -168,7 +173,5 @@ namespace lms::metadata using LyricsVisitor = std::function; virtual void visitLyricsTags(LyricsVisitor visitor) const = 0; - - virtual const AudioProperties& getAudioProperties() const = 0; }; -} // namespace lms::metadata +} // namespace lms::audio diff --git a/src/libs/av/include/av/ITranscoder.hpp b/src/libs/audio/include/audio/ITranscoder.hpp similarity index 58% rename from src/libs/av/include/av/ITranscoder.hpp rename to src/libs/audio/include/audio/ITranscoder.hpp index 165858b0..85273be0 100644 --- a/src/libs/av/include/av/ITranscoder.hpp +++ b/src/libs/audio/include/audio/ITranscoder.hpp @@ -20,37 +20,14 @@ #pragma once #include -#include #include #include -#include #include -namespace lms::av +#include "TranscodeTypes.hpp" + +namespace lms::audio { - struct InputParameters - { - std::filesystem::path file; // Path to the input file - std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from - std::optional streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set) - }; - - enum class OutputFormat - { - MP3, - OGG_OPUS, - MATROSKA_OPUS, - OGG_VORBIS, - WEBM_VORBIS, - }; - - struct OutputParameters - { - OutputFormat format; - std::size_t bitrate{ 128'000 }; - bool stripMetadata{ true }; - }; - class ITranscoder { public: @@ -62,10 +39,10 @@ namespace lms::av virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0; virtual std::string_view getOutputMimeType() const = 0; - virtual const OutputParameters& getOutputParameters() const = 0; + virtual const TranscodeOutputParameters& getOutputParameters() const = 0; virtual bool finished() const = 0; }; - std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); -} // namespace lms::av \ No newline at end of file + std::unique_ptr createTranscoder(const TranscodeParameters& parameters); +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/include/audio/TranscodeTypes.hpp b/src/libs/audio/include/audio/TranscodeTypes.hpp new file mode 100644 index 00000000..1aaf34cf --- /dev/null +++ b/src/libs/audio/include/audio/TranscodeTypes.hpp @@ -0,0 +1,60 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include + +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 + }; + + struct TranscodeOutputParameters + { + std::optional format; + std::optional bitrate; + std::optional bitsPerSample; + std::optional channelCount; + std::optional sampleRate; + bool stripMetadata{ true }; + }; + + struct TranscodeParameters + { + TranscodeInputParameters inputParameters; + TranscodeOutputParameters outputParameters; + }; +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt deleted file mode 100644 index 0833bad7..00000000 --- a/src/libs/av/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) - -add_library(lmsav STATIC - impl/AudioFile.cpp - impl/Transcoder.cpp - ) - -target_include_directories(lmsav INTERFACE - include - ) - -target_include_directories(lmsav PRIVATE - include - ${AVCODEC_INCLUDE_DIR} - ${AVFORMAT_INCLUDE_DIR} - ${AVUTIL_INCLUDE_DIR} - ) - -target_link_libraries(lmsav PUBLIC - lmscore - std::filesystem - ) - -target_link_libraries(lmsav PRIVATE - PkgConfig::LIBAV - ) diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp deleted file mode 100644 index 41cc3ab0..00000000 --- a/src/libs/av/impl/AudioFile.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include "av/IAudioFile.hpp" - -struct AVFormatContext; - -namespace lms::av -{ - class AudioFile final : public IAudioFile - { - public: - AudioFile(const std::filesystem::path& p); - ~AudioFile() override; - AudioFile(const AudioFile&) = delete; - AudioFile& operator=(const AudioFile&) = delete; - - const std::filesystem::path& getPath() const override; - ContainerInfo getContainerInfo() const override; - MetadataMap getMetaData() const override; - std::vector getStreamInfo() const override; - std::optional getBestStreamInfo() const override; - std::optional getBestStreamIndex() const override; - bool hasAttachedPictures() const override; - void visitAttachedPictures(std::function func) const override; - - private: - std::optional getStreamInfo(std::size_t streamIndex) const; - - const std::filesystem::path _p; - AVFormatContext* _context{}; - }; -} // namespace lms::av diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp deleted file mode 100644 index 63087058..00000000 --- a/src/libs/av/include/av/IAudioFile.hpp +++ /dev/null @@ -1,104 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace lms::av -{ - // List should be sync with the codecs shipped in the lms's docker version - enum class DecodingCodec - { - UNKNOWN, - MP3, - AAC, - AC3, - VORBIS, - WMAV1, - WMAV2, - FLAC, // Flac - ALAC, // Apple Lossless Audio Codec (ALAC) - WAVPACK, // WavPack - MUSEPACK7, // Musepack - MUSEPACK8, - APE, // Monkey's Audio - EAC3, // Enhanced AC-3 - MP4ALS, // MPEG-4 Audio Lossless Coding - OPUS, // Opus - SHORTEN, // Shorten (shn) - DSD_LSBF, // DSD (Direct Stream Digital), least significant bit first - DSD_LSBF_PLANAR, // DSD (Direct Stream Digital), least significant bit first, planar - DSD_MSBF, // DSD (Direct Stream Digital), most significant bit first - DSD_MSBF_PLANAR, // DSD (Direct Stream Digital), most significant bit first, planar - // TODO add PCM codecs - }; - - struct Picture - { - std::string mimeType; - std::span data; // valid as long as IAudioFile exists - }; - - struct ContainerInfo - { - std::size_t bitrate{}; - std::string name; - std::chrono::milliseconds duration{}; - }; - - struct StreamInfo - { - size_t index{}; - std::size_t bitrate{}; - std::size_t bitsPerSample{}; - std::size_t channelCount{}; - std::size_t sampleRate{}; - DecodingCodec codec; - std::string codecName; - }; - - class IAudioFile - { - public: - virtual ~IAudioFile() = default; - - // Keys are forced to be in upper case - using MetadataMap = std::unordered_map; - - virtual const std::filesystem::path& getPath() const = 0; - virtual ContainerInfo getContainerInfo() const = 0; - virtual MetadataMap getMetaData() const = 0; - virtual std::vector getStreamInfo() const = 0; - virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown - virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown - virtual bool hasAttachedPictures() const = 0; - virtual void visitAttachedPictures(std::function func) const = 0; - }; - - std::unique_ptr parseAudioFile(const std::filesystem::path& p); -} // namespace lms::av diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index a1ef7c32..d25b4264 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -50,7 +50,6 @@ target_link_libraries(lmscore PRIVATE ) target_link_libraries(lmscore PUBLIC - Boost::system std::filesystem Wt::Wt ) diff --git a/src/libs/core/impl/Logger.cpp b/src/libs/core/impl/Logger.cpp index 6173858b..a5f20884 100644 --- a/src/libs/core/impl/Logger.cpp +++ b/src/libs/core/impl/Logger.cpp @@ -37,10 +37,10 @@ namespace lms::core::logging { case Module::API_SUBSONIC: return "API_SUBSONIC"; + case Module::AUDIO: + return "AUDIO"; case Module::AUTH: return "AUTH"; - case Module::AV: - return "AV"; case Module::CHILDPROCESS: return "CHILDPROC"; case Module::COVER: diff --git a/src/libs/core/impl/MimeTypes.cpp b/src/libs/core/impl/MimeTypes.cpp index 1c8e5688..1f31a161 100644 --- a/src/libs/core/impl/MimeTypes.cpp +++ b/src/libs/core/impl/MimeTypes.cpp @@ -55,6 +55,7 @@ namespace lms::core { ".opus", "audio/opus" }, { ".pls", "audio/x-scpls" }, { ".shn", "audio/x-shn" }, + { ".tta", "audio/x-tta" }, { ".wav", "audio/x-wav" }, { ".webm", "audio/webm" }, { ".wma", "audio/x-ms-wma" }, diff --git a/src/libs/core/include/core/ILogger.hpp b/src/libs/core/include/core/ILogger.hpp index 3c300e7d..cb136d41 100644 --- a/src/libs/core/include/core/ILogger.hpp +++ b/src/libs/core/include/core/ILogger.hpp @@ -40,8 +40,8 @@ namespace lms::core::logging enum class Module { API_SUBSONIC, + AUDIO, AUTH, - AV, CHILDPROCESS, COVER, DB, diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/Types.cpp index 6100b86a..f07a30fd 100644 --- a/src/libs/database/impl/Types.cpp +++ b/src/libs/database/impl/Types.cpp @@ -23,6 +23,37 @@ namespace lms::db { + core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type) + { + switch (type) + { + case TrackArtistLinkType::Arranger: + return "arranger"; + case TrackArtistLinkType::Artist: + return "artist"; + case TrackArtistLinkType::Composer: + return "composer"; + case TrackArtistLinkType::Conductor: + return "conductor"; + case TrackArtistLinkType::Lyricist: + return "lyricist"; + case TrackArtistLinkType::Mixer: + return "mixer"; + case TrackArtistLinkType::Performer: + return "performer"; + case TrackArtistLinkType::Producer: + return "producer"; + case TrackArtistLinkType::ReleaseArtist: + return "albumartist"; + case TrackArtistLinkType::Remixer: + return "remixer"; + case TrackArtistLinkType::Writer: + return "writer"; + } + + return "unknown"; + } + static const std::set allowedAudioBitrates{ 64000, 96000, @@ -41,4 +72,5 @@ namespace lms::db { return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); } + } // namespace lms::db diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp index 3b0053ec..f74971d0 100644 --- a/src/libs/database/impl/objects/Artist.cpp +++ b/src/libs/database/impl/objects/Artist.cpp @@ -334,6 +334,31 @@ AND NOT EXISTS ( return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; } + RangeResults Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>(R"( + SELECT a FROM artist a + WHERE a.id IN ( + SELECT t_a_l.artist_id + FROM track_artist_link t_a_l + WHERE t_a_l.artist_mbid_matched = 1 + GROUP BY t_a_l.artist_id + HAVING COUNT(DISTINCT t_a_l.artist_name) > 1 + ) + AND a.id > ? + )") + .bind(lastRetrievedArtist) }; + + auto results{ utils::execRangeQuery(query, range) }; + + if (!results.results.empty()) + lastRetrievedArtist = results.results.back()->getId(); + + return results; + } + void Artist::updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId) { session.checkWriteTransaction(); diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index 472f423f..dd9cf913 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -289,7 +289,7 @@ namespace lms::db { // As we use the name to uniquely identoify release type, we must throw (and not truncate) if (name.size() > _maxNameLength) - throw Exception{ "Country name is too long: " + std::string{ name } + "'" }; + throw Exception{ "Country name is too long: '" + std::string{ name } + "'" }; } Country::pointer Country::create(Session& session, std::string_view name) @@ -314,7 +314,7 @@ namespace lms::db session.checkReadTransaction(); if (name.size() > _maxNameLength) - throw Exception{ "Requeted Country name is too long: " + std::string{ name } + "'" }; + throw Exception{ "Country name is too long: '" + std::string{ name } + "'" }; return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT c from country c").where("c.name = ?").bind(name)); } @@ -331,9 +331,9 @@ namespace lms::db Label::Label(std::string_view name) : _name{ name } { - // As we use the name to uniquely identoify release type, we must throw (and not truncate) + // As we use the name to uniquely identify this label, we must throw (and not truncate) if (name.size() > _maxNameLength) - throw Exception{ "Label name is too long: " + std::string{ name } + "'" }; + throw Exception{ "Label name is too long: '" + std::string{ name } + "'" }; } Label::pointer Label::create(Session& session, std::string_view name) @@ -358,7 +358,7 @@ namespace lms::db session.checkReadTransaction(); if (name.size() > _maxNameLength) - throw Exception{ "Requeted Label name is too long: " + std::string{ name } + "'" }; + throw Exception{ "Label name is too long: '" + std::string{ name } + "'" }; return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT l from label l").where("l.name = ?").bind(name)); } @@ -393,9 +393,9 @@ namespace lms::db ReleaseType::ReleaseType(std::string_view name) : _name{ name } { - // As we use the name to uniquely identoify release type, we must throw (and not truncate) + // As we use the name to uniquely identify release types, we must throw (and not truncate) if (name.size() > _maxNameLength) - throw Exception{ "ReleaseType name is too long: " + std::string{ name } + "'" }; + throw Exception{ "ReleaseType is too long: '" + std::string{ name } + "'" }; } ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name) @@ -420,7 +420,7 @@ namespace lms::db session.checkReadTransaction(); if (name.size() > _maxNameLength) - throw Exception{ "Requeted ReleaseType name is too long: " + std::string{ name } + "'" }; + throw Exception{ "ReleaseType is too long: '" + std::string{ name } + "'" }; return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name)); } diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index 119f9644..d42dcf48 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -162,6 +162,7 @@ namespace lms::db query.where("t.track_number = ?").bind(*params.trackNumber); if (params.sortMethod == TrackSortMethod::DateDescAndRelease + || params.sortMethod == TrackSortMethod::OriginalDateDescAndRelease || params.sortMethod == TrackSortMethod::Release) { query.join("medium m ON t.medium_id = m.id"); @@ -223,6 +224,9 @@ namespace lms::db case TrackSortMethod::DateDescAndRelease: query.orderBy("t.date DESC,t.release_id,m.position,t.track_number"); break; + case TrackSortMethod::OriginalDateDescAndRelease: + query.orderBy("COALESCE(t.original_date, t.date) DESC,t.release_id,m.position,t.track_number"); + break; case TrackSortMethod::Release: query.orderBy("m.position,t.track_number"); break; diff --git a/src/libs/database/impl/objects/TrackArtistLink.cpp b/src/libs/database/impl/objects/TrackArtistLink.cpp index 90a373ff..462f60f7 100644 --- a/src/libs/database/impl/objects/TrackArtistLink.cpp +++ b/src/libs/database/impl/objects/TrackArtistLink.cpp @@ -50,10 +50,25 @@ namespace lms::db if (params.artist.isValid()) query.where("t_a_l.artist_id = ?").bind(params.artist); - if (params.release.isValid()) + if (params.release.isValid() + || params.sortMethod == TrackArtistLinkSortMethod::OriginalDateDesc) { query.join("track t ON t.id = t_a_l.track_id"); - query.where("t.release_id = ?").bind(params.release); + + if (params.release.isValid()) + query.where("t.release_id = ?").bind(params.release); + } + + if (params.mbidMatched) + query.where("t_a_l.artist_mbid_matched = ?").bind(*params.mbidMatched); + + switch (params.sortMethod) + { + case TrackArtistLinkSortMethod::None: + break; + case TrackArtistLinkSortMethod::OriginalDateDesc: + query.orderBy("COALESCE(t.original_date, t.date) DESC"); + break; } return query; @@ -112,11 +127,8 @@ namespace lms::db void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function& func) { - const auto query{ createQuery(session, parameters) }; - - utils::forEachQueryResult(query, [&](const TrackArtistLink::pointer& link) { - func(link); - }); + auto query{ createQuery(session, parameters) }; + utils::forEachQueryRangeResult(query, parameters.range, func); } core::EnumSet TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId) diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp index a32efa7a..5ef0393c 100644 --- a/src/libs/database/impl/objects/TrackList.cpp +++ b/src/libs/database/impl/objects/TrackList.cpp @@ -50,7 +50,8 @@ namespace lms::db if (!params.filters.clusters.empty() || params.filters.mediaLibrary.isValid() - || params.filters.label.isValid()) + || params.filters.label.isValid() + || params.filters.releaseType.isValid()) { query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); query.groupBy("t_l.id"); diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index b5845738..37bfcfa4 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -27,6 +27,7 @@ #include #include "core/Exception.hpp" +#include "core/LiteralString.hpp" #include "core/TaggedType.hpp" namespace lms::db @@ -193,6 +194,12 @@ namespace lms::db Name, }; + enum class TrackArtistLinkSortMethod + { + None, + OriginalDateDesc, + }; + enum class TrackEmbeddedImageSortMethod { None, @@ -220,6 +227,7 @@ namespace lms::db AbsoluteFilePath, Name, DateDescAndRelease, + OriginalDateDescAndRelease, Release, // order by disc/track number TrackList, // order by asc order in tracklist TrackNumber, @@ -273,6 +281,8 @@ namespace lms::db Writer = 10, }; + core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type); + // User selectable transcoding output formats enum class TranscodingOutputFormat { diff --git a/src/libs/database/include/database/objects/Artist.hpp b/src/libs/database/include/database/objects/Artist.hpp index 6310e283..7cae8afd 100644 --- a/src/libs/database/include/database/objects/Artist.hpp +++ b/src/libs/database/include/database/objects/Artist.hpp @@ -136,6 +136,7 @@ namespace lms::db static RangeResults findIds(Session& session, const FindParameters& params); static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // No track related static bool exists(Session& session, ArtistId id); + static RangeResults findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range = std::nullopt); // Updates static void updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId); diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index 2e76f212..7fe7c4c2 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -78,7 +78,7 @@ namespace lms::db } private: - static constexpr std::size_t _maxNameLength{ 32 }; + static constexpr std::size_t _maxNameLength{ 1024 }; friend class Session; Country(std::string_view name); @@ -110,7 +110,7 @@ namespace lms::db } private: - static constexpr std::size_t _maxNameLength{ 512 }; + static constexpr std::size_t _maxNameLength{ 1024 }; friend class Session; Label(std::string_view name); @@ -142,7 +142,7 @@ namespace lms::db } private: - static constexpr std::size_t _maxNameLength{ 512 }; + static constexpr std::size_t _maxNameLength{ 1024 }; friend class Session; ReleaseType(std::string_view name); diff --git a/src/libs/database/include/database/objects/TrackArtistLink.hpp b/src/libs/database/include/database/objects/TrackArtistLink.hpp index 1f9e6117..f4e8406d 100644 --- a/src/libs/database/include/database/objects/TrackArtistLink.hpp +++ b/src/libs/database/include/database/objects/TrackArtistLink.hpp @@ -51,6 +51,8 @@ namespace lms::db ArtistId artist; // if set, links involved with this artist ReleaseId release; // if set, artists involved in this release TrackId track; // if set, artists involved in this track + std::optional mbidMatched; + TrackArtistLinkSortMethod sortMethod{ TrackArtistLinkSortMethod::None }; FindParameters& setRange(std::optional _range) { @@ -77,6 +79,16 @@ namespace lms::db track = _track; return *this; } + FindParameters& setMBIDMatched(std::optional _mbidMatched) + { + mbidMatched = _mbidMatched; + return *this; + } + FindParameters& setSortMethod(TrackArtistLinkSortMethod _method) + { + sortMethod = _method; + return *this; + } }; TrackArtistLink() = default; diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 69abbfd0..0e443db8 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -934,4 +934,43 @@ namespace lms::db::tests EXPECT_EQ(artist->getPreferredArtwork(), Artwork::pointer{}); } } + + TEST_F(DatabaseFixture, Artist_findWithMBIDNameVariants) + { + ScopedArtist artistA{ session, "ArtistA" }; + ScopedArtist artistB{ session, "ArtistB" }; + + ScopedTrack trackA1{ session }; + ScopedTrack trackA2{ session }; + ScopedTrack trackB1{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + { + auto link{ TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; + link.modify()->setArtistName("ArtistA"); + } + { + auto link{ TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; + link.modify()->setArtistName("AlternateArtistA"); + } + + { + auto link{ TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist, true) }; + link.modify()->setArtistName("ArtistB"); + } + } + + { + auto transaction{ session.createReadTransaction() }; + + ArtistId lastRetrievedArtist; + const auto results{ Artist::findWithMBIDNameVariants(session, lastRetrievedArtist) }; + + ASSERT_EQ(results.results.size(), 1); + EXPECT_EQ(results.results[0]->getId(), artistA.getId()); + EXPECT_EQ(lastRetrievedArtist, artistA.getId()); + } + } } // namespace lms::db::tests diff --git a/src/libs/database/test/TrackArtistLink.cpp b/src/libs/database/test/TrackArtistLink.cpp index d8e512a2..27d1a487 100644 --- a/src/libs/database/test/TrackArtistLink.cpp +++ b/src/libs/database/test/TrackArtistLink.cpp @@ -18,8 +18,8 @@ */ #include "Common.hpp" -#include "database/Types.hpp" +#include "database/Types.hpp" #include "database/objects/TrackArtistLink.hpp" namespace lms::db::tests @@ -183,4 +183,96 @@ namespace lms::db::tests } } } + + TEST_F(DatabaseFixture, TrackArtistLink_findWithOriginalDateDesc) + { + ScopedArtist artist{ session, "MyArtist" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + { + auto link1{ session.create(track1.get(), artist.get(), TrackArtistLinkType::Artist, false) }; + link1.modify()->setArtistName("MyArtistOldName"); + track1.get().modify()->setOriginalDate(core::PartialDateTime{ 1990, 1 }); + } + + { + auto link2{ session.create(track2.get(), artist.get(), TrackArtistLinkType::Artist, false) }; + link2.modify()->setArtistName("MyArtistNewName"); + track2.get().modify()->setOriginalDate(core::PartialDateTime{ 1995, 1 }); + } + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackArtistLink::FindParameters params; + params.setSortMethod(TrackArtistLinkSortMethod::OriginalDateDesc); + params.setArtist(artist->getId()); + + std::vector links; + TrackArtistLink::find(session, params, [&](const TrackArtistLink::pointer& link) { + links.push_back(link); + }); + ASSERT_EQ(links.size(), 2); + EXPECT_EQ(links[0]->getArtistName(), "MyArtistNewName"); + EXPECT_EQ(links[1]->getArtistName(), "MyArtistOldName"); + } + } + + TEST_F(DatabaseFixture, TrackArtistLink_findWithMBIDMatched) + { + ScopedArtist artist{ session, "MyArtist", core::UUID::fromString("97d1fb6f-db09-4760-b0b3-816559bcb632") }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + auto link1{ session.create(track1.get(), artist.get(), TrackArtistLinkType::Artist, false) }; + auto link2{ session.create(track2.get(), artist.get(), TrackArtistLinkType::Artist, true) }; + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackArtistLink::FindParameters params; + + std::vector links; + TrackArtistLink::find(session, params, [&](const TrackArtistLink::pointer& link) { + links.push_back(link); + }); + ASSERT_EQ(links.size(), 2); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackArtistLink::FindParameters params; + params.setMBIDMatched(false); + + std::vector links; + TrackArtistLink::find(session, params, [&](const TrackArtistLink::pointer& link) { + links.push_back(link); + }); + ASSERT_EQ(links.size(), 1); + EXPECT_EQ(links[0]->getTrack()->getId(), track1.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackArtistLink::FindParameters params; + params.setMBIDMatched(true); + + std::vector links; + TrackArtistLink::find(session, params, [&](const TrackArtistLink::pointer& link) { + links.push_back(link); + }); + ASSERT_EQ(links.size(), 1); + EXPECT_EQ(links[0]->getTrack()->getId(), track2.getId()); + } + } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/TrackList.cpp b/src/libs/database/test/TrackList.cpp index 36aaacf4..33320ebb 100644 --- a/src/libs/database/test/TrackList.cpp +++ b/src/libs/database/test/TrackList.cpp @@ -19,10 +19,15 @@ #include +#include "database/objects/Release.hpp" +#include "database/objects/TrackList.hpp" + #include "Common.hpp" namespace lms::db::tests { + using ScopedReleaseType = ScopedEntity; + TEST_F(DatabaseFixture, SingleTrackList) { { @@ -184,6 +189,50 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, TrackList_ReleaseType) + { + ScopedTrackList trackList1{ session, "MytrackList1", TrackListType::PlayList }; + ScopedTrackList trackList2{ session, "MytrackList2", TrackListType::PlayList }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedReleaseType releaseType1{ session, "MyReleaseType1" }; + ScopedReleaseType releaseType2{ session, "MyReleaseType2" }; + ScopedRelease release1{ session, "MyRelease1" }; + ScopedRelease release2{ session, "MyRelease2" }; + + { + auto transaction{ session.createWriteTransaction() }; + + session.create(track1.get(), trackList1.get()); + release1.get().modify()->addReleaseType(releaseType1.get()); + track1.get().modify()->setRelease(release1.get()); + + session.create(track2.get(), trackList2.get()); + release2.get().modify()->addReleaseType(releaseType2.get()); + track2.get().modify()->setRelease(release2.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + std::vector visitedTrackLists; + TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setReleaseType(releaseType1->getId())), [&](const TrackList::pointer& trackList) { + visitedTrackLists.push_back(trackList->getId()); + }); + ASSERT_EQ(visitedTrackLists.size(), 1); + EXPECT_EQ(visitedTrackLists[0], trackList1->getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + std::vector visitedTrackLists; + TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setReleaseType(releaseType2->getId())), [&](const TrackList::pointer& trackList) { + visitedTrackLists.push_back(trackList->getId()); + }); + ASSERT_EQ(visitedTrackLists.size(), 1); + EXPECT_EQ(visitedTrackLists[0], trackList2->getId()); + } + } + TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) { ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::PlayList }; diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt deleted file mode 100644 index 1c3c2967..00000000 --- a/src/libs/metadata/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) - -if(BUILD_TESTING) - add_subdirectory(test) -endif() - -if (BUILD_BENCHMARKS) - add_subdirectory(bench) -endif() - -add_library(lmsmetadata STATIC - impl/ArtistInfo.cpp - impl/AudioFileParser.cpp - impl/avformat/AvFormatImageReader.cpp - impl/avformat/AvFormatTagReader.cpp - impl/avformat/Utils.cpp - impl/Lyrics.cpp - impl/PlayList.cpp - impl/taglib/TagLibImageReader.cpp - impl/taglib/TagLibTagReader.cpp - impl/taglib/Utils.cpp - impl/Utils.cpp - ) - -target_include_directories(lmsmetadata INTERFACE - include - ) - -target_include_directories(lmsmetadata PRIVATE - include - impl - ) - -target_link_libraries(lmsmetadata PRIVATE - lmsav - PkgConfig::Taglib - pugixml::pugixml - ) - -target_link_libraries(lmsmetadata PUBLIC - lmscore - std::filesystem - ) diff --git a/src/libs/metadata/bench/CMakeLists.txt b/src/libs/metadata/bench/CMakeLists.txt deleted file mode 100644 index 4e3a392d..00000000 --- a/src/libs/metadata/bench/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ - -add_executable(bench-metadata - LyricsBench.cpp - Metadata.cpp - ) - -target_include_directories(bench-metadata PRIVATE - ../impl - ../test - ) - -target_link_libraries(bench-metadata PRIVATE - lmsmetadata - benchmark - ) diff --git a/src/libs/metadata/impl/AudioFileParser.hpp b/src/libs/metadata/impl/AudioFileParser.hpp deleted file mode 100644 index 48176296..00000000 --- a/src/libs/metadata/impl/AudioFileParser.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2018 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include - -#include "metadata/IAudioFileParser.hpp" - -namespace lms::metadata -{ - class IImageReader; - class ITagReader; - - class AudioFileParser : public IAudioFileParser - { - public: - AudioFileParser(const AudioFileParserParameters& params = {}); - ~AudioFileParser() override = default; - AudioFileParser(const AudioFileParser&) = delete; - AudioFileParser& operator=(const AudioFileParser&) = delete; - - protected: - std::unique_ptr parseMetaData(const std::filesystem::path& p) const override; - std::unique_ptr parseMetaData(const ITagReader& reader) const; - static void parseImages(const IImageReader& reader, ImageVisitor visitor); - - private: - void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const override; - std::span getSupportedExtensions() const override; - - void processTags(const ITagReader& reader, Track& track) const; - - std::optional getMedium(const ITagReader& tagReader) const; - std::optional getRelease(const ITagReader& tagReader) const; - - const AudioFileParserParameters _params; - }; -} // namespace lms::metadata diff --git a/src/libs/metadata/impl/taglib/Utils.hpp b/src/libs/metadata/impl/taglib/Utils.hpp deleted file mode 100644 index 7ddafdbd..00000000 --- a/src/libs/metadata/impl/taglib/Utils.hpp +++ /dev/null @@ -1,39 +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 . - */ - -#pragma once - -#include -#include -#include - -#include -#include - -#include "core/TaggedType.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata::taglib::utils -{ - std::span getSupportedExtensions(); - TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle); - - using ReadAudioProperties = core::TaggedBool; - std::unique_ptr parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties); -} // namespace lms::metadata::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/include/metadata/IAudioFileParser.hpp b/src/libs/metadata/include/metadata/IAudioFileParser.hpp deleted file mode 100644 index 599773f0..00000000 --- a/src/libs/metadata/include/metadata/IAudioFileParser.hpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2018 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include -#include -#include - -#include "metadata/Exception.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata -{ - class IAudioFileParser - { - public: - virtual ~IAudioFileParser() = default; - - virtual std::unique_ptr parseMetaData(const std::filesystem::path& p) const = 0; - - using ImageVisitor = std::function; - virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const = 0; - - virtual std::span getSupportedExtensions() const = 0; - }; - - std::unique_ptr createAudioFileParser(const AudioFileParserParameters& params); -} // namespace lms::metadata diff --git a/src/libs/metadata/test/AudioFileParser.cpp b/src/libs/metadata/test/AudioFileParser.cpp deleted file mode 100644 index c98b179c..00000000 --- a/src/libs/metadata/test/AudioFileParser.cpp +++ /dev/null @@ -1,933 +0,0 @@ -/* - * Copyright (C) 2024 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include - -#include - -#include - -#include "AudioFileParser.hpp" -#include "TestTagReader.hpp" - -namespace lms::metadata::tests -{ - class TestAudioFileParser : public AudioFileParser - { - public: - using AudioFileParser::AudioFileParser; - using AudioFileParser::parseMetaData; - }; - - TEST(AudioFileParser, generalTest) - { - AudioFileParserParameters params; - params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" }; - - TestAudioFileParser parser{ params }; - std::unique_ptr testTags{ createDefaultPopulatedTestTagReader() }; - - const std::unique_ptr track{ parser.parseMetaData(*testTags) }; - - // Audio properties - { - const AudioProperties& audioProperties{ testTags->getAudioProperties() }; - EXPECT_EQ(track->audioProperties.bitrate, audioProperties.bitrate); - EXPECT_EQ(track->audioProperties.bitsPerSample, audioProperties.bitsPerSample); - EXPECT_EQ(track->audioProperties.channelCount, audioProperties.channelCount); - EXPECT_EQ(track->audioProperties.duration, audioProperties.duration); - EXPECT_EQ(track->audioProperties.sampleRate, audioProperties.sampleRate); - } - - EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); - ASSERT_TRUE(track->advisory.has_value()); - EXPECT_EQ(track->advisory.value(), Track::Advisory::Clean); - EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2"); - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "MyArtist1"); - EXPECT_EQ(track->artists[0].sortName, "MyArtists1SortName"); - EXPECT_EQ(track->artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae")); - EXPECT_EQ(track->artists[1].name, "MyArtist2"); - EXPECT_EQ(track->artists[1].sortName, "MyArtists2SortName"); - EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229")); - ASSERT_EQ(track->comments.size(), 2); - EXPECT_EQ(track->comments[0], "Comment1"); - EXPECT_EQ(track->comments[1], "Comment2"); - ASSERT_EQ(track->composerArtists.size(), 2); - EXPECT_EQ(track->composerArtists[0].name, "MyComposer1"); - EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1"); - EXPECT_EQ(track->composerArtists[1].name, "MyComposer2"); - EXPECT_EQ(track->composerArtists[1].sortName, "MyComposerSortOrder2"); - ASSERT_EQ(track->conductorArtists.size(), 2); - EXPECT_EQ(track->conductorArtists[0].name, "MyConductor1"); - EXPECT_EQ(track->conductorArtists[1].name, "MyConductor2"); - EXPECT_EQ(track->copyright, "MyCopyright"); - EXPECT_EQ(track->copyrightURL, "MyCopyrightURL"); - ASSERT_TRUE(track->date.isValid()); - EXPECT_EQ(track->date.getYear(), 2020); - EXPECT_EQ(track->date.getMonth(), 3); - EXPECT_EQ(track->date.getDay(), 4); - ASSERT_EQ(track->genres.size(), 2); - EXPECT_EQ(track->genres[0], "Genre1"); - EXPECT_EQ(track->genres[1], "Genre2"); - ASSERT_EQ(track->groupings.size(), 2); - EXPECT_EQ(track->groupings[0], "Grouping1"); - EXPECT_EQ(track->groupings[1], "Grouping2"); - ASSERT_EQ(track->languages.size(), 2); - EXPECT_EQ(track->languages[0], "Language1"); - EXPECT_EQ(track->languages[1], "Language2"); - ASSERT_EQ(track->lyricistArtists.size(), 2); - EXPECT_EQ(track->lyricistArtists[0].name, "MyLyricist1"); - EXPECT_EQ(track->lyricistArtists[1].name, "MyLyricist2"); - ASSERT_EQ(track->lyrics.size(), 1); - EXPECT_EQ(track->lyrics.front().language, "eng"); - ASSERT_EQ(track->lyrics.front().synchronizedLines.size(), 2); - ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 })); - EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line"); - ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 })); - EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line"); - ASSERT_TRUE(track->mbid.has_value()); - EXPECT_EQ(track->mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a")); - ASSERT_EQ(track->mixerArtists.size(), 2); - EXPECT_EQ(track->mixerArtists[0].name, "MyMixer1"); - EXPECT_EQ(track->mixerArtists[1].name, "MyMixer2"); - ASSERT_EQ(track->moods.size(), 2); - EXPECT_EQ(track->moods[0], "Mood1"); - EXPECT_EQ(track->moods[1], "Mood2"); - ASSERT_TRUE(track->originalDate.isValid()); - EXPECT_EQ(track->originalDate.getYear(), 2019); - EXPECT_EQ(track->originalDate.getMonth(), 2); - EXPECT_EQ(track->originalDate.getDay(), 3); - ASSERT_TRUE(track->originalYear.has_value()); - EXPECT_EQ(track->originalYear.value(), 2019); - ASSERT_TRUE(track->performerArtists.contains("Rolea")); - ASSERT_EQ(track->performerArtists["Rolea"].size(), 2); - EXPECT_EQ(track->performerArtists["Rolea"][0].name, "MyPerformer1ForRoleA"); - EXPECT_EQ(track->performerArtists["Rolea"][1].name, "MyPerformer2ForRoleA"); - ASSERT_EQ(track->performerArtists["Roleb"].size(), 2); - EXPECT_EQ(track->performerArtists["Roleb"][0].name, "MyPerformer1ForRoleB"); - EXPECT_EQ(track->performerArtists["Roleb"][1].name, "MyPerformer2ForRoleB"); - ASSERT_TRUE(track->position.has_value()); - EXPECT_EQ(track->position.value(), 7); - ASSERT_EQ(track->producerArtists.size(), 2); - EXPECT_EQ(track->producerArtists[0].name, "MyProducer1"); - EXPECT_EQ(track->producerArtists[1].name, "MyProducer2"); - ASSERT_TRUE(track->recordingMBID.has_value()); - EXPECT_EQ(track->recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5")); - ASSERT_TRUE(track->replayGain.has_value()); - EXPECT_FLOAT_EQ(track->replayGain.value(), -0.33); - ASSERT_EQ(track->remixerArtists.size(), 2); - EXPECT_EQ(track->remixerArtists[0].name, "MyRemixer1"); - EXPECT_EQ(track->remixerArtists[1].name, "MyRemixer2"); - EXPECT_EQ(track->title, "MyTitle"); - ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"].size(), 2); - EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][0], "MyTagValue1ForTagA"); - EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][1], "MyTagValue2ForTagA"); - ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2); - EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB"); - EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB"); - - // Medium - ASSERT_TRUE(track->medium.has_value()); - EXPECT_EQ(track->medium->media, "CD"); - EXPECT_EQ(track->medium->name, "MySubtitle"); - ASSERT_TRUE(track->medium->position.has_value()); - EXPECT_EQ(track->medium->position.value(), 2); - ASSERT_TRUE(track->medium->replayGain.has_value()); - EXPECT_FLOAT_EQ(track->medium->replayGain.value(), -0.5); - ASSERT_TRUE(track->medium->trackCount.has_value()); - EXPECT_EQ(track->medium->trackCount.value(), 12); - - // Release - ASSERT_TRUE(track->medium->release.has_value()); - const Release& release{ track->medium->release.value() }; - EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2"); - ASSERT_EQ(release.artists.size(), 2); - EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1"); - EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName"); - EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7")); - EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2"); - EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName"); - EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1")); - EXPECT_TRUE(release.isCompilation); - EXPECT_EQ(release.barcode, "MyBarcode"); - ASSERT_EQ(release.labels.size(), 2); - EXPECT_EQ(release.labels[0], "Label1"); - EXPECT_EQ(release.labels[1], "Label2"); - ASSERT_TRUE(release.mbid.has_value()); - EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69")); - EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838")); - EXPECT_EQ(release.mediumCount, 3); - EXPECT_EQ(release.name, "MyAlbum"); - EXPECT_EQ(release.sortName, "MyAlbumSortName"); - EXPECT_EQ(release.comment, "MyAlbumComment"); - ASSERT_EQ(release.countries.size(), 2); - EXPECT_EQ(release.countries[0], "MyCountry1"); - EXPECT_EQ(release.countries[1], "MyCountry2"); - { - std::vector expectedReleaseTypes{ "Album", "Compilation" }; - EXPECT_EQ(release.releaseTypes, expectedReleaseTypes); - } - } - - TEST(AudioFileParser, trim) - { - const TestTagReader testTags{ - { - { TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->genres.size(), 3); - EXPECT_EQ(track->genres[0], "Genre1"); - EXPECT_EQ(track->genres[1], "Genre2"); - EXPECT_EQ(track->genres[2], "Genre3"); - } - - TEST(AudioFileParser, customDelimiters) - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } }, - { TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } }, - { TagType::Genre, { "Genre1 ; Genre2" } }, - { TagType::Language, { " Lang1/Lang2 / Lang3" } }, - } - }; - - AudioFileParserParameters params; - params.defaultTagDelimiters = { " ; ", "/" }; - params.artistTagDelimiters = { " / ", " feat. " }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 3); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artists[2].name, "Artist3"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit - ASSERT_EQ(track->genres.size(), 2); - EXPECT_EQ(track->genres[0], "Genre1"); - EXPECT_EQ(track->genres[1], "Genre2"); - ASSERT_EQ(track->languages.size(), 3); - EXPECT_EQ(track->languages[0], "Lang1"); - EXPECT_EQ(track->languages[1], "Lang2"); - EXPECT_EQ(track->languages[2], "Lang3"); - - // Medium - ASSERT_TRUE(track->medium.has_value()); - - // Release - ASSERT_TRUE(track->medium->release.has_value()); - EXPECT_EQ(track->medium->release->name, "MyAlbum"); - ASSERT_EQ(track->medium->release->artists.size(), 2); - EXPECT_EQ(track->medium->release->artists[0].name, "AlbumArtist1"); - EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2"); - EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2"); - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist) - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { " AC/DC " } }, - { TagType::Artist, { "AC/DC " } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].name, "AC/DC"); - EXPECT_EQ(track->artistDisplayName, "AC/DC"); - ASSERT_TRUE(track->medium.has_value()); - ASSERT_TRUE(track->medium->release.has_value()); - EXPECT_EQ(track->medium->release->name, "MyAlbum"); - ASSERT_EQ(track->medium->release->artists.size(), 1); - EXPECT_EQ(track->medium->release->artists[0].name, "AC/DC"); - EXPECT_EQ(track->medium->release->artistDisplayName, "AC/DC"); - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_artists) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "AC/DC and MyArtist" } }, - { TagType::Artists, { "AC/DC", "MyArtist" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/" }; - params.artistsToNotSplit = { " AC/DC " }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "AC/DC"); - EXPECT_EQ(track->artists[1].name, "MyArtist"); - EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_first) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "AC/DC;MyArtist" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/", ";" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "AC/DC"); - EXPECT_EQ(track->artists[1].name, "MyArtist"); - EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_middle) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/", ";" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 3); - EXPECT_EQ(track->artists[0].name, "MyArtist1"); - EXPECT_EQ(track->artists[1].name, "AC/DC"); - EXPECT_EQ(track->artists[2].name, "MyArtist2"); - EXPECT_EQ(track->artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_last) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " AC/DC; MyArtist" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { ";", "/" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "AC/DC"); - EXPECT_EQ(track->artists[1].name, "MyArtist"); - EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_longest_first) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " AC/DC; MyArtist" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { ";", "/" }; - params.artistsToNotSplit = { "AC", "DC", "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "AC/DC"); - EXPECT_EQ(track->artists[1].name, "MyArtist"); - EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_begin) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " AC/DC; MyArtist" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].name, "AC/DC; MyArtist"); - EXPECT_EQ(track->artistDisplayName, "AC/DC; MyArtist"); - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_middle) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].name, "MyArtist1; AC/DC ; MyArtist2"); - EXPECT_EQ(track->artistDisplayName, "MyArtist1; AC/DC ; MyArtist2"); - } - - TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_end) - { - const TestTagReader testTags{ - { - { TagType::Artist, { " MyArtist; AC/DC " } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "/" }; - params.artistsToNotSplit = { "AC/DC" }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].name, "MyArtist; AC/DC"); - EXPECT_EQ(track->artistDisplayName, "MyArtist; AC/DC"); - } - - TEST(AudioFileParser, customDelimiters_foundInArtist) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1; Artist2" } }, - { TagType::Artists, { "Artist1", "Artist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "; " }; - TestAudioFileParser parser{ params }; - - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist - } - - TEST(AudioFileParser, customDelimiters_foundInArtists) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 feat. Artist2" } }, - { TagType::Artists, { "Artist1; Artist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "; " }; - TestAudioFileParser parser{ params }; - - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2"); - } - - TEST(AudioFileParser, customDelimiters_notUsed) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 & Artist2" } }, - { TagType::Artists, { "Artist1", "Artist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { "; " }; - TestAudioFileParser parser{ params }; - - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2"); - } - - TEST(AudioFileParser, customDelimiters_onlyInArtist) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 & Artist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { " & " }; - TestAudioFileParser parser{ params }; - - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing - } - - TEST(AudioFileParser, customDelimitersUsedForArtists) - { - const TestTagReader testTags{ - { - { TagType::Artists, { "Artist1 & Artist2" } }, - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { " & " }; - TestAudioFileParser parser{ params }; - - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing - } - - TEST(AudioFileParser, noArtistInArtist) - { - const TestTagReader testTags{ - { - // nothing in Artist! - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 0); - EXPECT_EQ(track->artistDisplayName, ""); - } - - TEST(AudioFileParser, singleArtistInArtists) - { - const TestTagReader testTags{ - { - // nothing in Artist! - { TagType::Artists, { "Artist1" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artistDisplayName, "Artist1"); - } - - TEST(AudioFileParser, multipleArtistsInArtist) - { - const TestTagReader testTags{ - { - // nothing in Artists! - { TagType::Artist, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found - } - - TEST(AudioFileParser, multipleArtistsInArtists) - { - const TestTagReader testTags{ - { - // nothing in Artist! - { TagType::Artists, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist - } - - TEST(AudioFileParser, multipleArtistsInArtistsWithEndDelimiter) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 & (CV. Artist2)" } }, - { TagType::Artists, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)"); - } - - TEST(AudioFileParser, singleArtistInAlbumArtists) - { - const TestTagReader testTags{ - { - // nothing in AlbumArtist! - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtists, { "Artist1" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium); - ASSERT_TRUE(track->medium->release); - ASSERT_EQ(track->medium->release->artists.size(), 1); - EXPECT_EQ(track->medium->release->artists[0].name, "Artist1"); - EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1"); - } - - TEST(AudioFileParser, multipleArtistsInAlbumArtist) - { - const TestTagReader testTags{ - { - // nothing in AlbumArtists! - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium); - ASSERT_TRUE(track->medium->release); - ASSERT_EQ(track->medium->release->artists.size(), 2); - EXPECT_EQ(track->medium->release->artists[0].name, "Artist1"); - EXPECT_EQ(track->medium->release->artists[1].name, "Artist2"); - EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found - } - - TEST(AudioFileParser, multipleArtistsInAlbumArtists_displayName) - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { "Artist1 & Artist2" } }, - { TagType::AlbumArtists, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium); - ASSERT_TRUE(track->medium->release); - ASSERT_EQ(track->medium->release->artists.size(), 2); - EXPECT_EQ(track->medium->release->artists[0].name, "Artist1"); - EXPECT_EQ(track->medium->release->artists[1].name, "Artist2"); - EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2"); - } - - TEST(AudioFileParser, multipleArtistsInAlbumArtists) - { - const TestTagReader testTags{ - { - // nothing in AlbumArtist! - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtists, { "Artist1", "Artist2" } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium); - ASSERT_TRUE(track->medium->release); - ASSERT_EQ(track->medium->release->artists.size(), 2); - EXPECT_EQ(track->medium->release->artists[0].name, "Artist1"); - EXPECT_EQ(track->medium->release->artists[1].name, "Artist2"); - EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist - } - - TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 & Artist2" } }, - { TagType::Artists, { "Artist1", "Artist2" } }, - { TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[0].mbid, std::nullopt); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artists[1].mbid, std::nullopt); - EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2"); - } - - TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters) - { - const TestTagReader testTags{ - { - { TagType::Artist, { "Artist1 / Artist2" } }, - { TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added - } - }; - - AudioFileParserParameters params; - params.artistTagDelimiters = { " / " }; - TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 2); - EXPECT_EQ(track->artists[0].name, "Artist1"); - EXPECT_EQ(track->artists[0].mbid, std::nullopt); - EXPECT_EQ(track->artists[1].name, "Artist2"); - EXPECT_EQ(track->artists[1].mbid, std::nullopt); - EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name - } - - TEST(AudioFileParser, release_sortNameFallback) - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - // No AlbumSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium.has_value()); - ASSERT_TRUE(track->medium->release.has_value()); - EXPECT_EQ(track->medium->release->sortName, "MyAlbum"); - } - - TEST(AudioFileParser, artist_sortNameFallback) - { - { - const TestTagReader testTags{ - { - { TagType::Artist, { "MyArtist" } }, - { TagType::ArtistSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName"); - } - - { - const TestTagReader testTags{ - { - { TagType::Artist, { "MyArtist" } }, - { TagType::ArtistsSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName"); - } - - { - const TestTagReader testTags{ - { - { TagType::Artist, { "MyArtist" } }, - { TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } }, - { TagType::ArtistsSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->artists.size(), 1); - EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName"); - } - } - - TEST(AudioFileParser, albumartist_sortNameFallback) - { - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { "MyArtist" } }, - { TagType::AlbumArtistSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium.has_value()); - ASSERT_TRUE(track->medium->release.has_value()); - - const auto& artists{ track->medium->release->artists }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); - } - - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - { TagType::AlbumArtist, { "MyArtist" } }, - { TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium.has_value()); - ASSERT_TRUE(track->medium->release.has_value()); - - const auto& artists{ track->medium->release->artists }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); - } - - { - const TestTagReader testTags{ - { - { TagType::Album, { "MyAlbum" } }, - - { TagType::AlbumArtist, { "MyArtist" } }, - { TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } }, - { TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } }, - // No ArtistSortOrder - } - }; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_TRUE(track->medium.has_value()); - ASSERT_TRUE(track->medium->release.has_value()); - - const auto& artists{ track->medium->release->artists }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); - } - } - - TEST(AudioFileParser, advisory) - { - auto doTest = [](std::string_view value, std::optional expectedValue) { - const TestTagReader testTags{ - { - { TagType::Advisory, { value } }, - } - }; - - AudioFileParser parser; - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'"; - if (track->advisory.has_value()) - { - EXPECT_EQ(track->advisory.value(), expectedValue); - } - }; - - doTest("0", Track::Advisory::Unknown); - doTest("1", Track::Advisory::Explicit); - doTest("4", Track::Advisory::Explicit); - doTest("2", Track::Advisory::Clean); - doTest("", std::nullopt); - doTest("3", std::nullopt); - } - - TEST(AudioFileParser, encodingTime) - { - auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { - const TestTagReader testTags{ - { - { TagType::EncodingTime, { value } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'"; - }; - - doTest("", core::PartialDateTime{}); - doTest("foo", core::PartialDateTime{}); - doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 }); - doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); - doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); - } - - TEST(AudioFileParser, date) - { - auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { - const TestTagReader testTags{ - { - { TagType::Date, { value } }, - } - }; - - std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; - - ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'"; - }; - - doTest("", core::PartialDateTime{}); - doTest("foo", core::PartialDateTime{}); - doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); - doTest("2020-01", core::PartialDateTime{ 2020, 1 }); - doTest("2020", core::PartialDateTime{ 2020 }); - doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); - doTest("2020/01", core::PartialDateTime{ 2020, 1 }); - doTest("2020", core::PartialDateTime{ 2020 }); - } -} // namespace lms::metadata::tests diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt deleted file mode 100644 index d957cd3b..00000000 --- a/src/libs/metadata/test/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -include(GoogleTest) - -add_executable(test-metadata - ArtistInfo.cpp - Lyrics.cpp - Metadata.cpp - AudioFileParser.cpp - PlayList.cpp - Utils.cpp - ) - -target_include_directories(test-metadata PRIVATE - ../impl - ) - -target_link_libraries(test-metadata PRIVATE - lmsmetadata - GTest::GTest - ) - -if (NOT CMAKE_CROSSCOMPILING) - gtest_discover_tests(test-metadata) -endif() - diff --git a/src/libs/services/artwork/CMakeLists.txt b/src/libs/services/artwork/CMakeLists.txt index 56488436..3acefa6d 100644 --- a/src/libs/services/artwork/CMakeLists.txt +++ b/src/libs/services/artwork/CMakeLists.txt @@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE ) target_link_libraries(lmsartwork PRIVATE + lmsaudio lmsimage - lmsmetadata ) target_link_libraries(lmsartwork PUBLIC diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index d46bdf0c..7395ed20 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -23,6 +23,9 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" + +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -37,7 +40,6 @@ #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" -#include "metadata/IAudioFileParser.hpp" namespace lms::artwork { @@ -50,7 +52,6 @@ namespace lms::artwork const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath) : _db{ db } - , _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) } , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); @@ -107,7 +108,11 @@ namespace lms::artwork { std::size_t currentIndex{}; - _audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) { + audio::ParserOptions options; + options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images + + auto audioFile{ audio::parseAudioFile(p) }; + audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) { if (currentIndex++ != index) return; @@ -130,7 +135,7 @@ namespace lms::artwork } }); } - catch (const metadata::Exception& e) + catch (const audio::Exception& e) { LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what()); } diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index feb5b837..53bd1455 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -33,11 +33,6 @@ namespace lms::db class Session; } -namespace lms::metadata -{ - class IAudioFileParser; -} - namespace lms::artwork { class ArtworkService : public IArtworkService @@ -67,7 +62,6 @@ namespace lms::artwork db::IDb& _db; - std::unique_ptr _audioFileParser; ImageCache _cache; std::shared_ptr _defaultReleaseCover; std::shared_ptr _defaultArtistImage; diff --git a/src/libs/services/auth/CMakeLists.txt b/src/libs/services/auth/CMakeLists.txt index c664ca98..f5244ff4 100644 --- a/src/libs/services/auth/CMakeLists.txt +++ b/src/libs/services/auth/CMakeLists.txt @@ -19,7 +19,6 @@ target_include_directories(lmsauth PRIVATE ) target_link_libraries(lmsauth PUBLIC - Boost::system Wt::Wt lmscore lmsdatabase diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 20284ce9..055371cc 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -1,12 +1,17 @@ add_library(lmsscanner STATIC impl/helpers/ArtistHelpers.cpp - impl/scanners/ArtistInfoFileScanner.cpp - impl/scanners/AudioFileScanOperation.cpp + impl/scanners/artistinfo/ArtistInfoParser.cpp + impl/scanners/artistinfo/ArtistInfoFileScanner.cpp + impl/scanners/audiofile/AudioFileScanOperation.cpp + impl/scanners/audiofile/AudioFileScanner.cpp + impl/scanners/audiofile/TrackMetadataParser.cpp + impl/scanners/audiofile/Utils.cpp + impl/scanners/lyrics/LyricsFileScanner.cpp + impl/scanners/lyrics/LyricsParser.cpp + impl/scanners/playlist/PlayListFileScanner.cpp + impl/scanners/playlist/PlayListParser.cpp impl/scanners/FileScanOperationBase.cpp - impl/scanners/AudioFileScanner.cpp impl/scanners/ImageFileScanner.cpp - impl/scanners/LyricsFileScanner.cpp - impl/scanners/PlayListFileScanner.cpp impl/scanners/Utils.cpp impl/steps/JobQueue.cpp impl/steps/ScanErrorLogger.cpp @@ -43,13 +48,22 @@ target_include_directories(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE lmscore - lmsdatabase + lmsaudio lmsimage - lmsmetadata lmsrecommendation + pugixml::pugixml ) target_link_libraries(lmsscanner PUBLIC + lmsdatabase std::filesystem Wt::Wt ) + +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +if (BUILD_BENCHMARKS) + add_subdirectory(bench) +endif() diff --git a/src/libs/services/scanner/bench/CMakeLists.txt b/src/libs/services/scanner/bench/CMakeLists.txt new file mode 100644 index 00000000..78d75165 --- /dev/null +++ b/src/libs/services/scanner/bench/CMakeLists.txt @@ -0,0 +1,17 @@ + +add_executable(bench-scanner + Lyrics.cpp + Scanner.cpp + TrackMetadataParser.cpp + ) + +target_include_directories(bench-scanner PRIVATE + ../impl + ../test + ) + +target_link_libraries(bench-scanner PRIVATE + lmsscanner + lmsaudio + benchmark + ) diff --git a/src/libs/metadata/bench/LyricsBench.cpp b/src/libs/services/scanner/bench/Lyrics.cpp similarity index 96% rename from src/libs/metadata/bench/LyricsBench.cpp rename to src/libs/services/scanner/bench/Lyrics.cpp index 2e1a213e..a22bfdf8 100644 --- a/src/libs/metadata/bench/LyricsBench.cpp +++ b/src/libs/services/scanner/bench/Lyrics.cpp @@ -22,9 +22,9 @@ #include -#include "metadata/Lyrics.hpp" +#include "scanners/lyrics/LyricsParser.hpp" -namespace lms::metadata::benchmarks +namespace lms::scanner::benchmarks { static void BM_Lyrics(benchmark::State& state) { @@ -89,4 +89,4 @@ namespace lms::metadata::benchmarks BENCHMARK(BM_Lyrics); -} // namespace lms::metadata::benchmarks +} // namespace lms::scanner::benchmarks diff --git a/src/libs/services/scanner/bench/Scanner.cpp b/src/libs/services/scanner/bench/Scanner.cpp new file mode 100644 index 00000000..c394aec4 --- /dev/null +++ b/src/libs/services/scanner/bench/Scanner.cpp @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +#include + +BENCHMARK_MAIN(); \ No newline at end of file diff --git a/src/libs/metadata/bench/Metadata.cpp b/src/libs/services/scanner/bench/TrackMetadataParser.cpp similarity index 75% rename from src/libs/metadata/bench/Metadata.cpp rename to src/libs/services/scanner/bench/TrackMetadataParser.cpp index 16195076..b0aeaa41 100644 --- a/src/libs/metadata/bench/Metadata.cpp +++ b/src/libs/services/scanner/bench/TrackMetadataParser.cpp @@ -19,30 +19,22 @@ #include -#include "AudioFileParser.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" + #include "TestTagReader.hpp" -#include "core/String.hpp" -#include "metadata/Types.hpp" -namespace lms::metadata::benchmarks +namespace lms::scanner::benchmarks { - class TestAudioFileParser : public AudioFileParser - { - public: - using AudioFileParser::AudioFileParser; - using AudioFileParser::parseMetaData; - }; - static void BM_Metadata_parse(benchmark::State& state) { - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" }; - std::unique_ptr testTags{ tests::createDefaultPopulatedTestTagReader() }; - const TestAudioFileParser parser{ params }; + std::unique_ptr testTags{ tests::createDefaultPopulatedTestTagReader() }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(*testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(*testTags)); } } @@ -50,16 +42,15 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - const AudioFileParserParameters params; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -67,11 +58,11 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.artistTagDelimiters = { "/", ";" }; // The list itself is not important, the idea is to have some volume params.artistsToNotSplit = { "AC/DC", @@ -113,10 +104,10 @@ namespace lms::metadata::benchmarks "White/Light", "Yamantaka // Sonic Titan" }; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -124,18 +115,18 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.artistTagDelimiters = { "/", ";" }; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -144,6 +135,4 @@ namespace lms::metadata::benchmarks BENCHMARK(BM_Metadata_parseArtists_WithWhitelist); BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist); -} // namespace lms::metadata::benchmarks - -BENCHMARK_MAIN(); \ No newline at end of file +} // namespace lms::scanner::benchmarks diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index d29bdb70..4834547b 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -27,15 +27,16 @@ #include "core/IJobScheduler.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" + #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/ScanSettings.hpp" -#include "scanners/ArtistInfoFileScanner.hpp" -#include "scanners/AudioFileScanner.hpp" #include "scanners/ImageFileScanner.hpp" -#include "scanners/LyricsFileScanner.hpp" -#include "scanners/PlayListFileScanner.hpp" +#include "scanners/artistinfo/ArtistInfoFileScanner.hpp" +#include "scanners/audiofile/AudioFileScanner.hpp" +#include "scanners/lyrics/LyricsFileScanner.hpp" +#include "scanners/playlist/PlayListFileScanner.hpp" #include "steps/ScanStepArtistReconciliation.hpp" #include "steps/ScanStepAssociateArtistImages.hpp" diff --git a/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp b/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp index abb926cc..43d47ace 100644 --- a/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp +++ b/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp @@ -21,13 +21,14 @@ #include "core/ILogger.hpp" #include "database/Session.hpp" -#include "metadata/Types.hpp" + +#include "types/TrackMetadata.hpp" namespace lms::scanner::helpers { namespace { - db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo) + db::Artist::pointer createArtist(db::Session& session, const Artist& artistInfo) { db::Artist::pointer artist{ session.create(artistInfo.name) }; @@ -38,55 +39,25 @@ namespace lms::scanner::helpers return artist; } - - std::string optionalMBIDAsString(const std::optional& uuid) - { - return uuid ? std::string{ uuid->getAsString() } : ""; - } - - void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo) - { - // MBID may be set - if (artist->getMBID() != artistInfo.mbid) - artist.modify()->setMBID(artistInfo.mbid); - - // Name may have been updated - if (artist->getName() != artistInfo.name) - { - LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'"); - artist.modify()->setName(artistInfo.name); - } - - // Sortname may have been updated - // As the sort name is quite often not filled in, we update it only if already set (for now?) - if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName()) - { - LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'"); - artist.modify()->setSortName(*artistInfo.sortName); - } - } - } // namespace - db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { assert(artistInfo.mbid.has_value()); + db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) }; - if (artist) - { - updateArtistIfNeeded(artist, artistInfo); - } - else + if (!artist) { if (allowFallbackOnMBIDEntries.value()) { // an artist with the same name may already exist, let's recycle it for (const db::Artist::pointer& artistWithSameName : db::Artist::find(session, artistInfo.name)) { + assert(artistWithSameName->getMBID() != artistInfo.mbid); if (!artistWithSameName->hasMBID()) { artist = artistWithSameName; - updateArtistIfNeeded(artist, artistInfo); + artist.modify()->setMBID(artistInfo.mbid); break; } } @@ -99,8 +70,10 @@ namespace lms::scanner::helpers return artist; } - db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { + assert(artistInfo.mbid == std::nullopt); + db::Artist::pointer artist; // Here we can have only one artist with no MBID, others all have mbids @@ -111,10 +84,7 @@ namespace lms::scanner::helpers if (!allowFallbackOnMBIDEntries.value() || artistCountWithMBID > 1) { if (itArtistWithoutMBID != std::end(artistsWithSameName)) - { artist = *itArtistWithoutMBID; - updateArtistIfNeeded(artist, artistInfo); - } else artist = createArtist(session, artistInfo); } @@ -123,15 +93,9 @@ namespace lms::scanner::helpers const auto itArtistWithMBID{ std::find_if(std::begin(artistsWithSameName), std::end(artistsWithSameName), [](const db::Artist::pointer& artist) { return artist->hasMBID(); }) }; if (itArtistWithMBID != std::end(artistsWithSameName)) - { artist = *itArtistWithMBID; - // not updating artist here: consider metadata quality is less good - } else if (itArtistWithoutMBID != std::end(artistsWithSameName)) - { artist = *itArtistWithoutMBID; - updateArtistIfNeeded(artist, artistInfo); - } else artist = createArtist(session, artistInfo); } @@ -139,7 +103,7 @@ namespace lms::scanner::helpers return artist; } - db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { // First try to get by MBID if (artistInfo.mbid) diff --git a/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp b/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp index 1c81320a..503441aa 100644 --- a/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp +++ b/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp @@ -20,9 +20,10 @@ #pragma once #include "core/TaggedType.hpp" + #include "database/objects/Artist.hpp" -namespace lms::metadata +namespace lms::scanner { struct Artist; } @@ -31,8 +32,8 @@ namespace lms::scanner::helpers { using AllowFallbackOnMBIDEntry = core::TaggedBool; - db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); - db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); - db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); } // namespace lms::scanner::helpers \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp similarity index 82% rename from src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp index c6a984be..7e333441 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp @@ -24,19 +24,22 @@ #include "core/ILogger.hpp" #include "core/String.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/ArtistInfo.hpp" #include "database/objects/MediaLibrary.hpp" -#include "metadata/ArtistInfo.hpp" -#include "metadata/Types.hpp" + #include "services/scanner/ScanErrors.hpp" -#include "FileScanOperationBase.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/artistinfo/ArtistInfoParser.hpp" +#include "types/ArtistInfo.hpp" +#include "types/TrackMetadata.hpp" namespace lms::scanner { @@ -57,7 +60,7 @@ namespace lms::scanner std::string getArtistNameFromArtistInfoFilePath(); - std::optional _parsedArtistInfo; + std::optional _parsedArtistInfo; }; void ArtistInfoFileScanOperation::scan() @@ -72,14 +75,14 @@ namespace lms::scanner return; } - _parsedArtistInfo = metadata::parseArtistInfo(ifs); + _parsedArtistInfo = parseArtistInfo(ifs); if (_parsedArtistInfo->name.empty()) { _parsedArtistInfo->name = getFilePath().parent_path().filename(); LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'"); } } - catch (const metadata::ArtistInfoParseException& e) + catch (const ArtistInfoParseException& e) { addError(getFilePath()); } @@ -121,8 +124,22 @@ namespace lms::scanner db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary)); - const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional(_parsedArtistInfo->sortName) }; + const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional(_parsedArtistInfo->sortName) }; + db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) }; + // Artist info is the highest priority source for artist name/sort name, so update it as needed + if (artist->getName() != artistMetadata.name) + { + LMS_LOG(DBUPDATER, DEBUG, "Updated artist name from '" << artist->getName() << "' to '" << artistMetadata.name << "' using artist info file"); + artist.modify()->setName(artistMetadata.name); + } + + if (artist->getSortName() != artistMetadata.sortName) + { + LMS_LOG(DBUPDATER, DEBUG, "Updated artist sort name from '" << artist->getSortName() << "' to '" << (artistMetadata.sortName ? *artistMetadata.sortName : "") << "' using artist info file"); + artist.modify()->setSortName(artistMetadata.sortName ? *artistMetadata.sortName : ""); + } + artistInfo.modify()->setArtist(artist); artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID()); @@ -150,7 +167,7 @@ namespace lms::scanner std::span ArtistInfoFileScanner::getSupportedFiles() const { - return metadata::getSupportedArtistInfoFiles(); + return getSupportedArtistInfoFiles(); } std::span ArtistInfoFileScanner::getSupportedExtensions() const diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp index 8036d4e9..d64e7f19 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/ArtistInfo.cpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp similarity index 97% rename from src/libs/metadata/impl/ArtistInfo.cpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp index bcba6b88..4b1d7cea 100644 --- a/src/libs/metadata/impl/ArtistInfo.cpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "metadata/ArtistInfo.hpp" +#include "ArtistInfoParser.hpp" #include @@ -25,7 +25,7 @@ #include "core/LiteralString.hpp" #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { namespace { @@ -77,4 +77,4 @@ namespace lms::metadata return artistInfo; } -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/metadata/impl/IImageReader.hpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp similarity index 64% rename from src/libs/metadata/impl/IImageReader.hpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp index 84e42cc0..7b4ad3d3 100644 --- a/src/libs/metadata/impl/IImageReader.hpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp @@ -19,18 +19,22 @@ #pragma once -#include +#include +#include +#include -#include "metadata/Types.hpp" +#include "core/Exception.hpp" -namespace lms::metadata +#include "types/ArtistInfo.hpp" + +namespace lms::scanner { - class IImageReader + class ArtistInfoParseException : public core::LmsException { public: - virtual ~IImageReader() = default; - - using ImageVisitor = std::function; - virtual void visitImages(ImageVisitor visitor) const = 0; + using core::LmsException::LmsException; }; -} // namespace lms::metadata + + std::span getSupportedArtistInfoFiles(); + ArtistInfo parseArtistInfo(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp similarity index 66% rename from src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 87d8b92f..814b9242 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -24,6 +24,11 @@ #include "core/PartialDateTime.hpp" #include "core/Path.hpp" #include "core/XxHash3.hpp" + +#include "audio/IAudioFileInfo.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/Types.hpp" @@ -40,36 +45,34 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackFeatures.hpp" #include "database/objects/TrackLyrics.hpp" -#include "image/Exception.hpp" -#include "image/Image.hpp" -#include "metadata/Exception.hpp" -#include "metadata/IAudioFileParser.hpp" + #include "services/scanner/ScanErrors.hpp" -#include "IFileScanOperation.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" +#include "scanners/IFileScanOperation.hpp" +#include "scanners/Utils.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms::scanner { namespace { - void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) + void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) { - for (const metadata::Artist& artistInfo : artists) + for (const Artist& artist : artists) { - db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) }; + db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) }; - const bool matchedUsingMbid{ artistInfo.mbid.has_value() && artist->getMBID() == artistInfo.mbid }; - db::TrackArtistLink::pointer link{ session.create(track, artist, linkType, role, matchedUsingMbid) }; - link.modify()->setArtistName(artistInfo.name); - if (artistInfo.sortName) - link.modify()->setArtistSortName(*artistInfo.sortName); + const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid }; + db::TrackArtistLink::pointer link{ session.create(track, dbArtist, linkType, role, matchedUsingMbid) }; + link.modify()->setArtistName(artist.name); + if (artist.sortName) + link.modify()->setArtistSortName(*artist.sortName); } } - void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) + void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) { constexpr std::string_view noRole{}; createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback); @@ -102,128 +105,128 @@ namespace lms::scanner return label; } - void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo) + void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release) { - if (release->getName() != releaseInfo.name) - release.modify()->setName(releaseInfo.name); - if (release->getSortName() != releaseInfo.sortName) - release.modify()->setSortName(releaseInfo.sortName); - if (release->getGroupMBID() != releaseInfo.groupMBID) - release.modify()->setGroupMBID(releaseInfo.groupMBID); - if (release->getTotalDisc() != releaseInfo.mediumCount) - release.modify()->setTotalDisc(releaseInfo.mediumCount); - if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) - release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); - if (release->isCompilation() != releaseInfo.isCompilation) - release.modify()->setCompilation(releaseInfo.isCompilation); - if (release->getBarcode() != releaseInfo.barcode) - release.modify()->setBarcode(releaseInfo.barcode); - if (release->getComment() != releaseInfo.comment) - release.modify()->setComment(releaseInfo.comment); - if (release->getReleaseTypeNames() != releaseInfo.releaseTypes) + if (dbRelease->getName() != release.name) + dbRelease.modify()->setName(release.name); + if (dbRelease->getSortName() != release.sortName) + dbRelease.modify()->setSortName(release.sortName); + if (dbRelease->getGroupMBID() != release.groupMBID) + dbRelease.modify()->setGroupMBID(release.groupMBID); + if (dbRelease->getTotalDisc() != release.mediumCount) + dbRelease.modify()->setTotalDisc(release.mediumCount); + if (dbRelease->getArtistDisplayName() != release.artistDisplayName) + dbRelease.modify()->setArtistDisplayName(release.artistDisplayName); + if (dbRelease->isCompilation() != release.isCompilation) + dbRelease.modify()->setCompilation(release.isCompilation); + if (dbRelease->getBarcode() != release.barcode) + dbRelease.modify()->setBarcode(release.barcode); + if (dbRelease->getComment() != release.comment) + dbRelease.modify()->setComment(release.comment); + if (dbRelease->getReleaseTypeNames() != release.releaseTypes) { - release.modify()->clearReleaseTypes(); - for (std::string_view releaseType : releaseInfo.releaseTypes) - release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType)); + dbRelease.modify()->clearReleaseTypes(); + for (std::string_view releaseType : release.releaseTypes) + dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType)); } - if (release->getCountryNames() != releaseInfo.countries) + if (dbRelease->getCountryNames() != release.countries) { - release.modify()->clearCountries(); - for (std::string_view country : releaseInfo.countries) - release.modify()->addCountry(getOrCreateCountry(session, country)); + dbRelease.modify()->clearCountries(); + for (std::string_view country : release.countries) + dbRelease.modify()->addCountry(getOrCreateCountry(session, country)); } - if (release->getLabelNames() != releaseInfo.labels) + if (dbRelease->getLabelNames() != release.labels) { - release.modify()->clearLabels(); - for (std::string_view label : releaseInfo.labels) - release.modify()->addLabel(getOrCreateLabel(session, label)); + dbRelease.modify()->clearLabels(); + for (std::string_view label : release.labels) + dbRelease.modify()->addLabel(getOrCreateLabel(session, label)); } } // Compare release level info - bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo) + bool isReleaseMatching(const db::Release::pointer& dbCandidateRelease, const Release& release) { // TODO: add more criterias? - return candidateRelease->getName() == releaseInfo.name - && candidateRelease->getSortName() == releaseInfo.sortName - && candidateRelease->getTotalDisc() == releaseInfo.mediumCount - && candidateRelease->isCompilation() == releaseInfo.isCompilation - && candidateRelease->getLabelNames() == releaseInfo.labels - && candidateRelease->getBarcode() == releaseInfo.barcode; + return dbCandidateRelease->getName() == release.name + && dbCandidateRelease->getSortName() == release.sortName + && dbCandidateRelease->getTotalDisc() == release.mediumCount + && dbCandidateRelease->isCompilation() == release.isCompilation + && dbCandidateRelease->getLabelNames() == release.labels + && dbCandidateRelease->getBarcode() == release.barcode; } - db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory) + db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory) { - db::Release::pointer release; + db::Release::pointer dbRelease; // First try to get by MBID: fastest, safest - if (releaseInfo.mbid) + if (release.mbid) { - release = db::Release::find(session, *releaseInfo.mbid); - if (!release) - release = session.create(releaseInfo.name, releaseInfo.mbid); + dbRelease = db::Release::find(session, *release.mbid); + if (!dbRelease) + dbRelease = session.create(release.name, release.mbid); } - else if (releaseInfo.name.empty()) + else if (release.name.empty()) { // No release name (only mbid) -> nothing to do - return release; + return dbRelease; } // Fall back on release name (collisions may occur) // First try using all sibling directories (case for Album/DiscX), only if the disc number is set const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() }; - if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid()) + if (!dbRelease && release.mediumCount && *release.mediumCount > 1 && parentDirectoryId.isValid()) { db::Release::FindParameters params; params.setParentDirectory(parentDirectoryId); - params.setName(releaseInfo.name); - db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { + params.setName(release.name); + db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) { // Already found a candidate - if (release) + if (dbRelease) return; // Do not fallback on properly tagged releases - if (candidateRelease->getMBID().has_value()) + if (dbCandidateRelease->getMBID().has_value()) return; - if (!isReleaseMatching(candidateRelease, releaseInfo)) + if (!isReleaseMatching(dbCandidateRelease, release)) return; - release = candidateRelease; + dbRelease = dbCandidateRelease; }); } // Lastly try in the current directory: we do this at last to have // opportunities to merge releases in case of migration / rescan - if (!release) + if (!dbRelease) { db::Release::FindParameters params; params.setDirectory(currentDirectory->getId()); - params.setName(releaseInfo.name); - db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { + params.setName(release.name); + db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) { // Already found a candidate - if (release) + if (dbRelease) return; // Do not fallback on properly tagged releases - if (candidateRelease->getMBID().has_value()) + if (dbCandidateRelease->getMBID().has_value()) return; - if (!isReleaseMatching(candidateRelease, releaseInfo)) + if (!isReleaseMatching(dbCandidateRelease, release)) return; - release = candidateRelease; + dbRelease = dbCandidateRelease; }); } - if (!release) - release = session.create(releaseInfo.name); + if (!dbRelease) + dbRelease = session.create(release.name); - updateReleaseIfNeeded(session, release, releaseInfo); - return release; + updateReleaseIfNeeded(session, dbRelease, release); + return dbRelease; } - db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release) + db::Medium::pointer getOrCreateMedium(db::Session& session, const Medium& medium, const db::Release::pointer& release) { db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) }; if (!dbMedium) @@ -243,7 +246,7 @@ namespace lms::scanner return dbMedium; } - std::vector getOrCreateClusters(db::Session& session, const metadata::Track& track) + std::vector getOrCreateClusters(db::Session& session, const Track& track) { std::vector clusters; @@ -274,69 +277,69 @@ namespace lms::scanner return clusters; } - db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo) + db::TrackLyrics::pointer createLyrics(db::Session& session, const Lyrics& lyrics) { - db::TrackLyrics::pointer lyrics{ session.create() }; + db::TrackLyrics::pointer dbLyrics{ session.create() }; - lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx"); - lyrics.modify()->setOffset(lyricsInfo.offset); - lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist); - lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle); - if (!lyricsInfo.synchronizedLines.empty()) - lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines); + dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx"); + dbLyrics.modify()->setOffset(lyrics.offset); + dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist); + dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle); + if (!lyrics.synchronizedLines.empty()) + dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines); else - lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines); + dbLyrics.modify()->setUnsynchronizedLines(lyrics.unsynchronizedLines); - return lyrics; + return dbLyrics; } - db::ImageType convertImageType(metadata::Image::Type type) + db::ImageType convertImageType(audio::Image::Type type) { switch (type) { - case metadata::Image::Type::Unknown: + case audio::Image::Type::Unknown: return db::ImageType::Unknown; - case metadata::Image::Type::Other: + case audio::Image::Type::Other: return db::ImageType::Other; - case metadata::Image::Type::FileIcon: + case audio::Image::Type::FileIcon: return db::ImageType::FileIcon; - case metadata::Image::Type::OtherFileIcon: + case audio::Image::Type::OtherFileIcon: return db::ImageType::OtherFileIcon; - case metadata::Image::Type::FrontCover: + case audio::Image::Type::FrontCover: return db::ImageType::FrontCover; - case metadata::Image::Type::BackCover: + case audio::Image::Type::BackCover: return db::ImageType::BackCover; - case metadata::Image::Type::LeafletPage: + case audio::Image::Type::LeafletPage: return db::ImageType::LeafletPage; - case metadata::Image::Type::Media: + case audio::Image::Type::Media: return db::ImageType::Media; - case metadata::Image::Type::LeadArtist: + case audio::Image::Type::LeadArtist: return db::ImageType::LeadArtist; - case metadata::Image::Type::Artist: + case audio::Image::Type::Artist: return db::ImageType::Artist; - case metadata::Image::Type::Conductor: + case audio::Image::Type::Conductor: return db::ImageType::Conductor; - case metadata::Image::Type::Band: + case audio::Image::Type::Band: return db::ImageType::Band; - case metadata::Image::Type::Composer: + case audio::Image::Type::Composer: return db::ImageType::Composer; - case metadata::Image::Type::Lyricist: + case audio::Image::Type::Lyricist: return db::ImageType::Lyricist; - case metadata::Image::Type::RecordingLocation: + case audio::Image::Type::RecordingLocation: return db::ImageType::RecordingLocation; - case metadata::Image::Type::DuringRecording: + case audio::Image::Type::DuringRecording: return db::ImageType::DuringRecording; - case metadata::Image::Type::DuringPerformance: + case audio::Image::Type::DuringPerformance: return db::ImageType::DuringPerformance; - case metadata::Image::Type::MovieScreenCapture: + case audio::Image::Type::MovieScreenCapture: return db::ImageType::MovieScreenCapture; - case metadata::Image::Type::ColouredFish: + case audio::Image::Type::ColouredFish: return db::ImageType::ColouredFish; - case metadata::Image::Type::Illustration: + case audio::Image::Type::Illustration: return db::ImageType::Illustration; - case metadata::Image::Type::BandLogo: + case audio::Image::Type::BandLogo: return db::ImageType::BandLogo; - case metadata::Image::Type::PublisherLogo: + case audio::Image::Type::PublisherLogo: return db::ImageType::PublisherLogo; } @@ -361,10 +364,10 @@ namespace lms::scanner return image; } - db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo) + db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& dbTrack, const ImageInfo& imageInfo) { const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) }; - db::TrackEmbeddedImageLink::pointer imageLink{ session.create(track, image) }; + db::TrackEmbeddedImageLink::pointer imageLink{ session.create(dbTrack, image) }; imageLink.modify()->setIndex(imageInfo.index); imageLink.modify()->setType(convertImageType(imageInfo.type)); imageLink.modify()->setDescription(imageInfo.description); @@ -372,35 +375,35 @@ namespace lms::scanner return imageLink; } - void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span images) + void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span images) { - track.modify()->clearEmbeddedImageLinks(); + dbTrack.modify()->clearEmbeddedImageLinks(); for (const ImageInfo& imageInfo : images) { - db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) }; - track.modify()->addEmbeddedImageLink(link); + db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) }; + dbTrack.modify()->addEmbeddedImageLink(link); } } - db::Advisory getAdvisory(std::optional advisory) + db::Advisory getAdvisory(std::optional advisory) { if (!advisory) return db::Advisory::UnSet; switch (advisory.value()) { - case metadata::Track::Advisory::Clean: + case Track::Advisory::Clean: return db::Advisory::Clean; - case metadata::Track::Advisory::Explicit: + case Track::Advisory::Explicit: return db::Advisory::Explicit; - case metadata::Track::Advisory::Unknown: + case Track::Advisory::Unknown: return db::Advisory::Unknown; } return db::Advisory::UnSet; } - db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize) + db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize) { db::Track::FindParameters params; // Add as many fields as possible to limit errors @@ -436,9 +439,9 @@ namespace lms::scanner return res; } - void fillInArtistsWithMbid(std::span artists, std::unordered_map& artistsWithMbid) + void fillInArtistsWithMbid(std::span artists, std::unordered_map& artistsWithMbid) { - for (const metadata::Artist& artist : artists) + for (const Artist& artist : artists) { if (artist.mbid.has_value()) { @@ -448,9 +451,9 @@ namespace lms::scanner } } - void fillInMbids(std::span artists, const std::unordered_map& artistsWithMbid) + void fillInMbids(std::span artists, const std::unordered_map& artistsWithMbid) { - for (metadata::Artist& artist : artists) + for (Artist& artist : artists) { if (!artist.mbid) { @@ -461,7 +464,7 @@ namespace lms::scanner } } - void fillMissingMbids(metadata::Track& track) + void fillMissingMbids(Track& track) { // first pass: collect all artists that have mbids std::unordered_map artistsWithMbid; @@ -485,9 +488,10 @@ namespace lms::scanner } } // namespace - AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser) + AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions) : FileScanOperationBase{ std::move(fileToScan), db, settings } - , _parser{ parser } + , _metadataParser{ metadataParser } + , _parserOptions{ parserOptions } { } @@ -495,17 +499,20 @@ namespace lms::scanner void AudioFileScanOperation::scan() { - std::unique_ptr track; - try { - _parsedTrack = _parser.parseMetaData(getFilePath()); + auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) }; + + _file.emplace(); + + _file->audioProperties = audioFileInfo->getAudioProperties(); + _file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader()); // We fill missing artist mbids with mbids found on other artist roles - fillMissingMbids(*_parsedTrack); + fillMissingMbids(_file->track); std::size_t index{}; - _parser.parseImages(getFilePath(), [&](const metadata::Image& image) { + audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) { try { image::ImageProperties properties{ image::probeImage(image.data) }; @@ -522,7 +529,7 @@ namespace lms::scanner info.description = image.description; info.properties = properties; - _parsedImages.push_back(std::move(info)); + _file->images.push_back(std::move(info)); } catch (const image::Exception& e) { @@ -532,15 +539,11 @@ namespace lms::scanner index++; }); } - catch (const metadata::AudioFileNoAudioPropertiesException&) - { - addError(getFilePath()); - } - catch (const metadata::IOException& e) + catch (const audio::IOException& e) { addError(getFilePath(), e.getErrorCode()); } - catch (const metadata::Exception& e) + catch (const audio::Exception& e) { addError(getFilePath()); } @@ -552,7 +555,7 @@ namespace lms::scanner db::Session& dbSession{ getDb().getTLSSession() }; db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) }; - if (!_parsedTrack) + if (!_file) { if (track) { @@ -562,9 +565,9 @@ namespace lms::scanner return OperationResult::Skipped; } - if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID)) + if (_file->track.mbid && (!track || getScannerSettings().skipDuplicateTrackMBID)) { - std::vector duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) }; + std::vector duplicateTracks{ db::Track::findByMBID(dbSession, *_file->track.mbid) }; // find for an existing track MBID as the file may have just been moved if (!track && duplicateTracks.size() == 1) @@ -616,7 +619,7 @@ namespace lms::scanner if (!track) { // maybe the file just moved? - track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize()); + track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize()); if (track) { LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath()); @@ -625,7 +628,7 @@ namespace lms::scanner } // We estimate this is an audio file if the duration is not null - if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero()) + if (_file->audioProperties.duration == std::chrono::milliseconds::zero()) { addError(getFilePath()); @@ -639,8 +642,8 @@ namespace lms::scanner // ***** Title std::string title; - if (!_parsedTrack->title.empty()) - title = _parsedTrack->title; + if (!_file->track.title.empty()) + title = _file->track.title; else { // TODO parse file name to guess track etc. @@ -665,18 +668,18 @@ namespace lms::scanner track.modify()->setScanVersion(getScannerSettings().audioScanVersion); // Audio properties - track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate); - track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample); - track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount); - track.modify()->setDuration(_parsedTrack->audioProperties.duration); - track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate); + track.modify()->setBitrate(_file->audioProperties.bitrate); + track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0); + track.modify()->setChannelCount(_file->audioProperties.channelCount); + track.modify()->setDuration(_file->audioProperties.duration); + track.modify()->setSampleRate(_file->audioProperties.sampleRate); track.modify()->setFileSize(getFileSize()); track.modify()->setLastWriteTime(getLastWriteTime()); - if (_parsedTrack->encodingTime.isValid()) + if (_file->track.encodingTime.isValid()) { - const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime }; + const core::PartialDateTime& encodingTime{ _file->track.encodingTime }; Wt::WDate date; Wt::WTime time; if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day) @@ -696,61 +699,61 @@ namespace lms::scanner track.modify()->clearArtistLinks(); const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback); - if (_parsedTrack->medium && _parsedTrack->medium->release) - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback); + if (_file->track.medium && _file->track.medium->release) + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _file->track.remixerArtists, allowFallback); - for (const auto& [role, performers] : _parsedTrack->performerArtists) + for (const auto& [role, performers] : _file->track.performerArtists) createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); // For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set - if (_parsedTrack->medium && _parsedTrack->medium->release) + if (_file->track.medium && _file->track.medium->release) { - db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) }; + db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) }; assert(release); track.modify()->setRelease(release); - track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release)); + track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release)); } else { track.modify()->setRelease({}); track.modify()->setMedium({}); } - track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); + track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track)); track.modify()->setName(title); - track.modify()->setTrackNumber(_parsedTrack->position); - track.modify()->setDate(_parsedTrack->date); - track.modify()->setOriginalDate(_parsedTrack->originalDate); - if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) - track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear }); + track.modify()->setTrackNumber(_file->track.position); + track.modify()->setDate(_file->track.date); + track.modify()->setOriginalDate(_file->track.originalDate); + if (!track->getOriginalDate().isValid() && _file->track.originalYear) + track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear }); // If a file has an OriginalDate but no date, set it to ease filtering - if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid()) - track.modify()->setDate(_parsedTrack->originalDate); + if (!_file->track.date.isValid() && _file->track.originalDate.isValid()) + track.modify()->setDate(_file->track.originalDate); - track.modify()->setRecordingMBID(_parsedTrack->recordingMBID); - track.modify()->setTrackMBID(_parsedTrack->mbid); + track.modify()->setRecordingMBID(_file->track.recordingMBID); + track.modify()->setTrackMBID(_file->track.mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setCopyright(_parsedTrack->copyright); - track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); - track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); - track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now - track.modify()->setReplayGain(_parsedTrack->replayGain); - track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); + track.modify()->setCopyright(_file->track.copyright); + track.modify()->setCopyrightURL(_file->track.copyrightURL); + track.modify()->setAdvisory(getAdvisory(_file->track.advisory)); + track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now + track.modify()->setReplayGain(_file->track.replayGain); + track.modify()->setArtistDisplayName(_file->track.artistDisplayName); track.modify()->clearEmbeddedLyrics(); - for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics) + for (const Lyrics& lyricsInfo : _file->track.lyrics) track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo)); - updateEmbeddedImages(dbSession, track, _parsedImages); + updateEmbeddedImages(dbSession, track, _file->images); if (added) { diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp similarity index 67% rename from src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp index 20344ee9..7d5e00ff 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp @@ -19,33 +19,32 @@ #pragma once -#include "IFileScanOperation.hpp" - -#include #include +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" #include "image/Types.hpp" -#include "metadata/Types.hpp" -#include "FileScanOperationBase.hpp" -#include "FileToScan.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/FileToScan.hpp" +#include "scanners/IFileScanOperation.hpp" + +#include "types/TrackMetadata.hpp" namespace lms::db { class IDb; } // namespace lms::db -namespace lms::metadata -{ - class IAudioFileParser; -} // namespace lms::metadata - namespace lms::scanner { + class TrackMetadataParser; + struct ImageInfo { std::size_t index; - metadata::Image::Type type{ metadata::Image::Type::Unknown }; + audio::Image::Type type{ audio::Image::Type::Unknown }; std::uint64_t hash{}; std::size_t size{}; image::ImageProperties properties; @@ -56,7 +55,7 @@ namespace lms::scanner class AudioFileScanOperation : public FileScanOperationBase { public: - AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser); + AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions); ~AudioFileScanOperation() override; AudioFileScanOperation(const AudioFileScanOperation&) = delete; AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete; @@ -66,9 +65,15 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - metadata::IAudioFileParser& _parser; - std::unique_ptr _parsedTrack; - std::vector _parsedImages; - }; + const TrackMetadataParser& _metadataParser; + const audio::ParserOptions& _parserOptions; + struct AudioFileInfo + { + audio::AudioProperties audioProperties; + Track track; + std::vector images; + }; + std::optional _file; + }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp similarity index 71% rename from src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp index 5b8d7a0c..f38d65af 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp @@ -21,53 +21,64 @@ #include "core/IConfig.hpp" #include "core/Service.hpp" + +#include "audio/IAudioFileInfo.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/Track.hpp" -#include "metadata/IAudioFileParser.hpp" -#include "AudioFileScanOperation.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" +#include "scanners/Utils.hpp" +#include "scanners/audiofile/AudioFileScanOperation.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms::scanner { namespace { - metadata::ParserReadStyle getParserReadStyle() + audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle() { std::string_view readStyle{ core::Service::get()->getString("scanner-parser-read-style", "average") }; if (readStyle == "fast") - return metadata::ParserReadStyle::Fast; + return audio::ParserOptions::AudioPropertiesReadStyle::Fast; if (readStyle == "average") - return metadata::ParserReadStyle::Average; + return audio::ParserOptions::AudioPropertiesReadStyle::Average; if (readStyle == "accurate") - return metadata::ParserReadStyle::Accurate; + return audio::ParserOptions::AudioPropertiesReadStyle::Accurate; throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; } - metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings) + TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings) { - metadata::AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.userExtraTags = settings.extraTags; params.artistTagDelimiters = settings.artistTagDelimiters; params.defaultTagDelimiters = settings.defaultTagDelimiters; params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end()); - params.backend = metadata::ParserBackend::TagLib; - params.readStyle = getParserReadStyle(); return params; } + audio::ParserOptions createAudioFileParserOptions() + { + audio::ParserOptions options; + options.readStyle = getParserReadStyle(); + options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib + + return options; + } + } // namespace AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings) : _db{ db } , _settings{ settings } - , _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib + , _trackMetadataParser{ createTrackMetadataParserParameters(settings) } + , _parserOptions{ createAudioFileParserOptions() } { } @@ -85,7 +96,7 @@ namespace lms::scanner std::span AudioFileScanner::getSupportedExtensions() const { - return _metadataParser->getSupportedExtensions(); + return audio::getSupportedExtensions(_parserOptions.parser); } bool AudioFileScanner::needsScan(const FileToScan& file) const @@ -101,6 +112,6 @@ namespace lms::scanner std::unique_ptr AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(std::move(fileToScan), _db, _settings, *_metadataParser); + return std::make_unique(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp similarity index 88% rename from src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp index 924f4baf..5a8b5c32 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp @@ -19,7 +19,10 @@ #pragma once -#include "IFileScanner.hpp" +#include "audio/IAudioFileInfo.hpp" + +#include "scanners/IFileScanner.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms { @@ -55,6 +58,7 @@ namespace lms::scanner db::IDb& _db; const ScannerSettings& _settings; - std::unique_ptr _metadataParser; + const TrackMetadataParser _trackMetadataParser; + const audio::ParserOptions _parserOptions; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/metadata/impl/AudioFileParser.cpp b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp similarity index 78% rename from src/libs/metadata/impl/AudioFileParser.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp index 5878e7d4..230d2e30 100644 --- a/src/libs/metadata/impl/AudioFileParser.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp @@ -17,30 +17,26 @@ * along with LMS. If not, see . */ -#include "AudioFileParser.hpp" +#include "TrackMetadataParser.hpp" #include #include #include +#include "audio/IAudioFileInfo.hpp" #include "core/ILogger.hpp" #include "core/PartialDateTime.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" + +#include "scanners/lyrics/LyricsParser.hpp" #include "Utils.hpp" -#include "avformat/AvFormatImageReader.hpp" -#include "avformat/AvFormatTagReader.hpp" -#include "avformat/Utils.hpp" -#include "taglib/TagLibImageReader.hpp" -#include "taglib/TagLibTagReader.hpp" -#include "taglib/Utils.hpp" -namespace lms::metadata +namespace lms::scanner { namespace { - void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span tagDelimiters, ITagReader::TagValueVisitor visitor) + void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span tagDelimiters, audio::ITagReader::TagValueVisitor visitor) { tagReader.visitTagValues(tagType, [&](std::string_view value) { auto visitTagIfNonEmpty{ [&](std::string_view tag) { @@ -76,11 +72,11 @@ namespace lms::metadata } template - std::vector getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters, const WhiteList* whitelist = nullptr) + std::vector getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr) { std::vector res; - for (const TagType tagType : tagTypes) + for (const audio::TagType tagType : tagTypes) { tagReader.visitTagValues(tagType, [&](std::string_view value) { value = core::stringUtils::stringTrim(value); @@ -150,7 +146,7 @@ namespace lms::metadata } template - std::optional getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes) + std::optional getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list tagTypes) { std::optional res; std::vector values{ getTagValuesFirstMatchAs(tagReader, tagTypes, {} /* don't expect multiple values here */) }; @@ -161,45 +157,38 @@ namespace lms::metadata } template - std::vector getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span tagDelimiters) + std::vector getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span tagDelimiters) { return getTagValuesFirstMatchAs(tagReader, { tagType }, tagDelimiters); } template - std::optional getTagValueAs(const ITagReader& tagReader, TagType tagType) + std::optional getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType) { return getTagValueFirstMatchAs(tagReader, { tagType }); } - std::vector getLyrics(const ITagReader& tagReader) + std::vector getLyrics(const audio::ITagReader& tagReader) { std::vector res; tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) { std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?) - try - { - Lyrics lyrics{ parseLyrics(iss) }; - if (lyrics.language.empty()) - lyrics.language = language; + Lyrics lyrics{ parseLyrics(iss) }; + if (lyrics.language.empty()) + lyrics.language = language; - res.emplace_back(std::move(lyrics)); - } - catch (const LyricsException& e) - { - LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() }); - } + res.emplace_back(std::move(lyrics)); }); return res; } - std::vector getArtists(const ITagReader& tagReader, - std::initializer_list artistTagNames, - std::initializer_list artistSortTagNames, - std::initializer_list artistMBIDTagNames, - const AudioFileParserParameters& params) + std::vector getArtists(const audio::ITagReader& tagReader, + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames, + const TrackMetadataParser::Parameters& params) { std::vector artistNames{ getTagValuesFirstMatchAs(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) }; if (artistNames.empty()) @@ -224,7 +213,7 @@ namespace lms::metadata return artists; } - PerformerContainer getPerformerArtists(const ITagReader& tagReader) + PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader) { PerformerContainer performers; @@ -298,9 +287,9 @@ namespace lms::metadata return artistDisplayName; } - std::optional getAdvisory(const ITagReader& tagReader) + std::optional getAdvisory(const audio::ITagReader& tagReader) { - if (const auto value{ getTagValueAs(tagReader, TagType::Advisory) }) + if (const auto value{ getTagValueAs(tagReader, audio::TagType::Advisory) }) { switch (*value) { @@ -318,92 +307,24 @@ namespace lms::metadata } } // namespace - std::unique_ptr createAudioFileParser(const AudioFileParserParameters& params) - { - return std::make_unique(params); - } - - AudioFileParser::AudioFileParser(const AudioFileParserParameters& params) + TrackMetadataParser::TrackMetadataParser(const Parameters& params) : _params{ params } { - switch (_params.backend) - { - case ParserBackend::TagLib: - LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle)); - break; - - case ParserBackend::AvFormat: - LMS_LOG(METADATA, INFO, "Using AvFormat parser"); - break; - } } - std::span AudioFileParser::getSupportedExtensions() const + TrackMetadataParser::~TrackMetadataParser() = default; + + Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const { - switch (_params.backend) - { - case ParserBackend::TagLib: - return taglib::utils::getSupportedExtensions(); - break; - - case ParserBackend::AvFormat: - return avformat::utils::getSupportedExtensions(); - break; - } - - return {}; - } - - std::unique_ptr AudioFileParser::parseMetaData(const std::filesystem::path& p) const - { - std::unique_ptr tagReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - tagReader = std::make_unique(p, _params.readStyle, _params.debug); - break; - - case ParserBackend::AvFormat: - tagReader = std::make_unique(p, _params.debug); - break; - } - if (!tagReader) - throw AudioFileParsingException{ "Unhandled parser backend" }; - - return parseMetaData(*tagReader); - } - - void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const - { - std::unique_ptr imageReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - imageReader = std::make_unique(p); - break; - - case ParserBackend::AvFormat: - imageReader = std::make_unique(p); - break; - } - if (!imageReader) - throw AudioFileParsingException{ "Unhandled parser backend" }; - - parseImages(*imageReader, std::move(visitor)); - } - - std::unique_ptr AudioFileParser::parseMetaData(const ITagReader& tagReader) const - { - auto track{ std::make_unique() }; - - track->audioProperties = tagReader.getAudioProperties(); - processTags(tagReader, *track); - + Track track; + processTags(tagReader, track); return track; } - void AudioFileParser::processTags(const ITagReader& tagReader, Track& track) const + void TrackMetadataParser::processTags(const audio::ITagReader& tagReader, Track& track) const { + using namespace audio; + track.title = getTagValueAs(tagReader, TagType::TrackTitle).value_or(""); track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID); track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); @@ -469,8 +390,10 @@ namespace lms::metadata track.originalYear = track.originalDate.getYear(); } - std::optional AudioFileParser::getMedium(const ITagReader& tagReader) const + std::optional TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const { + using namespace audio; + std::optional medium; medium.emplace(); @@ -499,8 +422,10 @@ namespace lms::metadata return medium; } - std::optional AudioFileParser::getRelease(const ITagReader& tagReader) const + std::optional TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const { + using namespace audio; + std::optional release; auto releaseName{ getTagValueAs(tagReader, TagType::Album) }; @@ -536,11 +461,4 @@ namespace lms::metadata return release; } - - void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor) - { - reader.visitImages([&](const Image& image) { - visitor(image); - }); - } -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp new file mode 100644 index 00000000..72ccc19d --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "audio/IAudioFileInfo.hpp" +#include "audio/ITagReader.hpp" + +#include "types/TrackMetadata.hpp" + +namespace lms::scanner +{ + class TrackMetadataParser + { + public: + struct SortByLengthDesc + { + bool operator()(const std::string& a, const std::string& b) const + { + if (a.length() != b.length()) + return a.length() > b.length(); + return a < b; // Break ties using lexicographical order + } + }; + + using WhiteList = std::set; + struct Parameters + { + std::vector artistTagDelimiters; + WhiteList artistsToNotSplit; + std::vector defaultTagDelimiters; + std::vector userExtraTags; + }; + + TrackMetadataParser(const Parameters& params = {}); + ~TrackMetadataParser(); + TrackMetadataParser(const TrackMetadataParser&) = delete; + TrackMetadataParser& operator=(const TrackMetadataParser&) = delete; + + Track parseTrackMetaData(const audio::ITagReader& reader) const; + + private: + void processTags(const audio::ITagReader& reader, Track& track) const; + + std::optional getMedium(const audio::ITagReader& tagReader) const; + std::optional getRelease(const audio::ITagReader& tagReader) const; + + const Parameters _params; + }; +} // namespace lms::scanner diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp similarity index 88% rename from src/libs/metadata/impl/Utils.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp index 1152b95e..01a67610 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp @@ -24,9 +24,7 @@ #include #include -#include "core/Exception.hpp" - -namespace lms::metadata::utils +namespace lms::scanner::utils { Wt::WDate parseDate(std::string_view dateStr) { @@ -96,21 +94,6 @@ namespace lms::metadata::utils return result * sign; } - std::string_view readStyleToString(ParserReadStyle readStyle) - { - switch (readStyle) - { - case ParserReadStyle::Fast: - return "fast"; - case ParserReadStyle::Average: - return "average"; - case ParserReadStyle::Accurate: - return "accurate"; - } - - throw Exception{ "Unknown read style" }; - } - PerformerArtist extractPerformerAndRole(std::string_view entry) { std::string_view artistName; @@ -155,4 +138,4 @@ namespace lms::metadata::utils return PerformerArtist{ Artist{ artistName }, std::string{ role } }; } -} // namespace lms::metadata::utils \ No newline at end of file +} // namespace lms::scanner::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp similarity index 86% rename from src/libs/metadata/impl/Utils.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp index 3302fed0..f734229a 100644 --- a/src/libs/metadata/impl/Utils.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp @@ -25,13 +25,12 @@ #include -#include "metadata/Types.hpp" +#include "types/TrackMetadata.hpp" -namespace lms::metadata::utils +namespace lms::scanner::utils { Wt::WDate parseDate(std::string_view dateStr); std::optional parseYear(std::string_view yearStr); - std::string_view readStyleToString(ParserReadStyle readStyle); struct PerformerArtist { @@ -41,4 +40,4 @@ namespace lms::metadata::utils // format is "artist name (role)" PerformerArtist extractPerformerAndRole(std::string_view entry); -} // namespace lms::metadata::utils +} // namespace lms::scanner::utils diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp similarity index 87% rename from src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp index de85094a..b7c943e7 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp @@ -22,17 +22,19 @@ #include #include -#include "FileScanOperationBase.hpp" -#include "ScannerSettings.hpp" #include "core/ILogger.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/TrackLyrics.hpp" -#include "metadata/Lyrics.hpp" #include "services/scanner/ScanErrors.hpp" -#include "Utils.hpp" +#include "ScannerSettings.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/lyrics/LyricsParser.hpp" +#include "types/Lyrics.hpp" namespace lms::scanner { @@ -48,28 +50,21 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - std::optional _parsedLyrics; + std::optional _parsedLyrics; }; void LyricsFileScanOperation::scan() { - try + std::ifstream ifs{ getFilePath() }; + if (!ifs) { - std::ifstream ifs{ getFilePath() }; - if (!ifs) - { - const std::error_code ec{ errno, std::generic_category() }; + const std::error_code ec{ errno, std::generic_category() }; - addError(getFilePath(), ec); - return; - } + addError(getFilePath(), ec); + return; + } - _parsedLyrics = metadata::parseLyrics(ifs); - } - catch (const metadata::Exception& e) - { - addError(getFilePath()); - } + _parsedLyrics = parseLyrics(ifs); } LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult() @@ -140,7 +135,7 @@ namespace lms::scanner std::span LyricsFileScanner::getSupportedExtensions() const { - return metadata::getSupportedLyricsFileExtensions(); + return getSupportedLyricsFileExtensions(); } bool LyricsFileScanner::needsScan(const FileToScan& file) const diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp index 0b265b77..e713bd32 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/Lyrics.cpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp similarity index 98% rename from src/libs/metadata/impl/Lyrics.cpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp index 45b66cc8..f096a766 100644 --- a/src/libs/metadata/impl/Lyrics.cpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp @@ -17,14 +17,14 @@ * along with LMS. If not, see . */ -#include "metadata/Lyrics.hpp" +#include "LyricsParser.hpp" #include #include #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { std::span getSupportedLyricsFileExtensions() { @@ -227,4 +227,4 @@ namespace lms::metadata return lyrics; } -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp new file mode 100644 index 00000000..a20c40df --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "types/Lyrics.hpp" + +namespace lms::scanner +{ + std::span getSupportedLyricsFileExtensions(); + Lyrics parseLyrics(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp similarity index 85% rename from src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp index 809ed1fa..782eeff0 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp @@ -27,12 +27,12 @@ #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/PlayListFile.hpp" -#include "metadata/Exception.hpp" -#include "metadata/PlayList.hpp" -#include "FileScanOperationBase.hpp" -#include "ScanContext.hpp" -#include "Utils.hpp" +#include "services/scanner/ScanErrors.hpp" + +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/playlist/PlayListParser.hpp" namespace lms::scanner { @@ -51,28 +51,21 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - std::optional _parsedPlayList; + std::optional _parsedPlayList; }; void PlayListFileScanOperation::scan() { - try + std::ifstream ifs{ getFilePath() }; + if (!ifs) { - std::ifstream ifs{ getFilePath() }; - if (!ifs) - { - const std::error_code ec{ errno, std::generic_category() }; + const std::error_code ec{ errno, std::generic_category() }; - addError(getFilePath(), ec); - return; - } + addError(getFilePath(), ec); + return; + } - _parsedPlayList = metadata::parsePlayList(ifs); - } - catch (const metadata::Exception& e) - { - addError(getFilePath()); - } + _parsedPlayList = parsePlayList(ifs); } PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult() @@ -139,7 +132,7 @@ namespace lms::scanner std::span PlayListFileScanner::getSupportedExtensions() const { - return metadata::getSupportedPlayListFileExtensions(); + return getSupportedPlayListFileExtensions(); } bool PlayListFileScanner::needsScan(const FileToScan& file) const diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp index 0bbfb8a4..9da3db17 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/PlayList.cpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp similarity index 97% rename from src/libs/metadata/impl/PlayList.cpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp index ca0fadb5..b12c5b85 100644 --- a/src/libs/metadata/impl/PlayList.cpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp @@ -17,14 +17,14 @@ * along with LMS. If not, see . */ -#include "metadata/PlayList.hpp" +#include "PlayListParser.hpp" #include #include #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { std::span getSupportedPlayListFileExtensions() { @@ -102,4 +102,4 @@ namespace lms::metadata return playlist; } -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp new file mode 100644 index 00000000..edef9ca1 --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "types/PlayList.hpp" + +namespace lms::scanner +{ + std::span getSupportedPlayListFileExtensions(); + PlayList parsePlayList(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp index 77d2bbdd..3b095f18 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp @@ -23,6 +23,7 @@ #include #include "core/ILogger.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -30,11 +31,11 @@ #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackList.hpp" -#include "metadata/Types.hpp" #include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "helpers/ArtistHelpers.hpp" +#include "types/TrackMetadata.hpp" namespace lms::scanner { @@ -53,7 +54,7 @@ namespace lms::scanner { assert(!link->isArtistMBIDMatched()); - metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional(link->getArtistSortName()) }; + Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional(link->getArtistSortName()) }; db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) }; LMS_LOG(DB, DEBUG, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast(link->getType()) << " from " << link->getArtist() << " to " << newArtist); @@ -66,13 +67,31 @@ namespace lms::scanner { assert(!artistInfo->isMBIDMatched()); - const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional(artistInfo->getSortName()) }; + Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional(artistInfo->getSortName()) }; db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) }; LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist); assert(newArtist != artistInfo->getArtist()); artistInfo.modify()->setArtist(newArtist); } + + db::TrackArtistLink::pointer getMostRecentMBIDArtistLink(db::Session& session, db::ArtistId artistId, std::optional linkType = std::nullopt) + { + db::TrackArtistLink::FindParameters params; + params.setArtist(artistId); + params.setLinkType(linkType); + params.setSortMethod(db::TrackArtistLinkSortMethod::OriginalDateDesc); + params.setMBIDMatched(true); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackArtistLink::pointer foundLink; + db::TrackArtistLink::find(session, params, [&](const db::TrackArtistLink::pointer& link) { + foundLink = link; + }); + + return foundLink; + } + } // namespace bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const @@ -83,14 +102,17 @@ namespace lms::scanner void ScanStepArtistReconciliation::process(ScanContext& context) { - // Reconcile artist links + // Reconcile artist name differences when MBID was used to match + updateArtistPreferredName(context); + + // Reconcile artist links when MBID not used to match { // Order is important updateLinksForArtistNameNoLongerMatch(context); updateLinksWithArtistNameAmbiguity(context); } - // Reconcile artist info + // Reconcile artist info when MBID not used to match { // Order is important updateArtistInfoForArtistNameNoLongerMatch(context); @@ -98,6 +120,96 @@ namespace lms::scanner } } + void ScanStepArtistReconciliation::updateArtistPreferredName(ScanContext& context) + { + static constexpr std::size_t batchSize{ 50 }; + + db::Session& session{ _db.getTLSSession() }; + + // List artists that have different names when mbid matched. + // Possible reasons: + // - artist name changed over time (ex: Rhapsody then Rhapsody of Fire), legit use case + // - user renamed the artist + // Name to pick in order of priority: + // - name specified in artist info + // - name as referenced in the latest release of the artist + // - name as referenced in the latest link (any type) + + struct ArtistToUpdate + { + db::Artist::pointer artist; + std::string newName; + std::string newSortName; + }; + std::vector artistsToUpdate; + auto updateArtists{ [&] { + auto transaction{ session.createWriteTransaction() }; + + for (auto& artistToUpdate : artistsToUpdate) + { + artistToUpdate.artist.modify()->setName(artistToUpdate.newName); + artistToUpdate.artist.modify()->setSortName(artistToUpdate.newSortName); + } + } }; + + db::ArtistId lastRetrievedArtist; + while (!_abortScan) + { + { + auto transaction{ session.createReadTransaction() }; + + const auto artists{ db::Artist::findWithMBIDNameVariants(session, lastRetrievedArtist, db::Range{ .offset = 0, .size = batchSize }) }; + if (artists.results.empty()) + break; + + for (const db::Artist::pointer& artist : artists.results) + { + bool hasArtistInfo{}; + db::ArtistInfo::find(session, artist->getId(), db::Range{ .offset = 0, .size = 1 }, [&](const db::ArtistInfo::pointer&) { + hasArtistInfo = true; + }); + + // Scanning artist info should have updated the name of the artist + if (hasArtistInfo) + continue; + + db::TrackArtistLink::pointer artistMostRecentLink{ getMostRecentMBIDArtistLink(session, artist->getId(), db::TrackArtistLinkType::ReleaseArtist) }; + if (!artistMostRecentLink) + artistMostRecentLink = getMostRecentMBIDArtistLink(session, artist->getId()); + + if (!artistMostRecentLink) + { + LMS_LOG(DBUPDATER, DEBUG, "Unable to fix name discrepancy for artist " << artist << ": no link found!"); + continue; + } + + if (artistMostRecentLink->getArtistName() != artist->getName()) + { + ArtistToUpdate& artistToUpdate{ artistsToUpdate.emplace_back() }; + artistToUpdate.artist = artist; + artistToUpdate.newName = artistMostRecentLink->getArtistName(); + artistToUpdate.newSortName = artistMostRecentLink->getArtistSortName(); + + LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent '" << db::trackArtistLinkTypeToString(artistMostRecentLink->getType()) << "' link reference"); + } + } + + if (!artists.moreResults) + break; + } + + if (artistsToUpdate.size() > batchSize) + { + updateArtists(); + artistsToUpdate.clear(); + } + } + + updateArtists(); + + _progressCallback(context.currentStepStats); + } + void ScanStepArtistReconciliation::updateArtistInfoForArtistNameNoLongerMatch(ScanContext& context) { static constexpr std::size_t batchSize{ 50 }; diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.hpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.hpp index 6121dcb4..7e4e1e95 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.hpp @@ -34,6 +34,8 @@ namespace lms::scanner bool needProcess(const ScanContext& context) const override; void process(ScanContext& context) override; + void updateArtistPreferredName(ScanContext& context); + void updateLinksForArtistNameNoLongerMatch(ScanContext& context); void updateLinksWithArtistNameAmbiguity(ScanContext& context); void updateArtistInfoForArtistNameNoLongerMatch(ScanContext& context); diff --git a/src/libs/metadata/include/metadata/ArtistInfo.hpp b/src/libs/services/scanner/impl/types/ArtistInfo.hpp similarity index 76% rename from src/libs/metadata/include/metadata/ArtistInfo.hpp rename to src/libs/services/scanner/impl/types/ArtistInfo.hpp index 75af760f..e39be46c 100644 --- a/src/libs/metadata/include/metadata/ArtistInfo.hpp +++ b/src/libs/services/scanner/impl/types/ArtistInfo.hpp @@ -19,17 +19,12 @@ #pragma once -#include -#include #include -#include #include #include "core/UUID.hpp" -#include "metadata/Exception.hpp" - -namespace lms::metadata +namespace lms::scanner { // See: // - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists @@ -44,13 +39,4 @@ namespace lms::metadata std::string disambiguation; // mb std::string biography; }; - - class ArtistInfoParseException : public Exception - { - public: - using Exception::Exception; - }; - - std::span getSupportedArtistInfoFiles(); - ArtistInfo parseArtistInfo(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/Lyrics.hpp b/src/libs/services/scanner/impl/types/Lyrics.hpp similarity index 74% rename from src/libs/metadata/include/metadata/Lyrics.hpp rename to src/libs/services/scanner/impl/types/Lyrics.hpp index ccfb348c..5b4f9be4 100644 --- a/src/libs/metadata/include/metadata/Lyrics.hpp +++ b/src/libs/services/scanner/impl/types/Lyrics.hpp @@ -20,16 +20,11 @@ #pragma once #include -#include -#include #include -#include #include #include -#include "metadata/Exception.hpp" - -namespace lms::metadata +namespace lms::scanner { struct Lyrics { @@ -42,13 +37,4 @@ namespace lms::metadata std::map synchronizedLines; std::vector unsynchronizedLines; }; - - class LyricsException : public metadata::Exception - { - public: - using metadata::Exception::Exception; - }; - - std::span getSupportedLyricsFileExtensions(); - Lyrics parseLyrics(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/PlayList.hpp b/src/libs/services/scanner/impl/types/PlayList.hpp similarity index 80% rename from src/libs/metadata/include/metadata/PlayList.hpp rename to src/libs/services/scanner/impl/types/PlayList.hpp index b3f51a56..f849a9ba 100644 --- a/src/libs/metadata/include/metadata/PlayList.hpp +++ b/src/libs/services/scanner/impl/types/PlayList.hpp @@ -20,19 +20,14 @@ #pragma once #include -#include -#include #include #include -namespace lms::metadata +namespace lms::scanner { struct PlayList { std::string name; std::vector files; }; - - std::span getSupportedPlayListFileExtensions(); - PlayList parsePlayList(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/services/scanner/impl/types/TrackMetadata.hpp similarity index 55% rename from src/libs/metadata/include/metadata/Types.hpp rename to src/libs/services/scanner/impl/types/TrackMetadata.hpp index 873d71f5..319c5fd8 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/services/scanner/impl/types/TrackMetadata.hpp @@ -19,11 +19,8 @@ #pragma once -#include #include #include -#include -#include #include #include #include @@ -31,9 +28,9 @@ #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" -#include "Lyrics.hpp" +#include "types/Lyrics.hpp" -namespace lms::metadata +namespace lms::scanner { using Tags = std::map /* values */>; @@ -94,15 +91,6 @@ namespace lms::metadata } }; - struct AudioProperties - { - std::size_t bitrate{}; - std::size_t bitsPerSample{}; - std::size_t channelCount{}; - std::chrono::milliseconds duration{}; - std::size_t sampleRate{}; - }; - struct Track { enum class Advisory @@ -111,7 +99,7 @@ namespace lms::metadata Explicit, Clean, }; - AudioProperties audioProperties; + std::optional mbid; std::optional recordingMBID; std::string title; @@ -143,96 +131,4 @@ namespace lms::metadata std::vector producerArtists; std::vector remixerArtists; }; - - 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 }; - std::string mimeType{ "application/octet-stream" }; - std::string description; - std::span data; - }; - - enum class ParserBackend - { - TagLib, - AvFormat, - }; - - enum class ParserReadStyle - { - Fast, - Average, - Accurate, - }; - - struct SortByLengthDesc - { - bool operator()(const std::string& a, const std::string& b) const - { - if (a.length() != b.length()) - return a.length() > b.length(); - return a < b; // Break ties using lexicographical order - } - }; - - using WhiteList = std::set; - struct AudioFileParserParameters - { - ParserBackend backend{ ParserBackend::TagLib }; - ParserReadStyle readStyle{ ParserReadStyle::Average }; - std::vector artistTagDelimiters; - WhiteList artistsToNotSplit; - std::vector defaultTagDelimiters; - std::vector userExtraTags; - bool debug{}; - }; -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/metadata/test/ArtistInfo.cpp b/src/libs/services/scanner/test/ArtistInfo.cpp similarity index 98% rename from src/libs/metadata/test/ArtistInfo.cpp rename to src/libs/services/scanner/test/ArtistInfo.cpp index aa84b87e..c8f87b09 100644 --- a/src/libs/metadata/test/ArtistInfo.cpp +++ b/src/libs/services/scanner/test/ArtistInfo.cpp @@ -20,9 +20,9 @@ #include -#include "metadata/ArtistInfo.hpp" +#include "scanners/artistinfo/ArtistInfoParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { TEST(ArtistInfo, basic) { @@ -112,4 +112,4 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl ASSERT_EQ(artistInfo.sortName, "Artist, My"); ASSERT_EQ(artistInfo.disambiguation, "My Artist"); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/Utils.cpp b/src/libs/services/scanner/test/AudioFileUtils.cpp similarity index 98% rename from src/libs/metadata/test/Utils.cpp rename to src/libs/services/scanner/test/AudioFileUtils.cpp index 925675ea..c906a918 100644 --- a/src/libs/metadata/test/Utils.cpp +++ b/src/libs/services/scanner/test/AudioFileUtils.cpp @@ -20,9 +20,9 @@ #include -#include "Utils.hpp" +#include "scanners/audiofile/Utils.hpp" -namespace lms::metadata::utils::tests +namespace lms::scanner::utils::tests { TEST(MetaData, parseDate) { @@ -149,4 +149,4 @@ namespace lms::metadata::utils::tests EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; } } -} // namespace lms::metadata::utils::tests \ No newline at end of file +} // namespace lms::scanner::utils::tests \ No newline at end of file diff --git a/src/libs/services/scanner/test/CMakeLists.txt b/src/libs/services/scanner/test/CMakeLists.txt new file mode 100644 index 00000000..a2210067 --- /dev/null +++ b/src/libs/services/scanner/test/CMakeLists.txt @@ -0,0 +1,25 @@ +include(GoogleTest) + +add_executable(test-scanner + ArtistInfo.cpp + AudioFileUtils.cpp + Lyrics.cpp + PlayList.cpp + Scanner.cpp + TrackMetadataParser.cpp + ) + +target_include_directories(test-scanner PRIVATE + ../impl + ) + +target_link_libraries(test-scanner PRIVATE + lmsscanner + lmsaudio + GTest::GTest + ) + +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-scanner) +endif() + diff --git a/src/libs/metadata/test/Lyrics.cpp b/src/libs/services/scanner/test/Lyrics.cpp similarity index 99% rename from src/libs/metadata/test/Lyrics.cpp rename to src/libs/services/scanner/test/Lyrics.cpp index aef7835b..06d0ef3b 100644 --- a/src/libs/metadata/test/Lyrics.cpp +++ b/src/libs/services/scanner/test/Lyrics.cpp @@ -20,9 +20,9 @@ #include -#include "metadata/Lyrics.hpp" +#include "scanners/lyrics/LyricsParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { using namespace std::chrono_literals; @@ -397,4 +397,4 @@ I, I just woke up from a dream EXPECT_EQ(lyrics.unsynchronizedLines[3], "I, I just woke up from a dream"); EXPECT_EQ(lyrics.unsynchronizedLines[4], ""); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/PlayList.cpp b/src/libs/services/scanner/test/PlayList.cpp similarity index 92% rename from src/libs/metadata/test/PlayList.cpp rename to src/libs/services/scanner/test/PlayList.cpp index 03be747c..e067a56f 100644 --- a/src/libs/metadata/test/PlayList.cpp +++ b/src/libs/services/scanner/test/PlayList.cpp @@ -21,11 +21,11 @@ #include #include -#include "metadata/PlayList.hpp" +#include "scanners/playlist/PlayListParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { - TEST(PlayList, basic) + TEST(Scanner, playlist) { std::istringstream is{ R"(#EXTM3U #PLAYLIST:My super playlist @@ -53,7 +53,7 @@ one to be/../one to be/normalized/foo.mp3)" }; EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3"); } - TEST(PlayList, UTF8_bom) + TEST(Scanner, playlist_UTF8_bom) { const unsigned char content[] = { 0xEF, 0xBB, 0xBF, '#', 'E', 'X', 'T', 'M', '3', 'U', '\r', '\n', '\r', '\n', '.', '.', '/', 't', 'e', 's', 't', '.', 'm', 'p', '3', '\r', '\n' }; std::istringstream is{ std::string(reinterpret_cast(content), sizeof(content)) }; @@ -64,4 +64,4 @@ one to be/../one to be/normalized/foo.mp3)" }; EXPECT_EQ(playlist.files[0], "../test.mp3"); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/Metadata.cpp b/src/libs/services/scanner/test/Scanner.cpp similarity index 95% rename from src/libs/metadata/test/Metadata.cpp rename to src/libs/services/scanner/test/Scanner.cpp index dc673372..fa26648b 100644 --- a/src/libs/metadata/test/Metadata.cpp +++ b/src/libs/services/scanner/test/Scanner.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * diff --git a/src/libs/metadata/test/TestTagReader.hpp b/src/libs/services/scanner/test/TestTagReader.hpp similarity index 90% rename from src/libs/metadata/test/TestTagReader.hpp rename to src/libs/services/scanner/test/TestTagReader.hpp index d7f6b11a..3ba949a0 100644 --- a/src/libs/metadata/test/TestTagReader.hpp +++ b/src/libs/services/scanner/test/TestTagReader.hpp @@ -21,22 +21,14 @@ #include #include -#include "ITagReader.hpp" +#include "audio/ITagReader.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { - class TestTagReader : public ITagReader + class TestTagReader : public audio::ITagReader { public: - static constexpr AudioProperties audioProperties{ - .bitrate = 128000, - .bitsPerSample = 16, - .channelCount = 2, - .duration = std::chrono::seconds{ 180 }, - .sampleRate = 44000, - }; - - using Tags = std::unordered_map>; + using Tags = std::unordered_map>; using Performers = std::unordered_map /*names*/>; using ExtraUserTags = std::unordered_map>; using LyricsTags = std::unordered_map; @@ -63,7 +55,7 @@ namespace lms::metadata::tests _lyricsTags = std::move(lyricsTags); } - void visitTagValues(TagType tag, TagValueVisitor visitor) const override + void visitTagValues(audio::TagType tag, TagValueVisitor visitor) const override { auto itValues{ _tags.find(tag) }; if (itValues != std::cend(_tags)) @@ -97,8 +89,6 @@ namespace lms::metadata::tests visitor(language, lyrics); } - const AudioProperties& getAudioProperties() const override { return audioProperties; } - private: const Tags _tags; Performers _performers; @@ -106,8 +96,10 @@ namespace lms::metadata::tests LyricsTags _lyricsTags; }; - inline std::unique_ptr createDefaultPopulatedTestTagReader() + inline std::unique_ptr createDefaultPopulatedTestTagReader() { + using namespace audio; + std::unique_ptr testTags{ std::make_unique( TestTagReader::Tags{ { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, @@ -168,4 +160,4 @@ namespace lms::metadata::tests return testTags; } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/services/scanner/test/TrackMetadataParser.cpp b/src/libs/services/scanner/test/TrackMetadataParser.cpp new file mode 100644 index 00000000..ddfb87ab --- /dev/null +++ b/src/libs/services/scanner/test/TrackMetadataParser.cpp @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include + +#include + +#include "scanners/audiofile/TrackMetadataParser.hpp" + +#include "TestTagReader.hpp" + +namespace lms::scanner::tests +{ + TEST(TrackMetadataParser, generalTest) + { + TrackMetadataParser::Parameters params; + params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" }; + + TrackMetadataParser parser{ params }; + std::unique_ptr testTags{ createDefaultPopulatedTestTagReader() }; + + const Track track{ parser.parseTrackMetaData(*testTags) }; + + EXPECT_EQ(track.acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); + ASSERT_TRUE(track.advisory.has_value()); + EXPECT_EQ(track.advisory.value(), Track::Advisory::Clean); + EXPECT_EQ(track.artistDisplayName, "MyArtist1 & MyArtist2"); + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "MyArtist1"); + EXPECT_EQ(track.artists[0].sortName, "MyArtists1SortName"); + EXPECT_EQ(track.artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae")); + EXPECT_EQ(track.artists[1].name, "MyArtist2"); + EXPECT_EQ(track.artists[1].sortName, "MyArtists2SortName"); + EXPECT_EQ(track.artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229")); + ASSERT_EQ(track.comments.size(), 2); + EXPECT_EQ(track.comments[0], "Comment1"); + EXPECT_EQ(track.comments[1], "Comment2"); + ASSERT_EQ(track.composerArtists.size(), 2); + EXPECT_EQ(track.composerArtists[0].name, "MyComposer1"); + EXPECT_EQ(track.composerArtists[0].sortName, "MyComposerSortOrder1"); + EXPECT_EQ(track.composerArtists[1].name, "MyComposer2"); + EXPECT_EQ(track.composerArtists[1].sortName, "MyComposerSortOrder2"); + ASSERT_EQ(track.conductorArtists.size(), 2); + EXPECT_EQ(track.conductorArtists[0].name, "MyConductor1"); + EXPECT_EQ(track.conductorArtists[1].name, "MyConductor2"); + EXPECT_EQ(track.copyright, "MyCopyright"); + EXPECT_EQ(track.copyrightURL, "MyCopyrightURL"); + ASSERT_TRUE(track.date.isValid()); + EXPECT_EQ(track.date.getYear(), 2020); + EXPECT_EQ(track.date.getMonth(), 3); + EXPECT_EQ(track.date.getDay(), 4); + ASSERT_EQ(track.genres.size(), 2); + EXPECT_EQ(track.genres[0], "Genre1"); + EXPECT_EQ(track.genres[1], "Genre2"); + ASSERT_EQ(track.groupings.size(), 2); + EXPECT_EQ(track.groupings[0], "Grouping1"); + EXPECT_EQ(track.groupings[1], "Grouping2"); + ASSERT_EQ(track.languages.size(), 2); + EXPECT_EQ(track.languages[0], "Language1"); + EXPECT_EQ(track.languages[1], "Language2"); + ASSERT_EQ(track.lyricistArtists.size(), 2); + EXPECT_EQ(track.lyricistArtists[0].name, "MyLyricist1"); + EXPECT_EQ(track.lyricistArtists[1].name, "MyLyricist2"); + ASSERT_EQ(track.lyrics.size(), 1); + EXPECT_EQ(track.lyrics.front().language, "eng"); + ASSERT_EQ(track.lyrics.front().synchronizedLines.size(), 2); + ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 })); + EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line"); + ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 })); + EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line"); + ASSERT_TRUE(track.mbid.has_value()); + EXPECT_EQ(track.mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a")); + ASSERT_EQ(track.mixerArtists.size(), 2); + EXPECT_EQ(track.mixerArtists[0].name, "MyMixer1"); + EXPECT_EQ(track.mixerArtists[1].name, "MyMixer2"); + ASSERT_EQ(track.moods.size(), 2); + EXPECT_EQ(track.moods[0], "Mood1"); + EXPECT_EQ(track.moods[1], "Mood2"); + ASSERT_TRUE(track.originalDate.isValid()); + EXPECT_EQ(track.originalDate.getYear(), 2019); + EXPECT_EQ(track.originalDate.getMonth(), 2); + EXPECT_EQ(track.originalDate.getDay(), 3); + ASSERT_TRUE(track.originalYear.has_value()); + EXPECT_EQ(track.originalYear.value(), 2019); + ASSERT_TRUE(track.performerArtists.contains("Rolea")); + ASSERT_EQ(track.performerArtists.at("Rolea").size(), 2); + EXPECT_EQ(track.performerArtists.at("Rolea")[0].name, "MyPerformer1ForRoleA"); + EXPECT_EQ(track.performerArtists.at("Rolea")[1].name, "MyPerformer2ForRoleA"); + ASSERT_EQ(track.performerArtists.at("Roleb").size(), 2); + EXPECT_EQ(track.performerArtists.at("Roleb")[0].name, "MyPerformer1ForRoleB"); + EXPECT_EQ(track.performerArtists.at("Roleb")[1].name, "MyPerformer2ForRoleB"); + ASSERT_TRUE(track.position.has_value()); + EXPECT_EQ(track.position.value(), 7); + ASSERT_EQ(track.producerArtists.size(), 2); + EXPECT_EQ(track.producerArtists[0].name, "MyProducer1"); + EXPECT_EQ(track.producerArtists[1].name, "MyProducer2"); + ASSERT_TRUE(track.recordingMBID.has_value()); + EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5")); + ASSERT_TRUE(track.replayGain.has_value()); + EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33); + ASSERT_EQ(track.remixerArtists.size(), 2); + EXPECT_EQ(track.remixerArtists[0].name, "MyRemixer1"); + EXPECT_EQ(track.remixerArtists[1].name, "MyRemixer2"); + EXPECT_EQ(track.title, "MyTitle"); + ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A").size(), 2); + EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[0], "MyTagValue1ForTagA"); + EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[1], "MyTagValue2ForTagA"); + ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B").size(), 2); + EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[0], "MyTagValue1ForTagB"); + EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[1], "MyTagValue2ForTagB"); + + // Medium + ASSERT_TRUE(track.medium.has_value()); + EXPECT_EQ(track.medium->media, "CD"); + EXPECT_EQ(track.medium->name, "MySubtitle"); + ASSERT_TRUE(track.medium->position.has_value()); + EXPECT_EQ(track.medium->position.value(), 2); + ASSERT_TRUE(track.medium->replayGain.has_value()); + EXPECT_FLOAT_EQ(track.medium->replayGain.value(), -0.5); + ASSERT_TRUE(track.medium->trackCount.has_value()); + EXPECT_EQ(track.medium->trackCount.value(), 12); + + // Release + ASSERT_TRUE(track.medium->release.has_value()); + const Release& release{ track.medium->release.value() }; + EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2"); + ASSERT_EQ(release.artists.size(), 2); + EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1"); + EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName"); + EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7")); + EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2"); + EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName"); + EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1")); + EXPECT_TRUE(release.isCompilation); + EXPECT_EQ(release.barcode, "MyBarcode"); + ASSERT_EQ(release.labels.size(), 2); + EXPECT_EQ(release.labels[0], "Label1"); + EXPECT_EQ(release.labels[1], "Label2"); + ASSERT_TRUE(release.mbid.has_value()); + EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69")); + EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838")); + EXPECT_EQ(release.mediumCount, 3); + EXPECT_EQ(release.name, "MyAlbum"); + EXPECT_EQ(release.sortName, "MyAlbumSortName"); + EXPECT_EQ(release.comment, "MyAlbumComment"); + ASSERT_EQ(release.countries.size(), 2); + EXPECT_EQ(release.countries[0], "MyCountry1"); + EXPECT_EQ(release.countries[1], "MyCountry2"); + { + std::vector expectedReleaseTypes{ "Album", "Compilation" }; + EXPECT_EQ(release.releaseTypes, expectedReleaseTypes); + } + } + + TEST(TrackMetadataParser, trim) + { + const TestTagReader testTags{ + { + { audio::TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.genres.size(), 3); + EXPECT_EQ(track.genres[0], "Genre1"); + EXPECT_EQ(track.genres[1], "Genre2"); + EXPECT_EQ(track.genres[2], "Genre3"); + } + + TEST(TrackMetadataParser, customDelimiters) + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } }, + { audio::TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } }, + { audio::TagType::Genre, { "Genre1 ; Genre2" } }, + { audio::TagType::Language, { " Lang1/Lang2 / Lang3" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.defaultTagDelimiters = { " ; ", "/" }; + params.artistTagDelimiters = { " / ", " feat. " }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 3); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artists[2].name, "Artist3"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit + ASSERT_EQ(track.genres.size(), 2); + EXPECT_EQ(track.genres[0], "Genre1"); + EXPECT_EQ(track.genres[1], "Genre2"); + ASSERT_EQ(track.languages.size(), 3); + EXPECT_EQ(track.languages[0], "Lang1"); + EXPECT_EQ(track.languages[1], "Lang2"); + EXPECT_EQ(track.languages[2], "Lang3"); + + // Medium + ASSERT_TRUE(track.medium.has_value()); + + // Release + ASSERT_TRUE(track.medium->release.has_value()); + EXPECT_EQ(track.medium->release->name, "MyAlbum"); + ASSERT_EQ(track.medium->release->artists.size(), 2); + EXPECT_EQ(track.medium->release->artists[0].name, "AlbumArtist1"); + EXPECT_EQ(track.medium->release->artists[1].name, "AlbumArtist2"); + EXPECT_EQ(track.medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2"); + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist) + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { " AC/DC " } }, + { audio::TagType::Artist, { "AC/DC " } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "AC/DC"); + EXPECT_EQ(track.artistDisplayName, "AC/DC"); + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + EXPECT_EQ(track.medium->release->name, "MyAlbum"); + ASSERT_EQ(track.medium->release->artists.size(), 1); + EXPECT_EQ(track.medium->release->artists[0].name, "AC/DC"); + EXPECT_EQ(track.medium->release->artistDisplayName, "AC/DC"); + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_artists) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "AC/DC and MyArtist" } }, + { audio::TagType::Artists, { "AC/DC", "MyArtist" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/" }; + params.artistsToNotSplit = { " AC/DC " }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "AC/DC"); + EXPECT_EQ(track.artists[1].name, "MyArtist"); + EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_first) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "AC/DC;MyArtist" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/", ";" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "AC/DC"); + EXPECT_EQ(track.artists[1].name, "MyArtist"); + EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_middle) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/", ";" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 3); + EXPECT_EQ(track.artists[0].name, "MyArtist1"); + EXPECT_EQ(track.artists[1].name, "AC/DC"); + EXPECT_EQ(track.artists[2].name, "MyArtist2"); + EXPECT_EQ(track.artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_last) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " AC/DC; MyArtist" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { ";", "/" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "AC/DC"); + EXPECT_EQ(track.artists[1].name, "MyArtist"); + EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_longest_first) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " AC/DC; MyArtist" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { ";", "/" }; + params.artistsToNotSplit = { "AC", "DC", "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "AC/DC"); + EXPECT_EQ(track.artists[1].name, "MyArtist"); + EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_begin) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " AC/DC; MyArtist" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "AC/DC; MyArtist"); + EXPECT_EQ(track.artistDisplayName, "AC/DC; MyArtist"); + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_middle) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "MyArtist1; AC/DC ; MyArtist2"); + EXPECT_EQ(track.artistDisplayName, "MyArtist1; AC/DC ; MyArtist2"); + } + + TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_end) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { " MyArtist; AC/DC " } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "/" }; + params.artistsToNotSplit = { "AC/DC" }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "MyArtist; AC/DC"); + EXPECT_EQ(track.artistDisplayName, "MyArtist; AC/DC"); + } + + TEST(TrackMetadataParser, customDelimiters_foundInArtist) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1; Artist2" } }, + { audio::TagType::Artists, { "Artist1", "Artist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "; " }; + TrackMetadataParser parser{ params }; + + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist + } + + TEST(TrackMetadataParser, customDelimiters_foundInArtists) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 feat. Artist2" } }, + { audio::TagType::Artists, { "Artist1; Artist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "; " }; + TrackMetadataParser parser{ params }; + + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1 feat. Artist2"); + } + + TEST(TrackMetadataParser, customDelimiters_notUsed) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 & Artist2" } }, + { audio::TagType::Artists, { "Artist1", "Artist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { "; " }; + TrackMetadataParser parser{ params }; + + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2"); + } + + TEST(TrackMetadataParser, customDelimiters_onlyInArtist) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 & Artist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { " & " }; + TrackMetadataParser parser{ params }; + + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing + } + + TEST(TrackMetadataParser, customDelimitersUsedForArtists) + { + const TestTagReader testTags{ + { + { audio::TagType::Artists, { "Artist1 & Artist2" } }, + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { " & " }; + TrackMetadataParser parser{ params }; + + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing + } + + TEST(TrackMetadataParser, noArtistInArtist) + { + const TestTagReader testTags{ + { + // nothing in Artist! + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 0); + EXPECT_EQ(track.artistDisplayName, ""); + } + + TEST(TrackMetadataParser, singleArtistInArtists) + { + const TestTagReader testTags{ + { + // nothing in Artist! + { audio::TagType::Artists, { "Artist1" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artistDisplayName, "Artist1"); + } + + TEST(TrackMetadataParser, multipleArtistsInArtist) + { + const TestTagReader testTags{ + { + // nothing in Artists! + { audio::TagType::Artist, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found + } + + TEST(TrackMetadataParser, multipleArtistsInArtists) + { + const TestTagReader testTags{ + { + // nothing in Artist! + { audio::TagType::Artists, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist + } + + TEST(TrackMetadataParser, multipleArtistsInArtistsWithEndDelimiter) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 & (CV. Artist2)" } }, + { audio::TagType::Artists, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artistDisplayName, "Artist1 & (CV. Artist2)"); + } + + TEST(TrackMetadataParser, singleArtistInAlbumArtists) + { + const TestTagReader testTags{ + { + // nothing in AlbumArtist! + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtists, { "Artist1" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium); + ASSERT_TRUE(track.medium->release); + ASSERT_EQ(track.medium->release->artists.size(), 1); + EXPECT_EQ(track.medium->release->artists[0].name, "Artist1"); + EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1"); + } + + TEST(TrackMetadataParser, multipleArtistsInAlbumArtist) + { + const TestTagReader testTags{ + { + // nothing in AlbumArtists! + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium); + ASSERT_TRUE(track.medium->release); + ASSERT_EQ(track.medium->release->artists.size(), 2); + EXPECT_EQ(track.medium->release->artists[0].name, "Artist1"); + EXPECT_EQ(track.medium->release->artists[1].name, "Artist2"); + EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found + } + + TEST(TrackMetadataParser, multipleArtistsInAlbumArtists_displayName) + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "Artist1 & Artist2" } }, + { audio::TagType::AlbumArtists, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium); + ASSERT_TRUE(track.medium->release); + ASSERT_EQ(track.medium->release->artists.size(), 2); + EXPECT_EQ(track.medium->release->artists[0].name, "Artist1"); + EXPECT_EQ(track.medium->release->artists[1].name, "Artist2"); + EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1 & Artist2"); + } + + TEST(TrackMetadataParser, multipleArtistsInAlbumArtists) + { + const TestTagReader testTags{ + { + // nothing in AlbumArtist! + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtists, { "Artist1", "Artist2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium); + ASSERT_TRUE(track.medium->release); + ASSERT_EQ(track.medium->release->artists.size(), 2); + EXPECT_EQ(track.medium->release->artists[0].name, "Artist1"); + EXPECT_EQ(track.medium->release->artists[1].name, "Artist2"); + EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist + } + + TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 & Artist2" } }, + { audio::TagType::Artists, { "Artist1", "Artist2" } }, + { audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[0].mbid, std::nullopt); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artists[1].mbid, std::nullopt); + EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2"); + } + + TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters) + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "Artist1 / Artist2" } }, + { audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added + } + }; + + TrackMetadataParser::Parameters params; + params.artistTagDelimiters = { " / " }; + TrackMetadataParser parser{ params }; + const Track track{ parser.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 2); + EXPECT_EQ(track.artists[0].name, "Artist1"); + EXPECT_EQ(track.artists[0].mbid, std::nullopt); + EXPECT_EQ(track.artists[1].name, "Artist2"); + EXPECT_EQ(track.artists[1].mbid, std::nullopt); + EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name + } + + TEST(TrackMetadataParser, release_sortNameFallback) + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + // No AlbumSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + EXPECT_EQ(track.medium->release->sortName, "MyAlbum"); + } + + TEST(TrackMetadataParser, artist_sortNameFallback) + { + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "MyArtist" } }, + { audio::TagType::ArtistSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName"); + } + + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "MyArtist" } }, + { audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName"); + } + + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "MyArtist" } }, + { audio::TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } }, + { audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName"); + } + } + + TEST(TrackMetadataParser, albumartist_sortNameFallback) + { + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "MyArtist" } }, + { audio::TagType::AlbumArtistSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + + const auto& artists{ track.medium->release->artists }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); + } + + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "MyArtist" } }, + { audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + + const auto& artists{ track.medium->release->artists }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); + } + + { + const TestTagReader testTags{ + { + { audio::TagType::Album, { "MyAlbum" } }, + + { audio::TagType::AlbumArtist, { "MyArtist" } }, + { audio::TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } }, + { audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } }, + // No ArtistSortOrder + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + + const auto& artists{ track.medium->release->artists }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0].sortName, "MyArtistSortName"); + } + } + + TEST(TrackMetadataParser, advisory) + { + auto doTest = [](std::string_view value, std::optional expectedValue) { + const TestTagReader testTags{ + { + { audio::TagType::Advisory, { value } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'"; + if (track.advisory.has_value()) + { + EXPECT_EQ(track.advisory.value(), expectedValue); + } + }; + + doTest("0", Track::Advisory::Unknown); + doTest("1", Track::Advisory::Explicit); + doTest("4", Track::Advisory::Explicit); + doTest("2", Track::Advisory::Clean); + doTest("", std::nullopt); + doTest("3", std::nullopt); + } + + TEST(TrackMetadataParser, encodingTime) + { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { + const TestTagReader testTags{ + { + { audio::TagType::EncodingTime, { value } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.encodingTime, expectedValue) << "Value = '" << value << "'"; + }; + + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 }); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); + } + + TEST(TrackMetadataParser, date) + { + auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { + const TestTagReader testTags{ + { + { audio::TagType::Date, { value } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.date, expectedValue) << "Value = '" << value << "'"; + }; + + doTest("", core::PartialDateTime{}); + doTest("foo", core::PartialDateTime{}); + doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020-01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); + doTest("2020/01", core::PartialDateTime{ 2020, 1 }); + doTest("2020", core::PartialDateTime{ 2020 }); + } +} // namespace lms::scanner::tests diff --git a/src/libs/services/transcoding/CMakeLists.txt b/src/libs/services/transcoding/CMakeLists.txt index e57e0b5f..27caf7b2 100644 --- a/src/libs/services/transcoding/CMakeLists.txt +++ b/src/libs/services/transcoding/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(lmstranscoding STATIC - impl/TranscodingResourceHandler.cpp - impl/TranscodingService.cpp + impl/TranscodeResourceHandler.cpp + impl/TranscodeService.cpp ) target_include_directories(lmstranscoding INTERFACE @@ -13,7 +13,7 @@ target_include_directories(lmstranscoding PRIVATE ) target_link_libraries(lmstranscoding PRIVATE - lmsav + lmsaudio ) target_link_libraries(lmstranscoding PUBLIC diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp b/src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp similarity index 77% rename from src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp rename to src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp index 8c21b93f..40b6393a 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp +++ b/src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp @@ -17,42 +17,38 @@ * along with LMS. If not, see . */ -#include "TranscodingResourceHandler.hpp" +#include "TranscodeResourceHandler.hpp" -#include "av/Exception.hpp" -#include "av/ITranscoder.hpp" #include "core/ILogger.hpp" +#include "audio/Exception.hpp" +#include "audio/ITranscoder.hpp" + namespace lms::transcoding { - std::unique_ptr createResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, bool estimateContentLength) - { - return std::make_unique(inputParameters, outputParameters, estimateContentLength); - } - // TODO set some nice HTTP return code - TranscodingResourceHandler::TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength) + ResourceHandler::ResourceHandler(const audio::TranscodeParameters& parameters, std::optional estimatedContentLength) : _estimatedContentLength{ estimatedContentLength } { try { - _transcoder = av::createTranscoder(inputParameters, outputParameters); + _transcoder = createTranscoder(parameters); if (_estimatedContentLength) LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); else LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length"); } - catch (av::Exception& e) + catch (audio::Exception& e) { LMS_LOG(TRANSCODING, ERROR, "Failed to create transcoder: " << e.what()); } } - TranscodingResourceHandler::~TranscodingResourceHandler() = default; + ResourceHandler::~ResourceHandler() = default; - Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) + Wt::Http::ResponseContinuation* ResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { if (!_transcoder) { diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp b/src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp similarity index 70% rename from src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp rename to src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp index 0674a1a7..edd2b86f 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp +++ b/src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp @@ -23,19 +23,19 @@ #include #include -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" #include "core/IResourceHandler.hpp" namespace lms::transcoding { - class TranscodingResourceHandler final : public core::IResourceHandler + class ResourceHandler final : public core::IResourceHandler { public: - TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength); - ~TranscodingResourceHandler() override; + ResourceHandler(const audio::TranscodeParameters& parameters, std::optional estimatedContentLength); + ~ResourceHandler() override; - TranscodingResourceHandler(const TranscodingResourceHandler&) = delete; - TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete; + ResourceHandler(const ResourceHandler&) = delete; + ResourceHandler& operator=(const ResourceHandler&) = delete; private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; @@ -46,6 +46,6 @@ namespace lms::transcoding std::array _buffer; std::size_t _bytesReadyCount{}; std::size_t _totalServedByteCount{}; - std::unique_ptr _transcoder; + std::unique_ptr _transcoder; }; } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodeService.cpp similarity index 50% rename from src/libs/services/transcoding/impl/TranscodingService.cpp rename to src/libs/services/transcoding/impl/TranscodeService.cpp index 31c32f89..f9b503b5 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodeService.cpp @@ -17,24 +17,20 @@ * along with LMS. If not, see . */ -#include "TranscodingService.hpp" +#include "TranscodeService.hpp" -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" #include "core/ILogger.hpp" +#include "core/UUID.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" -#include "TranscodingResourceHandler.hpp" +#include "TranscodeResourceHandler.hpp" namespace lms::transcoding { namespace { - av::OutputParameters toAv(const OutputParameters& out) - { - return { .format = static_cast(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata }; - } - std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) { const std::size_t estimatedContentLength{ static_cast((bitrate / 8 * duration.count()) / 1000) }; @@ -42,40 +38,35 @@ namespace lms::transcoding } } // namespace - std::unique_ptr createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager) + std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) { - return std::make_unique(db, childProcessManager); + return std::make_unique(db, childProcessManager); } - TranscodingService::TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager) + TranscodeService::TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) : _db{ db } , _childProcessManager(childProcessManager) { LMS_LOG(TRANSCODING, INFO, "Service started!"); } - TranscodingService::~TranscodingService() + TranscodeService::~TranscodeService() { LMS_LOG(TRANSCODING, INFO, "Service stopped!"); } - std::unique_ptr TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + std::unique_ptr TranscodeService::createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) { - av::InputParameters avInputParams; std::optional estimatedContentLength; - avInputParams.file = inputParameters.filePath; - avInputParams.offset = inputParameters.offset; - avInputParams.streamIndex = inputParameters.streamIndex; - if (estimateContentLength) { - if (inputParameters.offset < inputParameters.duration) - estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, inputParameters.duration - inputParameters.offset); + if (parameters.inputParameters.offset < parameters.inputParameters.duration) + estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset); else - LMS_LOG(TRANSCODING, WARNING, "Offset " << inputParameters.offset << " is greater than audio file duration " << inputParameters.duration << ": not estimating content length"); + LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.duration << ": not estimating content length"); } - return std::make_unique(avInputParams, toAv(outputParameters), estimatedContentLength); + return std::make_unique(parameters, estimatedContentLength); } } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.hpp b/src/libs/services/transcoding/impl/TranscodeService.hpp similarity index 60% rename from src/libs/services/transcoding/impl/TranscodingService.hpp rename to src/libs/services/transcoding/impl/TranscodeService.hpp index d5ea4471..82b86f88 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.hpp +++ b/src/libs/services/transcoding/impl/TranscodeService.hpp @@ -19,21 +19,21 @@ #pragma once -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" namespace lms::transcoding { - class TranscodingService : public ITranscodingService + class TranscodeService : public ITranscodeService { public: - explicit TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager); - ~TranscodingService() override; + explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); + ~TranscodeService() override; - TranscodingService(const TranscodingService&) = delete; - TranscodingService& operator=(const TranscodingService&) = delete; + TranscodeService(const TranscodeService&) = delete; + TranscodeService& operator=(const TranscodeService&) = delete; private: - std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override; + std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) override; db::IDb& _db; core::IChildProcessManager& _childProcessManager; diff --git a/src/libs/subsonic/impl/ClientInfo.hpp b/src/libs/services/transcoding/include/services/transcoding/Exception.hpp similarity index 75% rename from src/libs/subsonic/impl/ClientInfo.hpp rename to src/libs/services/transcoding/include/services/transcoding/Exception.hpp index bd684f47..bac62dbc 100644 --- a/src/libs/subsonic/impl/ClientInfo.hpp +++ b/src/libs/services/transcoding/include/services/transcoding/Exception.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * @@ -19,15 +19,13 @@ #pragma once -#include +#include "core/Exception.hpp" -#include "ProtocolVersion.hpp" - -namespace lms::api::subsonic +namespace lms::transcoding { - struct ClientInfo + class Exception : public core::LmsException { - std::string name; - ProtocolVersion version; + public: + using LmsException::LmsException; }; -} // namespace lms::api::subsonic +} // namespace lms::transcoding \ No newline at end of file diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp new file mode 100644 index 00000000..a4def09c --- /dev/null +++ b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +#pragma once + +#include + +#include "audio/TranscodeTypes.hpp" + +namespace lms +{ + namespace core + { + class IChildProcessManager; + class IResourceHandler; + } // namespace core + + namespace db + { + class IDb; + } +} // namespace lms + +namespace lms::transcoding +{ + class ITranscodeService + { + public: + virtual ~ITranscodeService() = default; + + virtual std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength = false) = 0; + }; + + std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); +} // namespace lms::transcoding diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp deleted file mode 100644 index 28bf4943..00000000 --- a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp +++ /dev/null @@ -1,76 +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 . - */ - -#pragma once - -#include -#include -#include -#include - -namespace lms -{ - namespace core - { - class IChildProcessManager; - class IResourceHandler; - } // namespace core - - namespace db - { - class IDb; - } -} // namespace lms - -namespace lms::transcoding -{ - struct InputParameters - { - 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 - std::optional streamIndex; // Index of the stream to be transcoded (select the "best" audio stream if not set) - }; - - enum class OutputFormat - { - MP3, - OGG_OPUS, - MATROSKA_OPUS, - OGG_VORBIS, - WEBM_VORBIS, - }; - - struct OutputParameters - { - OutputFormat format; - std::size_t bitrate{ 128'000 }; - bool stripMetadata{ true }; - }; - - class ITranscodingService - { - public: - virtual ~ITranscodingService() = default; - - virtual std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0; - }; - - std::unique_ptr createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager); -} // namespace lms::transcoding diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 89519d64..802f5611 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -28,11 +28,13 @@ add_library(lmssubsonic STATIC impl/responses/Song.cpp impl/responses/User.cpp impl/CoverArtId.cpp + impl/RequestContext.cpp impl/ResponseFormat.cpp impl/ProtocolVersion.cpp impl/ParameterParsing.cpp impl/SubsonicId.cpp impl/SubsonicResource.cpp + impl/SubsonicResourceConfig.cpp impl/SubsonicResponse.cpp ) @@ -48,7 +50,7 @@ target_include_directories(lmssubsonic PRIVATE target_link_libraries(lmssubsonic PRIVATE lmsartwork lmsauth - lmsav + lmsaudio lmsdatabase lmsfeedback lmspodcast diff --git a/src/libs/subsonic/impl/RequestContext.cpp b/src/libs/subsonic/impl/RequestContext.cpp new file mode 100644 index 00000000..8f8c5283 --- /dev/null +++ b/src/libs/subsonic/impl/RequestContext.cpp @@ -0,0 +1,107 @@ +/* + * 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 . + */ + +#include "RequestContext.hpp" + +#include "ParameterParsing.hpp" +#include "SubsonicResourceConfig.hpp" +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic +{ + namespace + { + void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server) + { + if (client.major > server.major) + throw ServerMustUpgradeError{}; + if (client.major < server.major) + throw ClientMustUpgradeError{}; + if (client.minor > server.minor) + throw ServerMustUpgradeError{}; + if (client.minor == server.minor) + { + if (client.patch > server.patch) + throw ServerMustUpgradeError{}; + } + } + } // namespace + + RequestContext::RequestContext(const Wt::Http::Request& request, db::Session& dbSession, db::ObjectPtr user, const SubsonicResourceConfig& config) + : _request{ request } + , _dbSession{ dbSession } + , _user{ user } + , _config{ config } + , _clientName{ getMandatoryParameterAs(_request.getParameterMap(), "c") } + , _clientProtocolVersion{ getMandatoryParameterAs(_request.getParameterMap(), "v") } + , _responseFormat{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml } + , _serverProtocolVersion{ _config.serverProtocolVersionsByClient.contains(_clientName) ? _config.serverProtocolVersionsByClient.at(_clientName) : defaultServerProtocolVersion } + , _isOpenSubsonicEnabled{ !_config.openSubsonicDisabledClients.contains(_clientName) } + { + checkProtocolVersion(_clientProtocolVersion, _serverProtocolVersion); + } + + RequestContext::~RequestContext() = default; + + const RequestContext::ParameterMap& RequestContext::getParameters() const + { + return _request.getParameterMap(); + } + + std::istream& RequestContext::getBody() const + { + return _request.in(); + } + + db::Session& RequestContext::getDbSession() + { + return _dbSession; + } + + db::ObjectPtr RequestContext::getUser() const + { + return _user; + } + + std::string RequestContext::getClientIpAddr() const + { + return _request.clientAddress(); + } + + std::string_view RequestContext::getClientName() const + { + return _clientName; + } + + ProtocolVersion RequestContext::getServerProtocolVersion() const + { + return _serverProtocolVersion; + } + + ResponseFormat RequestContext::getResponseFormat() const + { + return _responseFormat; + } + + bool RequestContext::isOpenSubsonicEnabled() const + { + return _isOpenSubsonicEnabled; + } + +} // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 1b80bf70..7e295756 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -19,15 +19,17 @@ #pragma once +#include #include +#include #include #include "database/Object.hpp" -#include "ClientInfo.hpp" #include "ProtocolVersion.hpp" #include "ResponseFormat.hpp" +#include "SubsonicResourceConfig.hpp" namespace lms::db { @@ -37,15 +39,41 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext + class RequestContext { - const Wt::Http::ParameterMap& parameters; - db::Session& dbSession; - db::ObjectPtr user; - std::string clientIpAddr; - ClientInfo clientInfo; - ProtocolVersion serverProtocolVersion; - ResponseFormat responseFormat; - bool enableOpenSubsonic{ true }; + public: + RequestContext(const Wt::Http::Request& request, db::Session& dbSession, db::ObjectPtr user, const SubsonicResourceConfig& config); + ~RequestContext(); + RequestContext(const RequestContext&) = delete; + RequestContext& operator=(const RequestContext&) = delete; + + using ParameterMap = Wt::Http::ParameterMap; + + const ParameterMap& getParameters() const; + std::istream& getBody() const; + + db::Session& getDbSession(); + db::ObjectPtr getUser() const; + + std::string getClientIpAddr() const; + std::string_view getClientName() const; + + ProtocolVersion getServerProtocolVersion() const; + ResponseFormat getResponseFormat() const; + bool isOpenSubsonicEnabled() const; + + private: + const Wt::Http::Request& _request; + db::Session& _dbSession; + db::ObjectPtr _user; + const SubsonicResourceConfig& _config; + + const std::string _clientName; + const ProtocolVersion _clientProtocolVersion; + const ResponseFormat _responseFormat; + + const ProtocolVersion _serverProtocolVersion; + const bool _isOpenSubsonicEnabled; }; + } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 60731a61..ac1b4316 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -60,32 +60,6 @@ namespace lms::api::subsonic namespace { - std::unordered_map readConfigProtocolVersions() - { - std::unordered_map res; - - core::Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", - [&](std::string_view client) { - res.emplace(std::string{ client }, ProtocolVersion{ .major = 1, .minor = 12, .patch = 0 }); - }, - { "DSub" }); - - return res; - } - - std::unordered_set readOpenSubsonicDisabledClients() - { - std::unordered_set res; - - core::Service::get()->visitStrings("api-open-subsonic-disabled-clients", - [&](std::string_view client) { - res.emplace(std::string{ client }); - }, - { "DSub" }); - - return res; - } - std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { constexpr std::string_view redactedStr{ "*REDACTED*" }; @@ -292,24 +266,10 @@ namespace lms::api::subsonic throw UserNotAuthorizedError{}; } - - ClientInfo getClientInfo(const Wt::Http::Request& request) - { - const auto& parameters{ request.getParameterMap() }; - ClientInfo res; - - // Mandatory parameters - res.name = getMandatoryParameterAs(parameters, "c"); - res.version = getMandatoryParameterAs(parameters, "v"); - - return res; - } } // namespace SubsonicResource::SubsonicResource(db::IDb& db) - : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } - , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } - , _supportUserPasswordAuthentication{ core::Service::get()->getBool("api-subsonic-support-user-password-auth", true) } + : _config{ readSubsonicResourceConfig(*core::Service::get()) } , _db{ db } { } @@ -327,6 +287,24 @@ namespace lms::api::subsonic if (core::stringUtils::stringEndsWith(requestPath, ".view")) requestPath.resize(requestPath.length() - 5); + // First check for media retrieval endpoints + auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) }; + if (itStreamHandler != mediaRetrievalHandlers.end()) + { + try + { + LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first); + handleMediaRetrievalRequest(itStreamHandler->second, request, response); + LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!"); + } + catch (const Error& e) + { + LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestId << "', code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"); + } + + return; + } + // Optional parameters const ResponseFormat format{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; @@ -334,21 +312,20 @@ namespace lms::api::subsonic try { - // We need to parse client a soon as possible to make sure to answer with the right protocol version - protocolVersion = getServerProtocolVersion(getMandatoryParameterAs(request.getParameterMap(), "c")); - RequestContext requestContext{ buildRequestContext(request) }; - - auto itEntryPoint{ requestEntryPoints.find(requestPath) }; - if (itEntryPoint != requestEntryPoints.end()) + if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end()) { LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first); + db::User::pointer user; if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated) { - requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); - checkUserTypeIsAllowed(requestContext.user, itEntryPoint->second.allowedUserTypes); + user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); + checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes); } + RequestContext requestContext{ request, _db.getTLSSession(), user, _config }; + protocolVersion = requestContext.getServerProtocolVersion(); + const Response resp{ [&] { LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest"); return itEntryPoint->second.func(requestContext); @@ -365,21 +342,6 @@ namespace lms::api::subsonic return; } - auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) }; - if (itStreamHandler != mediaRetrievalHandlers.end()) - { - LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first); - - // Media retrieval endpoints are always authenticated - // Optim: no need to reauth user for each continuation - if (!request.continuation()) - requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); - - itStreamHandler->second(requestContext, request, response); - LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!"); - return; - } - // do not disclose unhandled commands for unauthenticated users authenticateUser(request); @@ -397,47 +359,50 @@ namespace lms::api::subsonic } } - ProtocolVersion SubsonicResource::getServerProtocolVersion(const std::string& clientName) const + void SubsonicResource::handleMediaRetrievalRequest(MediaRetrievalHandlerFunc handler, const Wt::Http::Request& request, Wt::Http::Response& response) { - auto it{ _serverProtocolVersionsByClient.find(clientName) }; - if (it == std::cend(_serverProtocolVersionsByClient)) - return defaultServerProtocolVersion; - - return it->second; - } - - void SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server) - { - if (client.major > server.major) - throw ServerMustUpgradeError{}; - if (client.major < server.major) - throw ClientMustUpgradeError{}; - if (client.minor > server.minor) - throw ServerMustUpgradeError{}; - if (client.minor == server.minor) + try { - if (client.patch > server.patch) - throw ServerMustUpgradeError{}; + // Media retrieval endpoints are always authenticated + // Optimization: no need to reauth user for each continuation + db::User::pointer user; + if (!request.continuation()) + user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); + + RequestContext requestContext{ request, _db.getTLSSession(), user, _config }; + + handler(requestContext, request, response); + } + catch (const UserNotAuthorizedError&) + { + response.setStatus(401); // Unauthorized + throw; + } + catch (const RequiredParameterMissingError&) + { + response.setStatus(400); // Bad Request + throw; + } + catch (const BadParameterGenericError&) + { + response.setStatus(400); // Bad Request + throw; + } + catch (const RequestedDataNotFoundError&) + { + response.setStatus(404); // Not Found + throw; + } + catch (const InternalErrorGenericError&) + { + response.setStatus(500); // Internal Server Error + throw; + } + catch (const Error&) + { + response.setStatus(400); // Assume bad request + throw; } - } - - RequestContext SubsonicResource::buildRequestContext(const Wt::Http::Request& request) - { - const Wt::Http::ParameterMap& parameters{ request.getParameterMap() }; - const ClientInfo clientInfo{ getClientInfo(request) }; - bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) }; - const ResponseFormat format{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; - - return RequestContext{ - .parameters = parameters, - .dbSession = _db.getTLSSession(), - .user = db::User::pointer{}, - .clientIpAddr = request.clientAddress(), - .clientInfo = clientInfo, - .serverProtocolVersion = getServerProtocolVersion(clientInfo.name), - .responseFormat = format, - .enableOpenSubsonic = enableOpenSubsonic, - }; } db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request) @@ -449,7 +414,7 @@ namespace lms::api::subsonic const auto user{ getParameterAs(parameters, "u") }; const auto password{ getParameterAs(parameters, "p") }; - if (!_supportUserPasswordAuthentication && (password || user)) + if (!_config.supportUserPasswordAuthentication && (password || user)) throw ProvidedAuthenticationMechanismNotSupportedError{}; const auto apiKey{ getParameterAs(parameters, "apiKey") }; diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index b0d89217..32bf4318 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -18,16 +18,13 @@ */ #pragma once -#include -#include -#include - +#include #include #include #include "database/objects/UserId.hpp" -#include "RequestContext.hpp" +#include "SubsonicResourceConfig.hpp" namespace lms::db { @@ -36,6 +33,8 @@ namespace lms::db namespace lms::api::subsonic { + class RequestContext; + class SubsonicResource final : public Wt::WResource { public: @@ -43,16 +42,13 @@ namespace lms::api::subsonic private: void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - ProtocolVersion getServerProtocolVersion(const std::string& clientName) const; - static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); - RequestContext buildRequestContext(const Wt::Http::Request& request); + using MediaRetrievalHandlerFunc = std::function; + void handleMediaRetrievalRequest(MediaRetrievalHandlerFunc handler, const Wt::Http::Request& request, Wt::Http::Response& response); + db::UserId authenticateUser(const Wt::Http::Request& request); - const std::unordered_map _serverProtocolVersionsByClient; - const std::unordered_set _openSubsonicDisabledClients; - const bool _supportUserPasswordAuthentication; - + const SubsonicResourceConfig _config; db::IDb& _db; }; } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResourceConfig.cpp b/src/libs/subsonic/impl/SubsonicResourceConfig.cpp new file mode 100644 index 00000000..f33d50b5 --- /dev/null +++ b/src/libs/subsonic/impl/SubsonicResourceConfig.cpp @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +#include "SubsonicResourceConfig.hpp" + +#include "core/IConfig.hpp" + +namespace lms::api::subsonic +{ + namespace + { + std::unordered_map readConfigProtocolVersions(core::IConfig& config) + { + std::unordered_map res; + + config.visitStrings("api-subsonic-old-server-protocol-clients", + [&](std::string_view client) { + res.emplace(std::string{ client }, ProtocolVersion{ .major = 1, .minor = 12, .patch = 0 }); + }, + { "DSub" }); + + return res; + } + + std::unordered_set readOpenSubsonicDisabledClients(core::IConfig& config) + { + std::unordered_set res; + + config.visitStrings("api-open-subsonic-disabled-clients", + [&](std::string_view client) { + res.emplace(std::string{ client }); + }, + { "DSub" }); + + return res; + } + } // namespace + + SubsonicResourceConfig readSubsonicResourceConfig(core::IConfig& config) + { + return SubsonicResourceConfig{ + .serverProtocolVersionsByClient = readConfigProtocolVersions(config), + .openSubsonicDisabledClients = readOpenSubsonicDisabledClients(config), + .supportUserPasswordAuthentication = config.getBool("api-subsonic-support-user-password-auth", true) + }; + } +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/SubsonicResourceConfig.hpp b/src/libs/subsonic/impl/SubsonicResourceConfig.hpp new file mode 100644 index 00000000..0eef5698 --- /dev/null +++ b/src/libs/subsonic/impl/SubsonicResourceConfig.hpp @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "ProtocolVersion.hpp" + +namespace lms::core +{ + class IConfig; +} + +namespace lms::api::subsonic +{ + struct SubsonicResourceConfig + { + std::unordered_map serverProtocolVersionsByClient; + std::unordered_set openSubsonicDisabledClients; + bool supportUserPasswordAuthentication; + }; + + SubsonicResourceConfig readSubsonicResourceConfig(core::IConfig& _config); +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index 2c9871d7..15e175a9 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -47,12 +47,12 @@ namespace lms::api::subsonic Response handleGetAlbumListRequestCommon(RequestContext& context, bool id3) { // Mandatory params - const std::string type{ getMandatoryParameterAs(context.parameters, "type") }; + const std::string type{ getMandatoryParameterAs(context.getParameters(), "type") }; // Optional params - const MediaLibraryId mediaLibraryId{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; - const std::size_t size{ getParameterAs(context.parameters, "size").value_or(10) }; - const std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + const MediaLibraryId mediaLibraryId{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; + const std::size_t size{ getParameterAs(context.getParameters(), "size").value_or(10) }; + const std::size_t offset{ getParameterAs(context.getParameters(), "offset").value_or(0) }; if (size > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; @@ -62,7 +62,7 @@ namespace lms::api::subsonic scrobbling::IScrobblingService& scrobblingService{ *core::Service::get() }; feedback::IFeedbackService& feedbackService{ *core::Service::get() }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; if (type == "alphabeticalByName") { @@ -71,7 +71,7 @@ namespace lms::api::subsonic params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } else if (type == "alphabeticalByArtist") { @@ -80,14 +80,14 @@ namespace lms::api::subsonic params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } else if (type == "byGenre") { // Mandatory param - const std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; + const std::string genre{ getMandatoryParameterAs(context.getParameters(), "genre") }; - if (const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") }) + if (const ClusterType::pointer clusterType{ ClusterType::find(context.getDbSession(), "GENRE") }) { if (const Cluster::pointer cluster{ clusterType->getCluster(genre) }) { @@ -97,14 +97,14 @@ namespace lms::api::subsonic params.setSortMethod(ReleaseSortMethod::Name); params.setRange(range); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } } } else if (type == "byYear") { - const int fromYear{ getMandatoryParameterAs(context.parameters, "fromYear") }; - const int toYear{ getMandatoryParameterAs(context.parameters, "toYear") }; + const int fromYear{ getMandatoryParameterAs(context.getParameters(), "fromYear") }; + const int toYear{ getMandatoryParameterAs(context.getParameters(), "toYear") }; Release::FindParameters params; params.setSortMethod(fromYear > toYear ? ReleaseSortMethod::DateDesc : ReleaseSortMethod::DateAsc); @@ -112,12 +112,12 @@ namespace lms::api::subsonic params.setDateRange(YearRange{ std::min(fromYear, toYear), std::max(fromYear, toYear) }); params.filters.setMediaLibrary(mediaLibraryId); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } else if (type == "frequent") { scrobbling::IScrobblingService::FindParameters params; - params.setUser(context.user->getId()); + params.setUser(context.getUser()->getId()); params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); @@ -130,7 +130,7 @@ namespace lms::api::subsonic params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } else if (type == "random") { @@ -141,12 +141,12 @@ namespace lms::api::subsonic params.setRange(Range{ 0, size }); params.filters.setMediaLibrary(mediaLibraryId); - releases = Release::findIds(context.dbSession, params); + releases = Release::findIds(context.getDbSession(), params); } else if (type == "recent") { scrobbling::IScrobblingService::FindParameters params; - params.setUser(context.user->getId()); + params.setUser(context.getUser()->getId()); params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); @@ -155,7 +155,7 @@ namespace lms::api::subsonic else if (type == "starred") { feedback::IFeedbackService::FindParameters params; - params.setUser(context.user->getId()); + params.setUser(context.getUser()->getId()); params.setRange(range); params.filters.setMediaLibrary(mediaLibraryId); @@ -166,12 +166,12 @@ namespace lms::api::subsonic throw NotImplementedGenericError{}; } - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) }; for (const ReleaseId releaseId : releases.results) { - const Release::pointer release{ Release::find(context.dbSession, releaseId) }; + const Release::pointer release{ Release::find(context.getDbSession(), releaseId) }; albumListNode.addArrayChild("album", createAlbumNode(context, release, id3)); } @@ -181,11 +181,11 @@ namespace lms::api::subsonic Response handleGetStarredRequestCommon(RequestContext& context, bool id3) { // Optional parameters - const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; + const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) }; feedback::IFeedbackService& feedbackService{ *core::Service::get() }; @@ -194,29 +194,29 @@ namespace lms::api::subsonic if (id3) { feedback::IFeedbackService::ArtistFindParameters artistFindParams; - artistFindParams.setUser(context.user->getId()); + artistFindParams.setUser(context.getUser()->getId()); artistFindParams.setSortMethod(ArtistSortMethod::SortName); for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) { - if (auto artist{ Artist::find(context.dbSession, artistId) }) + if (auto artist{ Artist::find(context.getDbSession(), artistId) }) starredNode.addArrayChild("artist", createArtistNode(context, artist)); } } feedback::IFeedbackService::FindParameters findParameters; - findParameters.setUser(context.user->getId()); + findParameters.setUser(context.getUser()->getId()); findParameters.filters.setMediaLibrary(mediaLibrary); for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results) { - if (auto release{ Release::find(context.dbSession, releaseId) }) + if (auto release{ Release::find(context.getDbSession(), releaseId) }) starredNode.addArrayChild("album", createAlbumNode(context, release, id3)); } for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results) { - if (auto track{ Track::find(context.dbSession, trackId) }) - starredNode.addArrayChild("song", createSongNode(context, track, context.user)); + if (auto track{ Track::find(context.getDbSession(), trackId) }) + starredNode.addArrayChild("song", createSongNode(context, track, context.getUser())); } return response; @@ -236,23 +236,23 @@ namespace lms::api::subsonic Response handleGetRandomSongsRequest(RequestContext& context) { // Optional params - const MediaLibraryId mediaLibraryId{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; - std::size_t size{ getParameterAs(context.parameters, "size").value_or(50) }; + const MediaLibraryId mediaLibraryId{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; + std::size_t size{ getParameterAs(context.getParameters(), "size").value_or(50) }; if (size > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& randomSongsNode{ response.createNode("randomSongs") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; Track::FindParameters params; params.setSortMethod(TrackSortMethod::Random); params.setRange(Range{ 0, size }); params.filters.setMediaLibrary(mediaLibraryId); - Track::find(context.dbSession, params, [&](const Track::pointer& track) { - randomSongsNode.addArrayChild("song", createSongNode(context, track, context.user)); + Track::find(context.getDbSession(), params, [&](const Track::pointer& track) { + randomSongsNode.addArrayChild("song", createSongNode(context, track, context.getUser())); }); return response; @@ -261,19 +261,19 @@ namespace lms::api::subsonic Response handleGetSongsByGenreRequest(RequestContext& context) { // Mandatory params - std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; + std::string genre{ getMandatoryParameterAs(context.getParameters(), "genre") }; // Optional params - const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; - std::size_t count{ getParameterAs(context.parameters, "count").value_or(10) }; + const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; + std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(10) }; if (count > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize }; - std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + std::size_t offset{ getParameterAs(context.getParameters(), "offset").value_or(0) }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - auto clusterType{ ClusterType::find(context.dbSession, "GENRE") }; + auto clusterType{ ClusterType::find(context.getDbSession(), "GENRE") }; if (!clusterType) throw RequestedDataNotFoundError{}; @@ -281,7 +281,7 @@ namespace lms::api::subsonic if (!cluster) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& songsByGenreNode{ response.createNode("songsByGenre") }; Track::FindParameters params; @@ -289,8 +289,8 @@ namespace lms::api::subsonic params.filters.setMediaLibrary(mediaLibrary); params.setRange(Range{ offset, count }); - Track::find(context.dbSession, params, [&](const Track::pointer& track) { - songsByGenreNode.addArrayChild("song", createSongNode(context, track, context.user)); + Track::find(context.getDbSession(), params, [&](const Track::pointer& track) { + songsByGenreNode.addArrayChild("song", createSongNode(context, track, context.getUser())); }); return response; diff --git a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp index 7587bed7..41ea02cc 100644 --- a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp +++ b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp @@ -38,18 +38,18 @@ namespace lms::api::subsonic Response handleGetBookmarks(RequestContext& context) { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const auto bookmarkIds{ TrackBookmark::find(context.dbSession, context.user->getId()) }; + const auto bookmarkIds{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId()) }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& bookmarksNode{ response.createNode("bookmarks") }; for (const TrackBookmarkId bookmarkId : bookmarkIds.results) { - const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) }; + const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.getDbSession(), bookmarkId) }; Response::Node bookmarkNode{ createBookmarkNode(bookmark) }; - bookmarkNode.addChild("entry", createSongNode(context, bookmark->getTrack(), context.user)); + bookmarkNode.addChild("entry", createSongNode(context, bookmark->getTrack(), context.getUser())); bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode)); } @@ -59,51 +59,51 @@ namespace lms::api::subsonic Response handleCreateBookmark(RequestContext& context) { // Mandatory params - TrackId trackId{ getMandatoryParameterAs(context.parameters, "id") }; - unsigned long position{ getMandatoryParameterAs(context.parameters, "position") }; - const std::optional comment{ getParameterAs(context.parameters, "comment") }; + TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; + unsigned long position{ getMandatoryParameterAs(context.getParameters(), "position") }; + const std::optional comment{ getParameterAs(context.getParameters(), "comment") }; - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; - const Track::pointer track{ Track::find(context.dbSession, trackId) }; + const Track::pointer track{ Track::find(context.getDbSession(), trackId) }; if (!track) throw RequestedDataNotFoundError{}; // Replace any existing bookmark - auto bookmark{ TrackBookmark::find(context.dbSession, context.user->getId(), trackId) }; + auto bookmark{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; if (!bookmark) - bookmark = context.dbSession.create(context.user, track); + bookmark = context.getDbSession().create(context.getUser(), track); bookmark.modify()->setOffset(std::chrono::milliseconds{ position }); if (comment) bookmark.modify()->setComment(*comment); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleDeleteBookmark(RequestContext& context) { // Mandatory params - TrackId trackId{ getMandatoryParameterAs(context.parameters, "id") }; + TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; - auto bookmark{ TrackBookmark::find(context.dbSession, context.user->getId(), trackId) }; + auto bookmark{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; if (!bookmark) throw RequestedDataNotFoundError{}; bookmark.remove(); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } // Use a dedicated internal playlist Response handleGetPlayQueue(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; - auto transaction{ context.dbSession.createReadTransaction() }; - const db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.dbSession, context.user->getId(), "subsonic") }; + auto transaction{ context.getDbSession().createReadTransaction() }; + const db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), "subsonic") }; if (playQueue) { Response::Node& playQueueNode{ response.createNode("playQueue") }; @@ -115,7 +115,7 @@ namespace lms::api::subsonic } // mandatory fields - playQueueNode.setAttribute("username", context.user->getLoginName()); + playQueueNode.setAttribute("username", context.getUser()->getLoginName()); playQueueNode.setAttribute("changed", core::stringUtils::toISO8601String(playQueue->getLastModifiedDateTime())); playQueueNode.setAttribute("changedBy", "unknown"); // we don't store the client name (could be several same clients on several devices...) @@ -130,9 +130,9 @@ namespace lms::api::subsonic Response handleSavePlayQueue(RequestContext& context) { // optional params - std::vector trackIds{ getMultiParametersAs(context.parameters, "id") }; - const std::optional currentTrackId{ getParameterAs(context.parameters, "current") }; - const std::chrono::milliseconds currentPositionInTrack{ getParameterAs(context.parameters, "current").value_or(0) }; + std::vector trackIds{ getMultiParametersAs(context.getParameters(), "id") }; + const std::optional currentTrackId{ getParameterAs(context.getParameters(), "current") }; + const std::chrono::milliseconds currentPositionInTrack{ getParameterAs(context.getParameters(), "current").value_or(0) }; std::vector tracks; tracks.reserve(trackIds.size()); @@ -140,20 +140,20 @@ namespace lms::api::subsonic // no id means we clear the play queue (see https://github.com/opensubsonic/open-subsonic-api/pull/106) if (!trackIds.empty()) { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; for (db::TrackId trackId : trackIds) { - if (db::Track::pointer track{ db::Track::find(context.dbSession, trackId) }) + if (db::Track::pointer track{ db::Track::find(context.getDbSession(), trackId) }) tracks.push_back(track); } } { - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; - db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.dbSession, context.user->getId(), "subsonic") }; + db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), "subsonic") }; if (!playQueue) - playQueue = context.dbSession.create(context.user, "subsonic"); + playQueue = context.getDbSession().create(context.getUser(), "subsonic"); playQueue.modify()->clear(); std::size_t index{}; @@ -171,6 +171,6 @@ namespace lms::api::subsonic playQueue.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); } - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index 5d0d9e10..561e57c2 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -91,7 +91,7 @@ namespace lms::api::subsonic params.setParentDirectory(parentDirectory->getId()); params.setSortMethod(DirectorySortMethod::Name); - Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) { + Directory::find(context.getDbSession(), params, [&](const Directory::pointer& directory) { const std::string_view name{ directory->getName() }; assert(!name.empty()); @@ -114,7 +114,7 @@ namespace lms::api::subsonic const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; std::vector tracks; tracks.reserve(count); @@ -126,7 +126,7 @@ namespace lms::api::subsonic params.setRange(Range{ 0, meanTrackCountPerArtist }); params.setSortMethod(TrackSortMethod::Random); - const auto artistTracks{ Track::findIds(context.dbSession, params) }; + const auto artistTracks{ Track::findIds(context.getDbSession(), params) }; tracks.insert(std::end(tracks), std::begin(artistTracks.results), std::end(artistTracks.results)); @@ -145,7 +145,7 @@ namespace lms::api::subsonic const std::size_t meanTrackCountPerRelease{ (count / releaseIds.size()) + 1 }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; std::vector tracks; tracks.reserve(count); @@ -157,7 +157,7 @@ namespace lms::api::subsonic params.setRange(Range{ 0, meanTrackCountPerRelease }); params.setSortMethod(TrackSortMethod::Random); - const auto releaseTracks{ Track::findIds(context.dbSession, params) }; + const auto releaseTracks{ Track::findIds(context.getDbSession(), params) }; tracks.insert(std::end(tracks), std::begin(releaseTracks.results), std::end(releaseTracks.results)); @@ -174,31 +174,31 @@ namespace lms::api::subsonic Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) { // Optional params - std::size_t count{ getParameterAs(context.parameters, "count").value_or(50) }; + std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(50) }; if (count > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize }; std::vector tracks; - if (const auto artistId{ getParameterAs(context.parameters, "id") }) + if (const auto artistId{ getParameterAs(context.getParameters(), "id") }) tracks = findSimilarSongs(context, *artistId, count); - else if (const auto releaseId{ getParameterAs(context.parameters, "id") }) + else if (const auto releaseId{ getParameterAs(context.getParameters(), "id") }) tracks = findSimilarSongs(context, *releaseId, count); - else if (const auto trackId{ getParameterAs(context.parameters, "id") }) + else if (const auto trackId{ getParameterAs(context.getParameters(), "id") }) tracks = findSimilarSongs(context, *trackId, count); else throw BadParameterGenericError{ "id" }; core::random::shuffleContainer(tracks); - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& similarSongsNode{ response.createNode(id3 ? Response::Node::Key{ "similarSongs2" } : Response::Node::Key{ "similarSongs" }) }; for (const TrackId trackId : tracks) { - const Track::pointer track{ Track::find(context.dbSession, trackId) }; - similarSongsNode.addArrayChild("song", createSongNode(context, track, context.user)); + const Track::pointer track{ Track::find(context.getDbSession(), trackId) }; + similarSongsNode.addArrayChild("song", createSongNode(context, track, context.getUser())); } return response; @@ -223,11 +223,11 @@ namespace lms::api::subsonic Response handleGetMusicFoldersRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& musicFoldersNode{ response.createNode("musicFolders") }; - auto transaction{ context.dbSession.createReadTransaction() }; - MediaLibrary::find(context.dbSession, [&](const MediaLibrary::pointer& library) { + auto transaction{ context.getDbSession().createReadTransaction() }; + MediaLibrary::find(context.getDbSession(), [&](const MediaLibrary::pointer& library) { Response::Node& musicFolderNode{ musicFoldersNode.createArrayChild("musicFolder") }; musicFolderNode.setAttribute("id", library->getId().getValue()); @@ -240,16 +240,16 @@ namespace lms::api::subsonic Response handleGetIndexesRequest(RequestContext& context) { // Optional params - const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; + const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& indexesNode{ response.createNode("indexes") }; indexesNode.setAttribute("ignoredArticles", ""); indexesNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write? - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const std::vector rootDirectories{ getRootDirectories(context.dbSession, mediaLibrary) }; + const std::vector rootDirectories{ getRootDirectories(context.getDbSession(), mediaLibrary) }; IndexMap indexedDirectories; for (const Directory::pointer& rootdirectory : rootDirectories) @@ -257,8 +257,8 @@ namespace lms::api::subsonic Track::FindParameters params; params.setDirectory(rootdirectory->getId()); - Track::find(context.dbSession, params, [&](const Track::pointer& track) { - indexesNode.addArrayChild("child", createSongNode(context, track, context.user)); + Track::find(context.getDbSession(), params, [&](const Track::pointer& track) { + indexesNode.addArrayChild("child", createSongNode(context, track, context.getUser())); }); getIndexedChildDirectories(context, rootdirectory, indexedDirectories); @@ -287,21 +287,21 @@ namespace lms::api::subsonic Response handleGetMusicDirectoryRequest(RequestContext& context) { // Mandatory params - const auto directoryId{ getMandatoryParameterAs(context.parameters, "id") }; + const auto directoryId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& directoryNode{ response.createNode("directory") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const Directory::pointer directory{ Directory::find(context.dbSession, directoryId) }; + const Directory::pointer directory{ Directory::find(context.getDbSession(), directoryId) }; if (!directory) throw RequestedDataNotFoundError{}; - if (const Release::pointer release{ getReleaseFromDirectory(context.dbSession, directoryId) }) + if (const Release::pointer release{ getReleaseFromDirectory(context.getDbSession(), directoryId) }) { - directoryNode.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), release->getId())); - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid()) + directoryNode.setAttribute("playCount", core::Service::get()->getCount(context.getUser()->getId(), release->getId())); + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.getUser()->getId(), release->getId()) }; dateTime.isValid()) directoryNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); } @@ -317,8 +317,8 @@ namespace lms::api::subsonic params.setParentDirectory(directory->getId()); params.setSortMethod(DirectorySortMethod::Name); - Directory::find(context.dbSession, params, [&](const Directory::pointer& subDirectory) { - const Release::pointer release{ getReleaseFromDirectory(context.dbSession, subDirectory->getId()) }; + Directory::find(context.getDbSession(), params, [&](const Directory::pointer& subDirectory) { + const Release::pointer release{ getReleaseFromDirectory(context.getDbSession(), subDirectory->getId()) }; if (release) { @@ -343,8 +343,8 @@ namespace lms::api::subsonic params.setDirectory(directory->getId()); params.setSortMethod(TrackSortMethod::AbsoluteFilePath); - Track::find(context.dbSession, params, [&](const Track::pointer& track) { - directoryNode.addArrayChild("child", createSongNode(context, track, context.user)); + Track::find(context.getDbSession(), params, [&](const Track::pointer& track) { + directoryNode.addArrayChild("child", createSongNode(context, track, context.getUser())); }); } @@ -353,13 +353,13 @@ namespace lms::api::subsonic Response handleGetGenresRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& genresNode{ response.createNode("genres") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") }; + const ClusterType::pointer clusterType{ ClusterType::find(context.getDbSession(), "GENRE") }; if (clusterType) { const auto clusters{ clusterType->getClusters() }; @@ -374,9 +374,9 @@ namespace lms::api::subsonic Response handleGetArtistsRequest(RequestContext& context) { // Optional params - const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; + const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& artistsNode{ response.createNode("artists") }; artistsNode.setAttribute("ignoredArticles", ""); @@ -384,10 +384,10 @@ namespace lms::api::subsonic Artist::FindParameters parameters; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; parameters.setSortMethod(ArtistSortMethod::SortName); - switch (context.user->getSubsonicArtistListMode()) + switch (context.getUser()->getSubsonicArtistListMode()) { case SubsonicArtistListMode::AllArtists: break; @@ -411,10 +411,10 @@ namespace lms::api::subsonic bool hasMoreArtists{ true }; while (hasMoreArtists) { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; parameters.setRange(Range{ currentArtistOffset, batchSize }); - const auto artists{ Artist::find(context.dbSession, parameters) }; + const auto artists{ Artist::find(context.getDbSession(), parameters) }; for (const Artist::pointer& artist : artists.results) { std::string_view sortName{ artist->getSortName() }; @@ -441,9 +441,9 @@ namespace lms::api::subsonic for (const ArtistId artistId : artistIds) { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) }) + if (const Artist::pointer artist{ Artist::find(context.getDbSession(), artistId) }) indexNode.addArrayChild("artist", createArtistNode(context, artist)); } } @@ -454,18 +454,18 @@ namespace lms::api::subsonic Response handleGetArtistRequest(RequestContext& context) { // Mandatory params - ArtistId id{ getMandatoryParameterAs(context.parameters, "id") }; + ArtistId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const Artist::pointer artist{ Artist::find(context.dbSession, id) }; + const Artist::pointer artist{ Artist::find(context.getDbSession(), id) }; if (!artist) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node artistNode{ createArtistNode(context, artist) }; - const auto releases{ Release::find(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) }; + const auto releases{ Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; for (const Release::pointer& release : releases.results) artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */)); @@ -477,18 +477,18 @@ namespace lms::api::subsonic Response handleGetAlbumRequest(RequestContext& context) { // Mandatory params - ReleaseId id{ getMandatoryParameterAs(context.parameters, "id") }; + ReleaseId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - Release::pointer release{ Release::find(context.dbSession, id) }; + Release::pointer release{ Release::find(context.getDbSession(), id) }; if (!release) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node albumNode{ createAlbumNode(context, release, true /* id3 */) }; - const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; + const auto tracks{ Track::find(context.getDbSession(), Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; for (const Track::pointer& track : tracks.results) albumNode.addArrayChild("song", createSongNode(context, track, true /* id3 */)); @@ -500,16 +500,16 @@ namespace lms::api::subsonic Response handleGetSongRequest(RequestContext& context) { // Mandatory params - TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; + TrackId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const Track::pointer track{ Track::find(context.dbSession, id) }; + const Track::pointer track{ Track::find(context.getDbSession(), id) }; if (!track) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - response.addNode("song", createSongNode(context, track, context.user)); + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + response.addNode("song", createSongNode(context, track, context.getUser())); return response; } @@ -517,24 +517,24 @@ namespace lms::api::subsonic Response handleGetArtistInfo2Request(RequestContext& context) { // Mandatory params - ArtistId id{ getMandatoryParameterAs(context.parameters, "id") }; + ArtistId id{ getMandatoryParameterAs(context.getParameters(), "id") }; // Optional params - std::size_t count{ getParameterAs(context.parameters, "count").value_or(20) }; + std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(20) }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& artistInfoNode{ response.createNode(Response::Node::Key{ "artistInfo2" }) }; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const Artist::pointer artist{ Artist::find(context.dbSession, id) }; + const Artist::pointer artist{ Artist::find(context.getDbSession(), id) }; if (!artist) throw RequestedDataNotFoundError{}; if (const std::optional artistMBID{ artist->getMBID() }) { - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: artistInfoNode.setAttribute("musicBrainzId", artistMBID->getAsString()); @@ -545,10 +545,10 @@ namespace lms::api::subsonic } } - ArtistInfo::find(context.dbSession, id, Range{ .offset = 0, .size = 1 }, [&](const ArtistInfo::pointer& artistInfo) { + ArtistInfo::find(context.getDbSession(), id, Range{ .offset = 0, .size = 1 }, [&](const ArtistInfo::pointer& artistInfo) { if (!artistInfo->getBiography().empty()) { - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: artistInfoNode.setAttribute("biography", artistInfo->getBiography()); @@ -564,11 +564,11 @@ namespace lms::api::subsonic auto similarArtistsId{ core::Service::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) }; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; for (const ArtistId similarArtistId : similarArtistsId) { - const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) }; + const Artist::pointer similarArtist{ Artist::find(context.getDbSession(), similarArtistId) }; if (similarArtist) artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist)); } @@ -579,14 +579,14 @@ namespace lms::api::subsonic Response handleGetAlbumInfo(RequestContext& context) { - const db::DirectoryId directoryId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::DirectoryId directoryId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - if (db::Release::pointer release{ getReleaseFromDirectory(context.dbSession, directoryId) }) + if (db::Release::pointer release{ getReleaseFromDirectory(context.getDbSession(), directoryId) }) response.addNode("albumInfo", createAlbumInfoNode(context, release)); } return response; @@ -594,14 +594,14 @@ namespace lms::api::subsonic Response handleGetAlbumInfo2(RequestContext& context) { - const db::ReleaseId releaseId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::ReleaseId releaseId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - if (db::Release::pointer release{ db::Release::find(context.dbSession, releaseId) }) + if (db::Release::pointer release{ db::Release::find(context.getDbSession(), releaseId) }) response.addNode("albumInfo", createAlbumInfoNode(context, release)); } @@ -621,29 +621,29 @@ namespace lms::api::subsonic Response handleGetTopSongs(RequestContext& context) { // Mandatory params - std::string_view artistName{ getMandatoryParameterAs(context.parameters, "artist") }; - std::size_t count{ getParameterAs(context.parameters, "count").value_or(50) }; + std::string_view artistName{ getMandatoryParameterAs(context.getParameters(), "artist") }; + std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(50) }; if (count > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& topSongs{ response.createNode("topSongs") }; - const auto artists{ Artist::find(context.dbSession, artistName) }; + const auto artists{ Artist::find(context.getDbSession(), artistName) }; if (artists.size() == 1) { scrobbling::IScrobblingService::FindParameters params; - params.setUser(context.user->getId()); + params.setUser(context.getUser()->getId()); params.setRange(db::Range{ 0, count }); params.setArtist(artists.front()->getId()); const auto trackIds{ core::Service::get()->getTopTracks(params) }; for (const TrackId trackId : trackIds.results) { - if (Track::pointer track{ Track::find(context.dbSession, trackId) }) - topSongs.addArrayChild("song", createSongNode(context, track, context.user)); + if (Track::pointer track{ Track::find(context.getDbSession(), trackId) }) + topSongs.addArrayChild("song", createSongNode(context, track, context.getUser())); } } diff --git a/src/libs/subsonic/impl/endpoints/Browsing.hpp b/src/libs/subsonic/impl/endpoints/Browsing.hpp index f0db9e58..c27ad595 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.hpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.hpp @@ -32,7 +32,6 @@ namespace lms::api::subsonic Response handleGetArtistRequest(RequestContext& context); Response handleGetAlbumRequest(RequestContext& context); Response handleGetSongRequest(RequestContext& context); - Response handleGetArtistInfoRequest(RequestContext& context); Response handleGetArtistInfo2Request(RequestContext& context); Response handleGetAlbumInfo(RequestContext& context); Response handleGetAlbumInfo2(RequestContext& context); diff --git a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp index 3bd79e77..ae033bae 100644 --- a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp @@ -113,72 +113,72 @@ namespace lms::api::subsonic Response handleStarRequest(RequestContext& context) { - StarParameters params{ getStarParameters(context.parameters) }; + StarParameters params{ getStarParameters(context.getParameters()) }; for (const DirectoryId id : params.directoryIds) { - if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.dbSession, id) }; releaseId.isValid()) - core::Service::get()->star(context.user->getId(), releaseId); + if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.getDbSession(), id) }; releaseId.isValid()) + core::Service::get()->star(context.getUser()->getId(), releaseId); } for (const ArtistId id : params.artistIds) - core::Service::get()->star(context.user->getId(), id); + core::Service::get()->star(context.getUser()->getId(), id); for (const ReleaseId id : params.releaseIds) - core::Service::get()->star(context.user->getId(), id); + core::Service::get()->star(context.getUser()->getId(), id); for (const TrackId id : params.trackIds) - core::Service::get()->star(context.user->getId(), id); + core::Service::get()->star(context.getUser()->getId(), id); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleUnstarRequest(RequestContext& context) { - const StarParameters params{ getStarParameters(context.parameters) }; + const StarParameters params{ getStarParameters(context.getParameters()) }; for (const DirectoryId id : params.directoryIds) { - if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.dbSession, id) }; releaseId.isValid()) - core::Service::get()->unstar(context.user->getId(), releaseId); + if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.getDbSession(), id) }; releaseId.isValid()) + core::Service::get()->unstar(context.getUser()->getId(), releaseId); } for (const ArtistId id : params.artistIds) - core::Service::get()->unstar(context.user->getId(), id); + core::Service::get()->unstar(context.getUser()->getId(), id); for (const ReleaseId id : params.releaseIds) - core::Service::get()->unstar(context.user->getId(), id); + core::Service::get()->unstar(context.getUser()->getId(), id); for (const TrackId id : params.trackIds) - core::Service::get()->unstar(context.user->getId(), id); + core::Service::get()->unstar(context.getUser()->getId(), id); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleSetRating(RequestContext& context) { - const RatingParameters params{ getRatingParameters(context.parameters) }; + const RatingParameters params{ getRatingParameters(context.getParameters()) }; if (const ArtistId * artistId{ std::get_if(¶ms.id) }) - core::Service::get()->setRating(context.user->getId(), *artistId, params.rating); + core::Service::get()->setRating(context.getUser()->getId(), *artistId, params.rating); else if (const DirectoryId * directoryId{ std::get_if(¶ms.id) }) { - if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.dbSession, *directoryId) }; releaseId.isValid()) - core::Service::get()->setRating(context.user->getId(), releaseId, params.rating); + if (const ReleaseId releaseId{ getReleaseIdFromDirectory(context.getDbSession(), *directoryId) }; releaseId.isValid()) + core::Service::get()->setRating(context.getUser()->getId(), releaseId, params.rating); } else if (const ReleaseId * releaseId{ std::get_if(¶ms.id) }) - core::Service::get()->setRating(context.user->getId(), *releaseId, params.rating); + core::Service::get()->setRating(context.getUser()->getId(), *releaseId, params.rating); else if (const TrackId * trackId{ std::get_if(¶ms.id) }) - core::Service::get()->setRating(context.user->getId(), *trackId, params.rating); + core::Service::get()->setRating(context.getUser()->getId(), *trackId, params.rating); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleScrobble(RequestContext& context) { - const std::vector ids{ getMandatoryMultiParametersAs(context.parameters, "id") }; - const std::vector times{ getMultiParametersAs(context.parameters, "time") }; - const bool submission{ getParameterAs(context.parameters, "submission").value_or(true) }; + const std::vector ids{ getMandatoryMultiParametersAs(context.getParameters(), "id") }; + const std::vector times{ getMultiParametersAs(context.getParameters(), "time") }; + const bool submission{ getParameterAs(context.getParameters(), "submission").value_or(true) }; // playing now => only one at a time if (!submission && ids.size() > 1) @@ -190,13 +190,13 @@ namespace lms::api::subsonic if (!submission) { - core::Service::get()->listenStarted({ context.user->getId(), ids.front() }); + core::Service::get()->listenStarted({ context.getUser()->getId(), ids.front() }); } else { if (times.empty()) { - core::Service::get()->listenFinished({ context.user->getId(), ids.front() }); + core::Service::get()->listenFinished({ context.getUser()->getId(), ids.front() }); } else { @@ -204,11 +204,11 @@ namespace lms::api::subsonic { const TrackId trackId{ ids[i] }; const unsigned long time{ times[i] }; - core::Service::get()->addTimedListen({ { context.user->getId(), trackId }, Wt::WDateTime::fromTime_t(static_cast(time / 1000)) }); + core::Service::get()->addTimedListen({ { context.getUser()->getId(), trackId }, Wt::WDateTime::fromTime_t(static_cast(time / 1000)) }); } } } - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp b/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp index 3a899b2b..2c09c161 100644 --- a/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp @@ -51,7 +51,7 @@ namespace lms::api::subsonic::Scan Response handleGetScanStatus(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; response.addNode("scanStatus", createStatusResponseNode()); return response; @@ -61,7 +61,7 @@ namespace lms::api::subsonic::Scan { core::Service::get()->requestImmediateScan(); - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; response.addNode("scanStatus", createStatusResponseNode()); return response; diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 1ad9d273..8254c229 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -27,8 +27,9 @@ #include "core/IResourceHandler.hpp" #include "core/String.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" +#include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/objects/PodcastEpisode.hpp" @@ -39,7 +40,7 @@ #include "services/artwork/IArtworkService.hpp" #include "services/podcast/IPodcastService.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "CoverArtId.hpp" #include "ParameterParsing.hpp" @@ -52,12 +53,12 @@ namespace lms::api::subsonic { namespace { - std::optional subsonicStreamFormatToAvOutputFormat(std::string_view format) + std::optional subsonicStreamFormatToTranscodingOutputFormat(std::string_view format) { - for (const auto& [str, avFormat] : std::initializer_list>{ - { "mp3", transcoding::OutputFormat::MP3 }, - { "opus", transcoding::OutputFormat::OGG_OPUS }, - { "vorbis", transcoding::OutputFormat::OGG_VORBIS }, + for (const auto& [str, avFormat] : std::initializer_list>{ + { "mp3", audio::OutputFormat::MP3 }, + { "opus", audio::OutputFormat::OGG_OPUS }, + { "vorbis", audio::OutputFormat::OGG_VORBIS }, }) { if (core::stringUtils::stringCaseInsensitiveEqual(str, format)) @@ -66,38 +67,39 @@ namespace lms::api::subsonic return std::nullopt; } - transcoding::OutputFormat userTranscodeFormatToAvFormat(db::TranscodingOutputFormat format) + audio::OutputFormat userTranscodeFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return transcoding::OutputFormat::MP3; + return audio::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return transcoding::OutputFormat::OGG_OPUS; + return audio::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return transcoding::OutputFormat::MATROSKA_OPUS; + return audio::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return transcoding::OutputFormat::OGG_VORBIS; + return audio::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return transcoding::OutputFormat::WEBM_VORBIS; + return audio::OutputFormat::WEBM_VORBIS; } - return transcoding::OutputFormat::OGG_OPUS; + + return audio::OutputFormat::OGG_OPUS; } - bool isCodecCompatibleWithOutputFormat(av::DecodingCodec codec, transcoding::OutputFormat outputFormat) + bool isCodecCompatibleWithOutputFormat(audio::CodecType codec, audio::OutputFormat outputFormat) { switch (outputFormat) { - case transcoding::OutputFormat::MP3: - return codec == av::DecodingCodec::MP3; + case audio::OutputFormat::MP3: + return codec == audio::CodecType::MP3; - case transcoding::OutputFormat::OGG_OPUS: - case transcoding::OutputFormat::MATROSKA_OPUS: - return codec == av::DecodingCodec::OPUS; + case audio::OutputFormat::OGG_OPUS: + case audio::OutputFormat::MATROSKA_OPUS: + return codec == audio::CodecType::Opus; - case transcoding::OutputFormat::OGG_VORBIS: - case transcoding::OutputFormat::WEBM_VORBIS: - return codec == av::DecodingCodec::VORBIS; + case audio::OutputFormat::OGG_VORBIS: + case audio::OutputFormat::WEBM_VORBIS: + return codec == audio::CodecType::Vorbis; } return true; @@ -105,26 +107,22 @@ namespace lms::api::subsonic struct StreamParameters { - transcoding::InputParameters inputParameters; - std::string inputMimeType; // set if known - std::optional outputParameters; + std::filesystem::path filePath; + std::string fileMimeType; // set if known + std::optional transcodeParameters; bool estimateContentLength{}; }; - bool isOutputFormatCompatible(const std::filesystem::path& trackPath, transcoding::OutputFormat outputFormat) + bool isOutputFormatCompatible(const std::filesystem::path& trackPath, audio::OutputFormat outputFormat) { // TODO: put this information in db during scan try { - const auto audioFile{ av::parseAudioFile(trackPath) }; + const auto audioFile{ audio::parseAudioFile(trackPath) }; - const auto streamInfo{ audioFile->getBestStreamInfo() }; - if (!streamInfo) - throw RequestedDataNotFoundError{}; // TODO 404? - - return isCodecCompatibleWithOutputFormat(streamInfo->codec, outputFormat); + return isCodecCompatibleWithOutputFormat(audioFile->getAudioProperties().codec, outputFormat); } - catch (const av::Exception& e) + catch (const audio::Exception& e) { // TODO 404? throw RequestedDataNotFoundError{}; @@ -176,37 +174,34 @@ namespace lms::api::subsonic StreamParameters getStreamParameters(RequestContext& context) { // Mandatory params - const auto trackId{ getParameterAs(context.parameters, "id") }; - const auto podcastEpisodeId{ getParameterAs(context.parameters, "id") }; + const auto trackId{ getParameterAs(context.getParameters(), "id") }; + const auto podcastEpisodeId{ getParameterAs(context.getParameters(), "id") }; if (!trackId && !podcastEpisodeId) throw RequiredParameterMissingError{ "id" }; const AudioFileId audioId{ trackId ? AudioFileId{ *trackId } : AudioFileId{ *podcastEpisodeId } }; // Optional params - std::size_t maxBitRate{ getParameterAs(context.parameters, "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs - const std::string format{ getParameterAs(context.parameters, "format").value_or("") }; - std::size_t timeOffset{ getParameterAs(context.parameters, "timeOffset").value_or(0) }; - bool estimateContentLength{ getParameterAs(context.parameters, "estimateContentLength").value_or(false) }; + std::size_t maxBitRate{ getParameterAs(context.getParameters(), "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs + const std::string format{ getParameterAs(context.getParameters(), "format").value_or("") }; + std::size_t timeOffset{ getParameterAs(context.getParameters(), "timeOffset").value_or(0) }; + bool estimateContentLength{ getParameterAs(context.getParameters(), "estimateContentLength").value_or(false) }; - const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.dbSession, audioId) }; + const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioId) }; StreamParameters parameters; - - parameters.inputParameters.filePath = audioFileInfo.path; - parameters.inputParameters.duration = audioFileInfo.duration; - parameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; - parameters.inputMimeType = audioFileInfo.mimeType; + parameters.filePath = audioFileInfo.path; + parameters.fileMimeType = audioFileInfo.mimeType; parameters.estimateContentLength = estimateContentLength; if (format == "raw") // raw => no transcoding return parameters; // TODO: what if offset is not 0? - std::optional requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) }; + std::optional requestedFormat{ subsonicStreamFormatToTranscodingOutputFormat(format) }; if (!requestedFormat) { - if (context.user->getSubsonicEnableTranscodingByDefault()) - requestedFormat = userTranscodeFormatToAvFormat(context.user->getSubsonicDefaultTranscodingOutputFormat()); + if (context.getUser()->getSubsonicEnableTranscodingByDefault()) + requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); } if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.bitrate <= maxBitRate)) @@ -231,16 +226,22 @@ namespace lms::api::subsonic // Need to transcode here if (!requestedFormat) - requestedFormat = userTranscodeFormatToAvFormat(context.user->getSubsonicDefaultTranscodingOutputFormat()); + requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); if (!bitrate) - bitrate = context.user->getSubsonicDefaultTranscodingOutputBitrate(); + bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate(); if (maxBitRate) bitrate = std::min(bitrate, maxBitRate); - transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; - outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - outputParameters.format = *requestedFormat; - outputParameters.bitrate = bitrate; + audio::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; + + transcodeParameters.inputParameters.filePath = audioFileInfo.path; + transcodeParameters.inputParameters.duration = audioFileInfo.duration; + transcodeParameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; + ; + + transcodeParameters.outputParameters.bitrate = bitrate; + transcodeParameters.outputParameters.format = *requestedFormat; + transcodeParameters.outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) return parameters; } @@ -248,13 +249,13 @@ namespace lms::api::subsonic Response handleGetLyrics(RequestContext& context) { - std::string artistName{ getParameterAs(context.parameters, "artist").value_or("") }; - std::string titleName{ getParameterAs(context.parameters, "title").value_or("") }; + std::string artistName{ getParameterAs(context.getParameters(), "artist").value_or("") }; + std::string titleName{ getParameterAs(context.getParameters(), "title").value_or("") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; // best effort search, as this API is really limited - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; db::Track::FindParameters params; params.setName(titleName); @@ -262,7 +263,7 @@ namespace lms::api::subsonic params.setRange(db::Range{ .offset = 0, .size = 2 }); // Choice: we return nothing if there are too many results - const auto tracks{ db::Track::findIds(context.dbSession, params) }; + const auto tracks{ db::Track::findIds(context.getDbSession(), params) }; if (tracks.results.size() == 1) { // Choice: we return only the first lyrics if the track has many lyrics @@ -271,7 +272,7 @@ namespace lms::api::subsonic lyricsParams.setSortMethod(db::TrackLyricsSortMethod::ExternalFirst); lyricsParams.setRange(db::Range{ 0, 1 }); - db::TrackLyrics::find(context.dbSession, lyricsParams, [&](const db::TrackLyrics::pointer& lyrics) { + db::TrackLyrics::find(context.getDbSession(), lyricsParams, [&](const db::TrackLyrics::pointer& lyrics) { response.addNode("lyrics", createLyricsNode(context, lyrics)); }); } @@ -282,14 +283,14 @@ namespace lms::api::subsonic Response handleGetLyricsBySongId(RequestContext& context) { // mandatory params - db::TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; + db::TrackId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& lyricsList{ response.createNode("lyricsList") }; lyricsList.createEmptyArrayChild("structuredLyrics"); - auto transaction{ context.dbSession.createReadTransaction() }; - const db::Track::pointer track{ db::Track::find(context.dbSession, id) }; + auto transaction{ context.getDbSession().createReadTransaction() }; + const db::Track::pointer track{ db::Track::find(context.getDbSession(), id) }; if (track) { db::TrackLyrics::FindParameters params; @@ -297,7 +298,7 @@ namespace lms::api::subsonic params.setExternal(true); // First try to only report external lyrics as they are often duplicate of embedded lyrics and support more features bool hasExternalLyrics{}; - db::TrackLyrics::find(context.dbSession, params, [&](const db::TrackLyrics::pointer& lyrics) { + db::TrackLyrics::find(context.getDbSession(), params, [&](const db::TrackLyrics::pointer& lyrics) { lyricsList.addArrayChild("structuredLyrics", createStructuredLyricsNode(context, lyrics)); hasExternalLyrics = true; }); @@ -305,7 +306,7 @@ namespace lms::api::subsonic if (!hasExternalLyrics) { params.setExternal(false); - db::TrackLyrics::find(context.dbSession, params, [&](const db::TrackLyrics::pointer& lyrics) { + db::TrackLyrics::find(context.getDbSession(), params, [&](const db::TrackLyrics::pointer& lyrics) { lyricsList.addArrayChild("structuredLyrics", createStructuredLyricsNode(context, lyrics)); }); } @@ -322,13 +323,13 @@ namespace lms::api::subsonic if (!continuation) { // Mandatory params - db::TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; + db::TrackId id{ getMandatoryParameterAs(context.getParameters(), "id") }; std::filesystem::path trackPath; { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - auto track{ db::Track::find(context.dbSession, id) }; + auto track{ db::Track::find(context.getDbSession(), id) }; if (!track) throw RequestedDataNotFoundError{}; @@ -357,10 +358,10 @@ namespace lms::api::subsonic if (!continuation) { StreamParameters streamParameters{ getStreamParameters(context) }; - if (streamParameters.outputParameters) - resourceHandler = core::Service::get()->createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); + if (streamParameters.transcodeParameters) + resourceHandler = core::Service::get()->createTranscodeResourceHandler(*streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = core::createFileResourceHandler(streamParameters.inputParameters.filePath, streamParameters.inputMimeType); + resourceHandler = core::createFileResourceHandler(streamParameters.filePath, streamParameters.fileMimeType); } else { @@ -371,7 +372,7 @@ namespace lms::api::subsonic if (continuation) continuation->setData(resourceHandler); } - catch (const av::Exception& e) + catch (const audio::Exception& e) { response.setStatus(404); // report not found if something wrong happened LMS_LOG(API_SUBSONIC, ERROR, "Caught Av exception: " << e.what()); @@ -381,9 +382,9 @@ namespace lms::api::subsonic void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { // Mandatory params - const CoverArtId coverArtId{ getMandatoryParameterAs(context.parameters, "id") }; + const CoverArtId coverArtId{ getMandatoryParameterAs(context.getParameters(), "id") }; - std::optional size{ getParameterAs(context.parameters, "size") }; + std::optional size{ getParameterAs(context.getParameters(), "size") }; if (size) *size = std::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 }); diff --git a/src/libs/subsonic/impl/endpoints/Playlists.cpp b/src/libs/subsonic/impl/endpoints/Playlists.cpp index cfa46fff..00ae150e 100644 --- a/src/libs/subsonic/impl/endpoints/Playlists.cpp +++ b/src/libs/subsonic/impl/endpoints/Playlists.cpp @@ -53,9 +53,9 @@ namespace lms::api::subsonic Response handleGetPlaylistsRequest(RequestContext& context) { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& playlistsNode{ response.createNode("playlists") }; auto addTrackList{ [&](const db::TrackList::pointer& trackList) { @@ -65,10 +65,10 @@ namespace lms::api::subsonic // First add user's playlists { TrackList::FindParameters params; - params.setUser(context.user->getId()); + params.setUser(context.getUser()->getId()); params.setType(TrackListType::PlayList); - db::TrackList::find(context.dbSession, params, [&](const db::TrackList::pointer& trackList) { + db::TrackList::find(context.getDbSession(), params, [&](const db::TrackList::pointer& trackList) { addTrackList(trackList); }); } @@ -78,10 +78,10 @@ namespace lms::api::subsonic TrackList::FindParameters params; params.setVisibility(TrackList::Visibility::Public); params.setType(TrackListType::PlayList); - params.setExcludedUser(context.user->getId()); + params.setExcludedUser(context.getUser()->getId()); - db::TrackList::find(context.dbSession, params, [&](const db::TrackList::pointer& trackList) { - assert(trackList->getUserId() != context.user->getId()); + db::TrackList::find(context.getDbSession(), params, [&](const db::TrackList::pointer& trackList) { + assert(trackList->getUserId() != context.getUser()->getId()); addTrackList(trackList); }); } @@ -92,23 +92,23 @@ namespace lms::api::subsonic Response handleGetPlaylistRequest(RequestContext& context) { // Mandatory params - TrackListId trackListId{ getMandatoryParameterAs(context.parameters, "id") }; + TrackListId trackListId{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - TrackList::pointer trackList{ TrackList::find(context.dbSession, trackListId) }; + TrackList::pointer trackList{ TrackList::find(context.getDbSession(), trackListId) }; if (!trackList || trackList->getType() != TrackListType::PlayList) throw RequestedDataNotFoundError{}; - if (trackList->getUserId() != context.user->getId() && trackList->getVisibility() != TrackList::Visibility::Public) + if (trackList->getUserId() != context.getUser()->getId() && trackList->getVisibility() != TrackList::Visibility::Public) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) - playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.user)); + playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.getUser())); response.addNode("playlist", std::move(playlistNode)); @@ -118,21 +118,21 @@ namespace lms::api::subsonic Response handleCreatePlaylistRequest(RequestContext& context) { // Optional params - const auto id{ getParameterAs(context.parameters, "playlistId") }; - auto name{ getParameterAs(context.parameters, "name") }; + const auto id{ getParameterAs(context.getParameters(), "playlistId") }; + auto name{ getParameterAs(context.getParameters(), "name") }; - std::vector trackIds{ getMultiParametersAs(context.parameters, "songId") }; + std::vector trackIds{ getMultiParametersAs(context.getParameters(), "songId") }; if (!name && !id) throw RequiredParameterMissingError{ "name or playlistId" }; - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; TrackList::pointer trackList; if (id) { - trackList = TrackList::find(context.dbSession, *id); - checkTrackListModificationAccess(trackList, context.user->getId()); + trackList = TrackList::find(context.getDbSession(), *id); + checkTrackListModificationAccess(trackList, context.getUser()->getId()); if (name) trackList.modify()->setName(*name); @@ -142,26 +142,26 @@ namespace lms::api::subsonic } else { - trackList = context.dbSession.create(*name, TrackListType::PlayList); - trackList.modify()->setUser(context.user); + trackList = context.getDbSession().create(*name, TrackListType::PlayList); + trackList.modify()->setUser(context.getUser()); trackList.modify()->setVisibility(TrackList::Visibility::Private); } for (const TrackId trackId : trackIds) { - Track::pointer track{ Track::find(context.dbSession, trackId) }; + Track::pointer track{ Track::find(context.getDbSession(), trackId) }; if (!track) continue; - context.dbSession.create(track, trackList); + context.getDbSession().create(track, trackList); } - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) - playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.user)); + playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.getUser())); response.addNode("playlist", std::move(playlistNode)); @@ -171,19 +171,19 @@ namespace lms::api::subsonic Response handleUpdatePlaylistRequest(RequestContext& context) { // Mandatory params - TrackListId id{ getMandatoryParameterAs(context.parameters, "playlistId") }; + TrackListId id{ getMandatoryParameterAs(context.getParameters(), "playlistId") }; // Optional parameters - auto name{ getParameterAs(context.parameters, "name") }; - auto isPublic{ getParameterAs(context.parameters, "public") }; + auto name{ getParameterAs(context.getParameters(), "name") }; + auto isPublic{ getParameterAs(context.getParameters(), "public") }; - std::vector trackIdsToAdd{ getMultiParametersAs(context.parameters, "songIdToAdd") }; - std::vector trackPositionsToRemove{ getMultiParametersAs(context.parameters, "songIndexToRemove") }; + std::vector trackIdsToAdd{ getMultiParametersAs(context.getParameters(), "songIdToAdd") }; + std::vector trackPositionsToRemove{ getMultiParametersAs(context.getParameters(), "songIndexToRemove") }; - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; - TrackList::pointer trackList{ TrackList::find(context.dbSession, id) }; - checkTrackListModificationAccess(trackList, context.user->getId()); + TrackList::pointer trackList{ TrackList::find(context.getDbSession(), id) }; + checkTrackListModificationAccess(trackList, context.getUser()->getId()); if (name) trackList.modify()->setName(*name); @@ -206,27 +206,27 @@ namespace lms::api::subsonic // Add tracks for (const TrackId trackIdToAdd : trackIdsToAdd) { - Track::pointer track{ Track::find(context.dbSession, trackIdToAdd) }; + Track::pointer track{ Track::find(context.getDbSession(), trackIdToAdd) }; if (!track) continue; - context.dbSession.create(track, trackList); + context.getDbSession().create(track, trackList); } - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleDeletePlaylistRequest(RequestContext& context) { - TrackListId id{ getMandatoryParameterAs(context.parameters, "id") }; + TrackListId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - auto transaction{ context.dbSession.createWriteTransaction() }; + auto transaction{ context.getDbSession().createWriteTransaction() }; - TrackList::pointer trackList{ TrackList::find(context.dbSession, id) }; - checkTrackListModificationAccess(trackList, context.user->getId()); + TrackList::pointer trackList{ TrackList::find(context.getDbSession(), id) }; + checkTrackListModificationAccess(trackList, context.getUser()->getId()); trackList.remove(); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/Podcast.cpp b/src/libs/subsonic/impl/endpoints/Podcast.cpp index 3d740c0d..c4d129ff 100644 --- a/src/libs/subsonic/impl/endpoints/Podcast.cpp +++ b/src/libs/subsonic/impl/endpoints/Podcast.cpp @@ -34,14 +34,14 @@ namespace lms::api::subsonic { Response handleGetPodcasts(RequestContext& context) { - const bool includeEpisodes{ getParameterAs(context.parameters, "includeEpisodes").value_or(true) }; - const std::optional podcastId{ getParameterAs(context.parameters, "id") }; + const bool includeEpisodes{ getParameterAs(context.getParameters(), "includeEpisodes").value_or(true) }; + const std::optional podcastId{ getParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& podcastsNode{ response.createNode("podcasts") }; podcastsNode.createEmptyArrayChild("channel"); - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; auto processPodcast{ [&](const db::Podcast::pointer& podcast) { podcastsNode.addArrayChild("channel", createPodcastNode(context, podcast, includeEpisodes)); @@ -49,34 +49,34 @@ namespace lms::api::subsonic if (podcastId.has_value()) { - db::Podcast::pointer podcast{ db::Podcast::find(context.dbSession, podcastId.value()) }; + db::Podcast::pointer podcast{ db::Podcast::find(context.getDbSession(), podcastId.value()) }; if (!podcast) throw RequestedDataNotFoundError{}; processPodcast(podcast); } else - db::Podcast::find(context.dbSession, processPodcast); + db::Podcast::find(context.getDbSession(), processPodcast); return response; } Response handleGetNewestPodcasts(RequestContext& context) { - std::size_t count{ getParameterAs(context.parameters, "count").value_or(20) }; + std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(20) }; count = std::min(count, 100); - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& newestPodcastsNode{ response.createNode("newestPodcasts") }; newestPodcastsNode.createEmptyArrayChild("episode"); { - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; db::PodcastEpisode::FindParameters findParameters; findParameters.setRange(db::Range{ .offset = 0, .size = count }); - db::PodcastEpisode::find(context.dbSession, findParameters, [&](const db::PodcastEpisode::pointer& episode) { + db::PodcastEpisode::find(context.getDbSession(), findParameters, [&](const db::PodcastEpisode::pointer& episode) { newestPodcastsNode.addArrayChild("episode", createPodcastEpisodeNode(episode)); }); } @@ -88,13 +88,13 @@ namespace lms::api::subsonic { core::Service::get()->refreshPodcasts(); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleCreatePodcastChannel(RequestContext& context) { // Mandatory parameters - const std::string url{ getMandatoryParameterAs(context.parameters, "url") }; + const std::string url{ getMandatoryParameterAs(context.getParameters(), "url") }; if (url.empty() || !(url.starts_with("http://") || url.starts_with("https://"))) throw BadParameterGenericError{ "Invalid url" }; @@ -102,52 +102,52 @@ namespace lms::api::subsonic // no effect if podcast already exists core::Service::get()->addPodcast(url); - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleDeletePodcastChannel(RequestContext& context) { // Mandatory parameters - const db::PodcastId podcastId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::PodcastId podcastId{ getMandatoryParameterAs(context.getParameters(), "id") }; if (!core::Service::get()->removePodcast(podcastId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleDeletePodcastEpisode(RequestContext& context) { // Mandatory parameters - const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.getParameters(), "id") }; if (!core::Service::get()->deletePodcastEpisode(episodeId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleDownloadPodcastEpisode(RequestContext& context) { // Mandatory parameters - const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.getParameters(), "id") }; if (!core::Service::get()->downloadPodcastEpisode(episodeId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleGetPodcastEpisode(RequestContext& context) { // Mandatory parameters - const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.parameters, "id") }; + const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; - const db::PodcastEpisode::pointer episode{ db::PodcastEpisode::find(context.dbSession, episodeId) }; + const db::PodcastEpisode::pointer episode{ db::PodcastEpisode::find(context.getDbSession(), episodeId) }; if (!episode) throw RequestedDataNotFoundError{}; diff --git a/src/libs/subsonic/impl/endpoints/Searching.cpp b/src/libs/subsonic/impl/endpoints/Searching.cpp index f38f1519..c8119c9d 100644 --- a/src/libs/subsonic/impl/endpoints/Searching.cpp +++ b/src/libs/subsonic/impl/endpoints/Searching.cpp @@ -118,14 +118,14 @@ namespace lms::api::subsonic { // For now, no need to optimize all this // Find all the directories that match the name and that do not contain any track (considered by the legacy API as artists) - const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; + const std::size_t artistCount{ getParameterAs(context.getParameters(), "artistCount").value_or(20) }; if (artistCount == 0) return; if (artistCount > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; - const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; + const std::size_t artistOffset{ getParameterAs(context.getParameters(), "artistOffset").value_or(0) }; Directory::FindParameters params; params.setKeywords(keywords); @@ -133,7 +133,7 @@ namespace lms::api::subsonic params.setWithNoTrack(true); params.setMediaLibrary(mediaLibrary); - Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) { + Directory::find(context.getDbSession(), params, [&](const Directory::pointer& directory) { Response::Node childNode; childNode.setAttribute("id", idToString(directory->getId())); childNode.setAttribute("name", directory->getName()); @@ -147,14 +147,14 @@ namespace lms::api::subsonic { static ScanTracker currentScansInProgress; - const std::size_t artistCount{ getParameterAs(context.parameters, "artistCount").value_or(20) }; + const std::size_t artistCount{ getParameterAs(context.getParameters(), "artistCount").value_or(20) }; if (artistCount == 0) return; if (artistCount > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; - const std::size_t artistOffset{ getParameterAs(context.parameters, "artistOffset").value_or(0) }; + const std::size_t artistOffset{ getParameterAs(context.getParameters(), "artistOffset").value_or(0) }; ArtistId lastRetrievedId; auto findArtists{ [&] { @@ -164,7 +164,7 @@ namespace lms::api::subsonic params.setRange(Range{ artistOffset, artistCount }); params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods - Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) { + Artist::find(context.getDbSession(), params, [&](const Artist::pointer& artist) { searchResultNode.addArrayChild("artist", createArtistNode(context, artist)); lastRetrievedId = artist->getId(); }); @@ -177,9 +177,9 @@ namespace lms::api::subsonic else { ScanTracker::ScanInfo scanInfo{ - .clientAddress = context.clientIpAddr, - .clientName = context.clientInfo.name, - .user = context.user->getId(), + .clientAddress = context.getClientIpAddr(), + .clientName = std::string{ context.getClientName() }, + .user = context.getUser()->getId(), .library = mediaLibrary, .offset = artistOffset }; @@ -187,7 +187,7 @@ namespace lms::api::subsonic if (ArtistId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) { Artist::find( - context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) { + context.getDbSession(), cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) { searchResultNode.addArrayChild("artist", createArtistNode(context, artist)); }, mediaLibrary); @@ -210,14 +210,14 @@ namespace lms::api::subsonic { static ScanTracker currentScansInProgress; - const std::size_t albumCount{ getParameterAs(context.parameters, "albumCount").value_or(20) }; + const std::size_t albumCount{ getParameterAs(context.getParameters(), "albumCount").value_or(20) }; if (albumCount == 0) return; if (albumCount > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; - const std::size_t albumOffset{ getParameterAs(context.parameters, "albumOffset").value_or(0) }; + const std::size_t albumOffset{ getParameterAs(context.getParameters(), "albumOffset").value_or(0) }; ReleaseId lastRetrievedId; @@ -228,7 +228,7 @@ namespace lms::api::subsonic params.filters.setMediaLibrary(mediaLibrary); params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods - Release::find(context.dbSession, params, [&](const Release::pointer& release) { + Release::find(context.getDbSession(), params, [&](const Release::pointer& release) { searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3)); lastRetrievedId = release->getId(); }); @@ -241,9 +241,9 @@ namespace lms::api::subsonic else { ScanTracker::ScanInfo scanInfo{ - .clientAddress = context.clientIpAddr, - .clientName = context.clientInfo.name, - .user = context.user->getId(), + .clientAddress = context.getClientIpAddr(), + .clientName = std::string{ context.getClientName() }, + .user = context.getUser()->getId(), .library = mediaLibrary, .offset = albumOffset }; @@ -251,7 +251,7 @@ namespace lms::api::subsonic if (ReleaseId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) { Release::find( - context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) { + context.getDbSession(), cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) { searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3)); }, mediaLibrary); @@ -274,14 +274,14 @@ namespace lms::api::subsonic { static ScanTracker currentScansInProgress; - const std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; + const std::size_t songCount{ getParameterAs(context.getParameters(), "songCount").value_or(20) }; if (songCount == 0) return; if (songCount > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; - const std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; + const std::size_t songOffset{ getParameterAs(context.getParameters(), "songOffset").value_or(0) }; TrackId lastRetrievedId; @@ -292,7 +292,7 @@ namespace lms::api::subsonic params.filters.setMediaLibrary(mediaLibrary); params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods - Track::find(context.dbSession, params, [&](const Track::pointer& track) { + Track::find(context.getDbSession(), params, [&](const Track::pointer& track) { searchResultNode.addArrayChild("song", createSongNode(context, track, id3)); lastRetrievedId = track->getId(); }); @@ -305,9 +305,9 @@ namespace lms::api::subsonic else { ScanTracker::ScanInfo scanInfo{ - .clientAddress = context.clientIpAddr, - .clientName = context.clientInfo.name, - .user = context.user->getId(), + .clientAddress = context.getClientIpAddr(), + .clientName = std::string{ context.getClientName() }, + .user = context.getUser()->getId(), .library = mediaLibrary, .offset = songOffset }; @@ -315,7 +315,7 @@ namespace lms::api::subsonic if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) { Track::find( - context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { + context.getDbSession(), cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { searchResultNode.addArrayChild("song", createSongNode(context, track, id3)); }, mediaLibrary); @@ -337,24 +337,24 @@ namespace lms::api::subsonic Response handleSearchRequestCommon(RequestContext& context, bool id3) { // Mandatory params - const std::string queryString{ getMandatoryParameterAs(context.parameters, "query") }; + const std::string queryString{ getMandatoryParameterAs(context.getParameters(), "query") }; std::string_view query{ queryString }; // Optional params - const MediaLibraryId mediaLibrary{ getParameterAs(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) }; + const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; // Symfonium adds extra "" - if (context.clientInfo.name == "Symfonium") + if (context.getClientName() == "Symfonium") query = core::stringUtils::stringTrim(query, "\""); std::vector keywords; if (!query.empty()) keywords = core::stringUtils::splitString(query, ' '); - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& searchResultNode{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; if (id3) findRequestedArtists(context, keywords, mediaLibrary, searchResultNode); diff --git a/src/libs/subsonic/impl/endpoints/System.cpp b/src/libs/subsonic/impl/endpoints/System.cpp index fd89ccf2..1b195949 100644 --- a/src/libs/subsonic/impl/endpoints/System.cpp +++ b/src/libs/subsonic/impl/endpoints/System.cpp @@ -4,12 +4,12 @@ namespace lms::api::subsonic { Response handlePingRequest(RequestContext& context) { - return Response::createOkResponse(context.serverProtocolVersion); + return Response::createOkResponse(context.getServerProtocolVersion()); } Response handleGetLicenseRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& licenseNode{ response.createNode("license") }; licenseNode.setAttribute("licenseExpires", "2035-09-03T14:46:43"); @@ -21,7 +21,7 @@ namespace lms::api::subsonic Response handleGetOpenSubsonicExtensions(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; { Response::Node& transcodeOffsetNode{ response.createArrayNode("openSubsonicExtensions") }; diff --git a/src/libs/subsonic/impl/endpoints/UserManagement.cpp b/src/libs/subsonic/impl/endpoints/UserManagement.cpp index 541b2a46..2781db15 100644 --- a/src/libs/subsonic/impl/endpoints/UserManagement.cpp +++ b/src/libs/subsonic/impl/endpoints/UserManagement.cpp @@ -16,24 +16,24 @@ namespace lms::api::subsonic { void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) { - if (context.user->getLoginName() != username && !context.user->isAdmin()) + if (context.getUser()->getLoginName() != username && !context.getUser()->isAdmin()) throw UserNotAuthorizedError{}; } } // namespace Response handleGetUserRequest(RequestContext& context) { - std::string username{ getMandatoryParameterAs(context.parameters, "username") }; + std::string username{ getMandatoryParameterAs(context.getParameters(), "username") }; - auto transaction{ context.dbSession.createReadTransaction() }; + auto transaction{ context.getDbSession().createReadTransaction() }; checkUserIsMySelfOrAdmin(context, username); - const User::pointer user{ User::find(context.dbSession, username) }; + const User::pointer user{ User::find(context.getDbSession(), username) }; if (!user) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; response.addNode("user", createUserNode(context, user)); return response; @@ -41,11 +41,11 @@ namespace lms::api::subsonic Response handleGetUsersRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& usersNode{ response.createNode("users") }; - auto transaction{ context.dbSession.createReadTransaction() }; - User::find(context.dbSession, User::FindParameters{}, [&](const User::pointer& user) { + auto transaction{ context.getDbSession().createReadTransaction() }; + User::find(context.getDbSession(), User::FindParameters{}, [&](const User::pointer& user) { usersNode.addArrayChild("user", createUserNode(context, user)); }); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 9ac5387f..61eaa8d1 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -69,7 +69,7 @@ namespace lms::api::subsonic Directory::FindParameters params; params.setRelease(release->getId()); params.setRange(Range{ 0, 1 }); // only support 1 directory <-> 1 release - Directory::find(context.dbSession, params, [&](const Directory::pointer& foundDirectory) { + Directory::find(context.getDbSession(), params, [&](const Directory::pointer& foundDirectory) { directoryToReport = foundDirectory; }); } @@ -116,10 +116,10 @@ namespace lms::api::subsonic } } - albumNode.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), release->getId())); + albumNode.setAttribute("playCount", core::Service::get()->getCount(context.getUser()->getId(), release->getId())); // Report the first GENRE for this track - const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; + const ClusterType::pointer genreClusterType{ ClusterType::find(context.getDbSession(), "GENRE") }; if (genreClusterType) { const auto clusters{ release->getClusters(genreClusterType->getId(), 1) }; @@ -127,14 +127,14 @@ namespace lms::api::subsonic albumNode.setAttribute("genre", clusters.front()->getName()); } - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.getUser()->getId(), release->getId()) }; dateTime.isValid()) albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); // Always report user rating, even if legacy API only specified it for directories - if (const auto rating{ core::Service::get()->getRating(context.user->getId(), release->getId()) }) + if (const auto rating{ core::Service::get()->getRating(context.getUser()->getId(), release->getId()) }) albumNode.setAttribute("userRating", *rating); - if (!context.enableOpenSubsonic) + if (!context.isOpenSubsonicEnabled()) return albumNode; // OpenSubsonic specific fields (must always be set) @@ -143,7 +143,7 @@ namespace lms::api::subsonic albumNode.setAttribute("mediaType", "album"); { - const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.user->getId(), release->getId()) }; + const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.getUser()->getId(), release->getId()) }; albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" }); } @@ -159,7 +159,7 @@ namespace lms::api::subsonic params.setRelease(release->getId()); params.setClusterTypeName(clusterTypeName); - Cluster::find(context.dbSession, params, [&](const Cluster::pointer& cluster) { + Cluster::find(context.getDbSession(), params, [&](const Cluster::pointer& cluster) { albumNode.addArrayValue(field, cluster->getName()); }); } }; @@ -174,7 +174,7 @@ namespace lms::api::subsonic params.setRelease(release->getId()); params.setClusterType(genreClusterType->getId()); - Cluster::find(context.dbSession, params, [&](const Cluster::pointer& cluster) { + Cluster::find(context.getDbSession(), params, [&](const Cluster::pointer& cluster) { albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName())); }); } diff --git a/src/libs/subsonic/impl/responses/AlbumInfo.cpp b/src/libs/subsonic/impl/responses/AlbumInfo.cpp index 0463609a..e58b07c7 100644 --- a/src/libs/subsonic/impl/responses/AlbumInfo.cpp +++ b/src/libs/subsonic/impl/responses/AlbumInfo.cpp @@ -31,7 +31,7 @@ namespace lms::api::subsonic if (const auto releaseMBID{ release->getMBID() }) { - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: albumInfo.setAttribute("musicBrainzId", releaseMBID->getAsString()); diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 08ad7cf7..af4e88b4 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -22,6 +22,7 @@ #include "core/ITraceLogger.hpp" #include "core/Service.hpp" #include "core/String.hpp" + #include "database/objects/Artist.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Release.hpp" @@ -57,33 +58,7 @@ namespace lms::api::subsonic std::string_view toString(TrackArtistLinkType type) { - switch (type) - { - case TrackArtistLinkType::Arranger: - return "arranger"; - case TrackArtistLinkType::Artist: - return "artist"; - case TrackArtistLinkType::Composer: - return "composer"; - case TrackArtistLinkType::Conductor: - return "conductor"; - case TrackArtistLinkType::Lyricist: - return "lyricist"; - case TrackArtistLinkType::Mixer: - return "mixer"; - case TrackArtistLinkType::Performer: - return "performer"; - case TrackArtistLinkType::Producer: - return "producer"; - case TrackArtistLinkType::ReleaseArtist: - return "albumartist"; - case TrackArtistLinkType::Remixer: - return "remixer"; - case TrackArtistLinkType::Writer: - return "writer"; - } - - return "unknown"; + return db::trackArtistLinkTypeToString(type).str(); } } // namespace utils @@ -101,17 +76,17 @@ namespace lms::api::subsonic artistNode.setAttribute("coverArt", idToString(coverArtId)); } - const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) }; + const std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; artistNode.setAttribute("albumCount", count); - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), artist->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.getUser()->getId(), artist->getId()) }; dateTime.isValid()) artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); - if (const auto rating{ core::Service::get()->getRating(context.user->getId(), artist->getId()) }) + if (const auto rating{ core::Service::get()->getRating(context.getUser()->getId(), artist->getId()) }) artistNode.setAttribute("userRating", *rating); // OpenSubsonic specific fields (must always be set) - if (context.enableOpenSubsonic) + if (context.isOpenSubsonicEnabled()) { artistNode.setAttribute("mediaType", "artist"); @@ -125,7 +100,7 @@ namespace lms::api::subsonic // roles Response::Node roles; artistNode.createEmptyArrayValue("roles"); - for (const TrackArtistLinkType linkType : TrackArtistLink::findUsedTypes(context.dbSession, artist->getId())) + for (const TrackArtistLinkType linkType : TrackArtistLink::findUsedTypes(context.getDbSession(), artist->getId())) artistNode.addArrayValue("roles", utils::toString(linkType)); } diff --git a/src/libs/subsonic/impl/responses/Genre.cpp b/src/libs/subsonic/impl/responses/Genre.cpp index 0b0504d4..9bc491fe 100644 --- a/src/libs/subsonic/impl/responses/Genre.cpp +++ b/src/libs/subsonic/impl/responses/Genre.cpp @@ -29,7 +29,7 @@ namespace lms::api::subsonic { Response::Node clusterNode; - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: clusterNode.setAttribute("value", cluster->getName()); diff --git a/src/libs/subsonic/impl/responses/Lyrics.cpp b/src/libs/subsonic/impl/responses/Lyrics.cpp index 2273d046..eb32438b 100644 --- a/src/libs/subsonic/impl/responses/Lyrics.cpp +++ b/src/libs/subsonic/impl/responses/Lyrics.cpp @@ -55,7 +55,7 @@ namespace lms::api::subsonic addLine(std::move(line)); } - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: lyricsNode.setAttribute("value", lyricsText); @@ -89,7 +89,7 @@ namespace lms::api::subsonic if (timestamp) lineNode.setAttribute("start", std::chrono::duration_cast(*timestamp).count()); - switch (context.responseFormat) + switch (context.getResponseFormat()) { case ResponseFormat::json: lineNode.setAttribute("value", std::move(line)); diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index e3a9befb..2ba03013 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -48,7 +48,7 @@ namespace lms::api::subsonic if (const db::ArtworkId artworkId{ core::Service::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid()) { - if (const auto artwork{ db::Artwork::find(context.dbSession, artworkId) }) + if (const auto artwork{ db::Artwork::find(context.getDbSession(), artworkId) }) { CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; playlistNode.setAttribute("coverArt", idToString(coverArtId)); diff --git a/src/libs/subsonic/impl/responses/Podcast.cpp b/src/libs/subsonic/impl/responses/Podcast.cpp index aab4be08..4f89d1f0 100644 --- a/src/libs/subsonic/impl/responses/Podcast.cpp +++ b/src/libs/subsonic/impl/responses/Podcast.cpp @@ -125,7 +125,7 @@ namespace lms::api::subsonic params.setPodcast(podcast->getId()); params.setSortMode(db::PodcastEpisodeSortMode::PubDateDesc); - db::PodcastEpisode::find(context.dbSession, params, [&](const db::PodcastEpisode::pointer& episode) { + db::PodcastEpisode::find(context.getDbSession(), params, [&](const db::PodcastEpisode::pointer& episode) { podcastNode.addArrayChild("episode", createPodcastEpisodeNode(episode)); }); } diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 6c03545a..87a0d753 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -87,9 +87,9 @@ namespace lms::api::subsonic { if (const auto directory{ track->getDirectory() }) trackResponse.setAttribute("parent", idToString(directory->getId())); - trackResponse.setAttribute("isDir", false); } + trackResponse.setAttribute("isDir", false); trackResponse.setAttribute("id", idToString(track->getId())); trackResponse.setAttribute("title", track->getName()); if (track->getTrackNumber()) @@ -100,7 +100,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("year", *originalYear); else if (const auto year{ track->getYear() }) trackResponse.setAttribute("year", *year); - trackResponse.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), track->getId())); + trackResponse.setAttribute("playCount", core::Service::get()->getCount(context.getUser()->getId(), track->getId())); // maybe not available if user just removed the library without rescanning if (const db::MediaLibrary::pointer library{ track->getMediaLibrary() }) @@ -119,9 +119,9 @@ namespace lms::api::subsonic trackResponse.setAttribute("suffix", extension.string().substr(1) /* skip leading .*/); } - if (context.user->getSubsonicEnableTranscodingByDefault()) + if (context.getUser()->getSubsonicEnableTranscodingByDefault()) { - const std::string fileSuffix{ formatToSuffix(context.user->getSubsonicDefaultTranscodingOutputFormat()) }; + const std::string fileSuffix{ formatToSuffix(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()) }; trackResponse.setAttribute("transcodedSuffix", fileSuffix); trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } @@ -160,10 +160,10 @@ namespace lms::api::subsonic trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getAddedTime())); trackResponse.setAttribute("contentType", core::getMimeType(track->getAbsoluteFilePath().extension())); - if (const auto rating{ core::Service::get()->getRating(context.user->getId(), track->getId()) }) + if (const auto rating{ core::Service::get()->getRating(context.getUser()->getId(), track->getId()) }) trackResponse.setAttribute("userRating", *rating); - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), track->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.getUser()->getId(), track->getId()) }; dateTime.isValid()) trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); // Report the first GENRE for this track @@ -173,13 +173,13 @@ namespace lms::api::subsonic params.setTrack(track->getId()); params.setClusterTypeName("GENRE"); - genres = Cluster::find(context.dbSession, params).results; + genres = Cluster::find(context.getDbSession(), params).results; if (!genres.empty()) trackResponse.setAttribute("genre", genres.front()->getName()); } // OpenSubsonic specific fields (must always be set) - if (!context.enableOpenSubsonic) + if (!context.isOpenSubsonicEnabled()) return trackResponse; trackResponse.setAttribute("comment", track->getComment()); @@ -190,7 +190,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("mediaType", "song"); { - const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.user->getId(), track->getId()) }; + const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.getUser()->getId(), track->getId()) }; trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : ""); } @@ -204,7 +204,7 @@ namespace lms::api::subsonic trackResponse.createEmptyArrayChild("artists"); trackResponse.createEmptyArrayChild("contributors"); - TrackArtistLink::find(context.dbSession, track->getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist) { + TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist) { switch (link->getType()) { case TrackArtistLinkType::Artist: @@ -230,7 +230,7 @@ namespace lms::api::subsonic params.setTrack(track->getId()); params.setClusterTypeName(clusterTypeName); - for (const auto& cluster : Cluster::find(context.dbSession, params).results) + for (const auto& cluster : Cluster::find(context.getDbSession(), params).results) trackResponse.addArrayValue(field, cluster->getName()); } }; diff --git a/src/libs/subsonic/impl/responses/User.cpp b/src/libs/subsonic/impl/responses/User.cpp index 81d4d786..567f12eb 100644 --- a/src/libs/subsonic/impl/responses/User.cpp +++ b/src/libs/subsonic/impl/responses/User.cpp @@ -46,7 +46,7 @@ namespace lms::api::subsonic userNode.setAttribute("shareRole", false); // not supported // users can access all libraries - db::MediaLibrary::find(context.dbSession, [&](const db::MediaLibrary::pointer& library) { + db::MediaLibrary::find(context.getDbSession(), [&](const db::MediaLibrary::pointer& library) { userNode.addArrayValue("folder", library->getId().getValue()); }); diff --git a/src/libs/subsonic/test/SubsonicResponseTest.cpp b/src/libs/subsonic/test/SubsonicResponseTest.cpp index 3bfa4a65..eab26e21 100644 --- a/src/libs/subsonic/test/SubsonicResponseTest.cpp +++ b/src/libs/subsonic/test/SubsonicResponseTest.cpp @@ -63,7 +63,7 @@ namespace lms::api::subsonic::tests std::ostringstream oss; response.write(oss, ResponseFormat::json); - EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.70.0","status":"ok","type":"lms","version":"1.16.0"}})"); + EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.72.0","status":"ok","type":"lms","version":"1.16.0"}})"); } TEST(SubsonicResponse, json) @@ -73,7 +73,7 @@ namespace lms::api::subsonic::tests std::ostringstream oss; response.write(oss, ResponseFormat::json); - EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.70.0","status":"ok","type":"lms","version":"1.16.0","MyNode":{"Attr1":"value1","Attr2":"value2","attr3":"","attr4":true,"attr5":false,"attr6":3.14159,"attr7":333666,"MyArrayChild":[{"Attr42":0},{"Attr42":1}],"MyArray1":["value1","value2","value1","value2"],"MyArray2":[0]}}})"); + EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.72.0","status":"ok","type":"lms","version":"1.16.0","MyNode":{"Attr1":"value1","Attr2":"value2","attr3":"","attr4":true,"attr5":false,"attr6":3.14159,"attr7":333666,"MyArrayChild":[{"Attr42":0},{"Attr42":1}],"MyArray1":["value1","value2","value1","value2"],"MyArray2":[0]}}})"); } TEST(SubsonicResponse, emptyXml) @@ -84,7 +84,7 @@ namespace lms::api::subsonic::tests response.write(oss, ResponseFormat::xml); EXPECT_EQ(oss.str(), R"( -)"); +)"); } TEST(SubsonicResponse, xml) @@ -95,7 +95,7 @@ namespace lms::api::subsonic::tests response.write(oss, ResponseFormat::xml); EXPECT_EQ(oss.str(), R"( -value1value2value1value20)"); +value1value2value1value20)"); } } // namespace lms::api::subsonic::tests diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 1d1256cd..98d06c54 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -67,7 +67,7 @@ target_link_libraries(lms PRIVATE Boost::iostreams Wt::Wt Wt::HTTP - lmsav + lmsaudio lmsauth lmsdatabase lmsfeedback diff --git a/src/lms/main.cpp b/src/lms/main.cpp index a217d6d5..14c149d2 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -47,7 +47,7 @@ #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "subsonic/SubsonicResource.hpp" #include "ui/Auth.hpp" #include "ui/LmsApplication.hpp" @@ -428,7 +428,7 @@ namespace lms core::Service recommendationService{ recommendation::createRecommendationService(*database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(*database, *recommendationService) }; core::Service scannerService{ scanner::createScannerService(*database, cachePath) }; - core::Service transcodingService{ transcoding::createTranscodingService(*database, *childProcessManagerService) }; + core::Service transcodingService{ transcoding::createTranscodeService(*database, *childProcessManagerService) }; core::Service podcastService{ podcast::createPodcastService(ioContext, *database, cachePath / "podcasts") }; scannerService->getEvents().scanComplete.connect([&] { diff --git a/src/lms/ui/LmsTheme.hpp b/src/lms/ui/LmsTheme.hpp index e2ee9c55..20be368b 100644 --- a/src/lms/ui/LmsTheme.hpp +++ b/src/lms/ui/LmsTheme.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include namespace lms::ui @@ -41,5 +42,10 @@ namespace lms::ui const Wt::WValidator::Result& validation, Wt::WFlags flags) const override; bool canBorderBoxElement(const Wt::DomElement&) const override { return true; } + +#if WT_VERSION >= 0x040C0100 // >= 4.12.1 + void applyFunctionalStyling(Wt::WWidget*, Wt::WWidget*, int) const override {} + void applyOptionalStyling(Wt::WWidget*, Wt::WWidget*, int) const override {} +#endif // WT_VERSION }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 61ef74b6..35ed3989 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -253,11 +253,7 @@ namespace lms::ui { Wt::WTemplate* releaseContainer{ releaseContainers->addNew(Wt::WString::tr("Lms.Explore.Artist.template.release-container")) }; - if (releaseType.primaryType || !releaseType.customTypes.empty()) - releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType)); - else - releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.releases")); // fallback when not tagged with MB or custom type - + releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType)); releases.container = releaseContainer->bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); releases.container->onRequestElements.connect(this, [this, &releases = releases] { addSomeReleases(releases); diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 042cedd2..7c3a3327 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -24,10 +24,13 @@ #include #include "core/EnumSet.hpp" +#include "core/Utils.hpp" + #include "database/objects/Artist.hpp" #include "database/objects/Release.hpp" #include "Utils.hpp" +#include "explore/ReleaseTypes.hpp" namespace lms::ui::releaseListHelpers { @@ -110,85 +113,106 @@ namespace lms::ui::releaseListHelpers namespace lms::ui::releaseHelpers { + namespace + { + static const Wt::WString releaseTypeSeparator{ " · " }; + + Wt::WString buildPicardReleaseTypeString(const PicardReleaseType& releaseType) + { + Wt::WString res; + + switch (releaseType.primaryType) + { + case PicardReleaseType::PrimaryType::Album: + res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); + break; + case PicardReleaseType::PrimaryType::Broadcast: + res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); + break; + case PicardReleaseType::PrimaryType::EP: + res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); + break; + case PicardReleaseType::PrimaryType::Single: + res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); + break; + case PicardReleaseType::PrimaryType::Other: + res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); + break; + } + + for (PicardReleaseType::SecondaryType secondaryType : releaseType.secondaryTypes) + { + if (!res.empty()) + res += releaseTypeSeparator; + + switch (secondaryType) + { + case PicardReleaseType::SecondaryType::Compilation: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); + break; + case PicardReleaseType::SecondaryType::Spokenword: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); + break; + case PicardReleaseType::SecondaryType::Soundtrack: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); + break; + case PicardReleaseType::SecondaryType::Interview: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); + break; + case PicardReleaseType::SecondaryType::Audiobook: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); + break; + case PicardReleaseType::SecondaryType::AudioDrama: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); + break; + case PicardReleaseType::SecondaryType::Live: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); + break; + case PicardReleaseType::SecondaryType::Remix: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); + break; + case PicardReleaseType::SecondaryType::DJMix: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); + break; + case PicardReleaseType::SecondaryType::Mixtape_Street: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); + break; + case PicardReleaseType::SecondaryType::Demo: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); + break; + case PicardReleaseType::SecondaryType::FieldRecording: + res += Wt::WString::tr("Lms.Explore.Release.type-secondary-field-recording"); + break; + } + } + + return res; + } + + Wt::WString buildCustomReleaseTypeString(const CustomReleaseType& releaseType) + { + Wt::WString res; + + for (const std::string& customType : releaseType.types) + { + if (!res.empty()) + res += releaseTypeSeparator; + + res += customType; + } + + return res; + } + } // namespace + Wt::WString buildReleaseTypeString(const ReleaseType& releaseType) { Wt::WString res; - if (releaseType.primaryType) - { - switch (*releaseType.primaryType) - { - case PrimaryReleaseType::Album: - res = Wt::WString::tr("Lms.Explore.Release.type-primary-album"); - break; - case PrimaryReleaseType::Broadcast: - res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast"); - break; - case PrimaryReleaseType::EP: - res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep"); - break; - case PrimaryReleaseType::Single: - res = Wt::WString::tr("Lms.Explore.Release.type-primary-single"); - break; - case PrimaryReleaseType::Other: - res = Wt::WString::tr("Lms.Explore.Release.type-primary-other"); - break; - } - } - - for (SecondaryReleaseType secondaryType : releaseType.secondaryTypes) - { - if (!res.empty()) - res += Wt::WString{ " · " }; - - switch (secondaryType) - { - case SecondaryReleaseType::Compilation: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation"); - break; - case SecondaryReleaseType::Spokenword: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword"); - break; - case SecondaryReleaseType::Soundtrack: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack"); - break; - case SecondaryReleaseType::Interview: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview"); - break; - case SecondaryReleaseType::Audiobook: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook"); - break; - case SecondaryReleaseType::AudioDrama: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama"); - break; - case SecondaryReleaseType::Live: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live"); - break; - case SecondaryReleaseType::Remix: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix"); - break; - case SecondaryReleaseType::DJMix: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix"); - break; - case SecondaryReleaseType::Mixtape_Street: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street"); - break; - case SecondaryReleaseType::Demo: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo"); - break; - case SecondaryReleaseType::FieldRecording: - res += Wt::WString::tr("Lms.Explore.Release.type-secondary-field-recording"); - break; - } - } - - for (const std::string& customType : releaseType.customTypes) - { - if (!res.empty()) - res += Wt::WString{ " · " }; - - res += customType; - } + std::visit(core::utils::overloads{ + [&](const PicardReleaseType& picardReleaseType) { res = buildPicardReleaseTypeString(picardReleaseType); }, + [&](const CustomReleaseType& customReleaseType) { res = buildCustomReleaseTypeString(customReleaseType); } }, + releaseType); return res; } diff --git a/src/lms/ui/explore/ReleaseTypes.cpp b/src/lms/ui/explore/ReleaseTypes.cpp index 6b54076a..12ef66c2 100644 --- a/src/lms/ui/explore/ReleaseTypes.cpp +++ b/src/lms/ui/explore/ReleaseTypes.cpp @@ -17,24 +17,24 @@ * along with LMS. If not, see . */ +#include "ReleaseTypes.hpp" + #include #include #include "core/String.hpp" -#include "ReleaseTypes.hpp" - namespace lms::core::stringUtils { template<> - std::optional readAs(std::string_view str) + std::optional readAs(std::string_view str) { - static const std::unordered_map entries{ - { "album", ui::PrimaryReleaseType::Album }, - { "single", ui::PrimaryReleaseType::Single }, - { "ep", ui::PrimaryReleaseType::EP }, - { "broadcast", ui::PrimaryReleaseType::Broadcast }, - { "other", ui::PrimaryReleaseType::Other }, + static const std::unordered_map entries{ + { "album", ui::PicardReleaseType::PrimaryType::Album }, + { "single", ui::PicardReleaseType::PrimaryType::Single }, + { "ep", ui::PicardReleaseType::PrimaryType::EP }, + { "broadcast", ui::PicardReleaseType::PrimaryType::Broadcast }, + { "other", ui::PicardReleaseType::PrimaryType::Other }, }; const auto it{ entries.find(stringToLower(stringTrim(str))) }; @@ -45,21 +45,21 @@ namespace lms::core::stringUtils } template<> - std::optional readAs(std::string_view str) + std::optional readAs(std::string_view str) { - static const std::unordered_map entries{ - { "compilation", ui::SecondaryReleaseType::Compilation }, - { "soundtrack", ui::SecondaryReleaseType::Soundtrack }, - { "spokenword", ui::SecondaryReleaseType::Spokenword }, - { "interview", ui::SecondaryReleaseType::Interview }, - { "audiobook", ui::SecondaryReleaseType::Audiobook }, - { "audio drama", ui::SecondaryReleaseType::AudioDrama }, - { "live", ui::SecondaryReleaseType::Live }, - { "remix", ui::SecondaryReleaseType::Remix }, - { "dj-mix", ui::SecondaryReleaseType::DJMix }, - { "mixtape/street", ui::SecondaryReleaseType::Mixtape_Street }, - { "demo", ui::SecondaryReleaseType::Demo }, - { "field recording", ui::SecondaryReleaseType::FieldRecording }, + static const std::unordered_map entries{ + { "compilation", ui::PicardReleaseType::SecondaryType::Compilation }, + { "soundtrack", ui::PicardReleaseType::SecondaryType::Soundtrack }, + { "spokenword", ui::PicardReleaseType::SecondaryType::Spokenword }, + { "interview", ui::PicardReleaseType::SecondaryType::Interview }, + { "audiobook", ui::PicardReleaseType::SecondaryType::Audiobook }, + { "audio drama", ui::PicardReleaseType::SecondaryType::AudioDrama }, + { "live", ui::PicardReleaseType::SecondaryType::Live }, + { "remix", ui::PicardReleaseType::SecondaryType::Remix }, + { "dj-mix", ui::PicardReleaseType::SecondaryType::DJMix }, + { "mixtape/street", ui::PicardReleaseType::SecondaryType::Mixtape_Street }, + { "demo", ui::PicardReleaseType::SecondaryType::Demo }, + { "field recording", ui::PicardReleaseType::SecondaryType::FieldRecording }, }; const auto it{ entries.find(stringToLower(stringTrim(str))) }; @@ -72,48 +72,48 @@ namespace lms::core::stringUtils namespace lms::ui { - ReleaseType parseReleaseType(const std::vector& releaseTypeNames) + std::optional parsePicardReleaseType(const std::vector& releaseTypeNames) { - ReleaseType res; + if (releaseTypeNames.empty()) + return std::nullopt; - for (std::string_view releaseTypeName : releaseTypeNames) + const auto primaryType{ core::stringUtils::readAs(releaseTypeNames[0]) }; + if (!primaryType) + return std::nullopt; + + PicardReleaseType res{ .primaryType = *primaryType, .secondaryTypes = {} }; + for (std::size_t i{ 1 }; i < releaseTypeNames.size(); ++i) { - if (auto primaryType{ core::stringUtils::readAs(releaseTypeName) }) - { - if (!res.primaryType) - res.primaryType = primaryType; - else - res.customTypes.push_back(std::string{ releaseTypeName }); - } - else if (auto secondaryType{ core::stringUtils::readAs(releaseTypeName) }) - { - res.secondaryTypes.insert(*secondaryType); - } - else - res.customTypes.push_back(std::string{ releaseTypeName }); + const auto secondaryType{ core::stringUtils::readAs(releaseTypeNames[i]) }; + if (!secondaryType) + return std::nullopt; + + res.secondaryTypes.insert(*secondaryType); } return res; } - bool operator<(std::optional typeA, std::optional typeB) + ReleaseType parseReleaseType(const std::vector& releaseTypeNames) { - if (!typeA && typeB) - return false; - else if (typeA && !typeB) - return true; - else - return static_cast(*typeA) < static_cast(*typeB); + if (const auto picardReleaseType{ parsePicardReleaseType(releaseTypeNames) }) + return *picardReleaseType; + + return CustomReleaseType{ .types = releaseTypeNames }; } - bool operator<(core::EnumSet typesA, core::EnumSet typesB) + bool operator<(core::EnumSet lhs, core::EnumSet rhs) { - return typesA.getBitfield() < typesB.getBitfield(); + return lhs.getBitfield() < rhs.getBitfield(); } - bool ReleaseType::operator<(const ReleaseType& other) const + bool PicardReleaseType::operator<(const PicardReleaseType& other) const { - // TODO : order custom types and compare for each element (size is not to be compared first) - return std::tie(primaryType, secondaryTypes, customTypes) < std::tie(other.primaryType, other.secondaryTypes, other.customTypes); + return std::tie(primaryType, secondaryTypes) < std::tie(other.primaryType, other.secondaryTypes); + } + + bool CustomReleaseType::operator<(const CustomReleaseType& other) const + { + return types < other.types; } } // namespace lms::ui diff --git a/src/lms/ui/explore/ReleaseTypes.hpp b/src/lms/ui/explore/ReleaseTypes.hpp index 882f4317..8a49d51c 100644 --- a/src/lms/ui/explore/ReleaseTypes.hpp +++ b/src/lms/ui/explore/ReleaseTypes.hpp @@ -19,49 +19,55 @@ #pragma once -#include #include +#include #include #include "core/EnumSet.hpp" namespace lms::ui { - // see https://musicbrainz.org/doc/Release_Group/Type - enum class PrimaryReleaseType + struct PicardReleaseType { - Album, - Single, - EP, - Broadcast, - Other + // see https://musicbrainz.org/doc/Release_Group/Type + enum class PrimaryType : std::uint8_t + { + Album, + Single, + EP, + Broadcast, + Other + }; + + enum class SecondaryType : std::uint8_t + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + FieldRecording, + }; + + PrimaryType primaryType; + core::EnumSet secondaryTypes; + + bool operator<(const PicardReleaseType&) const; }; - enum class SecondaryReleaseType + struct CustomReleaseType { - Compilation, - Soundtrack, - Spokenword, - Interview, - Audiobook, - AudioDrama, - Live, - Remix, - DJMix, - Mixtape_Street, - Demo, - FieldRecording, - }; - - struct ReleaseType - { - std::optional primaryType; - core::EnumSet secondaryTypes; - std::vector customTypes; - - bool operator<(const ReleaseType& other) const; + std::vector types; + + bool operator<(const CustomReleaseType&) const; }; + using ReleaseType = std::variant; ReleaseType parseReleaseType(const std::vector& releaseTypeNames); - } // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index c15a1913..63c451d6 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -27,8 +27,10 @@ #include #include -#include "av/IAudioFile.hpp" #include "core/String.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -141,16 +143,18 @@ namespace lms::ui // TODO: save in DB and aggregate all this for (const db::Track::pointer& track : db::Track::find(LmsApp->getDbSession(), db::Track::FindParameters{}.setRelease(releaseId).setRange(db::Range{ 0, 1 })).results) { - if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) + try { - const std::optional audioStream{ audioFile->getBestStreamInfo() }; - if (audioStream) + if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) { releaseInfo->setCondition("if-has-codec", true); - releaseInfo->bindString("codec", audioStream->codecName); + releaseInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); break; } } + catch (const audio::Exception& e) + { + } } if (const std::size_t meanBitrate{ release->getMeanBitrate() }) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index e4f44f30..8721745b 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -23,8 +23,10 @@ #include #include -#include "av/IAudioFile.hpp" #include "core/Service.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -127,15 +129,17 @@ namespace lms::ui::TrackListHelpers } } - if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) + try { - const std::optional audioStream{ audioFile->getBestStreamInfo() }; - if (audioStream) + if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) { trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", audioStream->codecName, Wt::TextFormat::Plain); + trackInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); } } + catch (const audio::Exception& e) + { + } trackInfo->bindString("duration", utils::durationToString(track->getDuration())); if (track->getBitrate()) diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index 8e26930a..a5977b8c 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -30,7 +30,7 @@ #include "database/Session.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "LmsApplication.hpp" @@ -72,23 +72,23 @@ namespace lms::ui { namespace { - std::optional AudioFormatToAvFormat(db::TranscodingOutputFormat format) + std::optional AudioFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return transcoding::OutputFormat::MP3; + return audio::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return transcoding::OutputFormat::OGG_OPUS; + return audio::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return transcoding::OutputFormat::MATROSKA_OPUS; + return audio::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return transcoding::OutputFormat::OGG_VORBIS; + return audio::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return transcoding::OutputFormat::WEBM_VORBIS; + return audio::OutputFormat::WEBM_VORBIS; } - TRANSCODE_LOG(ERROR, "Cannot convert from audio format to AV format"); + TRANSCODE_LOG(ERROR, "Cannot convert from db audio format to transcoding format"); return std::nullopt; } @@ -110,15 +110,9 @@ namespace lms::ui return res; } - struct TranscodingParameters + std::optional readTranscodingParameters(const Wt::Http::Request& request) { - transcoding::InputParameters inputParameters; - transcoding::OutputParameters outputParameters; - }; - - std::optional readTranscodingParameters(const Wt::Http::Request& request) - { - TranscodingParameters parameters; + audio::TranscodeParameters parameters; // mandatory parameters const std::optional trackId{ readParameterAs(request, "trackid") }; @@ -134,7 +128,7 @@ namespace lms::ui return std::nullopt; } - const std::optional avFormat{ AudioFormatToAvFormat(*format) }; + const std::optional avFormat{ AudioFormatToTranscodingFormat(*format) }; if (!avFormat) return std::nullopt; @@ -181,7 +175,7 @@ namespace lms::ui if (!continuation) { if (const auto& parameters{ readTranscodingParameters(request) }) - resourceHandler = core::Service::get()->createResourceHandler(parameters->inputParameters, parameters->outputParameters, false /* estimate content length */); + resourceHandler = core::Service::get()->createTranscodeResourceHandler(*parameters); } else { diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index a390bd6a..4759645e 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory(db-generator) -add_subdirectory(metadata) +add_subdirectory(audioinfo) add_subdirectory(recommendation) diff --git a/src/tools/audioinfo/CMakeLists.txt b/src/tools/audioinfo/CMakeLists.txt new file mode 100644 index 00000000..ccadf474 --- /dev/null +++ b/src/tools/audioinfo/CMakeLists.txt @@ -0,0 +1,12 @@ + +add_executable(lms-audioinfo + LmsAudioInfo.cpp + ) + +target_link_libraries(lms-audioinfo PRIVATE + lmsaudio + lmscore + Boost::program_options + ) + +install(TARGETS lms-audioinfo DESTINATION bin) diff --git a/src/tools/audioinfo/LmsAudioInfo.cpp b/src/tools/audioinfo/LmsAudioInfo.cpp new file mode 100644 index 00000000..2a05ad15 --- /dev/null +++ b/src/tools/audioinfo/LmsAudioInfo.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include +#include +#include + +#include +#include + +#include "core/ILogger.hpp" +#include "core/String.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" +#include "audio/ITagReader.hpp" + +namespace lms::audio +{ + std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) + { + os << "\tDuration: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast>(audioProperties.duration) << std::endl; + os << "\tContainer: " << containerTypeToString(audioProperties.container) << std::endl; + os << "\tCodec: " << codecTypeToString(audioProperties.codec) << std::endl; + os << "\tBitrate: " << audioProperties.bitrate << " bps" << std::endl; + if (audioProperties.bitsPerSample) + os << "\tBitsPerSample: " << *audioProperties.bitsPerSample << std::endl; + os << "\tChannelCount: " << audioProperties.channelCount << std::endl; + os << "\tSampleRate: " << audioProperties.sampleRate << std::endl; + + return os; + } + + void displayTags(const ITagReader& tagReader, TagType type) + { + std::vector strs; + + tagReader.visitTagValues(type, [&](std::string_view value) { + strs.push_back(std::string{ value }); + }); + + if (strs.empty()) + return; + + std::cout << "\t" << tagTypeToString(type) << ": "; + if (strs.size() == 1) + { + std::cout << "'" << strs.front() << "'\n"; + } + else + { + std::cout << "["; + bool first{ true }; + for (std::string_view str : strs) + { + if (!first) + std::cout << ", "; + first = false; + std::cout << "'" << str << "'"; + } + std::cout << "]\n"; + }; + } + + void displayLyrics(const ITagReader& tagReader) + { + tagReader.visitLyricsTags([](std::string_view language, std::string_view lyrics) { + std::cout << "Lyrics"; + if (!language.empty()) + std::cout << " (" << language << ")"; + std::cout << ":\n" + << lyrics << "\n\n"; + }); + } + + void displayPerformers(const ITagReader& tagReader) + { + tagReader.visitPerformerTags([](std::string_view role, std::string_view artist) { + std::cout << "\tPerformer"; + if (!role.empty()) + std::cout << " '" << role << "'"; + std::cout << ": '" << artist << "'\n"; + }); + } + + void displayTags(const ITagReader& tagReader) + { + std::cout << "Tags:\n"; + for (int i{}; i < static_cast(TagType::Count); ++i) + displayTags(tagReader, static_cast(i)); + + displayPerformers(tagReader); + displayLyrics(tagReader); + } + + std::ostream& operator<<(std::ostream& os, const Image& image) + { + os << "\ttype = " << imageTypeToString(image.type) << std::endl; + if (!image.description.empty()) + os << "\tdesc: " << image.description << std::endl; + os << "\tmimeType: " << image.mimeType << std::endl; + os << "\tsize: " << image.data.size() << std::endl; + + return os; + } + + void displayImages(const IImageReader& imageReader) + { + imageReader.visitImages([](const Image& image) { + std::cout << "Image:\n" + << image << "\n"; + }); + } + + void displayInfo(const IAudioFileInfo& fileInfo) + { + std::cout << "Audio properties:\n" + << fileInfo.getAudioProperties() << std::endl; + + displayImages(fileInfo.getImageReader()); + displayTags(fileInfo.getTagReader()); + + std::cout << "\n"; + } +} // namespace lms::audio + +int main(int argc, char* argv[]) +{ + try + { + using namespace lms; + namespace program_options = boost::program_options; + + program_options::options_description options{ "Options" }; + // clang-format off + options.add_options() + ("help,h", "Display this help message") + ("parser", program_options::value()->default_value(std::string{ "taglib" }, "taglib"), "Parser to be used (value can be \"taglib\", \"ffmpeg\")"); + // clang-format on + + program_options::options_description hiddenOptions{ "Hidden options" }; + hiddenOptions.add_options()("file", program_options::value>()->composing(), "file"); + + program_options::options_description allOptions; + allOptions.add(options).add(hiddenOptions); + + program_options::positional_options_description positional; + positional.add("file", -1); // Handle remaining arguments as input files + + program_options::variables_map vm; + // Parse command line arguments with positional option handling + program_options::store(program_options::command_line_parser(argc, argv) + .options(allOptions) + .positional(positional) + .run(), + vm); + + program_options::notify(vm); + + auto displayHelp = [&](std::ostream& os) { + os << "Usage: " << argv[0] << " [options] file..." << std::endl; + os << options << std::endl; + }; + + if (vm.count("help")) + { + displayHelp(std::cout); + return EXIT_SUCCESS; + } + + if (vm.count("file") == 0) + { + std::cerr << "No input file provided" << std::endl; + displayHelp(std::cerr); + return EXIT_FAILURE; + } + + if (vm.count("parser") != 1) + { + std::cerr << "You must specify a parser to be used" << std::endl; + return EXIT_FAILURE; + } + + audio::ParserOptions parserOptions; + parserOptions.enableExtraDebugLogs = true; + if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "taglib")) + parserOptions.parser = audio::ParserOptions::Parser::TagLib; + else if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "ffmpeg")) + parserOptions.parser = audio::ParserOptions::Parser::FFmpeg; + else + throw program_options::validation_error{ program_options::validation_error::invalid_option_value, "parser" }; + + const auto& inputFiles{ vm["file"].as>() }; + // log to stdout + core::Service logger{ core::logging::createLogger(core::logging::Severity::DEBUG) }; + + for (const std::string& inputFile : inputFiles) + { + const std::filesystem::path file{ inputFile }; + + try + { + std::cout << "Parsing file " << file << ":\n"; + const auto audioFileInfo{ audio::parseAudioFile(file, parserOptions) }; + + displayInfo(*audioFileInfo); + } + catch (const audio::Exception& e) + { + std::cerr << "Failed to parse file " << file << ": " << e.what() << std::endl; + continue; + } + } + } + catch (std::exception& e) + { + std::cerr << "Caught exception: " << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/tools/metadata/CMakeLists.txt b/src/tools/metadata/CMakeLists.txt deleted file mode 100644 index dcdd2394..00000000 --- a/src/tools/metadata/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ - -add_executable(lms-metadata - LmsMetadata.cpp - ) - -target_link_libraries(lms-metadata PRIVATE - lmsmetadata - lmscore - Boost::program_options - ) - -install(TARGETS lms-metadata DESTINATION bin) diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp deleted file mode 100644 index 190c4746..00000000 --- a/src/tools/metadata/LmsMetadata.cpp +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright (C) 2019 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "core/EnumSet.hpp" -#include "core/ILogger.hpp" -#include "core/String.hpp" -#include "metadata/Exception.hpp" -#include "metadata/IAudioFileParser.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata -{ - std::ostream& operator<<(std::ostream& os, const Lyrics& lyrics) - { - os << "\tTitle display name: " << lyrics.displayTitle << std::endl; - os << "\tArtist display name: " << lyrics.displayArtist << std::endl; - os << "\tAlbum display name: " << lyrics.displayAlbum << std::endl; - os << "\tOffset: " << lyrics.offset.count() << "ms" << std::endl; - os << "\tLanguage: " << lyrics.language << std::endl; - os << "\tSynchronized: " << !lyrics.synchronizedLines.empty() << std::endl; - for (const auto& [timestamp, line] : lyrics.synchronizedLines) - os << "\t" << core::stringUtils::formatTimestamp(timestamp) << " '" << line << "'" << std::endl; - for (const auto& line : lyrics.unsynchronizedLines) - os << "\t'" << line << "'" << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) - { - os << "\tBitrate: " << audioProperties.bitrate << " bps" << std::endl; - os << "\tBitsPerSample: " << audioProperties.bitsPerSample << std::endl; - os << "\tChannelCount: " << audioProperties.channelCount << std::endl; - os << "\tDuration: " << std::fixed << std::setprecision(2) << audioProperties.duration.count() / 1000. << "s" << std::endl; - os << "\tSampleRate: " << audioProperties.sampleRate << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Artist& artist) - { - os << artist.name; - - if (artist.mbid) - os << " (" << artist.mbid->getAsString() << ")"; - - if (artist.sortName) - os << " '" << *artist.sortName << "'"; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Release& release) - { - os << release.name; - if (release.sortName != release.name) - os << " '" << release.sortName << "'"; - os << std::endl; - - for (std::string_view releaseType : release.releaseTypes) - std::cout << "\tRelease type: " << releaseType << std::endl; - - if (release.mbid) - os << "\tRelease MBID = " << release.mbid->getAsString() << std::endl; - - if (release.groupMBID) - os << "\tRelease Group MBID = " << release.groupMBID->getAsString() << std::endl; - - if (release.mediumCount) - std::cout << "\tMediumCount: " << *release.mediumCount << std::endl; - - std::cout << "\tIsCompilation: " << std::boolalpha << release.isCompilation << std::endl; - - for (std::string_view label : release.labels) - std::cout << "\tLabel: " << label << std::endl; - - for (std::string_view country : release.countries) - std::cout << "\tCountry: " << country << std::endl; - - if (!release.barcode.empty()) - std::cout << "\tBarcode: " << release.barcode << std::endl; - - if (!release.comment.empty()) - std::cout << "\tComment: " << release.comment << std::endl; - - if (!release.artistDisplayName.empty()) - std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl; - - for (const Artist& artist : release.artists) - std::cout << "\tRelease artist: " << artist << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Medium& medium) - { - if (!medium.name.empty()) - os << medium.name; - os << std::endl; - - if (medium.position) - os << "\tPosition: " << *medium.position << std::endl; - - if (!medium.media.empty()) - os << "\tMedia: " << medium.media << std::endl; - - if (medium.trackCount) - std::cout << "\tTrackCount: " << *medium.trackCount << std::endl; - - if (medium.replayGain) - std::cout << "\tReplay gain: " << *medium.replayGain << std::endl; - - if (medium.release) - std::cout << "Release: " << *medium.release; - - return os; - } - - std::ostream& operator<<(std::ostream& os, Track::Advisory advisory) - { - switch (advisory) - { - case Track::Advisory::Clean: - os << "clean"; - break; - case Track::Advisory::Explicit: - os << "Explicit"; - break; - case Track::Advisory::Unknown: - os << "Unknown"; - break; - } - - return os; - } - - const char* imageTypeToString(Image::Type type) - { - switch (type) - { - case Image::Type::Other: - return "Other"; - case Image::Type::FileIcon: - return "FileIcon"; - case Image::Type::OtherFileIcon: - return "OtherFileIcon"; - case Image::Type::FrontCover: - return "FrontCover"; - case Image::Type::BackCover: - return "BackCover"; - case Image::Type::LeafletPage: - return "LeafletPage"; - case Image::Type::Media: - return "Media"; - case Image::Type::LeadArtist: - return "LeadArtist"; - case Image::Type::Artist: - return "Artist"; - case Image::Type::Conductor: - return "Conductor"; - case Image::Type::Band: - return "Band"; - case Image::Type::Composer: - return "Composer"; - case Image::Type::Lyricist: - return "Lyricist"; - case Image::Type::RecordingLocation: - return "RecordingLocation"; - case Image::Type::DuringRecording: - return "DuringRecording"; - case Image::Type::DuringPerformance: - return "DuringPerformance"; - case Image::Type::MovieScreenCapture: - return "MovieScreenCapture"; - case Image::Type::ColouredFish: - return "ColouredFish"; - case Image::Type::Illustration: - return "Illustration"; - case Image::Type::BandLogo: - return "BandLogo"; - case Image::Type::PublisherLogo: - return "PublisherLogo"; - case Image::Type::Unknown: - break; - } - - return "Unknown"; - } - - std::ostream& operator<<(std::ostream& os, const Image& image) - { - os << "type = " << imageTypeToString(image.type) << std::endl; - if (!image.description.empty()) - os << "\tdesc = " << image.description << std::endl; - os << "\tmimeType = " << image.mimeType << std::endl; - os << "\tsize = " << image.data.size() << std::endl; - - return os; - } - - void parseMetaData(IAudioFileParser& parser, const std::filesystem::path& file) - { - using namespace metadata; - - const auto start{ std::chrono::steady_clock::now() }; - std::unique_ptr track{ parser.parseMetaData(file) }; - const auto end{ std::chrono::steady_clock::now() }; - - std::cout << "MetaData parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; - - std::cout << "Audio properties:\n" - << track->audioProperties << std::endl; - - std::cout << "Parsed metadata:" << std::endl; - - if (!track->artistDisplayName.empty()) - std::cout << "Display artist: " << track->artistDisplayName << std::endl; - - for (const Artist& artist : track->artists) - std::cout << "Artist: " << artist << std::endl; - - for (const Artist& artist : track->conductorArtists) - std::cout << "Conductor: " << artist << std::endl; - - for (const Artist& artist : track->composerArtists) - std::cout << "Composer: " << artist << std::endl; - - for (const Artist& artist : track->lyricistArtists) - std::cout << "Lyricist: " << artist << std::endl; - - for (const Artist& artist : track->mixerArtists) - std::cout << "Mixer: " << artist << std::endl; - - for (const auto& [role, artists] : track->performerArtists) - { - std::cout << "Performer"; - if (!role.empty()) - std::cout << " (" << role << ")"; - std::cout << ":" << std::endl; - for (const Artist& artist : artists) - std::cout << "\t" << artist << std::endl; - } - - for (const Artist& artist : track->producerArtists) - std::cout << "Producer: " << artist << std::endl; - - for (const Artist& artist : track->remixerArtists) - std::cout << "Remixer: " << artist << std::endl; - - std::cout << "Title: " << track->title << std::endl; - - if (track->mbid) - std::cout << "Track MBID = " << track->mbid->getAsString() << std::endl; - - if (track->recordingMBID) - std::cout << "Recording MBID = " << track->recordingMBID->getAsString() << std::endl; - - for (std::string_view genre : track->genres) - std::cout << "Genre: " << genre << std::endl; - - for (std::string_view genre : track->moods) - std::cout << "Mood: " << genre << std::endl; - - for (std::string_view grouping : track->groupings) - std::cout << "Grouping: " << grouping << std::endl; - - for (std::string_view language : track->languages) - std::cout << "Language: " << language << std::endl; - - for (const auto& [tag, values] : track->userExtraTags) - { - std::cout << "Tag: " << tag << std::endl; - for (const auto& value : values) - { - std::cout << "\t" << value << std::endl; - } - } - - if (track->position) - std::cout << "Position: " << *track->position << std::endl; - - if (track->date.isValid()) - std::cout << "Date: " << track->date.toString() << std::endl; - - if (track->originalDate.isValid()) - std::cout << "Original date: " << track->originalDate.toString() << std::endl; - if (track->originalYear) - std::cout << "Original year: " << *track->originalYear << std::endl; - - if (track->replayGain) - std::cout << "Track replay gain: " << *track->replayGain << std::endl; - - if (track->acoustID) - std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl; - - if (!track->copyright.empty()) - std::cout << "Copyright: " << track->copyright << std::endl; - - for (const Lyrics& lyrics : track->lyrics) - std::cout << "Lyrics:\n" - << lyrics << std::endl; - - for (const auto& comment : track->comments) - std::cout << "Comment: '" << comment << "'" << std::endl; - - if (!track->copyrightURL.empty()) - std::cout << "CopyrightURL: " << track->copyrightURL << std::endl; - - if (track->advisory) - std::cout << "Advisory: " << *track->advisory << std::endl; - - if (track->encodingTime.isValid()) - std::cout << "Encoding time: " << track->encodingTime.toString() << std::endl; - - if (track->medium) - std::cout << "Medium: " << *track->medium; - - std::cout << std::endl; - } - - void parseImages(IAudioFileParser& parser, const std::filesystem::path& file) - { - using namespace metadata; - - const auto start{ std::chrono::steady_clock::now() }; - parser.parseImages(file, [](const Image& image) { - std::cout << "Image: " << image << std::endl; - }); - const auto end{ std::chrono::steady_clock::now() }; - - std::cout << "Image parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; - } - -} // namespace lms::metadata - -int main(int argc, char* argv[]) -{ - try - { - using namespace lms; - namespace program_options = boost::program_options; - - program_options::options_description options{ "Options" }; - // clang-format off - options.add_options() - ("help,h", "Display this help message") - ("tag-delimiter", program_options::value>()->default_value(std::vector{}, "[]"), "Tag delimiters (multiple allowed)") - ("artist-tag-delimiter", program_options::value>()->default_value(std::vector{}, "[]"), "Artist tag delimiters (multiple allowed)") - ("parser", program_options::value>()->default_value(std::vector{ "taglib" }, "[taglib]"), "Parser to be used (value can be \"taglib\", \"ffmpeg\" or \"lyrics\")"); - // clang-format on - - program_options::options_description hiddenOptions{ "Hidden options" }; - hiddenOptions.add_options()("file", program_options::value>()->composing(), "file"); - - program_options::options_description allOptions; - allOptions.add(options).add(hiddenOptions); - - program_options::positional_options_description positional; - positional.add("file", -1); // Handle remaining arguments as input files - - program_options::variables_map vm; - // Parse command line arguments with positional option handling - program_options::store(program_options::command_line_parser(argc, argv) - .options(allOptions) - .positional(positional) - .run(), - vm); - - program_options::notify(vm); - - auto displayHelp = [&](std::ostream& os) { - os << "Usage: " << argv[0] << " [options] file..." << std::endl; - os << options << std::endl; - }; - - if (vm.count("help")) - { - displayHelp(std::cout); - return EXIT_SUCCESS; - } - - if (vm.count("file") == 0) - { - std::cerr << "No input file provided" << std::endl; - displayHelp(std::cerr); - return EXIT_FAILURE; - } - - enum class Parser - { - Lyrics, - Taglib, - Ffmpeg, - }; - - if (vm.count("parser") == 0) - { - std::cerr << "You must specify at least one parser" << std::endl; - return EXIT_FAILURE; - } - - core::EnumSet parsers; - for (const std::string& strParser : vm["parser"].as>()) - { - if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "lyrics")) - parsers.insert(Parser::Lyrics); - else if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "taglib")) - parsers.insert(Parser::Taglib); - else if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "ffmpeg")) - parsers.insert(Parser::Ffmpeg); - else - { - std::cerr << "Invalid parser name '" << strParser << "'" << std::endl; - return EXIT_FAILURE; - } - } - - const auto& inputFiles{ vm["file"].as>() }; - const auto& tagDelimiters{ vm["tag-delimiter"].as>() }; - const auto& artistTagDelimiters{ vm["artist-tag-delimiter"].as>() }; - - for (std::string_view tagDelimiter : tagDelimiters) - { - std::cout << "Tag delimiter: '" << tagDelimiter << "'" << std::endl; - } - - for (std::string_view artistTagDelimiter : artistTagDelimiters) - { - std::cout << "Artist tag delimiter: '" << artistTagDelimiter << "'" << std::endl; - } - - // log to stdout - core::Service logger{ core::logging::createLogger(core::logging::Severity::DEBUG) }; - - for (const std::string& inputFile : inputFiles) - { - std::filesystem::path file{ inputFile }; - - std::cout << "Parsing file " << file << std::endl; - - if (parsers.contains(Parser::Lyrics)) - { - try - { - std::cout << "Using Lyrics:" << std::endl; - - std::ifstream ifs{ file }; - if (ifs) - { - const metadata::Lyrics lyrics{ metadata::parseLyrics(ifs) }; - std::cout << lyrics << std::endl; - } - else - std::cerr << "Cannot open file " << file; - } - catch (metadata::Exception& e) - { - std::cerr << "Parsing failed: " << e.what() << std::endl; - } - } - - auto parseAudioFile{ [&](metadata::ParserBackend backend) { - metadata::AudioFileParserParameters params; - params.artistTagDelimiters = artistTagDelimiters; - params.defaultTagDelimiters = tagDelimiters; - params.readStyle = metadata::ParserReadStyle::Accurate; - params.debug = true; - params.backend = backend; - - try - { - auto parser{ metadata::createAudioFileParser(params) }; - parseMetaData(*parser, file); - parseImages(*parser, file); - } - catch (metadata::Exception& e) - { - std::cerr << "Parsing failed: " << e.what() << std::endl; - } - } }; - - if (parsers.contains(Parser::Ffmpeg)) - parseAudioFile(metadata::ParserBackend::AvFormat); - - if (parsers.contains(Parser::Taglib)) - parseAudioFile(metadata::ParserBackend::TagLib); - } - } - catch (std::exception& e) - { - std::cerr << "Caught exception: " << e.what() << std::endl; - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} \ No newline at end of file