Merge branch 'develop' for release v3.72.0
This commit is contained in:
+2
-2
@@ -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
@@ -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} && \
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+14
-19
@@ -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
|
||||
+14
-38
@@ -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
|
||||
+11
-15
@@ -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
|
||||
+125
-131
@@ -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
|
||||
+12
-13
@@ -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
|
||||
+11
-5
@@ -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
|
||||
+59
-109
@@ -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
|
||||
+16
-17
@@ -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
|
||||
+9
-12
@@ -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
|
||||
+41
-16
@@ -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
|
||||
+6
-29
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -50,7 +50,6 @@ target_link_libraries(lmscore PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmscore PUBLIC
|
||||
Boost::system
|
||||
std::filesystem
|
||||
Wt::Wt
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -40,8 +40,8 @@ namespace lms::core::logging
|
||||
enum class Module
|
||||
{
|
||||
API_SUBSONIC,
|
||||
AUDIO,
|
||||
AUTH,
|
||||
AV,
|
||||
CHILDPROCESS,
|
||||
COVER,
|
||||
DB,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,6 @@ target_include_directories(lmsauth PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PUBLIC
|
||||
Boost::system
|
||||
Wt::Wt
|
||||
lmscore
|
||||
lmsdatabase
|
||||
|
||||
@@ -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
|
||||
)
|
||||
+3
-3
@@ -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,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
|
||||
+26
-9
@@ -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
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -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
|
||||
+13
-9
@@ -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
|
||||
+190
-187
@@ -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)
|
||||
{
|
||||
+22
-17
@@ -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
|
||||
+25
-14
@@ -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
|
||||
+6
-2
@@ -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
|
||||
+40
-122
@@ -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, ¶ms.artistsToNotSplit) };
|
||||
if (artistNames.empty())
|
||||
@@ -224,7 +213,7 @@ namespace lms::metadata
|
||||
return artists;
|
||||
}
|
||||
|
||||
PerformerContainer getPerformerArtists(const ITagReader& tagReader)
|
||||
PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
|
||||
@@ -298,9 +287,9 @@ namespace lms::metadata
|
||||
return artistDisplayName;
|
||||
}
|
||||
|
||||
std::optional<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
|
||||
+2
-19
@@ -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
|
||||
+3
-4
@@ -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
|
||||
+15
-20
@@ -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
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -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
|
||||
+14
-21
@@ -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
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -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
Reference in New Issue
Block a user