Merge branch 'develop' for release v3.72.0

This commit is contained in:
emeric
2025-11-16 11:51:19 +01:00
164 changed files with 5291 additions and 4094 deletions
+2 -2
View File
@@ -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)
+2 -3
View File
@@ -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} && \
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+9 -3
View File
@@ -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();
}
+1 -2
View File
@@ -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)
+40
View File
@@ -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
)
+105
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "audio/AudioTypes.hpp"
namespace lms::audio
{
core::LiteralString containerTypeToString(ContainerType type)
{
switch (type)
{
case ContainerType::AIFF:
return "AIFF";
case ContainerType::APE:
return "APE";
case ContainerType::ASF:
return "ASF";
case ContainerType::DSF:
return "DSF";
case ContainerType::FLAC:
return "FLAC";
case ContainerType::MP4:
return "MP4";
case ContainerType::MPC:
return "MPC";
case ContainerType::MPEG:
return "MPEG";
case ContainerType::Ogg:
return "Ogg";
case ContainerType::Shorten:
return "Shorten";
case ContainerType::TrueAudio:
return "TrueAudio";
case ContainerType::WAV:
return "WAV";
case ContainerType::WavPack:
return "WavPack";
}
return "";
}
core::LiteralString codecTypeToString(CodecType type)
{
switch (type)
{
case CodecType::AAC:
return "AAC";
case CodecType::ALAC:
return "ALAC";
case CodecType::APE:
return "APE";
case CodecType::DSD:
return "DSD";
case CodecType::FLAC:
return "FLAC";
case CodecType::MP3:
return "MP3";
case CodecType::MP4ALS:
return "MP4ALS";
case CodecType::MPC7:
return "MPC7";
case CodecType::MPC8:
return "MPC8";
case CodecType::Opus:
return "Opus";
case CodecType::PCM:
return "PCM";
case CodecType::Shorten:
return "Shorten";
case CodecType::TrueAudio:
return "TrueAudio";
case CodecType::Vorbis:
return "Vorbis";
case CodecType::WavPack:
return "WavPack";
case CodecType::WMA1:
return "WMA1";
case CodecType::WMA2:
return "WMA2";
case CodecType::WMA9Pro:
return "WMA9Pro";
case CodecType::WMA9Lossless:
return "WMA9Lossless";
}
return "";
}
} // namespace lms::audio
+77
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <memory>
#include "audio/IAudioFileInfo.hpp"
#include "ffmpeg/AudioFileInfo.hpp"
#include "ffmpeg/Utils.hpp"
#include "taglib/AudioFileInfo.hpp"
#include "taglib/Utils.hpp"
namespace lms::audio
{
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
{
switch (parserOptions.parser)
{
case ParserOptions::Parser::TagLib:
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
case ParserOptions::Parser::FFmpeg:
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
}
return {};
}
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
{
switch (parser)
{
case ParserOptions::Parser::TagLib:
return taglib::utils::getSupportedExtensions();
case ParserOptions::Parser::FFmpeg:
return ffmpeg::utils::getSupportedExtensions();
}
return {};
}
} // namespace lms::audio
+277
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -19,6 +19,9 @@
#include "AudioFile.hpp"
#include <array>
#include <unordered_map>
extern "C"
{
#define __STDC_CONSTANT_MACROS
@@ -27,15 +30,13 @@ extern "C"
#include <libavutil/error.h>
}
#include <array>
#include <unordered_map>
#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<ContainerType> 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<CodecType> 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<IAudioFile> parseAudioFile(const std::filesystem::path& p)
{
return std::make_unique<AudioFile>(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<std::size_t>(avstream->codecpar->bit_rate);
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
res->channelCount = static_cast<std::size_t>(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<std::size_t>(avstream->codecpar->bit_rate);
if (avstream->codecpar->bits_per_coded_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
else if (avstream->codecpar->bits_per_raw_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_raw_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
if (avstream->codecpar->channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
if (avstream->codecpar->ch_layout.nb_channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
assert(!res->codecName.empty()); // doc says it is never NULL
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
if (avstream->codecpar->sample_rate)
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
return res;
}
} // namespace lms::av
} // namespace lms::audio::ffmpeg
+91
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include "audio/AudioTypes.hpp"
extern "C"
{
struct AVFormatContext;
}
namespace lms::audio::ffmpeg
{
struct Picture
{
std::string mimeType;
std::span<const std::byte> data; // valid as long as IAudioFile exists
};
struct ContainerInfo
{
std::optional<ContainerType> container;
std::string containerName;
std::size_t bitrate{};
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
std::optional<CodecType> codec;
std::string codecName;
std::optional<size_t> bitrate;
std::optional<std::size_t> bitsPerSample;
std::optional<std::size_t> channelCount;
std::optional<std::size_t> 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<std::string, std::string>;
const std::filesystem::path& getPath() const;
ContainerInfo getContainerInfo() const;
MetadataMap getMetaData() const;
std::vector<StreamInfo> getStreamInfo() const;
std::optional<StreamInfo> getBestStreamInfo() const;
std::optional<std::size_t> getBestStreamIndex() const;
bool hasAttachedPictures() const;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context{};
};
} // namespace lms::audio::ffmpeg
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<AudioFile>(filePath) }
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#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> _audioFile;
std::unique_ptr<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
} // namespace lms::audio::ffmpeg
@@ -17,35 +17,30 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AvFormatImageReader.hpp"
#include "ImageReader.hpp"
#include "av/Exception.hpp"
#include "av/IAudioFile.hpp"
#include "metadata/Exception.hpp"
#include <algorithm>
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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -17,15 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#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<core::logging::ILogger>::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<core::logging::ILogger>::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
@@ -19,31 +19,27 @@
#pragma once
#include <filesystem>
#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
@@ -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<ITranscoder> createTranscoder(const TranscodeParameters& parameters)
{
return std::make_unique<ffmpeg::Transcoder>(parameters);
}
} // namespace lms::audio
namespace lms::audio::ffmpeg
{
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
{
return std::make_unique<Transcoder>(inputParameters, outputParameters);
}
static std::atomic<size_t> 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<std::string> 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<int>(_outputParams.format)) + ")" };
default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_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
@@ -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<core::IChildProcess> _childProcess;
};
} // namespace lms::av
} // namespace lms::audio::ffmpeg
@@ -19,11 +19,11 @@
#include "Utils.hpp"
namespace lms::metadata::avformat::utils
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions()
{
// TODO: use av capability to retrieve supported formats
// TODO: list demuxers to retrieve supported formats
static const std::array<std::filesystem::path, 18> fileExtensions{
".aac",
".alac",
@@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils
};
return fileExtensions;
}
} // namespace lms::metadata::avformat::utils
} // namespace lms::audio::ffmpeg::utils
@@ -22,7 +22,7 @@
#include <filesystem>
#include <span>
namespace lms::metadata::avformat::utils
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
} // namespace lms::metadata::avformat::utils
} // namespace lms::audio::ffmpeg::utils
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "AudioFileInfo.hpp"
#include <cassert>
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/trueaudiofile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#endif
#if LMS_TAGLIB_HAS_SHORTEN
#include <taglib/shortenfile.h>
#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<std::size_t>(properties.bitrate() * 1000);
if (audioProperties.bitrate == 0)
throw AudioFileParsingException{ "Cannot determine bitrate" };
audioProperties.channelCount = static_cast<std::size_t>(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<std::size_t>(properties.sampleRate());
if (audioProperties.sampleRate == 0)
throw AudioFileParsingException{ "Cannot determine sample rate" };
}
// Guess container from the file type
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
{
audioProperties.container = ContainerType::APE;
audioProperties.codec = CodecType::APE; // TODO version?
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
}
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&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<const ::TagLib::DSF::File*>(&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<const ::TagLib::FLAC::File*>(&file) })
{
audioProperties.container = ContainerType::FLAC;
audioProperties.codec = CodecType::FLAC;
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
}
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&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<const ::TagLib::MPC::File*>(&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<const ::TagLib::MPEG::File*>(&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<const ::TagLib::Ogg::Opus::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Opus;
}
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Vorbis;
}
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
{
audioProperties.container = ContainerType::AIFF;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
{
audioProperties.container = ContainerType::WAV;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_SHORTEN
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
{
audioProperties.container = ContainerType::Shorten;
audioProperties.codec = CodecType::Shorten;
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_SHORTEN
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
{
audioProperties.container = ContainerType::TrueAudio;
audioProperties.codec = CodecType::TrueAudio;
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
{
audioProperties.container = ContainerType::WavPack;
audioProperties.codec = CodecType::WavPack;
audioProperties.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<AudioProperties>(computeAudioProperties(*_file)) }
, _tagReader{ std::make_unique<TagReader>(*_file, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
} // namespace lms::audio::taglib
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagLibImageReader.hpp"
#include "ImageReader.hpp"
#include "TagLibDefs.hpp"
@@ -33,16 +33,14 @@
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/tfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#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<const TagLib::ID3v2::AttachedPictureFrame*>(frame) };
const auto* attachedPictureFrame{ dynamic_cast<const ::TagLib::ID3v2::AttachedPictureFrame*>(frame) };
if (!attachedPictureFrame)
continue;
TagLib::ByteVector picture{ attachedPictureFrame->picture() };
::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
@@ -303,11 +301,11 @@ namespace lms::metadata::taglib
}
}
void visitFLACImages(const TagLib::List<TagLib::FLAC::Picture*> pictureList, TagLibImageReader::ImageVisitor visitor)
void visitFLACImages(const ::TagLib::List<TagLib::FLAC::Picture*>& pictureList, const ImageReader::ImageVisitor& visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
TagLib::ByteVector picture{ flacPicture->data() };
::TagLib::ByteVector picture{ flacPicture->data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const TagLib::VariantMap& pictureProperty : pictureProperties)
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const ::TagLib::List<TagLib::VariantMap> 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<TagLib::MPEG::File*>(_file.get()) })
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
else if (const TagLib::MP4::File * mp4File{ dynamic_cast<const TagLib::MP4::File*>(&_file) })
{
visitMP4Images(*mp4File, std::move(visitor));
visitMP4Images(*mp4File, visitor);
}
// WMA
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
else if (const TagLib::ASF::File * asfFile{ dynamic_cast<const TagLib::ASF::File*>(&_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<TagLib::FLAC::File*>(_file.get()) })
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_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<TagLib::Ogg::Vorbis::File*>(_file.get()) })
else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<const TagLib::Ogg::Vorbis::File*>(&_file) })
{
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
}
// Ogg Opus
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.get()) })
else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(&_file) })
{
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
visitFLACImages(opusFile->tag()->pictureList(), visitor);
}
// Aiff
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
visitID3V2Images(*aiffFile->tag(), visitor);
}
// Wav
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(&_file) })
{
if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
visitAPEImages(*mpcFile->APETag(), visitor);
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
if (wavPackFile->hasAPETag())
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
visitAPEImages(*wavPackFile->APETag(), visitor);
}
}
} // namespace lms::metadata::taglib
} // namespace lms::audio::taglib
@@ -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<av::IAudioFile> _audioFile;
TagLib::File& _file;
};
} // namespace lms::metadata::avformat
} // namespace lms::audio::taglib
@@ -22,15 +22,21 @@
#include <taglib/taglib.h>
#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
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagLibTagReader.hpp"
#include "TagReader.hpp"
#include <unordered_map>
@@ -25,7 +25,6 @@
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/apeproperties.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
@@ -38,33 +37,25 @@
#include <taglib/speexfile.h>
#include <taglib/synchronizedlyricsframe.h>
#include <taglib/tag.h>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h>
#include <taglib/trueaudiofile.h>
#include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#include <taglib/dsffile.h>
#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<TagType, std::vector<std::string>> 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<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
enableExtraDebugLogs &= core::Service<core::logging::ILogger>::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<const TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::SynchronizedLyricsFrame*>(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<const TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
@@ -310,9 +292,9 @@ namespace lms::metadata::taglib
};
// WMA
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
if (const ::TagLib::ASF::File * asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&_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{ "<Non unicode>" }) << "'");
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "<Non unicode>" }) << "'");
}
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<TagLib::MPEG::File*>(_file.get()) })
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
processID3v2Tags(*mp3File->ID3v2Tag());
processID3v2Tags(*mp3File->ID3v2Tag(false));
getAPETags(mp3File->APETag());
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&_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<TagLib::MPC::File*>(_file.get()) })
else if (::TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
{
if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag());
}
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<const TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
processID3v2Tags(*aiffFile->tag());
}
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<const TagLib::RIFF::WAV::File*>(&_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<std::size_t>(properties->bitrate() * 1000);
_audioProperties.channelCount = static_cast<std::size_t>(_file->audioProperties()->channels());
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
if (const auto* apeProperties{ dynamic_cast<const TagLib::APE::Properties*>(properties) })
_audioProperties.bitsPerSample = apeProperties->bitsPerSample();
if (const auto* asfProperties{ dynamic_cast<const TagLib::ASF::Properties*>(properties) })
_audioProperties.bitsPerSample = asfProperties->bitsPerSample();
else if (const auto* flacProperties{ dynamic_cast<const TagLib::FLAC::Properties*>(properties) })
_audioProperties.bitsPerSample = flacProperties->bitsPerSample();
else if (const auto* mp4Properties{ dynamic_cast<const TagLib::MP4::Properties*>(properties) })
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
else if (const auto* wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
else if (const auto* aiffProperties{ dynamic_cast<const TagLib::RIFF::AIFF::Properties*>(properties) })
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
#if TAGLIB_HAS_DSF
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSDIFF::Properties*>(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
@@ -19,38 +19,37 @@
#pragma once
#include <filesystem>
#include <map>
#include <span>
#include <string>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h>
#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<TagLib::File> _file;
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
::TagLib::File& _file;
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::metadata::taglib
} // namespace lms::audio::taglib
@@ -39,40 +39,65 @@
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#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<const std::filesystem::path> getSupportedExtensions()
{
static const std::vector<std::filesystem::path> 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<const std::filesystem::path>{ 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<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (extension.empty())
@@ -149,11 +175,9 @@ namespace lms::metadata::taglib::utils
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "WAV")
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
else if (ext == "DSF")
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "DFF" || ext == "DSDIFF")
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
@@ -162,8 +186,9 @@ namespace lms::metadata::taglib::utils
return file;
}
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (TagLib::MPEG::File::isSupported(stream))
@@ -200,11 +225,9 @@ namespace lms::metadata::taglib::utils
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::RIFF::WAV::File::isSupported(stream))
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
else if (TagLib::DSF::File::isSupported(stream))
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::DSDIFF::File::isSupported(stream))
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
@@ -213,20 +236,27 @@ namespace lms::metadata::taglib::utils
return file;
}
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties)
std::unique_ptr<TagLib::File> 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<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) };
std::unique_ptr<TagLib::File> 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
} // namespace lms::audio::taglib::utils
@@ -19,19 +19,16 @@
#pragma once
#include "IImageReader.hpp"
#include <filesystem>
#include <memory>
#include <span>
#include <taglib/tfile.h>
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<TagLib::File> _file;
};
} // namespace lms::metadata::taglib
std::span<const std::filesystem::path> getSupportedExtensions();
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
} // namespace lms::audio::taglib::utils
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <optional>
#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<unsigned> bitsPerSample;
};
} // namespace lms::audio
@@ -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
@@ -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 <system_error>
#include <filesystem>
#include <span>
#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
Parser parser{ Parser::TagLib };
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
bool enableExtraDebugLogs{};
};
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
} // namespace lms::audio
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <span>
#include <string>
#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<const std::byte> data;
};
core::LiteralString imageTypeToString(Image::Type type);
class IImageReader
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(const ImageVisitor& visitor) const = 0;
};
} // namespace lms::audio
@@ -20,10 +20,11 @@
#pragma once
#include <functional>
#include <string_view>
#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<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
};
} // namespace lms::metadata
} // namespace lms::audio
@@ -20,37 +20,14 @@
#pragma once
#include <cstddef>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
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<std::size_t> 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<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
} // namespace lms::av
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters);
} // namespace lms::audio
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
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<OutputFormat> format;
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
std::optional<unsigned> sampleRate;
bool stripMetadata{ true };
};
struct TranscodeParameters
{
TranscodeInputParameters inputParameters;
TranscodeOutputParameters outputParameters;
};
} // namespace lms::audio
-26
View File
@@ -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
)
-51
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const override;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context{};
};
} // namespace lms::av
-104
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
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<const std::byte> 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<std::string, std::string>;
virtual const std::filesystem::path& getPath() const = 0;
virtual ContainerInfo getContainerInfo() const = 0;
virtual MetadataMap getMetaData() const = 0;
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap& metadata)> func) const = 0;
};
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
} // namespace lms::av
-1
View File
@@ -50,7 +50,6 @@ target_link_libraries(lmscore PRIVATE
)
target_link_libraries(lmscore PUBLIC
Boost::system
std::filesystem
Wt::Wt
)
+2 -2
View File
@@ -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:
+1
View File
@@ -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" },
+1 -1
View File
@@ -40,8 +40,8 @@ namespace lms::core::logging
enum class Module
{
API_SUBSONIC,
AUDIO,
AUTH,
AV,
CHILDPROCESS,
COVER,
DB,
+32
View File
@@ -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<Bitrate> allowedAudioBitrates{
64000,
96000,
@@ -41,4 +72,5 @@ namespace lms::db
{
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
}
} // namespace lms::db
+25
View File
@@ -334,6 +334,31 @@ AND NOT EXISTS (
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1;
}
RangeResults<Artist::pointer> Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Artist>>(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<Artist::pointer>(query, range) };
if (!results.results.empty())
lastRetrievedArtist = results.results.back()->getId();
return results;
}
void Artist::updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId)
{
session.checkWriteTransaction();
+8 -8
View File
@@ -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<Wt::Dbo::ptr<Country>>("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<Wt::Dbo::ptr<Label>>("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<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
}
+4
View File
@@ -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;
@@ -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<void(const TrackArtistLink::pointer&)>& 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<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
+2 -1
View File
@@ -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");
@@ -27,6 +27,7 @@
#include <Wt/WDate.h>
#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
{
@@ -136,6 +136,7 @@ namespace lms::db
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& params);
static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
static bool exists(Session& session, ArtistId id);
static RangeResults<pointer> findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range = std::nullopt);
// Updates
static void updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId);
@@ -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);
@@ -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<bool> mbidMatched;
TrackArtistLinkSortMethod sortMethod{ TrackArtistLinkSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
{
@@ -77,6 +79,16 @@ namespace lms::db
track = _track;
return *this;
}
FindParameters& setMBIDMatched(std::optional<bool> _mbidMatched)
{
mbidMatched = _mbidMatched;
return *this;
}
FindParameters& setSortMethod(TrackArtistLinkSortMethod _method)
{
sortMethod = _method;
return *this;
}
};
TrackArtistLink() = default;
+39
View File
@@ -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
+93 -1
View File
@@ -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<TrackArtistLink>(track1.get(), artist.get(), TrackArtistLinkType::Artist, false) };
link1.modify()->setArtistName("MyArtistOldName");
track1.get().modify()->setOriginalDate(core::PartialDateTime{ 1990, 1 });
}
{
auto link2{ session.create<TrackArtistLink>(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<TrackArtistLink::pointer> 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<TrackArtistLink>(track1.get(), artist.get(), TrackArtistLinkType::Artist, false) };
auto link2{ session.create<TrackArtistLink>(track2.get(), artist.get(), TrackArtistLinkType::Artist, true) };
}
{
auto transaction{ session.createReadTransaction() };
TrackArtistLink::FindParameters params;
std::vector<TrackArtistLink::pointer> 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<TrackArtistLink::pointer> 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<TrackArtistLink::pointer> 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
+49
View File
@@ -19,10 +19,15 @@
#include <list>
#include "database/objects/Release.hpp"
#include "database/objects/TrackList.hpp"
#include "Common.hpp"
namespace lms::db::tests
{
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
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<TrackListEntry>(track1.get(), trackList1.get());
release1.get().modify()->addReleaseType(releaseType1.get());
track1.get().modify()->setRelease(release1.get());
session.create<TrackListEntry>(track2.get(), trackList2.get());
release2.get().modify()->addReleaseType(releaseType2.get());
track2.get().modify()->setRelease(release2.get());
}
{
auto transaction{ session.createReadTransaction() };
std::vector<TrackListId> 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<TrackListId> 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 };
-43
View File
@@ -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
)
-15
View File
@@ -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
)
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#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<Track> parseMetaData(const std::filesystem::path& p) const override;
std::unique_ptr<Track> 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<const std::filesystem::path> getSupportedExtensions() const override;
void processTags(const ITagReader& reader, Track& track) const;
std::optional<Medium> getMedium(const ITagReader& tagReader) const;
std::optional<Release> getRelease(const ITagReader& tagReader) const;
const AudioFileParserParameters _params;
};
} // namespace lms::metadata
-39
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <memory>
#include <span>
#include <taglib/audioproperties.h>
#include <taglib/tfile.h>
#include "core/TaggedType.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle);
using ReadAudioProperties = core::TaggedBool<struct ReadAudioPropertiesTag>;
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties);
} // namespace lms::metadata::taglib::utils
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <span>
#include "metadata/Exception.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata
{
class IAudioFileParser
{
public:
virtual ~IAudioFileParser() = default;
virtual std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) const = 0;
using ImageVisitor = std::function<void(const Image&)>;
virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const = 0;
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
};
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params);
} // namespace lms::metadata
-933
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <vector>
#include <gtest/gtest.h>
#include <Wt/WTime.h>
#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<ITagReader> testTags{ createDefaultPopulatedTestTagReader() };
const std::unique_ptr<Track> 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<std::string> expectedReleaseTypes{ "Album", "Compilation" };
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
}
}
TEST(AudioFileParser, trim)
{
const TestTagReader testTags{
{
{ TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
}
};
std::unique_ptr<Track> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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<Track::Advisory> expectedValue) {
const TestTagReader testTags{
{
{ TagType::Advisory, { value } },
}
};
AudioFileParser parser;
std::unique_ptr<Track> 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> 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> 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
-24
View File
@@ -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()
+1 -1
View File
@@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE
)
target_link_libraries(lmsartwork PRIVATE
lmsaudio
lmsimage
lmsmetadata
)
target_link_libraries(lmsartwork PUBLIC
@@ -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<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
{
setJpegQuality(core::Service<core::IConfig>::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());
}
@@ -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<metadata::IAudioFileParser> _audioFileParser;
ImageCache _cache;
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
-1
View File
@@ -19,7 +19,6 @@ target_include_directories(lmsauth PRIVATE
)
target_link_libraries(lmsauth PUBLIC
Boost::system
Wt::Wt
lmscore
lmsdatabase
+21 -7
View File
@@ -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()
@@ -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
)
@@ -22,9 +22,9 @@
#include <benchmark/benchmark.h>
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <benchmark/benchmark.h>
BENCHMARK_MAIN();
@@ -19,30 +19,22 @@
#include <benchmark/benchmark.h>
#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<ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
const TestAudioFileParser parser{ params };
std::unique_ptr<audio::ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
const TrackMetadataParser parser{ params };
for (auto _ : state)
{
std::unique_ptr<Track> 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> 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> 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> 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();
} // namespace lms::scanner::benchmarks
@@ -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"
@@ -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<db::Artist>(artistInfo.name) };
@@ -38,55 +39,25 @@ namespace lms::scanner::helpers
return artist;
}
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
{
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
}
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)
@@ -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<struct AllowFallbackOnMBIDEntryTag>;
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
@@ -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<metadata::ArtistInfo> _parsedArtistInfo;
std::optional<ArtistInfo> _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<ArtistInfoFileScanError>(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<std::string>(_parsedArtistInfo->sortName) };
const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_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<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
{
return metadata::getSupportedArtistInfoFiles();
return getSupportedArtistInfoFiles();
}
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/ArtistInfo.hpp"
#include "ArtistInfoParser.hpp"
#include <pugixml.hpp>
@@ -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
} // namespace lms::scanner
@@ -19,18 +19,22 @@
#pragma once
#include <functional>
#include <filesystem>
#include <iosfwd>
#include <span>
#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<void(const Image& image)>;
virtual void visitImages(ImageVisitor visitor) const = 0;
using core::LmsException::LmsException;
};
} // namespace lms::metadata
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
ArtistInfo parseArtistInfo(std::istream& is);
} // namespace lms::scanner
@@ -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<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const Artist> 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<db::TrackArtistLink>(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<db::TrackArtistLink>(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<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const Artist> 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<db::Release>(releaseInfo.name, releaseInfo.mbid);
dbRelease = db::Release::find(session, *release.mbid);
if (!dbRelease)
dbRelease = session.create<db::Release>(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<db::Release>(releaseInfo.name);
if (!dbRelease)
dbRelease = session.create<db::Release>(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<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
{
std::vector<db::Cluster::pointer> 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>() };
db::TrackLyrics::pointer dbLyrics{ session.create<db::TrackLyrics>() };
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<db::TrackEmbeddedImageLink>(track, image) };
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(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<const ImageInfo> images)
void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span<const ImageInfo> 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<metadata::Track::Advisory> advisory)
db::Advisory getAdvisory(std::optional<Track::Advisory> 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<const metadata::Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& 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<metadata::Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& 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<std::string_view, core::UUID> 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<metadata::Track> 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<NoAudioTrackFoundError>(getFilePath());
}
catch (const metadata::IOException& e)
catch (const audio::IOException& e)
{
addError<IOScanError>(getFilePath(), e.getErrorCode());
}
catch (const metadata::Exception& e)
catch (const audio::Exception& e)
{
addError<AudioFileScanError>(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<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
std::vector<db::Track::pointer> 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<BadAudioDurationError>(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)
{
@@ -19,33 +19,32 @@
#pragma once
#include "IFileScanOperation.hpp"
#include <memory>
#include <vector>
#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<metadata::Track> _parsedTrack;
std::vector<ImageInfo> _parsedImages;
};
const TrackMetadataParser& _metadataParser;
const audio::ParserOptions& _parserOptions;
struct AudioFileInfo
{
audio::AudioProperties audioProperties;
Track track;
std::vector<ImageInfo> images;
};
std::optional<AudioFileInfo> _file;
};
} // namespace lms::scanner
@@ -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<core::IConfig>::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<const std::filesystem::path> 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<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
{
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
}
} // namespace lms::scanner
@@ -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<metadata::IAudioFileParser> _metadataParser;
const TrackMetadataParser _trackMetadataParser;
const audio::ParserOptions _parserOptions;
};
} // namespace lms::scanner
@@ -17,30 +17,26 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileParser.hpp"
#include "TrackMetadataParser.hpp"
#include <span>
#include <string>
#include <string_view>
#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<const std::string> tagDelimiters, ITagReader::TagValueVisitor visitor)
void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span<const std::string> 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<typename T>
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters, const WhiteList* whitelist = nullptr)
std::vector<T> getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes, std::span<const std::string> tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr)
{
std::vector<T> 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<typename T>
std::optional<T> getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes)
std::optional<T> getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes)
{
std::optional<T> res;
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
@@ -161,45 +157,38 @@ namespace lms::metadata
}
template<typename T>
std::vector<T> getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span<const std::string> tagDelimiters)
std::vector<T> getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span<const std::string> tagDelimiters)
{
return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
}
template<typename T>
std::optional<T> getTagValueAs(const ITagReader& tagReader, TagType tagType)
std::optional<T> getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType)
{
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
}
std::vector<Lyrics> getLyrics(const ITagReader& tagReader)
std::vector<Lyrics> getLyrics(const audio::ITagReader& tagReader)
{
std::vector<Lyrics> 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<Artist> getArtists(const ITagReader& tagReader,
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames,
const AudioFileParserParameters& params)
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
std::initializer_list<audio::TagType> artistTagNames,
std::initializer_list<audio::TagType> artistSortTagNames,
std::initializer_list<audio::TagType> artistMBIDTagNames,
const TrackMetadataParser::Parameters& params)
{
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, &params.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<Track::Advisory> getAdvisory(const ITagReader& tagReader)
std::optional<Track::Advisory> getAdvisory(const audio::ITagReader& tagReader)
{
if (const auto value{ getTagValueAs<int>(tagReader, TagType::Advisory) })
if (const auto value{ getTagValueAs<int>(tagReader, audio::TagType::Advisory) })
{
switch (*value)
{
@@ -318,92 +307,24 @@ namespace lms::metadata
}
} // namespace
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
{
return std::make_unique<AudioFileParser>(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<const std::filesystem::path> 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<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
{
std::unique_ptr<ITagReader> tagReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
tagReader = std::make_unique<taglib::TagLibTagReader>(p, _params.readStyle, _params.debug);
break;
case ParserBackend::AvFormat:
tagReader = std::make_unique<avformat::AvFormatTagReader>(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<IImageReader> imageReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
imageReader = std::make_unique<taglib::TagLibImageReader>(p);
break;
case ParserBackend::AvFormat:
imageReader = std::make_unique<avformat::AvFormatImageReader>(p);
break;
}
if (!imageReader)
throw AudioFileParsingException{ "Unhandled parser backend" };
parseImages(*imageReader, std::move(visitor));
}
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
{
auto track{ std::make_unique<Track>() };
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<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
@@ -469,8 +390,10 @@ namespace lms::metadata
track.originalYear = track.originalDate.getYear();
}
std::optional<Medium> AudioFileParser::getMedium(const ITagReader& tagReader) const
std::optional<Medium> TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const
{
using namespace audio;
std::optional<Medium> medium;
medium.emplace();
@@ -499,8 +422,10 @@ namespace lms::metadata
return medium;
}
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader) const
std::optional<Release> TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const
{
using namespace audio;
std::optional<Release> release;
auto releaseName{ getTagValueAs<std::string>(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
} // namespace lms::scanner
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <set>
#include <string>
#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<std::string, SortByLengthDesc>;
struct Parameters
{
std::vector<std::string> artistTagDelimiters;
WhiteList artistsToNotSplit;
std::vector<std::string> defaultTagDelimiters;
std::vector<std::string> 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<Medium> getMedium(const audio::ITagReader& tagReader) const;
std::optional<Release> getRelease(const audio::ITagReader& tagReader) const;
const Parameters _params;
};
} // namespace lms::scanner
@@ -24,9 +24,7 @@
#include <sstream>
#include <string_view>
#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
} // namespace lms::scanner::utils
@@ -25,13 +25,12 @@
#include <Wt/WDate.h>
#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<int> 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
@@ -22,17 +22,19 @@
#include <fstream>
#include <optional>
#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<metadata::Lyrics> _parsedLyrics;
std::optional<Lyrics> _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<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedLyrics = metadata::parseLyrics(ifs);
}
catch (const metadata::Exception& e)
{
addError<LyricsFileScanError>(getFilePath());
}
_parsedLyrics = parseLyrics(ifs);
}
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
@@ -140,7 +135,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedLyricsFileExtensions();
return getSupportedLyricsFileExtensions();
}
bool LyricsFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/Lyrics.hpp"
#include "LyricsParser.hpp"
#include <cassert>
#include <regex>
#include "core/String.hpp"
namespace lms::metadata
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
{
@@ -227,4 +227,4 @@ namespace lms::metadata
return lyrics;
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <iosfwd>
#include <span>
#include "types/Lyrics.hpp"
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
Lyrics parseLyrics(std::istream& is);
} // namespace lms::scanner
@@ -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<metadata::PlayList> _parsedPlayList;
std::optional<PlayList> _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<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedPlayList = metadata::parsePlayList(ifs);
}
catch (const metadata::Exception& e)
{
addError<PlayListFileScanError>(getFilePath());
}
_parsedPlayList = parsePlayList(ifs);
}
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
@@ -139,7 +132,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedPlayListFileExtensions();
return getSupportedPlayListFileExtensions();
}
bool PlayListFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/PlayList.hpp"
#include "PlayListParser.hpp"
#include <array>
#include <string_view>
#include "core/String.hpp"
namespace lms::metadata
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
{
@@ -102,4 +102,4 @@ namespace lms::metadata
return playlist;
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <iosfwd>
#include <span>
#include "types/PlayList.hpp"
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
PlayList parsePlayList(std::istream& is);
} // namespace lms::scanner
@@ -23,6 +23,7 @@
#include <ostream>
#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<std::string>(link->getArtistSortName()) };
Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(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<int>(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<std::string>(artistInfo->getSortName()) };
Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(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<db::TrackArtistLinkType> 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<ArtistToUpdate> 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 };
@@ -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);

Some files were not shown because too many files have changed in this diff Show More