diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3ebbd18d..3952fe39 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12)
-project(lms VERSION 3.70.0)
+project(lms VERSION 3.72.0)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
@@ -22,7 +22,7 @@ find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
find_package(Filesystem REQUIRED)
-find_package(Boost REQUIRED COMPONENTS system program_options iostreams)
+find_package(Boost REQUIRED COMPONENTS program_options iostreams)
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
find_package(Pugixml CONFIG REQUIRED)
diff --git a/Dockerfile-release b/Dockerfile-release
index 7bcf39f7..1bf40d82 100644
--- a/Dockerfile-release
+++ b/Dockerfile-release
@@ -39,7 +39,7 @@ ARG BUILD_PACKAGES=" \
RUN apk add --no-cache --update ${BUILD_PACKAGES}
# ffmpeg
-ARG FFMPEG_VERSION=6.1.2
+ARG FFMPEG_VERSION=6.1.3
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
@@ -61,7 +61,6 @@ RUN \
--enable-shared \
--disable-static \
--enable-gpl \
- --enable-small \
--enable-version3 \
--enable-nonfree \
--enable-libmp3lame \
@@ -80,7 +79,7 @@ RUN \
make -j$(nproc) install
# WT
-ARG WT_VERSION=4.11.4
+ARG WT_VERSION=4.12.1
ARG WT_DEBUG=OFF
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
diff --git a/README.md b/README.md
index b069afa2..d4344ff7 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ __Note__: If no name is provided in the `artist.nfo` file, the name of the conta
### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
-__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_.
+__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_.
### Multiple artists
_LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
diff --git a/SUBSONIC.md b/SUBSONIC.md
index c6a4fe76..025367b7 100644
--- a/SUBSONIC.md
+++ b/SUBSONIC.md
@@ -1,5 +1,5 @@
# Subsonic API
-The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_ and _Ultrasonic_.
+The API version implemented is 1.16.0 and has been tested on _Android_ using _DSub_, _Subsonic Player_, _Symfonium_, _Tempo_, [_Tempus_](https://github.com/eddyizm/tempus) and _Ultrasonic_.
Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists.
Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder.
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js
index bc594855..ca3f3bdb 100644
--- a/docroot/js/mediaplayer.js
+++ b/docroot/js/mediaplayer.js
@@ -184,7 +184,6 @@ class LMSMediaPlayer {
let source = this.#audioCtx.createMediaElementSource(this.#elems.audio);
source.connect(this.#gainNode);
this.#gainNode.connect(this.#audioCtx.destination);
- this.#audioCtx.resume(); // not sure of this
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("play", () => {
@@ -268,6 +267,10 @@ class LMSMediaPlayer {
#playPause() {
this.#initAudioCtx();
+ if (this.#audioCtx.state === "suspended") {
+ this.#audioCtx.resume().catch(err => console.warn(err));
+ }
+
if (this.#elems.audio.paused && this.#elems.audio.children.length > 0) {
this.#playTrack();
}
@@ -355,6 +358,8 @@ class LMSMediaPlayer {
if (!mode)
return;
+ let wasPlaying = !this.#elems.audio.paused;
+
switch (mode) {
case LMSMediaPlayer.#Mode.Transcoding:
this.#offset = seekTime;
@@ -362,15 +367,16 @@ class LMSMediaPlayer {
this.#addAudioSource(this.#audioTranscodingSrc + "&offset=" + this.#offset);
this.#elems.audio.load();
this.#elems.audio.currentTime = 0;
- this.#playTrack();
break;
case LMSMediaPlayer.#Mode.File:
this.#elems.audio.currentTime = seekTime;
- this.#playTrack();
break;
}
+ if (wasPlaying)
+ this.#playTrack();
+
this.#updateMediaSessionState();
}
diff --git a/src/libs/CMakeLists.txt b/src/libs/CMakeLists.txt
index 72a280ca..9a52d2f5 100644
--- a/src/libs/CMakeLists.txt
+++ b/src/libs/CMakeLists.txt
@@ -1,8 +1,7 @@
-add_subdirectory(av)
+add_subdirectory(audio)
add_subdirectory(core)
add_subdirectory(database)
add_subdirectory(image)
-add_subdirectory(metadata)
add_subdirectory(services)
add_subdirectory(som)
add_subdirectory(subsonic)
diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt
new file mode 100644
index 00000000..48f071bb
--- /dev/null
+++ b/src/libs/audio/CMakeLists.txt
@@ -0,0 +1,40 @@
+pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
+pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
+
+add_library(lmsaudio STATIC
+ impl/ffmpeg/AudioFile.cpp
+ impl/ffmpeg/AudioFileInfo.cpp
+ impl/ffmpeg/ImageReader.cpp
+ impl/ffmpeg/TagReader.cpp
+ impl/ffmpeg/Transcoder.cpp
+ impl/ffmpeg/Utils.cpp
+ impl/taglib/AudioFileInfo.cpp
+ impl/taglib/ImageReader.cpp
+ impl/taglib/TagReader.cpp
+ impl/taglib/Utils.cpp
+ impl/AudioTypes.cpp
+ impl/ImageReader.cpp
+ impl/ParseAudioFileInfo.cpp
+ impl/TagReader.cpp
+ )
+
+target_include_directories(lmsaudio INTERFACE
+ include
+ )
+
+target_include_directories(lmsaudio PRIVATE
+ include
+ ${AVCODEC_INCLUDE_DIR}
+ ${AVFORMAT_INCLUDE_DIR}
+ ${AVUTIL_INCLUDE_DIR}
+ )
+
+target_link_libraries(lmsaudio PUBLIC
+ lmscore
+ std::filesystem
+ )
+
+target_link_libraries(lmsaudio PRIVATE
+ PkgConfig::LIBAV
+ PkgConfig::Taglib
+ )
diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/audio/impl/AudioTypes.cpp
new file mode 100644
index 00000000..d0a5930b
--- /dev/null
+++ b/src/libs/audio/impl/AudioTypes.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "audio/AudioTypes.hpp"
+
+namespace lms::audio
+{
+ core::LiteralString containerTypeToString(ContainerType type)
+ {
+ switch (type)
+ {
+ case ContainerType::AIFF:
+ return "AIFF";
+ case ContainerType::APE:
+ return "APE";
+ case ContainerType::ASF:
+ return "ASF";
+ case ContainerType::DSF:
+ return "DSF";
+ case ContainerType::FLAC:
+ return "FLAC";
+ case ContainerType::MP4:
+ return "MP4";
+ case ContainerType::MPC:
+ return "MPC";
+ case ContainerType::MPEG:
+ return "MPEG";
+ case ContainerType::Ogg:
+ return "Ogg";
+ case ContainerType::Shorten:
+ return "Shorten";
+ case ContainerType::TrueAudio:
+ return "TrueAudio";
+ case ContainerType::WAV:
+ return "WAV";
+ case ContainerType::WavPack:
+ return "WavPack";
+ }
+
+ return "";
+ }
+
+ core::LiteralString codecTypeToString(CodecType type)
+ {
+ switch (type)
+ {
+ case CodecType::AAC:
+ return "AAC";
+ case CodecType::ALAC:
+ return "ALAC";
+ case CodecType::APE:
+ return "APE";
+ case CodecType::DSD:
+ return "DSD";
+ case CodecType::FLAC:
+ return "FLAC";
+ case CodecType::MP3:
+ return "MP3";
+ case CodecType::MP4ALS:
+ return "MP4ALS";
+ case CodecType::MPC7:
+ return "MPC7";
+ case CodecType::MPC8:
+ return "MPC8";
+ case CodecType::Opus:
+ return "Opus";
+ case CodecType::PCM:
+ return "PCM";
+ case CodecType::Shorten:
+ return "Shorten";
+ case CodecType::TrueAudio:
+ return "TrueAudio";
+ case CodecType::Vorbis:
+ return "Vorbis";
+ case CodecType::WavPack:
+ return "WavPack";
+ case CodecType::WMA1:
+ return "WMA1";
+ case CodecType::WMA2:
+ return "WMA2";
+ case CodecType::WMA9Pro:
+ return "WMA9Pro";
+ case CodecType::WMA9Lossless:
+ return "WMA9Lossless";
+ }
+
+ return "";
+ }
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/ImageReader.cpp b/src/libs/audio/impl/ImageReader.cpp
new file mode 100644
index 00000000..7b314875
--- /dev/null
+++ b/src/libs/audio/impl/ImageReader.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "audio/IImageReader.hpp"
+
+namespace lms::audio
+{
+ core::LiteralString imageTypeToString(Image::Type type)
+ {
+ switch (type)
+ {
+ case Image::Type::Other:
+ return "Other";
+ case Image::Type::FileIcon:
+ return "FileIcon";
+ case Image::Type::OtherFileIcon:
+ return "OtherFileIcon";
+ case Image::Type::FrontCover:
+ return "FrontCover";
+ case Image::Type::BackCover:
+ return "BackCover";
+ case Image::Type::LeafletPage:
+ return "LeafletPage";
+ case Image::Type::Media:
+ return "Media";
+ case Image::Type::LeadArtist:
+ return "LeadArtist";
+ case Image::Type::Artist:
+ return "Artist";
+ case Image::Type::Conductor:
+ return "Conductor";
+ case Image::Type::Band:
+ return "Band";
+ case Image::Type::Composer:
+ return "Composer";
+ case Image::Type::Lyricist:
+ return "Lyricist";
+ case Image::Type::RecordingLocation:
+ return "RecordingLocation";
+ case Image::Type::DuringRecording:
+ return "DuringRecording";
+ case Image::Type::DuringPerformance:
+ return "DuringPerformance";
+ case Image::Type::MovieScreenCapture:
+ return "MovieScreenCapture";
+ case Image::Type::ColouredFish:
+ return "ColouredFish";
+ case Image::Type::Illustration:
+ return "Illustration";
+ case Image::Type::BandLogo:
+ return "BandLogo";
+ case Image::Type::PublisherLogo:
+ return "PublisherLogo";
+ case Image::Type::Unknown:
+ break;
+ }
+
+ return "Unknown";
+ }
+
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/ParseAudioFileInfo.cpp b/src/libs/audio/impl/ParseAudioFileInfo.cpp
new file mode 100644
index 00000000..321d46a7
--- /dev/null
+++ b/src/libs/audio/impl/ParseAudioFileInfo.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include
+
+#include "audio/IAudioFileInfo.hpp"
+
+#include "ffmpeg/AudioFileInfo.hpp"
+#include "ffmpeg/Utils.hpp"
+#include "taglib/AudioFileInfo.hpp"
+#include "taglib/Utils.hpp"
+
+namespace lms::audio
+{
+ std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
+ {
+ switch (parserOptions.parser)
+ {
+ case ParserOptions::Parser::TagLib:
+ return std::make_unique(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
+ case ParserOptions::Parser::FFmpeg:
+ return std::make_unique(p, parserOptions.enableExtraDebugLogs);
+ }
+
+ return {};
+ }
+
+ std::span getSupportedExtensions(ParserOptions::Parser parser)
+ {
+ switch (parser)
+ {
+ case ParserOptions::Parser::TagLib:
+ return taglib::utils::getSupportedExtensions();
+ case ParserOptions::Parser::FFmpeg:
+ return ffmpeg::utils::getSupportedExtensions();
+ }
+
+ return {};
+ }
+
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/TagReader.cpp b/src/libs/audio/impl/TagReader.cpp
new file mode 100644
index 00000000..39ef83bd
--- /dev/null
+++ b/src/libs/audio/impl/TagReader.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "audio/ITagReader.hpp"
+
+namespace lms::audio
+{
+ core::LiteralString tagTypeToString(TagType type)
+ {
+ switch (type)
+ {
+ case TagType::AcoustID:
+ return "AcoustID";
+ case TagType::AcoustIDFingerprint:
+ return "AcoustIDFingerprint";
+ case TagType::Advisory:
+ return "Advisory";
+ case TagType::Album:
+ return "Album";
+ case TagType::AlbumArtist:
+ return "AlbumArtist";
+ case TagType::AlbumArtists:
+ return "AlbumArtists";
+ case TagType::AlbumArtistSortOrder:
+ return "AlbumArtistSortOrder";
+ case TagType::AlbumArtistsSortOrder:
+ return "AlbumArtistsSortOrder";
+ case TagType::AlbumComment:
+ return "AlbumComment";
+ case TagType::AlbumSortOrder:
+ return "AlbumSortOrder";
+ case TagType::Arranger:
+ return "Arranger";
+ case TagType::Artist:
+ return "Artist";
+ case TagType::ArtistSortOrder:
+ return "ArtistSortOrder";
+ case TagType::Artists:
+ return "Artists";
+ case TagType::ArtistsSortOrder:
+ return "ArtistsSortOrder";
+ case TagType::ASIN:
+ return "ASIN";
+ case TagType::Barcode:
+ return "Barcode";
+ case TagType::BPM:
+ return "BPM";
+ case TagType::CatalogNumber:
+ return "CatalogNumber";
+ case TagType::Comment:
+ return "Comment";
+ case TagType::Compilation:
+ return "Compilation";
+ case TagType::Composer:
+ return "Composer";
+ case TagType::ComposerSortOrder:
+ return "ComposerSortOrder";
+ case TagType::Composers:
+ return "Composers";
+ case TagType::ComposersSortOrder:
+ return "ComposersSortOrder";
+ case TagType::Conductor:
+ return "Conductor";
+ case TagType::ConductorSortOrder:
+ return "ConductorSortOrder";
+ case TagType::Conductors:
+ return "Conductors";
+ case TagType::ConductorsSortOrder:
+ return "ConductorsSortOrder";
+ case TagType::Copyright:
+ return "Copyright";
+ case TagType::CopyrightURL:
+ return "CopyrightURL";
+ case TagType::Date:
+ return "Date";
+ case TagType::Director:
+ return "Director";
+ case TagType::DiscNumber:
+ return "DiscNumber";
+ case TagType::DiscSubtitle:
+ return "DiscSubtitle";
+ case TagType::EncodedBy:
+ return "EncodedBy";
+ case TagType::EncoderSettings:
+ return "EncoderSettings";
+ case TagType::EncodingTime:
+ return "EncodingTime";
+ case TagType::Engineer:
+ return "Engineer";
+ case TagType::GaplessPlayback:
+ return "GaplessPlayback";
+ case TagType::Genre:
+ return "Genre";
+ case TagType::Grouping:
+ return "Grouping";
+ case TagType::InitialKey:
+ return "InitialKey";
+ case TagType::ISRC:
+ return "ISRC";
+ case TagType::Language:
+ return "Language";
+ case TagType::Lyricist:
+ return "Lyricist";
+ case TagType::LyricistSortOrder:
+ return "LyricistSortOrder";
+ case TagType::Lyricists:
+ return "Lyricists";
+ case TagType::LyricistsSortOrder:
+ return "LyricistsSortOrder";
+ case TagType::Media:
+ return "Media";
+ case TagType::MixDJ:
+ return "MixDJ";
+ case TagType::Mixer:
+ return "Mixer";
+ case TagType::MixerSortOrder:
+ return "MixerSortOrder";
+ case TagType::Mixers:
+ return "Mixers";
+ case TagType::MixersSortOrder:
+ return "MixersSortOrder";
+ case TagType::Movement:
+ return "Movement";
+ case TagType::MovementCount:
+ return "MovementCount";
+ case TagType::MovementNumber:
+ return "MovementNumber";
+ case TagType::Mood:
+ return "Mood";
+ case TagType::MusicBrainzArtistID:
+ return "MusicBrainzArtistID";
+ case TagType::MusicBrainzArrangerID:
+ return "MusicBrainzArrangerID";
+ case TagType::MusicBrainzComposerID:
+ return "MusicBrainzComposerID";
+ case TagType::MusicBrainzConductorID:
+ return "MusicBrainzConductorID";
+ case TagType::MusicBrainzDirectorID:
+ return "MusicBrainzDirectorID";
+ case TagType::MusicBrainzDiscID:
+ return "MusicBrainzDiscID";
+ case TagType::MusicBrainzLyricistID:
+ return "MusicBrainzLyricistID";
+ case TagType::MusicBrainzProducerID:
+ return "MusicBrainzProducerID";
+ case TagType::MusicBrainzOriginalArtistID:
+ return "MusicBrainzOriginalArtistID";
+ case TagType::MusicBrainzRecordingID:
+ return "MusicBrainzRecordingID";
+ case TagType::MusicBrainzRemixerID:
+ return "MusicBrainzRemixerID";
+ case TagType::MusicBrainzWorkID:
+ return "MusicBrainzWorkID";
+ case TagType::Remixer:
+ return "Remixer";
+ case TagType::Script:
+ return "Script";
+ case TagType::ShowName:
+ return "ShowName";
+ case TagType::ShowNameSortOrder:
+ return "ShowNameSortOrder";
+ case TagType::ShowWorkAndMovement:
+ return "ShowWorkAndMovement";
+ case TagType::Subtitle:
+ return "Subtitle";
+ case TagType::TrackNumber:
+ return "TrackNumber";
+ case TagType::TrackTitle:
+ return "TrackTitle";
+ case TagType::TrackTitleSortOrder:
+ return "TrackTitleSortOrder";
+ case TagType::WorkTitle:
+ return "WorkTitle";
+ case TagType::Writer:
+ return "Writer";
+ case TagType::License:
+ return "License";
+ case TagType::MusicBrainzOriginalReleaseID:
+ return "MusicBrainzOriginalReleaseID";
+ case TagType::MusicBrainzMixerID:
+ return "MusicBrainzMixerID";
+ case TagType::MusicBrainzReleaseArtistID:
+ return "MusicBrainzReleaseArtistID";
+ case TagType::MusicBrainzReleaseGroupID:
+ return "MusicBrainzReleaseGroupID";
+ case TagType::MusicBrainzReleaseID:
+ return "MusicBrainzReleaseID";
+ case TagType::MusicBrainzTrackID:
+ return "MusicBrainzTrackID";
+ case TagType::MusicIPFingerprint:
+ return "MusicIPFingerprint";
+ case TagType::MusicIPPUID:
+ return "MusicIPPUID";
+ case TagType::OriginalAlbum:
+ return "OriginalAlbum";
+ case TagType::OriginalArtist:
+ return "OriginalArtist";
+ case TagType::OriginalFilename:
+ return "OriginalFilename";
+ case TagType::OriginalReleaseDate:
+ return "OriginalReleaseDate";
+ case TagType::OriginalReleaseYear:
+ return "OriginalReleaseYear";
+ case TagType::Podcast:
+ return "Podcast";
+ case TagType::PodcastURL:
+ return "PodcastURL";
+ case TagType::Producer:
+ return "Producer";
+ case TagType::ProducerSortOrder:
+ return "ProducerSortOrder";
+ case TagType::Producers:
+ return "Producers";
+ case TagType::ProducersSortOrder:
+ return "ProducersSortOrder";
+ case TagType::Rating:
+ return "Rating";
+ case TagType::RecordLabel:
+ return "RecordLabel";
+ case TagType::ReleaseCountry:
+ return "ReleaseCountry";
+ case TagType::ReleaseDate:
+ return "ReleaseDate";
+ case TagType::ReleaseStatus:
+ return "ReleaseStatus";
+ case TagType::ReleaseType:
+ return "ReleaseType";
+ case TagType::RemixerSortOrder:
+ return "RemixerSortOrder";
+ case TagType::Remixers:
+ return "Remixers";
+ case TagType::RemixersSortOrder:
+ return "RemixersSortOrder";
+ case TagType::ReplayGainAlbumGain:
+ return "ReplayGainAlbumGain";
+ case TagType::ReplayGainAlbumPeak:
+ return "ReplayGainAlbumPeak";
+ case TagType::ReplayGainAlbumRange:
+ return "ReplayGainAlbumRange";
+ case TagType::ReplayGainReferenceLoudness:
+ return "ReplayGainReferenceLoudness";
+ case TagType::ReplayGainTrackGain:
+ return "ReplayGainTrackGain";
+ case TagType::ReplayGainTrackPeak:
+ return "ReplayGainTrackPeak";
+ case TagType::ReplayGainTrackRange:
+ return "ReplayGainTrackRange";
+ case TagType::TotalDiscs:
+ return "TotalDiscs";
+ case TagType::TotalTracks:
+ return "TotalTracks";
+ case TagType::Website:
+ return "Website";
+
+ case TagType::Count:
+ break;
+ }
+
+ return "";
+ }
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp
similarity index 66%
rename from src/libs/av/impl/AudioFile.cpp
rename to src/libs/audio/impl/ffmpeg/AudioFile.cpp
index 8c3ee0d2..ad1861a7 100644
--- a/src/libs/av/impl/AudioFile.cpp
+++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp
@@ -19,6 +19,9 @@
#include "AudioFile.hpp"
+#include
+#include
+
extern "C"
{
#define __STDC_CONSTANT_MACROS
@@ -27,15 +30,13 @@ extern "C"
#include
}
-#include
-#include
-
#include "core/ILogger.hpp"
#include "core/String.hpp"
-#include "av/Exception.hpp"
+#include "audio/AudioTypes.hpp"
+#include "audio/IAudioFileInfo.hpp"
-namespace lms::av
+namespace lms::audio::ffmpeg
{
namespace
{
@@ -49,11 +50,11 @@ namespace lms::av
return "Unknown error";
}
- class AudioFileException : public Exception
+ class AudioFileException : public AudioFileParsingException
{
public:
AudioFileException(int avError)
- : Exception{ "AudioFileException: " + averror_to_string(avError) }
+ : AudioFileParsingException{ averror_to_string(avError) }
{
}
};
@@ -70,76 +71,100 @@ namespace lms::av
}
}
- DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
+ std::optional avdemuxerToContainerType(std::string_view name)
+ {
+ if (name == "aiff")
+ return ContainerType::AIFF;
+ if (name == "ape")
+ return ContainerType::APE;
+ if (name.starts_with("asf"))
+ return ContainerType::ASF;
+ if (name == "dsf")
+ return ContainerType::DSF;
+ if (name == "flac")
+ return ContainerType::FLAC;
+ if (name.find("mp4") != std::string_view::npos)
+ return ContainerType::MP4;
+ if (name.starts_with("mpc"))
+ return ContainerType::MPC;
+ if (name == "mp3")
+ return ContainerType::MPEG;
+ if (name == "ogg")
+ return ContainerType::Ogg;
+ if (name == "shn")
+ return ContainerType::Shorten;
+ if (name == "tta")
+ return ContainerType::TrueAudio;
+ if (name == "wav")
+ return ContainerType::WAV;
+ if (name == "wv")
+ return ContainerType::WavPack;
+
+ return std::nullopt;
+ }
+
+ std::optional avcodecToCodecType(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_MP3:
- return DecodingCodec::MP3;
+ return CodecType::MP3;
case AV_CODEC_ID_AAC:
- return DecodingCodec::AAC;
- case AV_CODEC_ID_AC3:
- return DecodingCodec::AC3;
+ return CodecType::AAC;
case AV_CODEC_ID_VORBIS:
- return DecodingCodec::VORBIS;
+ return CodecType::Vorbis;
case AV_CODEC_ID_WMAV1:
- return DecodingCodec::WMAV1;
+ return CodecType::WMA1;
case AV_CODEC_ID_WMAV2:
- return DecodingCodec::WMAV2;
+ return CodecType::WMA2;
+ case AV_CODEC_ID_WMAPRO:
+ return CodecType::WMA9Pro;
+ case AV_CODEC_ID_WMALOSSLESS:
+ return CodecType::WMA9Lossless;
case AV_CODEC_ID_FLAC:
- return DecodingCodec::FLAC;
+ return CodecType::FLAC;
case AV_CODEC_ID_ALAC:
- return DecodingCodec::ALAC;
+ return CodecType::ALAC;
case AV_CODEC_ID_WAVPACK:
- return DecodingCodec::WAVPACK;
+ return CodecType::WavPack;
case AV_CODEC_ID_MUSEPACK7:
- return DecodingCodec::MUSEPACK7;
+ return CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8:
- return DecodingCodec::MUSEPACK8;
+ return CodecType::MPC8;
case AV_CODEC_ID_APE:
- return DecodingCodec::APE;
- case AV_CODEC_ID_EAC3:
- return DecodingCodec::EAC3;
+ return CodecType::APE;
case AV_CODEC_ID_MP4ALS:
- return DecodingCodec::MP4ALS;
+ return CodecType::MP4ALS;
case AV_CODEC_ID_OPUS:
- return DecodingCodec::OPUS;
+ return CodecType::Opus;
case AV_CODEC_ID_SHORTEN:
- return DecodingCodec::SHORTEN;
+ return CodecType::Shorten;
case AV_CODEC_ID_DSD_LSBF:
- return DecodingCodec::DSD_LSBF;
case AV_CODEC_ID_DSD_LSBF_PLANAR:
- return DecodingCodec::DSD_LSBF_PLANAR;
case AV_CODEC_ID_DSD_MSBF:
- return DecodingCodec::DSD_MSBF;
case AV_CODEC_ID_DSD_MSBF_PLANAR:
- return DecodingCodec::DSD_MSBF_PLANAR;
+ return CodecType::DSD;
default:
- return DecodingCodec::UNKNOWN;
+ return std::nullopt;
}
}
} // namespace
- std::unique_ptr parseAudioFile(const std::filesystem::path& p)
- {
- return std::make_unique(p);
- }
-
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0)
{
- LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
+ LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
throw AudioFileException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
- LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
+ LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
avformat_close_input(&_context);
throw AudioFileException{ error };
}
@@ -158,9 +183,12 @@ namespace lms::av
ContainerInfo AudioFile::getContainerInfo() const
{
ContainerInfo info;
+
+ info.container = avdemuxerToContainerType(_context->iformat->name);
+ info.containerName = _context->iformat->name;
+
info.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
- info.name = _context->iformat->name;
return info;
}
@@ -258,7 +286,7 @@ namespace lms::av
if (avstream->codecpar == nullptr)
{
- LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
+ LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
continue;
}
@@ -275,7 +303,7 @@ namespace lms::av
else
{
picture.mimeType = "application/octet-stream";
- LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
+ LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
@@ -297,7 +325,7 @@ namespace lms::av
if (!avstream->codecpar)
{
- LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
+ LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
@@ -305,19 +333,29 @@ namespace lms::av
return res;
res.emplace();
+
res->index = streamIndex;
- res->bitrate = static_cast(avstream->codecpar->bit_rate);
- res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample);
-#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
- res->channelCount = static_cast(avstream->codecpar->channels);
-#else
- res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels);
-#endif
- res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
+ res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
+
+ if (avstream->codecpar->bit_rate)
+ res->bitrate = static_cast(avstream->codecpar->bit_rate);
+ if (avstream->codecpar->bits_per_coded_sample)
+ res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample);
+ else if (avstream->codecpar->bits_per_raw_sample)
+ res->bitsPerSample = static_cast(avstream->codecpar->bits_per_raw_sample);
+
+#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
+ if (avstream->codecpar->channels)
+ res->channelCount = static_cast(avstream->codecpar->channels);
+#else
+ if (avstream->codecpar->ch_layout.nb_channels)
+ res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels);
+#endif
assert(!res->codecName.empty()); // doc says it is never NULL
- res->sampleRate = static_cast(avstream->codecpar->sample_rate);
+ if (avstream->codecpar->sample_rate)
+ res->sampleRate = static_cast(avstream->codecpar->sample_rate);
return res;
}
-} // namespace lms::av
+} // namespace lms::audio::ffmpeg
\ No newline at end of file
diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp
new file mode 100644
index 00000000..2af7fb1b
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "audio/AudioTypes.hpp"
+
+extern "C"
+{
+ struct AVFormatContext;
+}
+
+namespace lms::audio::ffmpeg
+{
+ struct Picture
+ {
+ std::string mimeType;
+ std::span data; // valid as long as IAudioFile exists
+ };
+
+ struct ContainerInfo
+ {
+ std::optional container;
+ std::string containerName;
+
+ std::size_t bitrate{};
+ std::chrono::milliseconds duration{};
+ };
+
+ struct StreamInfo
+ {
+ size_t index{};
+ std::optional codec;
+ std::string codecName;
+
+ std::optional bitrate;
+ std::optional bitsPerSample;
+ std::optional channelCount;
+ std::optional sampleRate;
+ };
+
+ class AudioFile
+ {
+ public:
+ AudioFile(const std::filesystem::path& p);
+ ~AudioFile();
+ AudioFile(const AudioFile&) = delete;
+ AudioFile& operator=(const AudioFile&) = delete;
+
+ using MetadataMap = std::unordered_map;
+
+ const std::filesystem::path& getPath() const;
+ ContainerInfo getContainerInfo() const;
+ MetadataMap getMetaData() const;
+ std::vector getStreamInfo() const;
+ std::optional getBestStreamInfo() const;
+ std::optional getBestStreamIndex() const;
+ bool hasAttachedPictures() const;
+ void visitAttachedPictures(std::function func) const;
+
+ private:
+ std::optional getStreamInfo(std::size_t streamIndex) const;
+
+ const std::filesystem::path _p;
+ AVFormatContext* _context{};
+ };
+} // namespace lms::audio::ffmpeg
\ No newline at end of file
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp
new file mode 100644
index 00000000..fe0793f9
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "AudioFileInfo.hpp"
+
+#include "audio/AudioTypes.hpp"
+#include "audio/IAudioFileInfo.hpp"
+
+#include "AudioFile.hpp"
+#include "ImageReader.hpp"
+#include "TagReader.hpp"
+
+namespace lms::audio::ffmpeg
+{
+ namespace
+ {
+ AudioProperties computeAudioProperties(const AudioFile& audioFile)
+ {
+ AudioProperties audioProperties;
+
+ const auto containerInfo{ audioFile.getContainerInfo() };
+ const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
+ if (!bestStreamInfo)
+ throw AudioFileParsingException{ "Cannot find best audio stream" };
+
+ if (!containerInfo.container)
+ throw AudioFileParsingException{ "Unhandled container type '" + containerInfo.containerName + "'" };
+
+ if (!bestStreamInfo->codec)
+ throw AudioFileParsingException{ "Unhandled codec type '" + bestStreamInfo->codecName + "'" };
+
+ if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0)
+ throw AudioFileParsingException{ "Cannot determine bitrate" };
+
+ if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0)
+ throw AudioFileParsingException{ "Cannot determine channel count" };
+
+ if (!bestStreamInfo->sampleRate || *bestStreamInfo->sampleRate == 0)
+ throw AudioFileParsingException{ "Cannot determine sample rate" };
+
+ audioProperties.container = *containerInfo.container;
+ audioProperties.duration = containerInfo.duration;
+ audioProperties.codec = *bestStreamInfo->codec;
+ audioProperties.bitrate = *bestStreamInfo->bitrate;
+ audioProperties.channelCount = *bestStreamInfo->channelCount;
+ audioProperties.sampleRate = *bestStreamInfo->sampleRate;
+ audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
+
+ return audioProperties;
+ }
+ } // namespace
+
+ AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
+ : _audioFile{ std::make_unique(filePath) }
+ , _audioProperties{ std::make_unique(computeAudioProperties(*_audioFile)) }
+ , _tagReader{ std::make_unique(*_audioFile, enableExtraDebugLogs) }
+ , _imageReader{ std::make_unique(*_audioFile) }
+ {
+ }
+
+ AudioFileInfo::~AudioFileInfo() = default;
+
+ const AudioProperties& AudioFileInfo::getAudioProperties() const
+ {
+ return *_audioProperties;
+ }
+
+ const IImageReader& AudioFileInfo::getImageReader() const
+ {
+ return *_imageReader;
+ }
+
+ const ITagReader& AudioFileInfo::getTagReader() const
+ {
+ return *_tagReader;
+ }
+
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp
new file mode 100644
index 00000000..e8c806a7
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "audio/AudioTypes.hpp"
+#include "audio/IAudioFileInfo.hpp"
+
+namespace lms::audio::ffmpeg
+{
+ class AudioFile;
+ class TagReader;
+ class ImageReader;
+
+ class AudioFileInfo final : public IAudioFileInfo
+ {
+ public:
+ AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
+ ~AudioFileInfo();
+ AudioFileInfo(const AudioFileInfo&) = delete;
+ AudioFileInfo& operator=(const AudioFileInfo&) = delete;
+
+ private:
+ const AudioProperties& getAudioProperties() const override;
+ const IImageReader& getImageReader() const override;
+ const ITagReader& getTagReader() const override;
+
+ std::unique_ptr _audioFile;
+ std::unique_ptr _audioProperties;
+ std::unique_ptr _tagReader;
+ std::unique_ptr _imageReader;
+ };
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp
similarity index 61%
rename from src/libs/metadata/impl/avformat/AvFormatImageReader.cpp
rename to src/libs/audio/impl/ffmpeg/ImageReader.cpp
index 8a420534..ad935d1e 100644
--- a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp
+++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp
@@ -17,35 +17,30 @@
* along with LMS. If not, see .
*/
-#include "AvFormatImageReader.hpp"
+#include "ImageReader.hpp"
-#include "av/Exception.hpp"
-#include "av/IAudioFile.hpp"
-#include "metadata/Exception.hpp"
+#include
-namespace lms::metadata::avformat
+#include "core/String.hpp"
+
+#include "AudioFile.hpp"
+
+namespace lms::audio::ffmpeg
{
- AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
+ ImageReader::ImageReader(const AudioFile& audioFile)
+ : _audioFile{ audioFile }
{
- try
- {
- _audioFile = av::parseAudioFile(p);
- }
- catch (av::Exception& e)
- {
- throw AudioFileParsingException{ e.what() };
- }
}
- AvFormatImageReader::~AvFormatImageReader() = default;
+ ImageReader::~ImageReader() = default;
- void AvFormatImageReader::visitImages(ImageVisitor visitor) const
+ void ImageReader::visitImages(const ImageVisitor& visitor) const
{
- auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) {
+ auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) {
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
} };
- _audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) {
+ _audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
@@ -58,4 +53,4 @@ namespace lms::metadata::avformat
});
}
-} // namespace lms::metadata::avformat
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.hpp b/src/libs/audio/impl/ffmpeg/ImageReader.hpp
new file mode 100644
index 00000000..27a24b30
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/ImageReader.hpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "audio/IImageReader.hpp"
+
+namespace lms::audio::ffmpeg
+{
+ class AudioFile;
+
+ class ImageReader : public IImageReader
+ {
+ public:
+ ImageReader(const AudioFile& audioFile);
+ ~ImageReader() override;
+
+ ImageReader(const ImageReader&) = delete;
+ ImageReader& operator=(const ImageReader&) = delete;
+
+ private:
+ void visitImages(const ImageVisitor& visitor) const override;
+
+ const AudioFile& _audioFile;
+ };
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp
similarity index 86%
rename from src/libs/metadata/impl/avformat/AvFormatTagReader.cpp
rename to src/libs/audio/impl/ffmpeg/TagReader.cpp
index fb7981c1..fa5d9e4f 100644
--- a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp
+++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp
@@ -17,15 +17,12 @@
* along with LMS. If not, see .
*/
-#include "AvFormatTagReader.hpp"
+#include "TagReader.hpp"
-#include "av/Exception.hpp"
-#include "av/IAudioFile.hpp"
#include "core/ILogger.hpp"
#include "core/String.hpp"
-#include "metadata/Exception.hpp"
-namespace lms::metadata::avformat
+namespace lms::audio::ffmpeg
{
namespace
{
@@ -146,41 +143,20 @@ namespace lms::metadata::avformat
};
} // namespace
- AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug)
+ TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
+ : _audioFile{ audioFile }
+ , _metaDataMap{ audioFile.getMetaData() }
{
- try
+ if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
- const auto audioFile{ av::parseAudioFile(p) };
-
- _audioProperties.duration = audioFile->getContainerInfo().duration;
-
- const auto bestAudioStream{ audioFile->getBestStreamInfo() };
- if (bestAudioStream)
- {
- _audioProperties.bitrate = bestAudioStream->bitrate;
- _audioProperties.bitsPerSample = bestAudioStream->bitsPerSample;
- _audioProperties.channelCount = bestAudioStream->channelCount;
- _audioProperties.sampleRate = bestAudioStream->sampleRate;
- }
-
- _containerInfo = audioFile->getContainerInfo();
- _metaDataMap = audioFile->getMetaData();
-
- if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
- {
- for (const auto& [key, value] : _metaDataMap)
- LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
- }
- }
- catch (av::Exception& e)
- {
- throw AudioFileParsingException{ e.what() };
+ for (const auto& [key, value] : _metaDataMap)
+ LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
}
}
- AvFormatTagReader::~AvFormatTagReader() = default;
+ TagReader::~TagReader() = default;
- void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
+ void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ avFormatTagMapping.find(tag) };
if (itTagNames == std::cend(avFormatTagMapping))
@@ -200,7 +176,7 @@ namespace lms::metadata::avformat
}
}
- void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
+ void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
{
auto itValues{ _metaDataMap.find(std::string{ key }) };
if (itValues == std::cend(_metaDataMap))
@@ -209,14 +185,14 @@ namespace lms::metadata::avformat
visitor(itValues->second);
}
- void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const
+ void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
});
}
- void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const
+ void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
// MPEG files: need to visit LYRICS-language entries
for (const auto& [tag, value] : _metaDataMap)
@@ -234,4 +210,4 @@ namespace lms::metadata::avformat
visitor("", value);
});
}
-} // namespace lms::metadata::avformat
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp b/src/libs/audio/impl/ffmpeg/TagReader.hpp
similarity index 61%
rename from src/libs/metadata/impl/avformat/AvFormatTagReader.hpp
rename to src/libs/audio/impl/ffmpeg/TagReader.hpp
index 44b38c38..25e7bd7f 100644
--- a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp
+++ b/src/libs/audio/impl/ffmpeg/TagReader.hpp
@@ -19,31 +19,27 @@
#pragma once
-#include
+#include "audio/ITagReader.hpp"
-#include "av/IAudioFile.hpp"
+#include "AudioFile.hpp"
-#include "ITagReader.hpp"
-
-namespace lms::metadata::avformat
+namespace lms::audio::ffmpeg
{
- class AvFormatTagReader : public ITagReader
+ class TagReader : public ITagReader
{
public:
- AvFormatTagReader(const std::filesystem::path& path, bool debug);
- ~AvFormatTagReader() override;
- AvFormatTagReader(const AvFormatTagReader&) = delete;
- AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
+ TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
+ ~TagReader() override;
+ TagReader(const TagReader&) = delete;
+ TagReader& operator=(const TagReader&) = delete;
private:
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
- const AudioProperties& getAudioProperties() const override { return _audioProperties; }
- AudioProperties _audioProperties;
- av::IAudioFile::MetadataMap _metaDataMap;
- av::ContainerInfo _containerInfo;
+ const AudioFile& _audioFile;
+ AudioFile::MetadataMap _metaDataMap;
};
-} // namespace lms::metadata::avformat
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp
similarity index 58%
rename from src/libs/av/impl/Transcoder.cpp
rename to src/libs/audio/impl/ffmpeg/Transcoder.cpp
index 8dc4d59d..cd6ea9d5 100644
--- a/src/libs/av/impl/Transcoder.cpp
+++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp
@@ -27,17 +27,21 @@
#include "core/ILogger.hpp"
#include "core/Service.hpp"
-#include "av/Exception.hpp"
+#include "audio/Exception.hpp"
+#include "audio/TranscodeTypes.hpp"
-namespace lms::av
+namespace lms::audio
+{
+ std::unique_ptr createTranscoder(const TranscodeParameters& parameters)
+ {
+ return std::make_unique(parameters);
+ }
+} // namespace lms::audio
+
+namespace lms::audio::ffmpeg
{
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
- std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
- {
- return std::make_unique(inputParameters, outputParameters);
- }
-
static std::atomic globalId{};
static std::filesystem::path ffmpegPath;
@@ -48,10 +52,10 @@ namespace lms::av
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
}
- Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams)
+ Transcoder::Transcoder(const TranscodeParameters& parameters)
: _debugId{ globalId++ }
- , _inputParams{ inputParams }
- , _outputParams{ outputParams }
+ , _inputParams{ parameters.inputParameters }
+ , _outputParams{ parameters.outputParameters }
{
start();
}
@@ -65,18 +69,18 @@ namespace lms::av
try
{
- if (!std::filesystem::exists(_inputParams.file))
- throw Exception{ "File " + _inputParams.file.string() + " does not exist!" };
- if (!std::filesystem::is_regular_file(_inputParams.file))
- throw Exception{ "File " + _inputParams.file.string() + " is not regular!" };
+ if (!std::filesystem::exists(_inputParams.filePath))
+ throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
+ if (!std::filesystem::is_regular_file(_inputParams.filePath))
+ throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
}
catch (const std::filesystem::filesystem_error& e)
{
// TODO store/raise e.code()
- throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() };
+ throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
}
- LOG(INFO, "Transcoding file " << _inputParams.file);
+ LOG(INFO, "Transcoding file " << _inputParams.filePath);
std::vector args;
@@ -101,14 +105,7 @@ namespace lms::av
// Input file
args.emplace_back("-i");
- args.emplace_back(_inputParams.file.string());
-
- // Stream mapping, if set
- if (_inputParams.streamIndex)
- {
- args.emplace_back("-map");
- args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex));
- }
+ args.emplace_back(_inputParams.filePath.string());
if (_outputParams.stripMetadata)
{
@@ -121,47 +118,53 @@ namespace lms::av
args.emplace_back("-vn");
// Output bitrates
- args.emplace_back("-b:a");
- args.emplace_back(std::to_string(_outputParams.bitrate));
+ if (_outputParams.bitrate)
+ {
+ args.emplace_back("-b:a");
+ args.emplace_back(std::to_string(*_outputParams.bitrate));
+ }
// Codecs and formats
- switch (_outputParams.format)
+ if (_outputParams.format)
{
- case OutputFormat::MP3:
- args.emplace_back("-f");
- args.emplace_back("mp3");
- break;
+ switch (*_outputParams.format)
+ {
+ case OutputFormat::MP3:
+ args.emplace_back("-f");
+ args.emplace_back("mp3");
+ break;
- case OutputFormat::OGG_OPUS:
- args.emplace_back("-acodec");
- args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("ogg");
- break;
+ case OutputFormat::OGG_OPUS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libopus");
+ args.emplace_back("-f");
+ args.emplace_back("ogg");
+ break;
- case OutputFormat::MATROSKA_OPUS:
- args.emplace_back("-acodec");
- args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("matroska");
- break;
+ case OutputFormat::MATROSKA_OPUS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libopus");
+ args.emplace_back("-f");
+ args.emplace_back("matroska");
+ break;
- case OutputFormat::OGG_VORBIS:
- args.emplace_back("-acodec");
- args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("ogg");
- break;
+ case OutputFormat::OGG_VORBIS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libvorbis");
+ args.emplace_back("-f");
+ args.emplace_back("ogg");
+ break;
- case OutputFormat::WEBM_VORBIS:
- args.emplace_back("-acodec");
- args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("webm");
- break;
+ case OutputFormat::WEBM_VORBIS:
+ args.emplace_back("-acodec");
+ args.emplace_back("libvorbis");
+ args.emplace_back("-f");
+ args.emplace_back("webm");
+ break;
- default:
- throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParams.format)) + ")" };
+ default:
+ throw Exception{ "Unhandled format (" + std::to_string(static_cast(*_outputParams.format)) + ")" };
+ }
}
args.emplace_back("pipe:1");
@@ -199,18 +202,22 @@ namespace lms::av
std::string_view Transcoder::getOutputMimeType() const
{
- switch (_outputParams.format)
+ // TODO: use input mime type
+ if (_outputParams.format)
{
- case OutputFormat::MP3:
- return "audio/mpeg";
- case OutputFormat::OGG_OPUS:
- return "audio/opus";
- case OutputFormat::MATROSKA_OPUS:
- return "audio/x-matroska";
- case OutputFormat::OGG_VORBIS:
- return "audio/ogg";
- case OutputFormat::WEBM_VORBIS:
- return "audio/webm";
+ switch (*_outputParams.format)
+ {
+ case OutputFormat::MP3:
+ return "audio/mpeg";
+ case OutputFormat::OGG_OPUS:
+ return "audio/opus";
+ case OutputFormat::MATROSKA_OPUS:
+ return "audio/x-matroska";
+ case OutputFormat::OGG_VORBIS:
+ return "audio/ogg";
+ case OutputFormat::WEBM_VORBIS:
+ return "audio/webm";
+ }
}
return "application/octet-stream"; // default, should not happen
@@ -222,5 +229,4 @@ namespace lms::av
return _childProcess->finished();
}
-
-} // namespace lms::av
+} // namespace lms::audio::ffmpeg
diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/audio/impl/ffmpeg/Transcoder.hpp
similarity index 79%
rename from src/libs/av/impl/Transcoder.hpp
rename to src/libs/audio/impl/ffmpeg/Transcoder.hpp
index 685d18b0..715c6d4f 100644
--- a/src/libs/av/impl/Transcoder.hpp
+++ b/src/libs/audio/impl/ffmpeg/Transcoder.hpp
@@ -19,19 +19,19 @@
#pragma once
-#include "av/ITranscoder.hpp"
+#include "audio/ITranscoder.hpp"
namespace lms::core
{
class IChildProcess;
}
-namespace lms::av
+namespace lms::audio::ffmpeg
{
class Transcoder : public ITranscoder
{
public:
- Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
+ Transcoder(const TranscodeParameters& parameters);
~Transcoder() override;
Transcoder(const Transcoder&) = delete;
Transcoder& operator=(const Transcoder&) = delete;
@@ -41,15 +41,15 @@ namespace lms::av
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
std::string_view getOutputMimeType() const override;
- const OutputParameters& getOutputParameters() const override { return _outputParams; }
+ const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; }
bool finished() const override;
static void init();
void start();
const std::size_t _debugId{};
- const InputParameters _inputParams;
- const OutputParameters _outputParams;
+ const TranscodeInputParameters _inputParams;
+ const TranscodeOutputParameters _outputParams;
std::unique_ptr _childProcess;
};
-} // namespace lms::av
\ No newline at end of file
+} // namespace lms::audio::ffmpeg
\ No newline at end of file
diff --git a/src/libs/metadata/impl/avformat/Utils.cpp b/src/libs/audio/impl/ffmpeg/Utils.cpp
similarity index 89%
rename from src/libs/metadata/impl/avformat/Utils.cpp
rename to src/libs/audio/impl/ffmpeg/Utils.cpp
index 83055d2f..a3ab243c 100644
--- a/src/libs/metadata/impl/avformat/Utils.cpp
+++ b/src/libs/audio/impl/ffmpeg/Utils.cpp
@@ -19,11 +19,11 @@
#include "Utils.hpp"
-namespace lms::metadata::avformat::utils
+namespace lms::audio::ffmpeg::utils
{
std::span getSupportedExtensions()
{
- // TODO: use av capability to retrieve supported formats
+ // TODO: list demuxers to retrieve supported formats
static const std::array fileExtensions{
".aac",
".alac",
@@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils
};
return fileExtensions;
}
-} // namespace lms::metadata::avformat::utils
\ No newline at end of file
+} // namespace lms::audio::ffmpeg::utils
\ No newline at end of file
diff --git a/src/libs/metadata/impl/avformat/Utils.hpp b/src/libs/audio/impl/ffmpeg/Utils.hpp
similarity index 90%
rename from src/libs/metadata/impl/avformat/Utils.hpp
rename to src/libs/audio/impl/ffmpeg/Utils.hpp
index b4f655fa..35d7be48 100644
--- a/src/libs/metadata/impl/avformat/Utils.hpp
+++ b/src/libs/audio/impl/ffmpeg/Utils.hpp
@@ -22,7 +22,7 @@
#include
#include
-namespace lms::metadata::avformat::utils
+namespace lms::audio::ffmpeg::utils
{
std::span getSupportedExtensions();
-} // namespace lms::metadata::avformat::utils
\ No newline at end of file
+} // namespace lms::audio::ffmpeg::utils
\ No newline at end of file
diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp
new file mode 100644
index 00000000..6cc81dea
--- /dev/null
+++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "AudioFileInfo.hpp"
+
+#include
+
+#include "TagLibDefs.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if LMS_TAGLIB_HAS_DSF
+ #include
+#endif
+#if LMS_TAGLIB_HAS_SHORTEN
+ #include
+#endif
+
+#include "audio/AudioTypes.hpp"
+#include "audio/IAudioFileInfo.hpp"
+
+#include "ImageReader.hpp"
+#include "TagReader.hpp"
+#include "Utils.hpp"
+
+namespace lms::audio::taglib
+{
+ namespace
+ {
+ AudioProperties computeAudioProperties(const ::TagLib::File& file)
+ {
+ assert(file.audioProperties());
+
+ AudioProperties audioProperties;
+
+ {
+ const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
+
+ // Common properties
+ audioProperties.bitrate = static_cast(properties.bitrate() * 1000);
+ if (audioProperties.bitrate == 0)
+ throw AudioFileParsingException{ "Cannot determine bitrate" };
+
+ audioProperties.channelCount = static_cast(properties.channels());
+ if (audioProperties.channelCount == 0)
+ throw AudioFileParsingException{ "Cannot determine channel count" };
+
+ audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
+ if (audioProperties.duration == decltype(audioProperties.duration)::zero())
+ throw AudioFileParsingException{ "Cannot determine duration" };
+
+ audioProperties.sampleRate = static_cast(properties.sampleRate());
+ if (audioProperties.sampleRate == 0)
+ throw AudioFileParsingException{ "Cannot determine sample rate" };
+ }
+
+ // Guess container from the file type
+ if (const auto* apeFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::APE;
+ audioProperties.codec = CodecType::APE; // TODO version?
+ audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
+ }
+ else if (const auto* asfFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::ASF;
+
+ switch (asfFile->audioProperties()->codec())
+ {
+ case ::TagLib::ASF::Properties::Codec::WMA1:
+ audioProperties.codec = CodecType::WMA1;
+ break;
+ case ::TagLib::ASF::Properties::Codec::WMA2:
+ audioProperties.codec = CodecType::WMA2;
+ break;
+ case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
+ audioProperties.codec = CodecType::WMA9Lossless;
+ break;
+ case ::TagLib::ASF::Properties::Codec::WMA9Pro:
+ audioProperties.codec = CodecType::WMA9Pro;
+ break;
+ case ::TagLib::ASF::Properties::Codec::Unknown:
+ throw AudioFileParsingException{ "Unhandled ASF codec" };
+ }
+
+ audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
+ }
+#if LMS_TAGLIB_HAS_DSF
+ else if (const auto* dsfFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::DSF;
+ audioProperties.codec = CodecType::DSD;
+ audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
+ }
+#endif // LMS_TAGLIB_HAS_DSF
+ else if (const auto* flacFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::FLAC;
+ audioProperties.codec = CodecType::FLAC;
+ audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
+ }
+ else if (const auto* mp4File{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::MP4;
+ switch (mp4File->audioProperties()->codec())
+ {
+ case ::TagLib::MP4::Properties::Codec::AAC:
+ audioProperties.codec = CodecType::AAC;
+ break;
+ case ::TagLib::MP4::Properties::Codec::ALAC:
+ audioProperties.codec = CodecType::ALAC;
+ break;
+ case ::TagLib::MP4::Properties::Codec::Unknown:
+ throw AudioFileParsingException{ "Unhandled MP4 codec" };
+ }
+
+ audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
+ }
+ else if (const auto* mpcFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::MPC;
+
+ switch (mpcFile->audioProperties()->mpcVersion())
+ {
+ case 7:
+ audioProperties.codec = CodecType::MPC7;
+ break;
+ case 8:
+ audioProperties.codec = CodecType::MPC8;
+ break;
+ default:
+ throw AudioFileParsingException{ "Unhandled MPC codec" };
+ }
+ }
+ else if (const auto* mpegFile{ dynamic_cast(&file) })
+ {
+ const auto& properties{ *mpegFile->audioProperties() };
+
+ audioProperties.container = ContainerType::MPEG;
+ if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
+ && mpegFile->audioProperties()->layer() == 3)
+ audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
+#if LMS_TAGLIB_HAS_ADTS
+ else if (mpegFile->audioProperties()->isADTS()) // likely AAC
+ audioProperties.codec = CodecType::AAC;
+#endif
+ else
+ throw AudioFileParsingException{ "Unhandled MPEG codec" };
+ }
+ else if (dynamic_cast(&file))
+ {
+ audioProperties.container = ContainerType::Ogg;
+ audioProperties.codec = CodecType::Opus;
+ }
+ else if (dynamic_cast(&file))
+ {
+ audioProperties.container = ContainerType::Ogg;
+ audioProperties.codec = CodecType::Vorbis;
+ }
+ else if (const auto* aiffFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::AIFF;
+ audioProperties.codec = CodecType::PCM;
+ audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
+ }
+ else if (const auto* wavFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::WAV;
+ audioProperties.codec = CodecType::PCM;
+ audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
+ }
+#if LMS_TAGLIB_HAS_SHORTEN
+ else if (const auto* shortenFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::Shorten;
+ audioProperties.codec = CodecType::Shorten;
+ audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
+ }
+#endif // LMS_TAGLIB_HAS_SHORTEN
+ else if (const auto* trueAudioFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::TrueAudio;
+ audioProperties.codec = CodecType::TrueAudio;
+ audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
+ }
+ else if (const auto* wavPackFile{ dynamic_cast(&file) })
+ {
+ audioProperties.container = ContainerType::WavPack;
+ audioProperties.codec = CodecType::WavPack;
+ audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
+ }
+ else
+ {
+ throw AudioFileParsingException{ "Unhandled file type" };
+ }
+
+ if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0)
+ audioProperties.bitsPerSample.reset();
+
+ return audioProperties;
+ }
+ } // namespace
+
+ AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
+ : _filePath{ filePath }
+ , _file{ utils::parseFile(filePath, readStyle) }
+ , _audioProperties{ std::make_unique(computeAudioProperties(*_file)) }
+ , _tagReader{ std::make_unique(*_file, enableExtraDebugLogs) }
+ , _imageReader{ std::make_unique(*_file) }
+ {
+ }
+
+ AudioFileInfo::~AudioFileInfo() = default;
+
+ const AudioProperties& AudioFileInfo::getAudioProperties() const
+ {
+ return *_audioProperties;
+ }
+
+ const IImageReader& AudioFileInfo::getImageReader() const
+ {
+ return *_imageReader;
+ }
+
+ const ITagReader& AudioFileInfo::getTagReader() const
+ {
+ return *_tagReader;
+ }
+
+} // namespace lms::audio::taglib
diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp
new file mode 100644
index 00000000..4cd85452
--- /dev/null
+++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "audio/AudioTypes.hpp"
+#include "audio/IAudioFileInfo.hpp"
+#include "audio/IImageReader.hpp"
+#include "audio/ITagReader.hpp"
+
+namespace TagLib
+{
+ class File;
+}
+
+namespace lms::audio::taglib
+{
+ class ImageReader;
+ class TagReader;
+
+ class AudioFileInfo final : public IAudioFileInfo
+ {
+ public:
+ AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
+ ~AudioFileInfo() override;
+
+ AudioFileInfo(const AudioFileInfo&) = delete;
+ AudioFileInfo& operator=(const AudioFileInfo&) = delete;
+
+ private:
+ const AudioProperties& getAudioProperties() const override;
+ const IImageReader& getImageReader() const override;
+ const ITagReader& getTagReader() const override;
+
+ const std::filesystem::path _filePath;
+ std::unique_ptr<::TagLib::File> _file;
+ std::unique_ptr _audioProperties;
+ std::unique_ptr _tagReader;
+ std::unique_ptr _imageReader;
+ };
+} // namespace lms::audio::taglib
diff --git a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp b/src/libs/audio/impl/taglib/ImageReader.cpp
similarity index 55%
rename from src/libs/metadata/impl/taglib/TagLibImageReader.cpp
rename to src/libs/audio/impl/taglib/ImageReader.cpp
index 927da6c4..adad0a24 100644
--- a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp
+++ b/src/libs/audio/impl/taglib/ImageReader.cpp
@@ -17,7 +17,7 @@
* along with LMS. If not, see .
*/
-#include "TagLibImageReader.hpp"
+#include "ImageReader.hpp"
#include "TagLibDefs.hpp"
@@ -33,16 +33,14 @@
#include
#include
#include
+#include
#include
#include
#include
-#include "core/ILogger.hpp"
-#include "metadata/Exception.hpp"
+#include "core/String.hpp"
-#include "taglib/Utils.hpp"
-
-namespace lms::metadata::taglib
+namespace lms::audio::taglib
{
namespace
{
@@ -50,47 +48,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
- case TagLib::ID3v2::AttachedPictureFrame::Type::Other:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other;
- case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon;
- case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
- case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover;
- case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover;
- case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Media:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media;
- case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Band:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist;
- case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation;
- case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording;
- case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance;
- case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
- case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish;
- case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration;
- case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo;
- case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
+ case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -101,47 +99,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
- case TagLib::ASF::Picture::Type::Other:
+ case ::TagLib::ASF::Picture::Type::Other:
return Image::Type::Other;
- case TagLib::ASF::Picture::Type::FileIcon:
+ case ::TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon;
- case TagLib::ASF::Picture::Type::OtherFileIcon:
+ case ::TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
- case TagLib::ASF::Picture::Type::FrontCover:
+ case ::TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover;
- case TagLib::ASF::Picture::Type::BackCover:
+ case ::TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover;
- case TagLib::ASF::Picture::Type::LeafletPage:
+ case ::TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
- case TagLib::ASF::Picture::Type::Media:
+ case ::TagLib::ASF::Picture::Type::Media:
return Image::Type::Media;
- case TagLib::ASF::Picture::Type::LeadArtist:
+ case ::TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
- case TagLib::ASF::Picture::Type::Artist:
+ case ::TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist;
- case TagLib::ASF::Picture::Type::Conductor:
+ case ::TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor;
- case TagLib::ASF::Picture::Type::Band:
+ case ::TagLib::ASF::Picture::Type::Band:
return Image::Type::Band;
- case TagLib::ASF::Picture::Type::Composer:
+ case ::TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer;
- case TagLib::ASF::Picture::Type::Lyricist:
+ case ::TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist;
- case TagLib::ASF::Picture::Type::RecordingLocation:
+ case ::TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
- case TagLib::ASF::Picture::Type::DuringRecording:
+ case ::TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
- case TagLib::ASF::Picture::Type::DuringPerformance:
+ case ::TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
- case TagLib::ASF::Picture::Type::MovieScreenCapture:
+ case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
- case TagLib::ASF::Picture::Type::ColouredFish:
+ case ::TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
- case TagLib::ASF::Picture::Type::Illustration:
+ case ::TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration;
- case TagLib::ASF::Picture::Type::BandLogo:
+ case ::TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo;
- case TagLib::ASF::Picture::Type::PublisherLogo:
+ case ::TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -152,47 +150,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
- case TagLib::FLAC::Picture::Type::Other:
+ case ::TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other;
- case TagLib::FLAC::Picture::Type::FileIcon:
+ case ::TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon;
- case TagLib::FLAC::Picture::Type::OtherFileIcon:
+ case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
- case TagLib::FLAC::Picture::Type::FrontCover:
+ case ::TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover;
- case TagLib::FLAC::Picture::Type::BackCover:
+ case ::TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover;
- case TagLib::FLAC::Picture::Type::LeafletPage:
+ case ::TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
- case TagLib::FLAC::Picture::Type::Media:
+ case ::TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media;
- case TagLib::FLAC::Picture::Type::LeadArtist:
+ case ::TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
- case TagLib::FLAC::Picture::Type::Artist:
+ case ::TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist;
- case TagLib::FLAC::Picture::Type::Conductor:
+ case ::TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor;
- case TagLib::FLAC::Picture::Type::Band:
+ case ::TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band;
- case TagLib::FLAC::Picture::Type::Composer:
+ case ::TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer;
- case TagLib::FLAC::Picture::Type::Lyricist:
+ case ::TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist;
- case TagLib::FLAC::Picture::Type::RecordingLocation:
+ case ::TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
- case TagLib::FLAC::Picture::Type::DuringRecording:
+ case ::TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
- case TagLib::FLAC::Picture::Type::DuringPerformance:
+ case ::TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
- case TagLib::FLAC::Picture::Type::MovieScreenCapture:
+ case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
- case TagLib::FLAC::Picture::Type::ColouredFish:
+ case ::TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
- case TagLib::FLAC::Picture::Type::Illustration:
+ case ::TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration;
- case TagLib::FLAC::Picture::Type::BandLogo:
+ case ::TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo;
- case TagLib::FLAC::Picture::Type::PublisherLogo:
+ case ::TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -203,44 +201,44 @@ namespace lms::metadata::taglib
{
switch (format)
{
- case TagLib::MP4::CoverArt::Format::BMP:
+ case ::TagLib::MP4::CoverArt::Format::BMP:
return "image/bmp";
- case TagLib::MP4::CoverArt::Format::GIF:
+ case ::TagLib::MP4::CoverArt::Format::GIF:
return "image/gif";
- case TagLib::MP4::CoverArt::Format::JPEG:
+ case ::TagLib::MP4::CoverArt::Format::JPEG:
return "image/jpeg";
- case TagLib::MP4::CoverArt::Format::PNG:
+ case ::TagLib::MP4::CoverArt::Format::PNG:
return "image/png";
- case TagLib::MP4::CoverArt::Format::Unknown:
+ case ::TagLib::MP4::CoverArt::Format::Unknown:
return "application/octet-stream";
}
return "application/octet-stream";
}
-#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
+#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover;
- else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
+ if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover;
return Image::Type::Unknown;
}
-#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
+#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
- void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor)
+ void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor)
{
const auto& frameListMap{ id3v2Tags.frameListMap() };
- for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
+ for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
{
- const auto* attachedPictureFrame{ dynamic_cast(frame) };
+ const auto* attachedPictureFrame{ dynamic_cast(frame) };
if (!attachedPictureFrame)
continue;
- TagLib::ByteVector picture{ attachedPictureFrame->picture() };
+ ::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span pictureData{ reinterpret_cast(picture.data()), picture.size() };
Image image;
@@ -252,15 +250,15 @@ namespace lms::metadata::taglib
}
}
- void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor)
+ void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor)
{
- for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
+ for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
{
- TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
+ ::TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
if (!asfPicture.isValid())
continue;
- TagLib::ByteVector picture{ asfPicture.picture() };
+ ::TagLib::ByteVector picture{ asfPicture.picture() };
std::span pictureData{ reinterpret_cast(picture.data()), picture.size() };
Image image;
@@ -273,22 +271,22 @@ namespace lms::metadata::taglib
}
}
- void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor)
+ void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor)
{
- const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
+ const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
if (!coverItem.isValid())
return;
-#if TAGLIB_HAS_MP4_ITEM_TYPE
- if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList)
+#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
+ if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
return;
#endif
- TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
+ ::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
bool firstCover{ true };
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
{
- TagLib::ByteVector picture{ coverArt.data() };
+ ::TagLib::ByteVector picture{ coverArt.data() };
std::span pictureData{ reinterpret_cast(picture.data()), picture.size() };
Image image;
@@ -303,11 +301,11 @@ namespace lms::metadata::taglib
}
}
- void visitFLACImages(const TagLib::List pictureList, TagLibImageReader::ImageVisitor visitor)
+ void visitFLACImages(const ::TagLib::List& pictureList, const ImageReader::ImageVisitor& visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
- TagLib::ByteVector picture{ flacPicture->data() };
+ ::TagLib::ByteVector picture{ flacPicture->data() };
std::span pictureData{ reinterpret_cast(picture.data()), picture.size() };
Image image;
@@ -320,14 +318,14 @@ namespace lms::metadata::taglib
}
}
- void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor)
+ void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor)
{
-#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
- const TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") };
- for (const TagLib::VariantMap& pictureProperty : pictureProperties)
+#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
+ const ::TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") };
+ for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
{
Image image;
- TagLib::ByteVector picture;
+ ::TagLib::ByteVector picture;
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
@@ -344,81 +342,77 @@ namespace lms::metadata::taglib
if (!image.data.empty())
visitor(image);
}
-
-#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
+#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
}
} // namespace
- TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
- : _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) }
+ ImageReader::ImageReader(::TagLib::File& file)
+ : _file{ file }
{
- if (!_file)
- {
- LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
- throw AudioFileParsingException{};
- }
}
- void TagLibImageReader::visitImages(ImageVisitor visitor) const
+ ImageReader::~ImageReader() = default;
+
+ void ImageReader::visitImages(const ImageVisitor& visitor) const
{
// MP3
- if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) })
+ if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) })
{
if (mp3File->hasID3v2Tag())
- visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
+ visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
}
// MP4
- else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) })
+ else if (const TagLib::MP4::File * mp4File{ dynamic_cast(&_file) })
{
- visitMP4Images(*mp4File, std::move(visitor));
+ visitMP4Images(*mp4File, visitor);
}
// WMA
- else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::ASF::File * asfFile{ dynamic_cast(&_file) })
{
- if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
- visitASFImages(*tag, std::move(visitor));
+ if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
+ visitASFImages(*tag, visitor);
}
// FLAC
- else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) })
+ else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) })
{
if (flacFile->hasID3v2Tag()) // usage discouraged
- visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
+ visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
else
- visitFLACImages(flacFile->pictureList(), std::move(visitor));
+ visitFLACImages(flacFile->pictureList(), visitor);
}
// Ogg vorbis
- else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(&_file) })
{
- visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
+ visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
}
// Ogg Opus
- else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(&_file) })
{
- visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
+ visitFLACImages(opusFile->tag()->pictureList(), visitor);
}
// Aiff
- else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) })
{
if (aiffFile->hasID3v2Tag())
- visitID3V2Images(*aiffFile->tag(), std::move(visitor));
+ visitID3V2Images(*aiffFile->tag(), visitor);
}
// Wav
- else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) })
{
if (wavFile->hasID3v2Tag())
- visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
+ visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
}
// MPC
- else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) })
+ else if (TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) })
{
if (mpcFile->hasAPETag())
- visitAPEImages(*mpcFile->APETag(), std::move(visitor));
+ visitAPEImages(*mpcFile->APETag(), visitor);
}
// WavPack
- else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) })
+ else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) })
{
if (wavPackFile->hasAPETag())
- visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
+ visitAPEImages(*wavPackFile->APETag(), visitor);
}
}
-} // namespace lms::metadata::taglib
+} // namespace lms::audio::taglib
diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp b/src/libs/audio/impl/taglib/ImageReader.hpp
similarity index 57%
rename from src/libs/metadata/impl/avformat/AvFormatImageReader.hpp
rename to src/libs/audio/impl/taglib/ImageReader.hpp
index 0658b8a7..b4adb32c 100644
--- a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp
+++ b/src/libs/audio/impl/taglib/ImageReader.hpp
@@ -19,27 +19,26 @@
#pragma once
-#include "IImageReader.hpp"
+#include "audio/IImageReader.hpp"
-namespace lms::av
+namespace TagLib
{
- class IAudioFile;
+ class File;
}
-namespace lms::metadata::avformat
+namespace lms::audio::taglib
{
- class AvFormatImageReader : public IImageReader
+ class ImageReader : public IImageReader
{
public:
- AvFormatImageReader(const std::filesystem::path& p);
- ~AvFormatImageReader() override;
-
- AvFormatImageReader(const AvFormatImageReader&) = delete;
- AvFormatImageReader& operator=(const AvFormatImageReader&) = delete;
+ ImageReader(TagLib::File& _file);
+ ~ImageReader() override;
+ ImageReader(const ImageReader&) = delete;
+ ImageReader& operator=(const ImageReader&) = delete;
private:
- void visitImages(ImageVisitor visitor) const override;
+ void visitImages(const ImageVisitor& visitor) const override;
- std::unique_ptr _audioFile;
+ TagLib::File& _file;
};
-} // namespace lms::metadata::avformat
+} // namespace lms::audio::taglib
diff --git a/src/libs/metadata/impl/taglib/TagLibDefs.hpp b/src/libs/audio/impl/taglib/TagLibDefs.hpp
similarity index 70%
rename from src/libs/metadata/impl/taglib/TagLibDefs.hpp
rename to src/libs/audio/impl/taglib/TagLibDefs.hpp
index cbcbbf6d..580c72bf 100644
--- a/src/libs/metadata/impl/taglib/TagLibDefs.hpp
+++ b/src/libs/audio/impl/taglib/TagLibDefs.hpp
@@ -22,15 +22,21 @@
#include
#if (TAGLIB_MAJOR_VERSION >= 2)
- #define TAGLIB_HAS_DSF 1
+ #define LMS_TAGLIB_HAS_DSF 1
+ #define LMS_TAGLIB_HAS_ADTS 1
#endif
-// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
+// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
- #define TAGLIB_HAS_MP4_ITEM_TYPE 1
+ #define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1
#endif
-// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
+// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
- #define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
+ #define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
+#endif
+
+// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1
+#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1))
+ #define LMS_TAGLIB_HAS_SHORTEN 1
#endif
diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp b/src/libs/audio/impl/taglib/TagReader.cpp
similarity index 72%
rename from src/libs/metadata/impl/taglib/TagLibTagReader.cpp
rename to src/libs/audio/impl/taglib/TagReader.cpp
index b2fd0db8..a905f7b5 100644
--- a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp
+++ b/src/libs/audio/impl/taglib/TagReader.cpp
@@ -17,7 +17,7 @@
* along with LMS. If not, see .
*/
-#include "TagLibTagReader.hpp"
+#include "TagReader.hpp"
#include
@@ -25,7 +25,6 @@
#include
#include
-#include
#include
#include
#include
@@ -38,33 +37,25 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
#include
-#if TAGLIB_HAS_DSF
+#if LMS_TAGLIB_HAS_DSF
#include
#include
#endif
#include "core/ILogger.hpp"
#include "core/String.hpp"
-#include "metadata/Exception.hpp"
-#include "Utils.hpp"
-
-namespace lms::metadata::taglib
+namespace lms::audio::taglib
{
namespace
{
- class TagParsingFailedException : public Exception
- {
- public:
- using Exception::Exception;
- };
-
// Mapping to internal taglib names and/or common alternative custom names
const std::unordered_map> tagLibTagMapping{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
@@ -181,7 +172,7 @@ namespace lms::metadata::taglib
{ TagType::Writer, { "WRITER" } },
};
- void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
+ void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
{
for (auto&& [tag, values] : src)
{
@@ -190,49 +181,36 @@ namespace lms::metadata::taglib
}
}
- void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file)
+ void dedupTagValues(TagLib::PropertyMap& propertyMap)
{
for (auto& [key, values] : propertyMap)
{
if (values.size() <= 1)
continue;
- TagLib::StringList newList;
- for (const TagLib::String& value : values)
+ ::TagLib::StringList newList;
+ for (const ::TagLib::String& value : values)
{
- if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; }))
+ if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; }))
newList.append(value);
}
if (values != newList)
{
- LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
+ LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
values = newList;
}
}
}
} // namespace
- TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
- : _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) }
+ TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
+ : _file{ file }
{
- if (!_file)
- {
- LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
- throw AudioFileParsingException{ "Parsing failed" };
- }
+ _propertyMap = _file.properties();
- if (!_file->audioProperties())
- {
- LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
- throw AudioFileNoAudioPropertiesException{};
- }
-
- computeAudioProperties();
-
- _propertyMap = _file->properties();
-
- if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
+ enableExtraDebugLogs &= core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG);
+ if (enableExtraDebugLogs)
{
for (const auto& [key, values] : _propertyMap)
{
@@ -245,7 +223,7 @@ namespace lms::metadata::taglib
}
// Some tags may not be known by TagLib
- auto getAPETags = [&](const TagLib::APE::Tag* apeTag) {
+ auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) {
if (!apeTag)
return;
@@ -254,7 +232,7 @@ namespace lms::metadata::taglib
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
- dedupTagValues(_propertyMap, p);
+ dedupTagValues(_propertyMap);
const auto& frameListMap{ id3v2Tags.frameListMap() };
@@ -264,26 +242,30 @@ namespace lms::metadata::taglib
// consider each frame hold a different set of lyrics
// Synchronized lyrics frames
- for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
+ for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
{
- const auto* lyricsFrame{ dynamic_cast(frame) };
+ const auto* lyricsFrame{ dynamic_cast(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
std::string lyrics;
- for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
+ for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
{
std::chrono::milliseconds timestamp{};
switch (lyricsFrame->timestampFormat())
{
- case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
+ case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
timestamp = std::chrono::milliseconds{ synchedText.time };
break;
- case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
- timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 };
+ case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
+ {
+ const ::TagLib::AudioProperties* properties{ file.audioProperties() };
+ if (properties && properties->sampleRate())
+ timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
+ }
break;
- case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
+ case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
break;
}
@@ -298,9 +280,9 @@ namespace lms::metadata::taglib
}
// Unsynchronized lyrics frames
- for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
+ for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
{
- const auto* lyricsFrame{ dynamic_cast(frame) };
+ const auto* lyricsFrame{ dynamic_cast(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
@@ -310,9 +292,9 @@ namespace lms::metadata::taglib
};
// WMA
- if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) })
+ if (const ::TagLib::ASF::File * asfFile{ dynamic_cast(&_file) })
{
- if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
+ if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
{
for (const auto& [name, attributeList] : tag->attributeListMap())
{
@@ -320,19 +302,19 @@ namespace lms::metadata::taglib
continue;
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
- if (debug)
+ if (enableExtraDebugLogs)
{
for (const auto& attribute : attributeList)
- LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "" }) << "'");
+ LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "" }) << "'");
}
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
continue;
- TagLib::StringList strAttributes;
- for (const TagLib::ASF::Attribute& attribute : attributeList)
+ ::TagLib::StringList strAttributes;
+ for (const ::TagLib::ASF::Attribute& attribute : attributeList)
{
- if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
+ if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
strAttributes.append(attribute.toString());
}
@@ -356,23 +338,23 @@ namespace lms::metadata::taglib
}
}
// MP3
- else if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) })
+ else if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) })
{
if (mp3File->hasID3v2Tag())
- processID3v2Tags(*mp3File->ID3v2Tag());
+ processID3v2Tags(*mp3File->ID3v2Tag(false));
getAPETags(mp3File->APETag());
}
// MP4
- else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) })
+ else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast(&_file) })
{
// Taglib does not expose rtng in properties
- if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
+ if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
{
-#if TAGLIB_HAS_MP4_ITEM_TYPE
- if (rtngItem.type() == TagLib::MP4::Item::Type::Byte)
+#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
+ if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
#endif
- _propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) };
+ _propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
}
if (!_propertyMap.contains("ORIGINALDATE"))
@@ -386,7 +368,7 @@ namespace lms::metadata::taglib
auto itOrigDateTag{ tags.find(origDateString) };
if (itOrigDateTag != std::cend(tags))
{
- const TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
+ const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
if (!dates.isEmpty())
{
_propertyMap["ORIGINALDATE"] = dates.front();
@@ -397,68 +379,36 @@ namespace lms::metadata::taglib
}
}
// MPC
- else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) })
+ else if (::TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
- else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) })
+ else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
- else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) })
+ else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) })
{
if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag());
}
- else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) })
{
if (aiffFile->hasID3v2Tag())
processID3v2Tags(*aiffFile->tag());
}
- else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) })
+ else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) })
{
if (wavFile->hasID3v2Tag())
processID3v2Tags(*wavFile->ID3v2Tag());
}
}
- TagLibTagReader::~TagLibTagReader() = default;
+ TagReader::~TagReader() = default;
- void TagLibTagReader::computeAudioProperties()
- {
- const TagLib::AudioProperties* properties{ _file->audioProperties() };
-
- // Common properties
- _audioProperties.bitrate = static_cast(properties->bitrate() * 1000);
- _audioProperties.channelCount = static_cast(_file->audioProperties()->channels());
- _audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
- _audioProperties.sampleRate = static_cast(properties->sampleRate());
-
- if (const auto* apeProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = apeProperties->bitsPerSample();
- if (const auto* asfProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = asfProperties->bitsPerSample();
- else if (const auto* flacProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = flacProperties->bitsPerSample();
- else if (const auto* mp4Properties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
- else if (const auto* wavePackProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
- else if (const auto* aiffProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
- else if (const auto* wavProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = wavProperties->bitsPerSample();
-#if TAGLIB_HAS_DSF
- else if (const auto* dsfProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
- else if (const auto* dsfProperties{ dynamic_cast(properties) })
- _audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
-#endif
- }
-
- void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
+ void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ tagLibTagMapping.find(tag) };
if (itTagNames == std::cend(tagLibTagMapping))
@@ -478,19 +428,19 @@ namespace lms::metadata::taglib
}
}
- void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
+ void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
{
- TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 };
+ ::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 };
auto itValues{ _propertyMap.find(key) };
if (itValues == std::cend(_propertyMap))
return;
- for (const TagLib::String& value : itValues->second)
+ for (const ::TagLib::String& value : itValues->second)
visitor(value.to8Bit(true));
}
- void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const
+ void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
@@ -505,7 +455,7 @@ namespace lms::metadata::taglib
assert(rolePos != std::string::npos);
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
- for (const TagLib::String& value : values)
+ for (const ::TagLib::String& value : values)
{
const std::string name{ value.to8Bit(true) };
visitor(role, name);
@@ -514,7 +464,7 @@ namespace lms::metadata::taglib
}
}
- void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const
+ void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
if (!_id3v2Lyrics.empty())
{
@@ -529,4 +479,4 @@ namespace lms::metadata::taglib
});
}
}
-} // namespace lms::metadata::taglib
+} // namespace lms::audio::taglib
diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp b/src/libs/audio/impl/taglib/TagReader.hpp
similarity index 61%
rename from src/libs/metadata/impl/taglib/TagLibTagReader.hpp
rename to src/libs/audio/impl/taglib/TagReader.hpp
index 07034a38..7bb98e84 100644
--- a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp
+++ b/src/libs/audio/impl/taglib/TagReader.hpp
@@ -19,38 +19,37 @@
#pragma once
-#include
#include