Scan for audio properties, metadata, and embedded images in one single pass. Removed now useless lmsmetadata library + reworked code accordingly
This commit is contained in:
@@ -54,7 +54,7 @@ __Note__: If no name is provided in the `artist.nfo` file, the name of the conta
|
||||
### Filtering
|
||||
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
|
||||
|
||||
__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_.
|
||||
__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_.
|
||||
|
||||
### Multiple artists
|
||||
_LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
add_subdirectory(av)
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(image)
|
||||
add_subdirectory(metadata)
|
||||
add_subdirectory(services)
|
||||
add_subdirectory(som)
|
||||
add_subdirectory(subsonic)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
|
||||
add_library(lmsaudio STATIC
|
||||
impl/ffmpeg/AudioFile.cpp
|
||||
impl/ffmpeg/AudioFileInfo.cpp
|
||||
impl/ffmpeg/ImageReader.cpp
|
||||
impl/ffmpeg/TagReader.cpp
|
||||
impl/ffmpeg/Transcoder.cpp
|
||||
impl/ffmpeg/Utils.cpp
|
||||
impl/taglib/AudioFileInfo.cpp
|
||||
impl/taglib/ImageReader.cpp
|
||||
impl/taglib/TagReader.cpp
|
||||
impl/taglib/Utils.cpp
|
||||
impl/AudioTypes.cpp
|
||||
impl/ImageReader.cpp
|
||||
impl/ParseAudioFileInfo.cpp
|
||||
impl/TagReader.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsaudio INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsaudio PRIVATE
|
||||
include
|
||||
${AVCODEC_INCLUDE_DIR}
|
||||
${AVFORMAT_INCLUDE_DIR}
|
||||
${AVUTIL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(lmsaudio PUBLIC
|
||||
lmscore
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
target_link_libraries(lmsaudio PRIVATE
|
||||
PkgConfig::LIBAV
|
||||
PkgConfig::Taglib
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString containerTypeToString(ContainerType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ContainerType::AIFF:
|
||||
return "AIFF";
|
||||
case ContainerType::APE:
|
||||
return "APE";
|
||||
case ContainerType::ASF:
|
||||
return "ASF";
|
||||
case ContainerType::DSF:
|
||||
return "DSF";
|
||||
case ContainerType::FLAC:
|
||||
return "FLAC";
|
||||
case ContainerType::MP4:
|
||||
return "MP4";
|
||||
case ContainerType::MPC:
|
||||
return "MPC";
|
||||
case ContainerType::MPEG:
|
||||
return "MPEG";
|
||||
case ContainerType::Ogg:
|
||||
return "Ogg";
|
||||
case ContainerType::Shorten:
|
||||
return "Shorten";
|
||||
case ContainerType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case ContainerType::WAV:
|
||||
return "WAV";
|
||||
case ContainerType::WavPack:
|
||||
return "WavPack";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
core::LiteralString codecTypeToString(CodecType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CodecType::AAC:
|
||||
return "AAC";
|
||||
case CodecType::AC3:
|
||||
return "AC3";
|
||||
case CodecType::ALAC:
|
||||
return "ALAC";
|
||||
case CodecType::APE:
|
||||
return "APE";
|
||||
case CodecType::EAC3:
|
||||
return "EAC3";
|
||||
case CodecType::DSD:
|
||||
return "DSD";
|
||||
case CodecType::FLAC:
|
||||
return "FLAC";
|
||||
case CodecType::MP3:
|
||||
return "MP3";
|
||||
case CodecType::MP4ALS:
|
||||
return "MP4ALS";
|
||||
case CodecType::MPC7:
|
||||
return "MPC7";
|
||||
case CodecType::MPC8:
|
||||
return "MPC8";
|
||||
case CodecType::Opus:
|
||||
return "Opus";
|
||||
case CodecType::PCM:
|
||||
return "PCM";
|
||||
case CodecType::Shorten:
|
||||
return "Shorten";
|
||||
case CodecType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case CodecType::Vorbis:
|
||||
return "Vorbis";
|
||||
case CodecType::WavPack:
|
||||
return "WavPack";
|
||||
case CodecType::WMA1:
|
||||
return "WMA1";
|
||||
case CodecType::WMA2:
|
||||
return "WMA2";
|
||||
case CodecType::WMA9Pro:
|
||||
return "WMA9Pro";
|
||||
case CodecType::WMA9Lossless:
|
||||
return "WMA9Lossless";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString imageTypeToString(Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Image::Type::Other:
|
||||
return "Other";
|
||||
case Image::Type::FileIcon:
|
||||
return "FileIcon";
|
||||
case Image::Type::OtherFileIcon:
|
||||
return "OtherFileIcon";
|
||||
case Image::Type::FrontCover:
|
||||
return "FrontCover";
|
||||
case Image::Type::BackCover:
|
||||
return "BackCover";
|
||||
case Image::Type::LeafletPage:
|
||||
return "LeafletPage";
|
||||
case Image::Type::Media:
|
||||
return "Media";
|
||||
case Image::Type::LeadArtist:
|
||||
return "LeadArtist";
|
||||
case Image::Type::Artist:
|
||||
return "Artist";
|
||||
case Image::Type::Conductor:
|
||||
return "Conductor";
|
||||
case Image::Type::Band:
|
||||
return "Band";
|
||||
case Image::Type::Composer:
|
||||
return "Composer";
|
||||
case Image::Type::Lyricist:
|
||||
return "Lyricist";
|
||||
case Image::Type::RecordingLocation:
|
||||
return "RecordingLocation";
|
||||
case Image::Type::DuringRecording:
|
||||
return "DuringRecording";
|
||||
case Image::Type::DuringPerformance:
|
||||
return "DuringPerformance";
|
||||
case Image::Type::MovieScreenCapture:
|
||||
return "MovieScreenCapture";
|
||||
case Image::Type::ColouredFish:
|
||||
return "ColouredFish";
|
||||
case Image::Type::Illustration:
|
||||
return "Illustration";
|
||||
case Image::Type::BandLogo:
|
||||
return "BandLogo";
|
||||
case Image::Type::PublisherLogo:
|
||||
return "PublisherLogo";
|
||||
case Image::Type::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "ffmpeg/AudioFileInfo.hpp"
|
||||
#include "ffmpeg/Utils.hpp"
|
||||
#include "taglib/AudioFileInfo.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
|
||||
{
|
||||
switch (parserOptions.parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
|
||||
{
|
||||
switch (parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return taglib::utils::getSupportedExtensions();
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return ffmpeg::utils::getSupportedExtensions();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString tagTypeToString(TagType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TagType::AcoustID:
|
||||
return "AcoustID";
|
||||
case TagType::AcoustIDFingerprint:
|
||||
return "AcoustIDFingerprint";
|
||||
case TagType::Advisory:
|
||||
return "Advisory";
|
||||
case TagType::Album:
|
||||
return "Album";
|
||||
case TagType::AlbumArtist:
|
||||
return "AlbumArtist";
|
||||
case TagType::AlbumArtists:
|
||||
return "AlbumArtists";
|
||||
case TagType::AlbumArtistSortOrder:
|
||||
return "AlbumArtistSortOrder";
|
||||
case TagType::AlbumArtistsSortOrder:
|
||||
return "AlbumArtistsSortOrder";
|
||||
case TagType::AlbumComment:
|
||||
return "AlbumComment";
|
||||
case TagType::AlbumSortOrder:
|
||||
return "AlbumSortOrder";
|
||||
case TagType::Arranger:
|
||||
return "Arranger";
|
||||
case TagType::Artist:
|
||||
return "Artist";
|
||||
case TagType::ArtistSortOrder:
|
||||
return "ArtistSortOrder";
|
||||
case TagType::Artists:
|
||||
return "Artists";
|
||||
case TagType::ArtistsSortOrder:
|
||||
return "ArtistsSortOrder";
|
||||
case TagType::ASIN:
|
||||
return "ASIN";
|
||||
case TagType::Barcode:
|
||||
return "Barcode";
|
||||
case TagType::BPM:
|
||||
return "BPM";
|
||||
case TagType::CatalogNumber:
|
||||
return "CatalogNumber";
|
||||
case TagType::Comment:
|
||||
return "Comment";
|
||||
case TagType::Compilation:
|
||||
return "Compilation";
|
||||
case TagType::Composer:
|
||||
return "Composer";
|
||||
case TagType::ComposerSortOrder:
|
||||
return "ComposerSortOrder";
|
||||
case TagType::Composers:
|
||||
return "Composers";
|
||||
case TagType::ComposersSortOrder:
|
||||
return "ComposersSortOrder";
|
||||
case TagType::Conductor:
|
||||
return "Conductor";
|
||||
case TagType::ConductorSortOrder:
|
||||
return "ConductorSortOrder";
|
||||
case TagType::Conductors:
|
||||
return "Conductors";
|
||||
case TagType::ConductorsSortOrder:
|
||||
return "ConductorsSortOrder";
|
||||
case TagType::Copyright:
|
||||
return "Copyright";
|
||||
case TagType::CopyrightURL:
|
||||
return "CopyrightURL";
|
||||
case TagType::Date:
|
||||
return "Date";
|
||||
case TagType::Director:
|
||||
return "Director";
|
||||
case TagType::DiscNumber:
|
||||
return "DiscNumber";
|
||||
case TagType::DiscSubtitle:
|
||||
return "DiscSubtitle";
|
||||
case TagType::EncodedBy:
|
||||
return "EncodedBy";
|
||||
case TagType::EncoderSettings:
|
||||
return "EncoderSettings";
|
||||
case TagType::EncodingTime:
|
||||
return "EncodingTime";
|
||||
case TagType::Engineer:
|
||||
return "Engineer";
|
||||
case TagType::GaplessPlayback:
|
||||
return "GaplessPlayback";
|
||||
case TagType::Genre:
|
||||
return "Genre";
|
||||
case TagType::Grouping:
|
||||
return "Grouping";
|
||||
case TagType::InitialKey:
|
||||
return "InitialKey";
|
||||
case TagType::ISRC:
|
||||
return "ISRC";
|
||||
case TagType::Language:
|
||||
return "Language";
|
||||
case TagType::Lyricist:
|
||||
return "Lyricist";
|
||||
case TagType::LyricistSortOrder:
|
||||
return "LyricistSortOrder";
|
||||
case TagType::Lyricists:
|
||||
return "Lyricists";
|
||||
case TagType::LyricistsSortOrder:
|
||||
return "LyricistsSortOrder";
|
||||
case TagType::Media:
|
||||
return "Media";
|
||||
case TagType::MixDJ:
|
||||
return "MixDJ";
|
||||
case TagType::Mixer:
|
||||
return "Mixer";
|
||||
case TagType::MixerSortOrder:
|
||||
return "MixerSortOrder";
|
||||
case TagType::Mixers:
|
||||
return "Mixers";
|
||||
case TagType::MixersSortOrder:
|
||||
return "MixersSortOrder";
|
||||
case TagType::Movement:
|
||||
return "Movement";
|
||||
case TagType::MovementCount:
|
||||
return "MovementCount";
|
||||
case TagType::MovementNumber:
|
||||
return "MovementNumber";
|
||||
case TagType::Mood:
|
||||
return "Mood";
|
||||
case TagType::MusicBrainzArtistID:
|
||||
return "MusicBrainzArtistID";
|
||||
case TagType::MusicBrainzArrangerID:
|
||||
return "MusicBrainzArrangerID";
|
||||
case TagType::MusicBrainzComposerID:
|
||||
return "MusicBrainzComposerID";
|
||||
case TagType::MusicBrainzConductorID:
|
||||
return "MusicBrainzConductorID";
|
||||
case TagType::MusicBrainzDirectorID:
|
||||
return "MusicBrainzDirectorID";
|
||||
case TagType::MusicBrainzDiscID:
|
||||
return "MusicBrainzDiscID";
|
||||
case TagType::MusicBrainzLyricistID:
|
||||
return "MusicBrainzLyricistID";
|
||||
case TagType::MusicBrainzProducerID:
|
||||
return "MusicBrainzProducerID";
|
||||
case TagType::MusicBrainzOriginalArtistID:
|
||||
return "MusicBrainzOriginalArtistID";
|
||||
case TagType::MusicBrainzRecordingID:
|
||||
return "MusicBrainzRecordingID";
|
||||
case TagType::MusicBrainzRemixerID:
|
||||
return "MusicBrainzRemixerID";
|
||||
case TagType::MusicBrainzWorkID:
|
||||
return "MusicBrainzWorkID";
|
||||
case TagType::Remixer:
|
||||
return "Remixer";
|
||||
case TagType::Script:
|
||||
return "Script";
|
||||
case TagType::ShowName:
|
||||
return "ShowName";
|
||||
case TagType::ShowNameSortOrder:
|
||||
return "ShowNameSortOrder";
|
||||
case TagType::ShowWorkAndMovement:
|
||||
return "ShowWorkAndMovement";
|
||||
case TagType::Subtitle:
|
||||
return "Subtitle";
|
||||
case TagType::TrackNumber:
|
||||
return "TrackNumber";
|
||||
case TagType::TrackTitle:
|
||||
return "TrackTitle";
|
||||
case TagType::TrackTitleSortOrder:
|
||||
return "TrackTitleSortOrder";
|
||||
case TagType::WorkTitle:
|
||||
return "WorkTitle";
|
||||
case TagType::Writer:
|
||||
return "Writer";
|
||||
case TagType::License:
|
||||
return "License";
|
||||
case TagType::MusicBrainzOriginalReleaseID:
|
||||
return "MusicBrainzOriginalReleaseID";
|
||||
case TagType::MusicBrainzMixerID:
|
||||
return "MusicBrainzMixerID";
|
||||
case TagType::MusicBrainzReleaseArtistID:
|
||||
return "MusicBrainzReleaseArtistID";
|
||||
case TagType::MusicBrainzReleaseGroupID:
|
||||
return "MusicBrainzReleaseGroupID";
|
||||
case TagType::MusicBrainzReleaseID:
|
||||
return "MusicBrainzReleaseID";
|
||||
case TagType::MusicBrainzTrackID:
|
||||
return "MusicBrainzTrackID";
|
||||
case TagType::MusicIPFingerprint:
|
||||
return "MusicIPFingerprint";
|
||||
case TagType::MusicIPPUID:
|
||||
return "MusicIPPUID";
|
||||
case TagType::OriginalAlbum:
|
||||
return "OriginalAlbum";
|
||||
case TagType::OriginalArtist:
|
||||
return "OriginalArtist";
|
||||
case TagType::OriginalFilename:
|
||||
return "OriginalFilename";
|
||||
case TagType::OriginalReleaseDate:
|
||||
return "OriginalReleaseDate";
|
||||
case TagType::OriginalReleaseYear:
|
||||
return "OriginalReleaseYear";
|
||||
case TagType::Podcast:
|
||||
return "Podcast";
|
||||
case TagType::PodcastURL:
|
||||
return "PodcastURL";
|
||||
case TagType::Producer:
|
||||
return "Producer";
|
||||
case TagType::ProducerSortOrder:
|
||||
return "ProducerSortOrder";
|
||||
case TagType::Producers:
|
||||
return "Producers";
|
||||
case TagType::ProducersSortOrder:
|
||||
return "ProducersSortOrder";
|
||||
case TagType::Rating:
|
||||
return "Rating";
|
||||
case TagType::RecordLabel:
|
||||
return "RecordLabel";
|
||||
case TagType::ReleaseCountry:
|
||||
return "ReleaseCountry";
|
||||
case TagType::ReleaseDate:
|
||||
return "ReleaseDate";
|
||||
case TagType::ReleaseStatus:
|
||||
return "ReleaseStatus";
|
||||
case TagType::ReleaseType:
|
||||
return "ReleaseType";
|
||||
case TagType::RemixerSortOrder:
|
||||
return "RemixerSortOrder";
|
||||
case TagType::Remixers:
|
||||
return "Remixers";
|
||||
case TagType::RemixersSortOrder:
|
||||
return "RemixersSortOrder";
|
||||
case TagType::ReplayGainAlbumGain:
|
||||
return "ReplayGainAlbumGain";
|
||||
case TagType::ReplayGainAlbumPeak:
|
||||
return "ReplayGainAlbumPeak";
|
||||
case TagType::ReplayGainAlbumRange:
|
||||
return "ReplayGainAlbumRange";
|
||||
case TagType::ReplayGainReferenceLoudness:
|
||||
return "ReplayGainReferenceLoudness";
|
||||
case TagType::ReplayGainTrackGain:
|
||||
return "ReplayGainTrackGain";
|
||||
case TagType::ReplayGainTrackPeak:
|
||||
return "ReplayGainTrackPeak";
|
||||
case TagType::ReplayGainTrackRange:
|
||||
return "ReplayGainTrackRange";
|
||||
case TagType::TotalDiscs:
|
||||
return "TotalDiscs";
|
||||
case TagType::TotalTracks:
|
||||
return "TotalTracks";
|
||||
case TagType::Website:
|
||||
return "Website";
|
||||
|
||||
case TagType::Count:
|
||||
break;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#define __STDC_CONSTANT_MACROS
|
||||
@@ -27,15 +30,13 @@ extern "C"
|
||||
#include <libavutil/error.h>
|
||||
}
|
||||
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::av
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -49,11 +50,11 @@ namespace lms::av
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
class AudioFileException : public Exception
|
||||
class AudioFileException : public AudioFileParsingException
|
||||
{
|
||||
public:
|
||||
AudioFileException(int avError)
|
||||
: Exception{ "AudioFileException: " + averror_to_string(avError) }
|
||||
: AudioFileParsingException{ averror_to_string(avError) }
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -70,76 +71,104 @@ namespace lms::av
|
||||
}
|
||||
}
|
||||
|
||||
DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
|
||||
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
|
||||
{
|
||||
if (name == "aiff")
|
||||
return ContainerType::AIFF;
|
||||
if (name == "ape")
|
||||
return ContainerType::APE;
|
||||
if (name.starts_with("asf"))
|
||||
return ContainerType::ASF;
|
||||
if (name == "dsf")
|
||||
return ContainerType::DSF;
|
||||
if (name == "flac")
|
||||
return ContainerType::FLAC;
|
||||
if (name.find("mp4") != std::string_view::npos)
|
||||
return ContainerType::MP4;
|
||||
if (name.starts_with("mpc"))
|
||||
return ContainerType::MPC;
|
||||
if (name == "mp3")
|
||||
return ContainerType::MPEG;
|
||||
if (name == "ogg")
|
||||
return ContainerType::Ogg;
|
||||
if (name == "shn")
|
||||
return ContainerType::Shorten;
|
||||
if (name == "tta")
|
||||
return ContainerType::TrueAudio;
|
||||
if (name == "wav")
|
||||
return ContainerType::WAV;
|
||||
if (name == "wv")
|
||||
return ContainerType::WavPack;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
|
||||
{
|
||||
switch (codec)
|
||||
{
|
||||
case AV_CODEC_ID_MP3:
|
||||
return DecodingCodec::MP3;
|
||||
return CodecType::MP3;
|
||||
case AV_CODEC_ID_AAC:
|
||||
return DecodingCodec::AAC;
|
||||
return CodecType::AAC;
|
||||
case AV_CODEC_ID_AC3:
|
||||
return DecodingCodec::AC3;
|
||||
return CodecType::AC3;
|
||||
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;
|
||||
return CodecType::APE;
|
||||
case AV_CODEC_ID_EAC3:
|
||||
return DecodingCodec::EAC3;
|
||||
return CodecType::EAC3;
|
||||
case AV_CODEC_ID_MP4ALS:
|
||||
return DecodingCodec::MP4ALS;
|
||||
return CodecType::MP4ALS;
|
||||
case AV_CODEC_ID_OPUS:
|
||||
return DecodingCodec::OPUS;
|
||||
return CodecType::Opus;
|
||||
case AV_CODEC_ID_SHORTEN:
|
||||
return DecodingCodec::SHORTEN;
|
||||
return CodecType::Shorten;
|
||||
case AV_CODEC_ID_DSD_LSBF:
|
||||
return DecodingCodec::DSD_LSBF;
|
||||
case AV_CODEC_ID_DSD_LSBF_PLANAR:
|
||||
return DecodingCodec::DSD_LSBF_PLANAR;
|
||||
case AV_CODEC_ID_DSD_MSBF:
|
||||
return DecodingCodec::DSD_MSBF;
|
||||
case AV_CODEC_ID_DSD_MSBF_PLANAR:
|
||||
return DecodingCodec::DSD_MSBF_PLANAR;
|
||||
return CodecType::DSD;
|
||||
|
||||
default:
|
||||
return DecodingCodec::UNKNOWN;
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
|
||||
{
|
||||
return std::make_unique<AudioFile>(p);
|
||||
}
|
||||
|
||||
AudioFile::AudioFile(const std::filesystem::path& p)
|
||||
: _p{ p }
|
||||
{
|
||||
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
|
||||
error = avformat_find_stream_info(_context, nullptr);
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
|
||||
avformat_close_input(&_context);
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
@@ -158,9 +187,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 +290,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 +307,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 +329,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 +337,29 @@ namespace lms::av
|
||||
return res;
|
||||
|
||||
res.emplace();
|
||||
|
||||
res->index = streamIndex;
|
||||
res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
|
||||
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||
|
||||
if (avstream->codecpar->bit_rate)
|
||||
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
|
||||
if (avstream->codecpar->bits_per_coded_sample)
|
||||
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
|
||||
else if (avstream->codecpar->bits_per_raw_sample)
|
||||
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_raw_sample);
|
||||
|
||||
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
|
||||
if (avstream->codecpar->channels)
|
||||
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
|
||||
#else
|
||||
if (avstream->codecpar->ch_layout.nb_channels)
|
||||
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
|
||||
#endif
|
||||
res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
|
||||
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||
assert(!res->codecName.empty()); // doc says it is never NULL
|
||||
if (avstream->codecpar->sample_rate)
|
||||
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace lms::av
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
struct AVFormatContext;
|
||||
}
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
struct Picture
|
||||
{
|
||||
std::string mimeType;
|
||||
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||
};
|
||||
|
||||
struct ContainerInfo
|
||||
{
|
||||
std::optional<ContainerType> container;
|
||||
std::string containerName;
|
||||
|
||||
std::size_t bitrate{};
|
||||
std::chrono::milliseconds duration{};
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t index{};
|
||||
std::optional<CodecType> codec;
|
||||
std::string codecName;
|
||||
|
||||
std::optional<size_t> bitrate;
|
||||
std::optional<std::size_t> bitsPerSample;
|
||||
std::optional<std::size_t> channelCount;
|
||||
std::optional<std::size_t> sampleRate;
|
||||
};
|
||||
|
||||
class AudioFile
|
||||
{
|
||||
public:
|
||||
AudioFile(const std::filesystem::path& p);
|
||||
~AudioFile();
|
||||
AudioFile(const AudioFile&) = delete;
|
||||
AudioFile& operator=(const AudioFile&) = delete;
|
||||
|
||||
using MetadataMap = std::unordered_map<std::string, std::string>;
|
||||
|
||||
const std::filesystem::path& getPath() const;
|
||||
ContainerInfo getContainerInfo() const;
|
||||
MetadataMap getMetaData() const;
|
||||
std::vector<StreamInfo> getStreamInfo() const;
|
||||
std::optional<StreamInfo> getBestStreamInfo() const;
|
||||
std::optional<std::size_t> getBestStreamIndex() const;
|
||||
bool hasAttachedPictures() const;
|
||||
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
|
||||
|
||||
private:
|
||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context{};
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
#include "ImageReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const AudioFile& audioFile)
|
||||
{
|
||||
AudioProperties audioProperties;
|
||||
|
||||
const auto containerInfo{ audioFile.getContainerInfo() };
|
||||
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
|
||||
if (!bestStreamInfo)
|
||||
throw AudioFileParsingException{ audioFile.getPath(), "Cannot find best audio stream" };
|
||||
|
||||
if (!containerInfo.container)
|
||||
throw AudioFileParsingException{ audioFile.getPath(), "Unhandled container type '" + containerInfo.containerName + "'" };
|
||||
|
||||
audioProperties.container = *containerInfo.container;
|
||||
audioProperties.duration = containerInfo.duration;
|
||||
audioProperties.codec = bestStreamInfo->codec;
|
||||
audioProperties.bitrate = bestStreamInfo->bitrate;
|
||||
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
|
||||
audioProperties.channelCount = bestStreamInfo->channelCount;
|
||||
audioProperties.sampleRate = bestStreamInfo->sampleRate;
|
||||
|
||||
return audioProperties;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
|
||||
: _audioFile{ std::make_unique<AudioFile>(filePath) }
|
||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
|
||||
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
|
||||
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
|
||||
{
|
||||
}
|
||||
|
||||
AudioFileInfo::~AudioFileInfo() = default;
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return *_audioProperties;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return *_imageReader;
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return *_tagReader;
|
||||
}
|
||||
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AudioFile;
|
||||
class TagReader;
|
||||
class ImageReader;
|
||||
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
|
||||
~AudioFileInfo();
|
||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||
|
||||
private:
|
||||
const AudioProperties& getAudioProperties() const override;
|
||||
const IImageReader& getImageReader() const override;
|
||||
const ITagReader& getTagReader() const override;
|
||||
|
||||
std::unique_ptr<AudioFile> _audioFile;
|
||||
std::unique_ptr<AudioProperties> _audioProperties;
|
||||
std::unique_ptr<TagReader> _tagReader;
|
||||
std::unique_ptr<ImageReader> _imageReader;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
+14
-19
@@ -17,35 +17,30 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvFormatImageReader.hpp"
|
||||
#include "ImageReader.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace lms::metadata::avformat
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
|
||||
ImageReader::ImageReader(const AudioFile& audioFile)
|
||||
: _audioFile{ audioFile }
|
||||
{
|
||||
try
|
||||
{
|
||||
_audioFile = av::parseAudioFile(p);
|
||||
}
|
||||
catch (av::Exception& e)
|
||||
{
|
||||
throw AudioFileParsingException{ e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
AvFormatImageReader::~AvFormatImageReader() = default;
|
||||
ImageReader::~ImageReader() = default;
|
||||
|
||||
void AvFormatImageReader::visitImages(ImageVisitor visitor) const
|
||||
void ImageReader::visitImages(const ImageVisitor& visitor) const
|
||||
{
|
||||
auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) {
|
||||
auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) {
|
||||
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
|
||||
} };
|
||||
|
||||
_audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) {
|
||||
_audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
|
||||
Image image;
|
||||
image.data = picture.data;
|
||||
image.mimeType = picture.mimeType;
|
||||
@@ -58,4 +53,4 @@ namespace lms::metadata::avformat
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lms::metadata::avformat
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AudioFile;
|
||||
|
||||
class ImageReader : public IImageReader
|
||||
{
|
||||
public:
|
||||
ImageReader(const AudioFile& audioFile);
|
||||
~ImageReader() override;
|
||||
|
||||
ImageReader(const ImageReader&) = delete;
|
||||
ImageReader& operator=(const ImageReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitImages(const ImageVisitor& visitor) const override;
|
||||
|
||||
const AudioFile& _audioFile;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
+12
-36
@@ -17,15 +17,12 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvFormatTagReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
namespace lms::metadata::avformat
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -146,41 +143,20 @@ namespace lms::metadata::avformat
|
||||
};
|
||||
} // namespace
|
||||
|
||||
AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug)
|
||||
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
|
||||
: _audioFile{ audioFile }
|
||||
, _metaDataMap{ audioFile.getMetaData() }
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto audioFile{ av::parseAudioFile(p) };
|
||||
|
||||
_audioProperties.duration = audioFile->getContainerInfo().duration;
|
||||
|
||||
const auto bestAudioStream{ audioFile->getBestStreamInfo() };
|
||||
if (bestAudioStream)
|
||||
{
|
||||
_audioProperties.bitrate = bestAudioStream->bitrate;
|
||||
_audioProperties.bitsPerSample = bestAudioStream->bitsPerSample;
|
||||
_audioProperties.channelCount = bestAudioStream->channelCount;
|
||||
_audioProperties.sampleRate = bestAudioStream->sampleRate;
|
||||
}
|
||||
|
||||
_containerInfo = audioFile->getContainerInfo();
|
||||
_metaDataMap = audioFile->getMetaData();
|
||||
|
||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
for (const auto& [key, value] : _metaDataMap)
|
||||
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
|
||||
}
|
||||
}
|
||||
catch (av::Exception& e)
|
||||
{
|
||||
throw AudioFileParsingException{ e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
AvFormatTagReader::~AvFormatTagReader() = default;
|
||||
TagReader::~TagReader() = default;
|
||||
|
||||
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(avFormatTagMapping))
|
||||
@@ -200,7 +176,7 @@ namespace lms::metadata::avformat
|
||||
}
|
||||
}
|
||||
|
||||
void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
|
||||
void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itValues{ _metaDataMap.find(std::string{ key }) };
|
||||
if (itValues == std::cend(_metaDataMap))
|
||||
@@ -209,14 +185,14 @@ namespace lms::metadata::avformat
|
||||
visitor(itValues->second);
|
||||
}
|
||||
|
||||
void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
{
|
||||
visitTagValues("PERFORMER", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
|
||||
void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
{
|
||||
// MPEG files: need to visit LYRICS-language entries
|
||||
for (const auto& [tag, value] : _metaDataMap)
|
||||
@@ -234,4 +210,4 @@ namespace lms::metadata::avformat
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
} // namespace lms::metadata::avformat
|
||||
} // namespace lms::audio::ffmpeg
|
||||
+11
-15
@@ -19,31 +19,27 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
#include "ITagReader.hpp"
|
||||
|
||||
namespace lms::metadata::avformat
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AvFormatTagReader : public ITagReader
|
||||
class TagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
||||
~AvFormatTagReader() override;
|
||||
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
||||
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
||||
TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
|
||||
~TagReader() override;
|
||||
TagReader(const TagReader&) = delete;
|
||||
TagReader& operator=(const TagReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||
|
||||
AudioProperties _audioProperties;
|
||||
av::IAudioFile::MetadataMap _metaDataMap;
|
||||
av::ContainerInfo _containerInfo;
|
||||
const AudioFile& _audioFile;
|
||||
AudioFile::MetadataMap _metaDataMap;
|
||||
};
|
||||
} // namespace lms::metadata::avformat
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -27,17 +27,21 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/TranscodeTypes.hpp"
|
||||
|
||||
namespace lms::av
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters)
|
||||
{
|
||||
return std::make_unique<ffmpeg::Transcoder>(parameters);
|
||||
}
|
||||
} // namespace lms::audio
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
|
||||
{
|
||||
return std::make_unique<Transcoder>(inputParameters, outputParameters);
|
||||
}
|
||||
|
||||
static std::atomic<size_t> globalId{};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
@@ -48,10 +52,10 @@ namespace lms::av
|
||||
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams)
|
||||
Transcoder::Transcoder(const TranscodeParameters& parameters)
|
||||
: _debugId{ globalId++ }
|
||||
, _inputParams{ inputParams }
|
||||
, _outputParams{ outputParams }
|
||||
, _inputParams{ parameters.inputParameters }
|
||||
, _outputParams{ parameters.outputParameters }
|
||||
{
|
||||
start();
|
||||
}
|
||||
@@ -65,18 +69,18 @@ namespace lms::av
|
||||
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(_inputParams.file))
|
||||
throw Exception{ "File " + _inputParams.file.string() + " does not exist!" };
|
||||
if (!std::filesystem::is_regular_file(_inputParams.file))
|
||||
throw Exception{ "File " + _inputParams.file.string() + " is not regular!" };
|
||||
if (!std::filesystem::exists(_inputParams.filePath))
|
||||
throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
|
||||
if (!std::filesystem::is_regular_file(_inputParams.filePath))
|
||||
throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
// TODO store/raise e.code()
|
||||
throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() };
|
||||
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
|
||||
}
|
||||
|
||||
LOG(INFO, "Transcoding file " << _inputParams.file);
|
||||
LOG(INFO, "Transcoding file " << _inputParams.filePath);
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
@@ -101,14 +105,7 @@ namespace lms::av
|
||||
|
||||
// Input file
|
||||
args.emplace_back("-i");
|
||||
args.emplace_back(_inputParams.file.string());
|
||||
|
||||
// Stream mapping, if set
|
||||
if (_inputParams.streamIndex)
|
||||
{
|
||||
args.emplace_back("-map");
|
||||
args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex));
|
||||
}
|
||||
args.emplace_back(_inputParams.filePath.string());
|
||||
|
||||
if (_outputParams.stripMetadata)
|
||||
{
|
||||
@@ -121,11 +118,16 @@ namespace lms::av
|
||||
args.emplace_back("-vn");
|
||||
|
||||
// Output bitrates
|
||||
if (_outputParams.bitrate)
|
||||
{
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_outputParams.bitrate));
|
||||
args.emplace_back(std::to_string(*_outputParams.bitrate));
|
||||
}
|
||||
|
||||
// Codecs and formats
|
||||
switch (_outputParams.format)
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
args.emplace_back("-f");
|
||||
@@ -161,7 +163,8 @@ namespace lms::av
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParams.format)) + ")" };
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
|
||||
}
|
||||
}
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
@@ -199,7 +202,10 @@ namespace lms::av
|
||||
|
||||
std::string_view Transcoder::getOutputMimeType() const
|
||||
{
|
||||
switch (_outputParams.format)
|
||||
// TODO: use input mime type
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
return "audio/mpeg";
|
||||
@@ -212,6 +218,7 @@ namespace lms::av
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
return "audio/webm";
|
||||
}
|
||||
}
|
||||
|
||||
return "application/octet-stream"; // default, should not happen
|
||||
}
|
||||
@@ -222,5 +229,4 @@ namespace lms::av
|
||||
|
||||
return _childProcess->finished();
|
||||
}
|
||||
|
||||
} // namespace lms::av
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -19,19 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "av/ITranscoder.hpp"
|
||||
#include "audio/ITranscoder.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
class IChildProcess;
|
||||
}
|
||||
|
||||
namespace lms::av
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class Transcoder : public ITranscoder
|
||||
{
|
||||
public:
|
||||
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
||||
Transcoder(const TranscodeParameters& parameters);
|
||||
~Transcoder() override;
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
Transcoder& operator=(const Transcoder&) = delete;
|
||||
@@ -41,15 +41,15 @@ namespace lms::av
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
|
||||
|
||||
std::string_view getOutputMimeType() const override;
|
||||
const OutputParameters& getOutputParameters() const override { return _outputParams; }
|
||||
const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; }
|
||||
|
||||
bool finished() const override;
|
||||
static void init();
|
||||
void start();
|
||||
|
||||
const std::size_t _debugId{};
|
||||
const InputParameters _inputParams;
|
||||
const OutputParameters _outputParams;
|
||||
const TranscodeInputParameters _inputParams;
|
||||
const TranscodeOutputParameters _outputParams;
|
||||
std::unique_ptr<core::IChildProcess> _childProcess;
|
||||
};
|
||||
} // namespace lms::av
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::metadata::avformat::utils
|
||||
namespace lms::audio::ffmpeg::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
// TODO: use av capability to retrieve supported formats
|
||||
// TODO: list demuxers to retrieve supported formats
|
||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||
".aac",
|
||||
".alac",
|
||||
@@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils
|
||||
};
|
||||
return fileExtensions;
|
||||
}
|
||||
} // namespace lms::metadata::avformat::utils
|
||||
} // namespace lms::audio::ffmpeg::utils
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
namespace lms::metadata::avformat::utils
|
||||
namespace lms::audio::ffmpeg::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
} // namespace lms::metadata::avformat::utils
|
||||
} // namespace lms::audio::ffmpeg::utils
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
#if LMS_TAGLIB_HAS_SHORTEN
|
||||
#include <taglib/shortenfile.h>
|
||||
#endif
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const ::TagLib::File& file)
|
||||
{
|
||||
assert(file.audioProperties());
|
||||
const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
|
||||
|
||||
AudioProperties audioProperties;
|
||||
|
||||
// Common properties
|
||||
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
|
||||
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
|
||||
audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
|
||||
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
|
||||
|
||||
// Guess container from the file type
|
||||
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::APE;
|
||||
audioProperties.codec = CodecType::APE; // TODO version?
|
||||
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::ASF;
|
||||
|
||||
switch (asfFile->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::ASF::Properties::Codec::WMA1:
|
||||
audioProperties.codec = CodecType::WMA1;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA2:
|
||||
audioProperties.codec = CodecType::WMA2;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
|
||||
audioProperties.codec = CodecType::WMA9Lossless;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
|
||||
audioProperties.codec = CodecType::WMA9Pro;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::Unknown:
|
||||
audioProperties.codec = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* dsfFile{ dynamic_cast<const ::TagLib::DSF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::DSF;
|
||||
audioProperties.codec = CodecType::DSD;
|
||||
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* flacFile{ dynamic_cast<const ::TagLib::FLAC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::FLAC;
|
||||
audioProperties.codec = CodecType::FLAC;
|
||||
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MP4;
|
||||
switch (mp4File->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::MP4::Properties::Codec::AAC:
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::ALAC:
|
||||
audioProperties.codec = CodecType::ALAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::Unknown:
|
||||
audioProperties.codec = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MPC;
|
||||
|
||||
switch (mpcFile->audioProperties()->mpcVersion())
|
||||
{
|
||||
case 7:
|
||||
audioProperties.codec = CodecType::MPC7;
|
||||
break;
|
||||
case 8:
|
||||
audioProperties.codec = CodecType::MPC8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
|
||||
{
|
||||
const auto& properties{ *mpegFile->audioProperties() };
|
||||
|
||||
audioProperties.container = ContainerType::MPEG;
|
||||
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
|
||||
&& mpegFile->audioProperties()->layer() == 3)
|
||||
audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
|
||||
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Opus;
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Vorbis;
|
||||
}
|
||||
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::AIFF;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WAV;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#if LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::Shorten;
|
||||
audioProperties.codec = CodecType::Shorten;
|
||||
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::TrueAudio;
|
||||
audioProperties.codec = CodecType::TrueAudio;
|
||||
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WavPack;
|
||||
audioProperties.codec = CodecType::WavPack;
|
||||
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
|
||||
return audioProperties;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
|
||||
: _filePath{ filePath }
|
||||
, _file{ utils::parseFile(filePath, readStyle) }
|
||||
, _audioProperties{ computeAudioProperties(*_file) }
|
||||
, _tagReader{ *_file, enableExtraDebugLogs }
|
||||
, _imageReader{ *_file }
|
||||
{
|
||||
}
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return _audioProperties;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return _imageReader;
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return _tagReader;
|
||||
}
|
||||
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "ImageReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
|
||||
|
||||
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;
|
||||
const AudioProperties _audioProperties;
|
||||
const TagReader _tagReader;
|
||||
const ImageReader _imageReader;
|
||||
};
|
||||
} // namespace lms::audio::taglib
|
||||
+125
-131
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagLibImageReader.hpp"
|
||||
#include "ImageReader.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
@@ -33,16 +33,14 @@
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::metadata::taglib
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -50,47 +48,47 @@ namespace lms::metadata::taglib
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Other:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Media:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Band:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
@@ -101,47 +99,47 @@ namespace lms::metadata::taglib
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TagLib::ASF::Picture::Type::Other:
|
||||
case ::TagLib::ASF::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case TagLib::ASF::Picture::Type::FileIcon:
|
||||
case ::TagLib::ASF::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case TagLib::ASF::Picture::Type::OtherFileIcon:
|
||||
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case TagLib::ASF::Picture::Type::FrontCover:
|
||||
case ::TagLib::ASF::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case TagLib::ASF::Picture::Type::BackCover:
|
||||
case ::TagLib::ASF::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case TagLib::ASF::Picture::Type::LeafletPage:
|
||||
case ::TagLib::ASF::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case TagLib::ASF::Picture::Type::Media:
|
||||
case ::TagLib::ASF::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case TagLib::ASF::Picture::Type::LeadArtist:
|
||||
case ::TagLib::ASF::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case TagLib::ASF::Picture::Type::Artist:
|
||||
case ::TagLib::ASF::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case TagLib::ASF::Picture::Type::Conductor:
|
||||
case ::TagLib::ASF::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case TagLib::ASF::Picture::Type::Band:
|
||||
case ::TagLib::ASF::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case TagLib::ASF::Picture::Type::Composer:
|
||||
case ::TagLib::ASF::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case TagLib::ASF::Picture::Type::Lyricist:
|
||||
case ::TagLib::ASF::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case TagLib::ASF::Picture::Type::RecordingLocation:
|
||||
case ::TagLib::ASF::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case TagLib::ASF::Picture::Type::DuringRecording:
|
||||
case ::TagLib::ASF::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case TagLib::ASF::Picture::Type::DuringPerformance:
|
||||
case ::TagLib::ASF::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case TagLib::ASF::Picture::Type::MovieScreenCapture:
|
||||
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case TagLib::ASF::Picture::Type::ColouredFish:
|
||||
case ::TagLib::ASF::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case TagLib::ASF::Picture::Type::Illustration:
|
||||
case ::TagLib::ASF::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case TagLib::ASF::Picture::Type::BandLogo:
|
||||
case ::TagLib::ASF::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case TagLib::ASF::Picture::Type::PublisherLogo:
|
||||
case ::TagLib::ASF::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
@@ -152,47 +150,47 @@ namespace lms::metadata::taglib
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TagLib::FLAC::Picture::Type::Other:
|
||||
case ::TagLib::FLAC::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case TagLib::FLAC::Picture::Type::FileIcon:
|
||||
case ::TagLib::FLAC::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case TagLib::FLAC::Picture::Type::OtherFileIcon:
|
||||
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case TagLib::FLAC::Picture::Type::FrontCover:
|
||||
case ::TagLib::FLAC::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case TagLib::FLAC::Picture::Type::BackCover:
|
||||
case ::TagLib::FLAC::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case TagLib::FLAC::Picture::Type::LeafletPage:
|
||||
case ::TagLib::FLAC::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case TagLib::FLAC::Picture::Type::Media:
|
||||
case ::TagLib::FLAC::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case TagLib::FLAC::Picture::Type::LeadArtist:
|
||||
case ::TagLib::FLAC::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case TagLib::FLAC::Picture::Type::Artist:
|
||||
case ::TagLib::FLAC::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case TagLib::FLAC::Picture::Type::Conductor:
|
||||
case ::TagLib::FLAC::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case TagLib::FLAC::Picture::Type::Band:
|
||||
case ::TagLib::FLAC::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case TagLib::FLAC::Picture::Type::Composer:
|
||||
case ::TagLib::FLAC::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case TagLib::FLAC::Picture::Type::Lyricist:
|
||||
case ::TagLib::FLAC::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case TagLib::FLAC::Picture::Type::RecordingLocation:
|
||||
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case TagLib::FLAC::Picture::Type::DuringRecording:
|
||||
case ::TagLib::FLAC::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case TagLib::FLAC::Picture::Type::DuringPerformance:
|
||||
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case TagLib::FLAC::Picture::Type::MovieScreenCapture:
|
||||
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case TagLib::FLAC::Picture::Type::ColouredFish:
|
||||
case ::TagLib::FLAC::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case TagLib::FLAC::Picture::Type::Illustration:
|
||||
case ::TagLib::FLAC::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case TagLib::FLAC::Picture::Type::BandLogo:
|
||||
case ::TagLib::FLAC::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case TagLib::FLAC::Picture::Type::PublisherLogo:
|
||||
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
@@ -203,44 +201,44 @@ namespace lms::metadata::taglib
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case TagLib::MP4::CoverArt::Format::BMP:
|
||||
case ::TagLib::MP4::CoverArt::Format::BMP:
|
||||
return "image/bmp";
|
||||
case TagLib::MP4::CoverArt::Format::GIF:
|
||||
case ::TagLib::MP4::CoverArt::Format::GIF:
|
||||
return "image/gif";
|
||||
case TagLib::MP4::CoverArt::Format::JPEG:
|
||||
case ::TagLib::MP4::CoverArt::Format::JPEG:
|
||||
return "image/jpeg";
|
||||
case TagLib::MP4::CoverArt::Format::PNG:
|
||||
case ::TagLib::MP4::CoverArt::Format::PNG:
|
||||
return "image/png";
|
||||
case TagLib::MP4::CoverArt::Format::Unknown:
|
||||
case ::TagLib::MP4::CoverArt::Format::Unknown:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
|
||||
{
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
|
||||
return Image::Type::FrontCover;
|
||||
else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
|
||||
return Image::Type::BackCover;
|
||||
|
||||
return Image::Type::Unknown;
|
||||
}
|
||||
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
|
||||
void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor)
|
||||
void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
const auto& frameListMap{ id3v2Tags.frameListMap() };
|
||||
|
||||
for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
|
||||
{
|
||||
const auto* attachedPictureFrame{ dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(frame) };
|
||||
const auto* attachedPictureFrame{ dynamic_cast<const ::TagLib::ID3v2::AttachedPictureFrame*>(frame) };
|
||||
if (!attachedPictureFrame)
|
||||
continue;
|
||||
|
||||
TagLib::ByteVector picture{ attachedPictureFrame->picture() };
|
||||
::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
@@ -252,15 +250,15 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor)
|
||||
void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
|
||||
for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
|
||||
{
|
||||
TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
|
||||
::TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
|
||||
if (!asfPicture.isValid())
|
||||
continue;
|
||||
|
||||
TagLib::ByteVector picture{ asfPicture.picture() };
|
||||
::TagLib::ByteVector picture{ asfPicture.picture() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
@@ -273,22 +271,22 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor)
|
||||
void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
|
||||
const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
|
||||
if (!coverItem.isValid())
|
||||
return;
|
||||
|
||||
#if TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList)
|
||||
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
|
||||
return;
|
||||
#endif
|
||||
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
|
||||
::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
|
||||
|
||||
bool firstCover{ true };
|
||||
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
|
||||
{
|
||||
TagLib::ByteVector picture{ coverArt.data() };
|
||||
::TagLib::ByteVector picture{ coverArt.data() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
@@ -303,11 +301,11 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void visitFLACImages(const TagLib::List<TagLib::FLAC::Picture*> pictureList, TagLibImageReader::ImageVisitor visitor)
|
||||
void visitFLACImages(const ::TagLib::List<TagLib::FLAC::Picture*>& pictureList, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
for (TagLib::FLAC::Picture* flacPicture : pictureList)
|
||||
{
|
||||
TagLib::ByteVector picture{ flacPicture->data() };
|
||||
::TagLib::ByteVector picture{ flacPicture->data() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
@@ -320,14 +318,14 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor)
|
||||
void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
const TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
|
||||
for (const TagLib::VariantMap& pictureProperty : pictureProperties)
|
||||
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
const ::TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
|
||||
for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
|
||||
{
|
||||
Image image;
|
||||
TagLib::ByteVector picture;
|
||||
::TagLib::ByteVector picture;
|
||||
|
||||
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
|
||||
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
|
||||
@@ -344,81 +342,77 @@ namespace lms::metadata::taglib
|
||||
if (!image.data.empty())
|
||||
visitor(image);
|
||||
}
|
||||
|
||||
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
|
||||
: _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) }
|
||||
ImageReader::ImageReader(::TagLib::File& file)
|
||||
: _file{ file }
|
||||
{
|
||||
if (!_file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw AudioFileParsingException{};
|
||||
}
|
||||
}
|
||||
|
||||
void TagLibImageReader::visitImages(ImageVisitor visitor) const
|
||||
ImageReader::~ImageReader() = default;
|
||||
|
||||
void ImageReader::visitImages(const ImageVisitor& visitor) const
|
||||
{
|
||||
// MP3
|
||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
|
||||
visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
|
||||
}
|
||||
// MP4
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||
else if (const TagLib::MP4::File * mp4File{ dynamic_cast<const TagLib::MP4::File*>(&_file) })
|
||||
{
|
||||
visitMP4Images(*mp4File, std::move(visitor));
|
||||
visitMP4Images(*mp4File, visitor);
|
||||
}
|
||||
// WMA
|
||||
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||
else if (const TagLib::ASF::File * asfFile{ dynamic_cast<const TagLib::ASF::File*>(&_file) })
|
||||
{
|
||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
visitASFImages(*tag, std::move(visitor));
|
||||
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
visitASFImages(*tag, visitor);
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // usage discouraged
|
||||
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
|
||||
visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
|
||||
else
|
||||
visitFLACImages(flacFile->pictureList(), std::move(visitor));
|
||||
visitFLACImages(flacFile->pictureList(), visitor);
|
||||
}
|
||||
// Ogg vorbis
|
||||
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.get()) })
|
||||
else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<const TagLib::Ogg::Vorbis::File*>(&_file) })
|
||||
{
|
||||
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
|
||||
visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
|
||||
}
|
||||
// Ogg Opus
|
||||
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.get()) })
|
||||
else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(&_file) })
|
||||
{
|
||||
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
|
||||
visitFLACImages(opusFile->tag()->pictureList(), visitor);
|
||||
}
|
||||
// Aiff
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(&_file) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
|
||||
visitID3V2Images(*aiffFile->tag(), visitor);
|
||||
}
|
||||
// Wav
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(&_file) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
|
||||
visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
|
||||
{
|
||||
if (mpcFile->hasAPETag())
|
||||
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
|
||||
visitAPEImages(*mpcFile->APETag(), visitor);
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
|
||||
{
|
||||
if (wavPackFile->hasAPETag())
|
||||
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
|
||||
visitAPEImages(*wavPackFile->APETag(), visitor);
|
||||
}
|
||||
}
|
||||
} // namespace lms::metadata::taglib
|
||||
} // namespace lms::audio::taglib
|
||||
+12
-13
@@ -19,27 +19,26 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IImageReader.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace lms::av
|
||||
namespace TagLib
|
||||
{
|
||||
class IAudioFile;
|
||||
class File;
|
||||
}
|
||||
|
||||
namespace lms::metadata::avformat
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class AvFormatImageReader : public IImageReader
|
||||
class ImageReader : public IImageReader
|
||||
{
|
||||
public:
|
||||
AvFormatImageReader(const std::filesystem::path& p);
|
||||
~AvFormatImageReader() override;
|
||||
|
||||
AvFormatImageReader(const AvFormatImageReader&) = delete;
|
||||
AvFormatImageReader& operator=(const AvFormatImageReader&) = delete;
|
||||
ImageReader(TagLib::File& _file);
|
||||
~ImageReader() override;
|
||||
ImageReader(const ImageReader&) = delete;
|
||||
ImageReader& operator=(const ImageReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitImages(ImageVisitor visitor) const override;
|
||||
void visitImages(const ImageVisitor& visitor) const override;
|
||||
|
||||
std::unique_ptr<av::IAudioFile> _audioFile;
|
||||
TagLib::File& _file;
|
||||
};
|
||||
} // namespace lms::metadata::avformat
|
||||
} // namespace lms::audio::taglib
|
||||
+10
-5
@@ -22,15 +22,20 @@
|
||||
#include <taglib/taglib.h>
|
||||
|
||||
#if (TAGLIB_MAJOR_VERSION >= 2)
|
||||
#define TAGLIB_HAS_DSF 1
|
||||
#define LMS_TAGLIB_HAS_DSF 1
|
||||
#endif
|
||||
|
||||
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
||||
// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
|
||||
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
|
||||
#define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1
|
||||
#endif
|
||||
|
||||
// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
|
||||
// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
|
||||
#define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
|
||||
#define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
|
||||
#endif
|
||||
|
||||
// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1))
|
||||
#define LMS_TAGLIB_HAS_SHORTEN 1
|
||||
#endif
|
||||
+59
-109
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagLibTagReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/apeproperties.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/flacfile.h>
|
||||
@@ -38,33 +37,25 @@
|
||||
#include <taglib/speexfile.h>
|
||||
#include <taglib/synchronizedlyricsframe.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/unsynchronizedlyricsframe.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if TAGLIB_HAS_DSF
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsdifffile.h>
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::metadata::taglib
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class TagParsingFailedException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
// Mapping to internal taglib names and/or common alternative custom names
|
||||
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
|
||||
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||
@@ -181,7 +172,7 @@ namespace lms::metadata::taglib
|
||||
{ TagType::Writer, { "WRITER" } },
|
||||
};
|
||||
|
||||
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
|
||||
void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
|
||||
{
|
||||
for (auto&& [tag, values] : src)
|
||||
{
|
||||
@@ -190,49 +181,36 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file)
|
||||
void dedupTagValues(TagLib::PropertyMap& propertyMap)
|
||||
{
|
||||
for (auto& [key, values] : propertyMap)
|
||||
{
|
||||
if (values.size() <= 1)
|
||||
continue;
|
||||
|
||||
TagLib::StringList newList;
|
||||
for (const TagLib::String& value : values)
|
||||
::TagLib::StringList newList;
|
||||
for (const ::TagLib::String& value : values)
|
||||
{
|
||||
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; }))
|
||||
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; }))
|
||||
newList.append(value);
|
||||
}
|
||||
|
||||
if (values != newList)
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
|
||||
LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
|
||||
values = newList;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
|
||||
: _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) }
|
||||
TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
|
||||
: _file{ file }
|
||||
{
|
||||
if (!_file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw AudioFileParsingException{ "Parsing failed" };
|
||||
}
|
||||
_propertyMap = _file.properties();
|
||||
|
||||
if (!_file->audioProperties())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||
throw AudioFileNoAudioPropertiesException{};
|
||||
}
|
||||
|
||||
computeAudioProperties();
|
||||
|
||||
_propertyMap = _file->properties();
|
||||
|
||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
enableExtraDebugLogs &= core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG);
|
||||
if (enableExtraDebugLogs)
|
||||
{
|
||||
for (const auto& [key, values] : _propertyMap)
|
||||
{
|
||||
@@ -245,7 +223,7 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
|
||||
// Some tags may not be known by TagLib
|
||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag) {
|
||||
auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) {
|
||||
if (!apeTag)
|
||||
return;
|
||||
|
||||
@@ -254,7 +232,7 @@ namespace lms::metadata::taglib
|
||||
|
||||
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
|
||||
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
|
||||
dedupTagValues(_propertyMap, p);
|
||||
dedupTagValues(_propertyMap);
|
||||
|
||||
const auto& frameListMap{ id3v2Tags.frameListMap() };
|
||||
|
||||
@@ -264,26 +242,30 @@ namespace lms::metadata::taglib
|
||||
|
||||
// consider each frame hold a different set of lyrics
|
||||
// Synchronized lyrics frames
|
||||
for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
|
||||
{
|
||||
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
|
||||
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
|
||||
if (!lyricsFrame)
|
||||
continue; // TODO log or assert?
|
||||
|
||||
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
|
||||
std::string lyrics;
|
||||
for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
|
||||
for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
|
||||
{
|
||||
std::chrono::milliseconds timestamp{};
|
||||
switch (lyricsFrame->timestampFormat())
|
||||
{
|
||||
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
|
||||
timestamp = std::chrono::milliseconds{ synchedText.time };
|
||||
break;
|
||||
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
|
||||
timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 };
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
|
||||
{
|
||||
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
|
||||
if (properties && properties->sampleRate())
|
||||
timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
|
||||
}
|
||||
break;
|
||||
case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -298,9 +280,9 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
|
||||
// Unsynchronized lyrics frames
|
||||
for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
|
||||
{
|
||||
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
|
||||
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
|
||||
if (!lyricsFrame)
|
||||
continue; // TODO log or assert?
|
||||
|
||||
@@ -310,9 +292,9 @@ namespace lms::metadata::taglib
|
||||
};
|
||||
|
||||
// WMA
|
||||
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||
if (const ::TagLib::ASF::File * asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&_file) })
|
||||
{
|
||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
{
|
||||
for (const auto& [name, attributeList] : tag->attributeListMap())
|
||||
{
|
||||
@@ -320,19 +302,19 @@ namespace lms::metadata::taglib
|
||||
continue;
|
||||
|
||||
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
|
||||
if (debug)
|
||||
if (enableExtraDebugLogs)
|
||||
{
|
||||
for (const auto& attribute : attributeList)
|
||||
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "<Non unicode>" }) << "'");
|
||||
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "<Non unicode>" }) << "'");
|
||||
}
|
||||
|
||||
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
|
||||
continue;
|
||||
|
||||
TagLib::StringList strAttributes;
|
||||
for (const TagLib::ASF::Attribute& attribute : attributeList)
|
||||
::TagLib::StringList strAttributes;
|
||||
for (const ::TagLib::ASF::Attribute& attribute : attributeList)
|
||||
{
|
||||
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
|
||||
if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
|
||||
strAttributes.append(attribute.toString());
|
||||
}
|
||||
|
||||
@@ -356,23 +338,23 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
// MP3
|
||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
processID3v2Tags(*mp3File->ID3v2Tag());
|
||||
processID3v2Tags(*mp3File->ID3v2Tag(false));
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
// MP4
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||
else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&_file) })
|
||||
{
|
||||
// Taglib does not expose rtng in properties
|
||||
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||
if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||
{
|
||||
#if TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (rtngItem.type() == TagLib::MP4::Item::Type::Byte)
|
||||
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
|
||||
#endif
|
||||
_propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) };
|
||||
_propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
|
||||
}
|
||||
|
||||
if (!_propertyMap.contains("ORIGINALDATE"))
|
||||
@@ -386,7 +368,7 @@ namespace lms::metadata::taglib
|
||||
auto itOrigDateTag{ tags.find(origDateString) };
|
||||
if (itOrigDateTag != std::cend(tags))
|
||||
{
|
||||
const TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
|
||||
const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
|
||||
if (!dates.isEmpty())
|
||||
{
|
||||
_propertyMap["ORIGINALDATE"] = dates.front();
|
||||
@@ -397,68 +379,36 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||
else if (::TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
|
||||
{
|
||||
getAPETags(wavPackFile->APETag());
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // discouraged usage
|
||||
processID3v2Tags(*flacFile->ID3v2Tag());
|
||||
}
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<const TagLib::RIFF::AIFF::File*>(&_file) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
processID3v2Tags(*aiffFile->tag());
|
||||
}
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<const TagLib::RIFF::WAV::File*>(&_file) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
processID3v2Tags(*wavFile->ID3v2Tag());
|
||||
}
|
||||
}
|
||||
|
||||
TagLibTagReader::~TagLibTagReader() = default;
|
||||
TagReader::~TagReader() = default;
|
||||
|
||||
void TagLibTagReader::computeAudioProperties()
|
||||
{
|
||||
const TagLib::AudioProperties* properties{ _file->audioProperties() };
|
||||
|
||||
// Common properties
|
||||
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||
_audioProperties.channelCount = static_cast<std::size_t>(_file->audioProperties()->channels());
|
||||
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
||||
|
||||
if (const auto* apeProperties{ dynamic_cast<const TagLib::APE::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = apeProperties->bitsPerSample();
|
||||
if (const auto* asfProperties{ dynamic_cast<const TagLib::ASF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = asfProperties->bitsPerSample();
|
||||
else if (const auto* flacProperties{ dynamic_cast<const TagLib::FLAC::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = flacProperties->bitsPerSample();
|
||||
else if (const auto* mp4Properties{ dynamic_cast<const TagLib::MP4::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
|
||||
else if (const auto* wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
|
||||
else if (const auto* aiffProperties{ dynamic_cast<const TagLib::RIFF::AIFF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
|
||||
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
|
||||
#if TAGLIB_HAS_DSF
|
||||
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSDIFF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ tagLibTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(tagLibTagMapping))
|
||||
@@ -478,19 +428,19 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
|
||||
void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
|
||||
{
|
||||
TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 };
|
||||
::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 };
|
||||
|
||||
auto itValues{ _propertyMap.find(key) };
|
||||
if (itValues == std::cend(_propertyMap))
|
||||
return;
|
||||
|
||||
for (const TagLib::String& value : itValues->second)
|
||||
for (const ::TagLib::String& value : itValues->second)
|
||||
visitor(value.to8Bit(true));
|
||||
}
|
||||
|
||||
void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
{
|
||||
visitTagValues("PERFORMER", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
@@ -505,7 +455,7 @@ namespace lms::metadata::taglib
|
||||
assert(rolePos != std::string::npos);
|
||||
|
||||
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
|
||||
for (const TagLib::String& value : values)
|
||||
for (const ::TagLib::String& value : values)
|
||||
{
|
||||
const std::string name{ value.to8Bit(true) };
|
||||
visitor(role, name);
|
||||
@@ -514,7 +464,7 @@ namespace lms::metadata::taglib
|
||||
}
|
||||
}
|
||||
|
||||
void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
{
|
||||
if (!_id3v2Lyrics.empty())
|
||||
{
|
||||
@@ -529,4 +479,4 @@ namespace lms::metadata::taglib
|
||||
});
|
||||
}
|
||||
}
|
||||
} // namespace lms::metadata::taglib
|
||||
} // namespace lms::audio::taglib
|
||||
+16
-17
@@ -19,38 +19,37 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
|
||||
#include "ITagReader.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace lms::metadata::taglib
|
||||
namespace TagLib
|
||||
{
|
||||
class TagLibTagReader : public ITagReader
|
||||
class File;
|
||||
}
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class TagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
||||
~TagLibTagReader() override;
|
||||
TagLibTagReader(const TagLibTagReader&) = delete;
|
||||
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
||||
TagReader(::TagLib::File& file, bool enableExtraDebugLogs);
|
||||
~TagReader() override;
|
||||
|
||||
TagReader(const TagReader&) = delete;
|
||||
TagReader& operator=(const TagReader&) = delete;
|
||||
|
||||
private:
|
||||
void computeAudioProperties();
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||
|
||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||
|
||||
std::unique_ptr<TagLib::File> _file;
|
||||
AudioProperties _audioProperties;
|
||||
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||
::TagLib::File& _file;
|
||||
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
||||
};
|
||||
} // namespace lms::metadata::taglib
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -39,15 +39,17 @@
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if TAGLIB_HAS_DSF
|
||||
#include <taglib/dsdifffile.h>
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::metadata::taglib::utils
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
@@ -56,23 +58,23 @@ namespace lms::metadata::taglib::utils
|
||||
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
|
||||
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
|
||||
".wav",
|
||||
#if TAGLIB_HAS_DSF
|
||||
".dsf", ".dff", ".dsdiff"
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
".dsf"
|
||||
#endif
|
||||
};
|
||||
|
||||
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||
}
|
||||
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast:
|
||||
case ParserOptions::AudioPropertiesReadStyle::Fast:
|
||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserReadStyle::Average:
|
||||
case ParserOptions::AudioPropertiesReadStyle::Average:
|
||||
return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserReadStyle::Accurate:
|
||||
case ParserOptions::AudioPropertiesReadStyle::Accurate:
|
||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
@@ -100,8 +102,9 @@ namespace lms::metadata::taglib::utils
|
||||
return TagLib::FileStream{ fd, true };
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
|
||||
{
|
||||
constexpr bool readAudioProperties{ true };
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
if (extension.empty())
|
||||
@@ -149,11 +152,9 @@ namespace lms::metadata::taglib::utils
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "WAV")
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if TAGLIB_HAS_DSF
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (ext == "DSF")
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "DFF" || ext == "DSDIFF")
|
||||
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
@@ -162,8 +163,9 @@ namespace lms::metadata::taglib::utils
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
|
||||
{
|
||||
constexpr bool readAudioProperties{ true };
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
if (TagLib::MPEG::File::isSupported(stream))
|
||||
@@ -200,11 +202,9 @@ namespace lms::metadata::taglib::utils
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::RIFF::WAV::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if TAGLIB_HAS_DSF
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (TagLib::DSF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::DSDIFF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
@@ -213,20 +213,33 @@ namespace lms::metadata::taglib::utils
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties)
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||
|
||||
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
|
||||
TagLib::FileStream fileStream{ createFileStream(p) };
|
||||
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) };
|
||||
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) };
|
||||
if (!file)
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
|
||||
file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value());
|
||||
file = parseFileByContent(&fileStream, tagLibReadStyle);
|
||||
if (!file)
|
||||
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content");
|
||||
}
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw AudioFileParsingException{ p, "Parsing failed" };
|
||||
}
|
||||
|
||||
if (!file->audioProperties())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||
throw AudioFileNoAudioPropertiesException{ p };
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
} // namespace lms::metadata::taglib::utils
|
||||
} // namespace lms::audio::taglib::utils
|
||||
+9
-12
@@ -19,19 +19,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IImageReader.hpp"
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
namespace lms::metadata::taglib
|
||||
{
|
||||
class TagLibImageReader : public IImageReader
|
||||
{
|
||||
public:
|
||||
TagLibImageReader(const std::filesystem::path& p);
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
void visitImages(ImageVisitor visitor) const override;
|
||||
|
||||
std::unique_ptr<TagLib::File> _file;
|
||||
};
|
||||
} // namespace lms::metadata::taglib
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
|
||||
} // namespace lms::audio::taglib::utils
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
enum class ContainerType
|
||||
{
|
||||
AIFF,
|
||||
APE, // Monkey's Audio
|
||||
ASF, // Advanced Systems Format
|
||||
DSF,
|
||||
FLAC,
|
||||
MP4,
|
||||
MPC, // Musepack
|
||||
MPEG,
|
||||
Ogg,
|
||||
Shorten,
|
||||
TrueAudio,
|
||||
WAV,
|
||||
WavPack,
|
||||
};
|
||||
|
||||
core::LiteralString containerTypeToString(ContainerType type);
|
||||
|
||||
enum class CodecType
|
||||
{
|
||||
AAC,
|
||||
AC3,
|
||||
ALAC, // Apple Lossless Audio Codec (ALAC)
|
||||
APE, // Monkey's Audio
|
||||
EAC3, // Enhanced AC-3
|
||||
DSD, // DSD
|
||||
FLAC, // Flac
|
||||
MP3,
|
||||
MP4ALS, // MPEG-4 Audio Lossless Coding
|
||||
MPC7, // Musepack
|
||||
MPC8, // Musepack
|
||||
Opus, // Opus
|
||||
PCM,
|
||||
Shorten, // Shorten (shn)
|
||||
TrueAudio,
|
||||
Vorbis,
|
||||
WavPack, // WavPack
|
||||
WMA1,
|
||||
WMA2,
|
||||
WMA9Pro,
|
||||
WMA9Lossless,
|
||||
};
|
||||
|
||||
core::LiteralString codecTypeToString(CodecType type);
|
||||
|
||||
struct AudioProperties
|
||||
{
|
||||
std::optional<ContainerType> container;
|
||||
std::optional<CodecType> codec;
|
||||
std::chrono::milliseconds duration{};
|
||||
std::optional<unsigned> bitrate;
|
||||
std::optional<unsigned> bitsPerSample;
|
||||
std::optional<unsigned> channelCount;
|
||||
std::optional<unsigned> sampleRate;
|
||||
};
|
||||
} // namespace lms::audio
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::av
|
||||
namespace lms::audio
|
||||
{
|
||||
class Exception : public core::LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
} // namespace lms::av
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
class AudioProperties;
|
||||
class IImageReader;
|
||||
class ITagReader;
|
||||
|
||||
class IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioFileInfo() = default;
|
||||
|
||||
virtual const AudioProperties& getAudioProperties() const = 0;
|
||||
virtual const IImageReader& getImageReader() const = 0;
|
||||
virtual const ITagReader& getTagReader() const = 0;
|
||||
};
|
||||
|
||||
class AudioFileParsingException : public Exception
|
||||
{
|
||||
public:
|
||||
AudioFileParsingException(const std::filesystem::path& path, std::string_view error = "")
|
||||
: Exception{ error }, _path{ path } {}
|
||||
|
||||
const std::filesystem::path& getPath() const { return _path; }
|
||||
|
||||
private:
|
||||
std::filesystem::path _path;
|
||||
};
|
||||
|
||||
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
|
||||
{
|
||||
public:
|
||||
using AudioFileParsingException::AudioFileParsingException;
|
||||
};
|
||||
|
||||
class IOException : public Exception
|
||||
{
|
||||
public:
|
||||
IOException(std::string_view message, std::error_code err)
|
||||
: Exception{ std::string{ message } + ": " + err.message() }
|
||||
, _err{ err }
|
||||
{
|
||||
}
|
||||
|
||||
std::error_code getErrorCode() const { return _err; }
|
||||
|
||||
private:
|
||||
std::error_code _err;
|
||||
};
|
||||
|
||||
struct ParserOptions
|
||||
{
|
||||
enum class Parser
|
||||
{
|
||||
TagLib,
|
||||
FFmpeg,
|
||||
};
|
||||
|
||||
enum class AudioPropertiesReadStyle
|
||||
{
|
||||
Fast,
|
||||
Average,
|
||||
Accurate,
|
||||
};
|
||||
|
||||
Parser parser{ Parser::TagLib };
|
||||
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
|
||||
bool enableExtraDebugLogs{};
|
||||
};
|
||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
struct Image
|
||||
{
|
||||
// See TagLib types (based on ID3v2 APIC types)
|
||||
enum class Type
|
||||
{
|
||||
// No information
|
||||
Unknown,
|
||||
// A type not enumerated below
|
||||
Other,
|
||||
// 32x32 PNG image that should be used as the file icon
|
||||
FileIcon,
|
||||
// File icon of a different size or format
|
||||
OtherFileIcon,
|
||||
// Front cover image of the album
|
||||
FrontCover,
|
||||
// Back cover image of the album
|
||||
BackCover,
|
||||
// Inside leaflet page of the album
|
||||
LeafletPage,
|
||||
// Image from the album itself
|
||||
Media,
|
||||
// Picture of the lead artist or soloist
|
||||
LeadArtist,
|
||||
// Picture of the artist or performer
|
||||
Artist,
|
||||
// Picture of the conductor
|
||||
Conductor,
|
||||
// Picture of the band or orchestra
|
||||
Band,
|
||||
// Picture of the composer
|
||||
Composer,
|
||||
// Picture of the lyricist or text writer
|
||||
Lyricist,
|
||||
// Picture of the recording location or studio
|
||||
RecordingLocation,
|
||||
// Picture of the artists during recording
|
||||
DuringRecording,
|
||||
// Picture of the artists during performance
|
||||
DuringPerformance,
|
||||
// Picture from a movie or video related to the track
|
||||
MovieScreenCapture,
|
||||
// Picture of a large, coloured fish
|
||||
ColouredFish,
|
||||
// Illustration related to the track
|
||||
Illustration,
|
||||
// Logo of the band or performer
|
||||
BandLogo,
|
||||
// Logo of the publisher (record company)
|
||||
PublisherLogo
|
||||
};
|
||||
|
||||
Type type{ Type::Unknown };
|
||||
std::string mimeType{ "application/octet-stream" };
|
||||
std::string description;
|
||||
std::span<const std::byte> data;
|
||||
};
|
||||
|
||||
core::LiteralString imageTypeToString(Image::Type type);
|
||||
|
||||
class IImageReader
|
||||
{
|
||||
public:
|
||||
virtual ~IImageReader() = default;
|
||||
|
||||
using ImageVisitor = std::function<void(const Image& image)>;
|
||||
virtual void visitImages(const ImageVisitor& visitor) const = 0;
|
||||
};
|
||||
} // namespace lms::audio
|
||||
@@ -20,10 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
|
||||
#include "metadata/Types.hpp"
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::audio
|
||||
{
|
||||
// prefer using picard internal names
|
||||
// see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
|
||||
@@ -152,8 +153,12 @@ namespace lms::metadata
|
||||
Website,
|
||||
WorkTitle,
|
||||
Writer,
|
||||
|
||||
Count, // Special value used to count the number of tags
|
||||
};
|
||||
|
||||
core::LiteralString tagTypeToString(TagType type);
|
||||
|
||||
class ITagReader
|
||||
{
|
||||
public:
|
||||
@@ -168,7 +173,5 @@ namespace lms::metadata
|
||||
|
||||
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
|
||||
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
|
||||
|
||||
virtual const AudioProperties& getAudioProperties() const = 0;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::audio
|
||||
+5
-28
@@ -20,37 +20,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::av
|
||||
{
|
||||
struct InputParameters
|
||||
{
|
||||
std::filesystem::path file; // Path to the input file
|
||||
std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from
|
||||
std::optional<std::size_t> streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set)
|
||||
};
|
||||
#include "TranscodeTypes.hpp"
|
||||
|
||||
enum class OutputFormat
|
||||
namespace lms::audio
|
||||
{
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
struct OutputParameters
|
||||
{
|
||||
OutputFormat format;
|
||||
std::size_t bitrate{ 128'000 };
|
||||
bool stripMetadata{ true };
|
||||
};
|
||||
|
||||
class ITranscoder
|
||||
{
|
||||
public:
|
||||
@@ -62,10 +39,10 @@ namespace lms::av
|
||||
virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0;
|
||||
|
||||
virtual std::string_view getOutputMimeType() const = 0;
|
||||
virtual const OutputParameters& getOutputParameters() const = 0;
|
||||
virtual const TranscodeOutputParameters& getOutputParameters() const = 0;
|
||||
|
||||
virtual bool finished() const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
||||
} // namespace lms::av
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters);
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
// #include "AudioTypes.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
// TODO deprecated
|
||||
enum class OutputFormat
|
||||
{
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
struct TranscodeInputParameters
|
||||
{
|
||||
std::filesystem::path filePath;
|
||||
std::chrono::milliseconds duration{}; // Duration of the audio file
|
||||
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
|
||||
};
|
||||
|
||||
struct TranscodeOutputParameters
|
||||
{
|
||||
std::optional<OutputFormat> format;
|
||||
std::optional<unsigned> bitrate;
|
||||
std::optional<unsigned> bitsPerSample;
|
||||
std::optional<unsigned> channelCount;
|
||||
std::optional<unsigned> sampleRate;
|
||||
bool stripMetadata{ true };
|
||||
};
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
TranscodeInputParameters inputParameters;
|
||||
TranscodeOutputParameters outputParameters;
|
||||
};
|
||||
} // namespace lms::audio
|
||||
@@ -1,26 +0,0 @@
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
|
||||
add_library(lmsav STATIC
|
||||
impl/AudioFile.cpp
|
||||
impl/Transcoder.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsav INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsav PRIVATE
|
||||
include
|
||||
${AVCODEC_INCLUDE_DIR}
|
||||
${AVFORMAT_INCLUDE_DIR}
|
||||
${AVUTIL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(lmsav PUBLIC
|
||||
lmscore
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
target_link_libraries(lmsav PRIVATE
|
||||
PkgConfig::LIBAV
|
||||
)
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
|
||||
struct AVFormatContext;
|
||||
|
||||
namespace lms::av
|
||||
{
|
||||
class AudioFile final : public IAudioFile
|
||||
{
|
||||
public:
|
||||
AudioFile(const std::filesystem::path& p);
|
||||
~AudioFile() override;
|
||||
AudioFile(const AudioFile&) = delete;
|
||||
AudioFile& operator=(const AudioFile&) = delete;
|
||||
|
||||
const std::filesystem::path& getPath() const override;
|
||||
ContainerInfo getContainerInfo() const override;
|
||||
MetadataMap getMetaData() const override;
|
||||
std::vector<StreamInfo> getStreamInfo() const override;
|
||||
std::optional<StreamInfo> getBestStreamInfo() const override;
|
||||
std::optional<std::size_t> getBestStreamIndex() const override;
|
||||
bool hasAttachedPictures() const override;
|
||||
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const override;
|
||||
|
||||
private:
|
||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context{};
|
||||
};
|
||||
} // namespace lms::av
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::av
|
||||
{
|
||||
// List should be sync with the codecs shipped in the lms's docker version
|
||||
enum class DecodingCodec
|
||||
{
|
||||
UNKNOWN,
|
||||
MP3,
|
||||
AAC,
|
||||
AC3,
|
||||
VORBIS,
|
||||
WMAV1,
|
||||
WMAV2,
|
||||
FLAC, // Flac
|
||||
ALAC, // Apple Lossless Audio Codec (ALAC)
|
||||
WAVPACK, // WavPack
|
||||
MUSEPACK7, // Musepack
|
||||
MUSEPACK8,
|
||||
APE, // Monkey's Audio
|
||||
EAC3, // Enhanced AC-3
|
||||
MP4ALS, // MPEG-4 Audio Lossless Coding
|
||||
OPUS, // Opus
|
||||
SHORTEN, // Shorten (shn)
|
||||
DSD_LSBF, // DSD (Direct Stream Digital), least significant bit first
|
||||
DSD_LSBF_PLANAR, // DSD (Direct Stream Digital), least significant bit first, planar
|
||||
DSD_MSBF, // DSD (Direct Stream Digital), most significant bit first
|
||||
DSD_MSBF_PLANAR, // DSD (Direct Stream Digital), most significant bit first, planar
|
||||
// TODO add PCM codecs
|
||||
};
|
||||
|
||||
struct Picture
|
||||
{
|
||||
std::string mimeType;
|
||||
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||
};
|
||||
|
||||
struct ContainerInfo
|
||||
{
|
||||
std::size_t bitrate{};
|
||||
std::string name;
|
||||
std::chrono::milliseconds duration{};
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t index{};
|
||||
std::size_t bitrate{};
|
||||
std::size_t bitsPerSample{};
|
||||
std::size_t channelCount{};
|
||||
std::size_t sampleRate{};
|
||||
DecodingCodec codec;
|
||||
std::string codecName;
|
||||
};
|
||||
|
||||
class IAudioFile
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioFile() = default;
|
||||
|
||||
// Keys are forced to be in upper case
|
||||
using MetadataMap = std::unordered_map<std::string, std::string>;
|
||||
|
||||
virtual const std::filesystem::path& getPath() const = 0;
|
||||
virtual ContainerInfo getContainerInfo() const = 0;
|
||||
virtual MetadataMap getMetaData() const = 0;
|
||||
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
|
||||
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
||||
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
|
||||
virtual bool hasAttachedPictures() const = 0;
|
||||
virtual void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap& metadata)> func) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
|
||||
} // namespace lms::av
|
||||
@@ -37,10 +37,10 @@ namespace lms::core::logging
|
||||
{
|
||||
case Module::API_SUBSONIC:
|
||||
return "API_SUBSONIC";
|
||||
case Module::AUDIO:
|
||||
return "AUDIO";
|
||||
case Module::AUTH:
|
||||
return "AUTH";
|
||||
case Module::AV:
|
||||
return "AV";
|
||||
case Module::CHILDPROCESS:
|
||||
return "CHILDPROC";
|
||||
case Module::COVER:
|
||||
|
||||
@@ -40,8 +40,8 @@ namespace lms::core::logging
|
||||
enum class Module
|
||||
{
|
||||
API_SUBSONIC,
|
||||
AUDIO,
|
||||
AUTH,
|
||||
AV,
|
||||
CHILDPROCESS,
|
||||
COVER,
|
||||
DB,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
if (BUILD_BENCHMARKS)
|
||||
add_subdirectory(bench)
|
||||
endif()
|
||||
|
||||
add_library(lmsmetadata STATIC
|
||||
impl/ArtistInfo.cpp
|
||||
impl/AudioFileParser.cpp
|
||||
impl/avformat/AvFormatImageReader.cpp
|
||||
impl/avformat/AvFormatTagReader.cpp
|
||||
impl/avformat/Utils.cpp
|
||||
impl/Lyrics.cpp
|
||||
impl/PlayList.cpp
|
||||
impl/taglib/TagLibImageReader.cpp
|
||||
impl/taglib/TagLibTagReader.cpp
|
||||
impl/taglib/Utils.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsmetadata INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsmetadata PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PRIVATE
|
||||
lmsav
|
||||
PkgConfig::Taglib
|
||||
pugixml::pugixml
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PUBLIC
|
||||
lmscore
|
||||
std::filesystem
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
add_executable(bench-metadata
|
||||
LyricsBench.cpp
|
||||
Metadata.cpp
|
||||
)
|
||||
|
||||
target_include_directories(bench-metadata PRIVATE
|
||||
../impl
|
||||
../test
|
||||
)
|
||||
|
||||
target_link_libraries(bench-metadata PRIVATE
|
||||
lmsmetadata
|
||||
benchmark
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class IImageReader;
|
||||
class ITagReader;
|
||||
|
||||
class AudioFileParser : public IAudioFileParser
|
||||
{
|
||||
public:
|
||||
AudioFileParser(const AudioFileParserParameters& params = {});
|
||||
~AudioFileParser() override = default;
|
||||
AudioFileParser(const AudioFileParser&) = delete;
|
||||
AudioFileParser& operator=(const AudioFileParser&) = delete;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) const override;
|
||||
std::unique_ptr<Track> parseMetaData(const ITagReader& reader) const;
|
||||
static void parseImages(const IImageReader& reader, ImageVisitor visitor);
|
||||
|
||||
private:
|
||||
void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const override;
|
||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||
|
||||
void processTags(const ITagReader& reader, Track& track) const;
|
||||
|
||||
std::optional<Medium> getMedium(const ITagReader& tagReader) const;
|
||||
std::optional<Release> getRelease(const ITagReader& tagReader) const;
|
||||
|
||||
const AudioFileParserParameters _params;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <taglib/audioproperties.h>
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "core/TaggedType.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle);
|
||||
|
||||
using ReadAudioProperties = core::TaggedBool<struct ReadAudioPropertiesTag>;
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties);
|
||||
} // namespace lms::metadata::taglib::utils
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <system_error>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class Exception : public core::LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
class IOException : public Exception
|
||||
{
|
||||
public:
|
||||
IOException(std::string_view message, std::error_code err)
|
||||
: Exception{ std::string{ message } + ": " + err.message() }
|
||||
, _err{ err }
|
||||
{
|
||||
}
|
||||
|
||||
std::error_code getErrorCode() const { return _err; }
|
||||
|
||||
private:
|
||||
std::error_code _err;
|
||||
};
|
||||
|
||||
class AudioFileParsingException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
|
||||
{
|
||||
public:
|
||||
using AudioFileParsingException::AudioFileParsingException;
|
||||
};
|
||||
|
||||
} // namespace lms::metadata
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class IAudioFileParser
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioFileParser() = default;
|
||||
|
||||
virtual std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) const = 0;
|
||||
|
||||
using ImageVisitor = std::function<void(const Image&)>;
|
||||
virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const = 0;
|
||||
|
||||
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params);
|
||||
} // namespace lms::metadata
|
||||
@@ -1,933 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "AudioFileParser.hpp"
|
||||
#include "TestTagReader.hpp"
|
||||
|
||||
namespace lms::metadata::tests
|
||||
{
|
||||
class TestAudioFileParser : public AudioFileParser
|
||||
{
|
||||
public:
|
||||
using AudioFileParser::AudioFileParser;
|
||||
using AudioFileParser::parseMetaData;
|
||||
};
|
||||
|
||||
TEST(AudioFileParser, generalTest)
|
||||
{
|
||||
AudioFileParserParameters params;
|
||||
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
|
||||
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<ITagReader> testTags{ createDefaultPopulatedTestTagReader() };
|
||||
|
||||
const std::unique_ptr<Track> track{ parser.parseMetaData(*testTags) };
|
||||
|
||||
// Audio properties
|
||||
{
|
||||
const AudioProperties& audioProperties{ testTags->getAudioProperties() };
|
||||
EXPECT_EQ(track->audioProperties.bitrate, audioProperties.bitrate);
|
||||
EXPECT_EQ(track->audioProperties.bitsPerSample, audioProperties.bitsPerSample);
|
||||
EXPECT_EQ(track->audioProperties.channelCount, audioProperties.channelCount);
|
||||
EXPECT_EQ(track->audioProperties.duration, audioProperties.duration);
|
||||
EXPECT_EQ(track->audioProperties.sampleRate, audioProperties.sampleRate);
|
||||
}
|
||||
|
||||
EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
|
||||
ASSERT_TRUE(track->advisory.has_value());
|
||||
EXPECT_EQ(track->advisory.value(), Track::Advisory::Clean);
|
||||
EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2");
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "MyArtist1");
|
||||
EXPECT_EQ(track->artists[0].sortName, "MyArtists1SortName");
|
||||
EXPECT_EQ(track->artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae"));
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist2");
|
||||
EXPECT_EQ(track->artists[1].sortName, "MyArtists2SortName");
|
||||
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
|
||||
ASSERT_EQ(track->comments.size(), 2);
|
||||
EXPECT_EQ(track->comments[0], "Comment1");
|
||||
EXPECT_EQ(track->comments[1], "Comment2");
|
||||
ASSERT_EQ(track->composerArtists.size(), 2);
|
||||
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
|
||||
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
|
||||
EXPECT_EQ(track->composerArtists[1].name, "MyComposer2");
|
||||
EXPECT_EQ(track->composerArtists[1].sortName, "MyComposerSortOrder2");
|
||||
ASSERT_EQ(track->conductorArtists.size(), 2);
|
||||
EXPECT_EQ(track->conductorArtists[0].name, "MyConductor1");
|
||||
EXPECT_EQ(track->conductorArtists[1].name, "MyConductor2");
|
||||
EXPECT_EQ(track->copyright, "MyCopyright");
|
||||
EXPECT_EQ(track->copyrightURL, "MyCopyrightURL");
|
||||
ASSERT_TRUE(track->date.isValid());
|
||||
EXPECT_EQ(track->date.getYear(), 2020);
|
||||
EXPECT_EQ(track->date.getMonth(), 3);
|
||||
EXPECT_EQ(track->date.getDay(), 4);
|
||||
ASSERT_EQ(track->genres.size(), 2);
|
||||
EXPECT_EQ(track->genres[0], "Genre1");
|
||||
EXPECT_EQ(track->genres[1], "Genre2");
|
||||
ASSERT_EQ(track->groupings.size(), 2);
|
||||
EXPECT_EQ(track->groupings[0], "Grouping1");
|
||||
EXPECT_EQ(track->groupings[1], "Grouping2");
|
||||
ASSERT_EQ(track->languages.size(), 2);
|
||||
EXPECT_EQ(track->languages[0], "Language1");
|
||||
EXPECT_EQ(track->languages[1], "Language2");
|
||||
ASSERT_EQ(track->lyricistArtists.size(), 2);
|
||||
EXPECT_EQ(track->lyricistArtists[0].name, "MyLyricist1");
|
||||
EXPECT_EQ(track->lyricistArtists[1].name, "MyLyricist2");
|
||||
ASSERT_EQ(track->lyrics.size(), 1);
|
||||
EXPECT_EQ(track->lyrics.front().language, "eng");
|
||||
ASSERT_EQ(track->lyrics.front().synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 }));
|
||||
EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line");
|
||||
ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 }));
|
||||
EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line");
|
||||
ASSERT_TRUE(track->mbid.has_value());
|
||||
EXPECT_EQ(track->mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a"));
|
||||
ASSERT_EQ(track->mixerArtists.size(), 2);
|
||||
EXPECT_EQ(track->mixerArtists[0].name, "MyMixer1");
|
||||
EXPECT_EQ(track->mixerArtists[1].name, "MyMixer2");
|
||||
ASSERT_EQ(track->moods.size(), 2);
|
||||
EXPECT_EQ(track->moods[0], "Mood1");
|
||||
EXPECT_EQ(track->moods[1], "Mood2");
|
||||
ASSERT_TRUE(track->originalDate.isValid());
|
||||
EXPECT_EQ(track->originalDate.getYear(), 2019);
|
||||
EXPECT_EQ(track->originalDate.getMonth(), 2);
|
||||
EXPECT_EQ(track->originalDate.getDay(), 3);
|
||||
ASSERT_TRUE(track->originalYear.has_value());
|
||||
EXPECT_EQ(track->originalYear.value(), 2019);
|
||||
ASSERT_TRUE(track->performerArtists.contains("Rolea"));
|
||||
ASSERT_EQ(track->performerArtists["Rolea"].size(), 2);
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][0].name, "MyPerformer1ForRoleA");
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][1].name, "MyPerformer2ForRoleA");
|
||||
ASSERT_EQ(track->performerArtists["Roleb"].size(), 2);
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][0].name, "MyPerformer1ForRoleB");
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][1].name, "MyPerformer2ForRoleB");
|
||||
ASSERT_TRUE(track->position.has_value());
|
||||
EXPECT_EQ(track->position.value(), 7);
|
||||
ASSERT_EQ(track->producerArtists.size(), 2);
|
||||
EXPECT_EQ(track->producerArtists[0].name, "MyProducer1");
|
||||
EXPECT_EQ(track->producerArtists[1].name, "MyProducer2");
|
||||
ASSERT_TRUE(track->recordingMBID.has_value());
|
||||
EXPECT_EQ(track->recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
|
||||
ASSERT_TRUE(track->replayGain.has_value());
|
||||
EXPECT_FLOAT_EQ(track->replayGain.value(), -0.33);
|
||||
ASSERT_EQ(track->remixerArtists.size(), 2);
|
||||
EXPECT_EQ(track->remixerArtists[0].name, "MyRemixer1");
|
||||
EXPECT_EQ(track->remixerArtists[1].name, "MyRemixer2");
|
||||
EXPECT_EQ(track->title, "MyTitle");
|
||||
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"].size(), 2);
|
||||
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][0], "MyTagValue1ForTagA");
|
||||
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][1], "MyTagValue2ForTagA");
|
||||
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2);
|
||||
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB");
|
||||
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB");
|
||||
|
||||
// Medium
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
EXPECT_EQ(track->medium->media, "CD");
|
||||
EXPECT_EQ(track->medium->name, "MySubtitle");
|
||||
ASSERT_TRUE(track->medium->position.has_value());
|
||||
EXPECT_EQ(track->medium->position.value(), 2);
|
||||
ASSERT_TRUE(track->medium->replayGain.has_value());
|
||||
EXPECT_FLOAT_EQ(track->medium->replayGain.value(), -0.5);
|
||||
ASSERT_TRUE(track->medium->trackCount.has_value());
|
||||
EXPECT_EQ(track->medium->trackCount.value(), 12);
|
||||
|
||||
// Release
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
const Release& release{ track->medium->release.value() };
|
||||
EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
|
||||
ASSERT_EQ(release.artists.size(), 2);
|
||||
EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1");
|
||||
EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName");
|
||||
EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
|
||||
EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2");
|
||||
EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName");
|
||||
EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
|
||||
EXPECT_TRUE(release.isCompilation);
|
||||
EXPECT_EQ(release.barcode, "MyBarcode");
|
||||
ASSERT_EQ(release.labels.size(), 2);
|
||||
EXPECT_EQ(release.labels[0], "Label1");
|
||||
EXPECT_EQ(release.labels[1], "Label2");
|
||||
ASSERT_TRUE(release.mbid.has_value());
|
||||
EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
|
||||
EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
|
||||
EXPECT_EQ(release.mediumCount, 3);
|
||||
EXPECT_EQ(release.name, "MyAlbum");
|
||||
EXPECT_EQ(release.sortName, "MyAlbumSortName");
|
||||
EXPECT_EQ(release.comment, "MyAlbumComment");
|
||||
ASSERT_EQ(release.countries.size(), 2);
|
||||
EXPECT_EQ(release.countries[0], "MyCountry1");
|
||||
EXPECT_EQ(release.countries[1], "MyCountry2");
|
||||
{
|
||||
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
|
||||
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, trim)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->genres.size(), 3);
|
||||
EXPECT_EQ(track->genres[0], "Genre1");
|
||||
EXPECT_EQ(track->genres[1], "Genre2");
|
||||
EXPECT_EQ(track->genres[2], "Genre3");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
||||
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
|
||||
{ TagType::Genre, { "Genre1 ; Genre2" } },
|
||||
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.defaultTagDelimiters = { " ; ", "/" };
|
||||
params.artistTagDelimiters = { " / ", " feat. " };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 3);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artists[2].name, "Artist3");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit
|
||||
ASSERT_EQ(track->genres.size(), 2);
|
||||
EXPECT_EQ(track->genres[0], "Genre1");
|
||||
EXPECT_EQ(track->genres[1], "Genre2");
|
||||
ASSERT_EQ(track->languages.size(), 3);
|
||||
EXPECT_EQ(track->languages[0], "Lang1");
|
||||
EXPECT_EQ(track->languages[1], "Lang2");
|
||||
EXPECT_EQ(track->languages[2], "Lang3");
|
||||
|
||||
// Medium
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
|
||||
// Release
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
EXPECT_EQ(track->medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "AlbumArtist1");
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { " AC/DC " } },
|
||||
{ TagType::Artist, { "AC/DC " } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC");
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
EXPECT_EQ(track->medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 1);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AC/DC");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_artists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC and MyArtist" } },
|
||||
{ TagType::Artists, { "AC/DC", "MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { " AC/DC " };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_first)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC;MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_middle)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 3);
|
||||
EXPECT_EQ(track->artists[0].name, "MyArtist1");
|
||||
EXPECT_EQ(track->artists[1].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[2].name, "MyArtist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_last)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { ";", "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_longest_first)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { ";", "/" };
|
||||
params.artistsToNotSplit = { "AC", "DC", "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_begin)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC; MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC; MyArtist");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_middle)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "MyArtist1; AC/DC ; MyArtist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "MyArtist1; AC/DC ; MyArtist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_end)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { " MyArtist; AC/DC " } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "MyArtist; AC/DC");
|
||||
EXPECT_EQ(track->artistDisplayName, "MyArtist; AC/DC");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters_foundInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1; Artist2" } },
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TestAudioFileParser parser{ params };
|
||||
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters_foundInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 feat. Artist2" } },
|
||||
{ TagType::Artists, { "Artist1; Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TestAudioFileParser parser{ params };
|
||||
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters_notUsed)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 & Artist2" } },
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TestAudioFileParser parser{ params };
|
||||
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters_onlyInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 & Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { " & " };
|
||||
TestAudioFileParser parser{ params };
|
||||
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimitersUsedForArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artists, { "Artist1 & Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { " & " };
|
||||
TestAudioFileParser parser{ params };
|
||||
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, noArtistInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 0);
|
||||
EXPECT_EQ(track->artistDisplayName, "");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, singleArtistInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ TagType::Artists, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artists!
|
||||
{ TagType::Artist, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInArtistsWithEndDelimiter)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 & (CV. Artist2)" } },
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, singleArtistInAlbumArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtist!
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtists, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium);
|
||||
ASSERT_TRUE(track->medium->release);
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 1);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInAlbumArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtists!
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium);
|
||||
ASSERT_TRUE(track->medium->release);
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInAlbumArtists_displayName)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "Artist1 & Artist2" } },
|
||||
{ TagType::AlbumArtists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium);
|
||||
ASSERT_TRUE(track->medium->release);
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInAlbumArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtist!
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium);
|
||||
ASSERT_TRUE(track->medium->release);
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 & Artist2" } },
|
||||
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1 / Artist2" } },
|
||||
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { " / " };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, release_sortNameFallback)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
// No AlbumSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, artist_sortNameFallback)
|
||||
{
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "MyArtist" } },
|
||||
{ TagType::ArtistSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "MyArtist" } },
|
||||
{ TagType::ArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "MyArtist" } },
|
||||
{ TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } },
|
||||
{ TagType::ArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, albumartist_sortNameFallback)
|
||||
{
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ TagType::AlbumArtistSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
|
||||
const auto& artists{ track->medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
|
||||
const auto& artists{ track->medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
|
||||
{ TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
|
||||
const auto& artists{ track->medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, advisory)
|
||||
{
|
||||
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Advisory, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParser parser;
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
|
||||
if (track->advisory.has_value())
|
||||
{
|
||||
EXPECT_EQ(track->advisory.value(), expectedValue);
|
||||
}
|
||||
};
|
||||
|
||||
doTest("0", Track::Advisory::Unknown);
|
||||
doTest("1", Track::Advisory::Explicit);
|
||||
doTest("4", Track::Advisory::Explicit);
|
||||
doTest("2", Track::Advisory::Clean);
|
||||
doTest("", std::nullopt);
|
||||
doTest("3", std::nullopt);
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, encodingTime)
|
||||
{
|
||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::EncodingTime, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'";
|
||||
};
|
||||
|
||||
doTest("", core::PartialDateTime{});
|
||||
doTest("foo", core::PartialDateTime{});
|
||||
doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 });
|
||||
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, date)
|
||||
{
|
||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Date, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
|
||||
};
|
||||
|
||||
doTest("", core::PartialDateTime{});
|
||||
doTest("foo", core::PartialDateTime{});
|
||||
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020-01", core::PartialDateTime{ 2020, 1 });
|
||||
doTest("2020", core::PartialDateTime{ 2020 });
|
||||
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020/01", core::PartialDateTime{ 2020, 1 });
|
||||
doTest("2020", core::PartialDateTime{ 2020 });
|
||||
}
|
||||
} // namespace lms::metadata::tests
|
||||
@@ -1,24 +0,0 @@
|
||||
include(GoogleTest)
|
||||
|
||||
add_executable(test-metadata
|
||||
ArtistInfo.cpp
|
||||
Lyrics.cpp
|
||||
Metadata.cpp
|
||||
AudioFileParser.cpp
|
||||
PlayList.cpp
|
||||
Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(test-metadata PRIVATE
|
||||
../impl
|
||||
)
|
||||
|
||||
target_link_libraries(test-metadata PRIVATE
|
||||
lmsmetadata
|
||||
GTest::GTest
|
||||
)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
gtest_discover_tests(test-metadata)
|
||||
endif()
|
||||
|
||||
@@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmsartwork PRIVATE
|
||||
lmsaudio
|
||||
lmsimage
|
||||
lmsmetadata
|
||||
)
|
||||
|
||||
target_link_libraries(lmsartwork PUBLIC
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
@@ -37,7 +40,6 @@
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
namespace lms::artwork
|
||||
{
|
||||
@@ -50,7 +52,6 @@ namespace lms::artwork
|
||||
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
||||
const std::filesystem::path& defaultArtistImageSvgPath)
|
||||
: _db{ db }
|
||||
, _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) }
|
||||
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||
{
|
||||
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||
@@ -107,7 +108,11 @@ namespace lms::artwork
|
||||
{
|
||||
std::size_t currentIndex{};
|
||||
|
||||
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
|
||||
audio::ParserOptions options;
|
||||
options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images
|
||||
|
||||
auto audioFile{ audio::parseAudioFile(p) };
|
||||
audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) {
|
||||
if (currentIndex++ != index)
|
||||
return;
|
||||
|
||||
@@ -130,7 +135,7 @@ namespace lms::artwork
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
catch (const audio::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
|
||||
}
|
||||
|
||||
@@ -33,11 +33,6 @@ namespace lms::db
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class IAudioFileParser;
|
||||
}
|
||||
|
||||
namespace lms::artwork
|
||||
{
|
||||
class ArtworkService : public IArtworkService
|
||||
@@ -67,7 +62,6 @@ namespace lms::artwork
|
||||
|
||||
db::IDb& _db;
|
||||
|
||||
std::unique_ptr<metadata::IAudioFileParser> _audioFileParser;
|
||||
ImageCache _cache;
|
||||
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
||||
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
add_library(lmsscanner STATIC
|
||||
impl/helpers/ArtistHelpers.cpp
|
||||
impl/scanners/ArtistInfoFileScanner.cpp
|
||||
impl/scanners/AudioFileScanOperation.cpp
|
||||
impl/scanners/artistinfo/ArtistInfoParser.cpp
|
||||
impl/scanners/artistinfo/ArtistInfoFileScanner.cpp
|
||||
impl/scanners/audiofile/AudioFileScanOperation.cpp
|
||||
impl/scanners/audiofile/AudioFileScanner.cpp
|
||||
impl/scanners/audiofile/TrackMetadataParser.cpp
|
||||
impl/scanners/audiofile/Utils.cpp
|
||||
impl/scanners/lyrics/LyricsFileScanner.cpp
|
||||
impl/scanners/lyrics/LyricsParser.cpp
|
||||
impl/scanners/playlist/PlayListFileScanner.cpp
|
||||
impl/scanners/playlist/PlayListParser.cpp
|
||||
impl/scanners/FileScanOperationBase.cpp
|
||||
impl/scanners/AudioFileScanner.cpp
|
||||
impl/scanners/ImageFileScanner.cpp
|
||||
impl/scanners/LyricsFileScanner.cpp
|
||||
impl/scanners/PlayListFileScanner.cpp
|
||||
impl/scanners/Utils.cpp
|
||||
impl/steps/JobQueue.cpp
|
||||
impl/steps/ScanErrorLogger.cpp
|
||||
@@ -43,13 +48,22 @@ target_include_directories(lmsscanner PRIVATE
|
||||
|
||||
target_link_libraries(lmsscanner PRIVATE
|
||||
lmscore
|
||||
lmsdatabase
|
||||
lmsaudio
|
||||
lmsimage
|
||||
lmsmetadata
|
||||
lmsrecommendation
|
||||
pugixml::pugixml
|
||||
)
|
||||
|
||||
target_link_libraries(lmsscanner PUBLIC
|
||||
lmsdatabase
|
||||
std::filesystem
|
||||
Wt::Wt
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
if (BUILD_BENCHMARKS)
|
||||
add_subdirectory(bench)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
add_executable(bench-scanner
|
||||
Lyrics.cpp
|
||||
Scanner.cpp
|
||||
TrackMetadataParser.cpp
|
||||
)
|
||||
|
||||
target_include_directories(bench-scanner PRIVATE
|
||||
../impl
|
||||
../test
|
||||
)
|
||||
|
||||
target_link_libraries(bench-scanner PRIVATE
|
||||
lmsscanner
|
||||
lmsaudio
|
||||
benchmark
|
||||
)
|
||||
+3
-3
@@ -22,9 +22,9 @@
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
namespace lms::metadata::benchmarks
|
||||
namespace lms::scanner::benchmarks
|
||||
{
|
||||
static void BM_Lyrics(benchmark::State& state)
|
||||
{
|
||||
@@ -89,4 +89,4 @@ namespace lms::metadata::benchmarks
|
||||
|
||||
BENCHMARK(BM_Lyrics);
|
||||
|
||||
} // namespace lms::metadata::benchmarks
|
||||
} // namespace lms::scanner::benchmarks
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
BENCHMARK_MAIN();
|
||||
+19
-30
@@ -19,30 +19,22 @@
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "AudioFileParser.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
#include "TestTagReader.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata::benchmarks
|
||||
namespace lms::scanner::benchmarks
|
||||
{
|
||||
class TestAudioFileParser : public AudioFileParser
|
||||
{
|
||||
public:
|
||||
using AudioFileParser::AudioFileParser;
|
||||
using AudioFileParser::parseMetaData;
|
||||
};
|
||||
|
||||
static void BM_Metadata_parse(benchmark::State& state)
|
||||
{
|
||||
AudioFileParserParameters params;
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
|
||||
|
||||
std::unique_ptr<ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
|
||||
const TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<audio::ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
|
||||
const TrackMetadataParser parser{ params };
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(*testTags) };
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(*testTags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,16 +42,15 @@ namespace lms::metadata::benchmarks
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
const AudioFileParserParameters params;
|
||||
const TestAudioFileParser parser{ params };
|
||||
const TrackMetadataParser parser;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +58,11 @@ namespace lms::metadata::benchmarks
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
// The list itself is not important, the idea is to have some volume
|
||||
params.artistsToNotSplit = { "AC/DC",
|
||||
@@ -113,10 +104,10 @@ namespace lms::metadata::benchmarks
|
||||
"White/Light",
|
||||
"Yamantaka // Sonic Titan" };
|
||||
|
||||
const TestAudioFileParser parser{ params };
|
||||
const TrackMetadataParser parser{ params };
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,18 +115,18 @@ namespace lms::metadata::benchmarks
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
|
||||
const TestAudioFileParser parser{ params };
|
||||
const TrackMetadataParser parser{ params };
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +135,4 @@ namespace lms::metadata::benchmarks
|
||||
BENCHMARK(BM_Metadata_parseArtists_WithWhitelist);
|
||||
BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist);
|
||||
|
||||
} // namespace lms::metadata::benchmarks
|
||||
|
||||
BENCHMARK_MAIN();
|
||||
} // namespace lms::scanner::benchmarks
|
||||
@@ -27,15 +27,16 @@
|
||||
#include "core/IJobScheduler.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/ScanSettings.hpp"
|
||||
|
||||
#include "scanners/ArtistInfoFileScanner.hpp"
|
||||
#include "scanners/AudioFileScanner.hpp"
|
||||
#include "scanners/ImageFileScanner.hpp"
|
||||
#include "scanners/LyricsFileScanner.hpp"
|
||||
#include "scanners/PlayListFileScanner.hpp"
|
||||
#include "scanners/artistinfo/ArtistInfoFileScanner.hpp"
|
||||
#include "scanners/audiofile/AudioFileScanner.hpp"
|
||||
#include "scanners/lyrics/LyricsFileScanner.hpp"
|
||||
#include "scanners/playlist/PlayListFileScanner.hpp"
|
||||
|
||||
#include "steps/ScanStepArtistReconciliation.hpp"
|
||||
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||
|
||||
@@ -21,13 +21,14 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner::helpers
|
||||
{
|
||||
namespace
|
||||
{
|
||||
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
|
||||
db::Artist::pointer createArtist(db::Session& session, const Artist& artistInfo)
|
||||
{
|
||||
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace lms::scanner::helpers
|
||||
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
||||
}
|
||||
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const Artist& artistInfo)
|
||||
{
|
||||
// MBID may be set
|
||||
if (artist->getMBID() != artistInfo.mbid)
|
||||
@@ -68,7 +69,7 @@ namespace lms::scanner::helpers
|
||||
|
||||
} // namespace
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
assert(artistInfo.mbid.has_value());
|
||||
db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) };
|
||||
@@ -99,7 +100,7 @@ namespace lms::scanner::helpers
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
db::Artist::pointer artist;
|
||||
|
||||
@@ -139,7 +140,7 @@ namespace lms::scanner::helpers
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/TaggedType.hpp"
|
||||
|
||||
#include "database/objects/Artist.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct Artist;
|
||||
}
|
||||
@@ -31,8 +32,8 @@ namespace lms::scanner::helpers
|
||||
{
|
||||
using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>;
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
|
||||
} // namespace lms::scanner::helpers
|
||||
+12
-9
@@ -24,19 +24,22 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/ArtistInfo.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "metadata/ArtistInfo.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/artistinfo/ArtistInfoParser.hpp"
|
||||
#include "types/ArtistInfo.hpp"
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -57,7 +60,7 @@ namespace lms::scanner
|
||||
|
||||
std::string getArtistNameFromArtistInfoFilePath();
|
||||
|
||||
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
||||
std::optional<ArtistInfo> _parsedArtistInfo;
|
||||
};
|
||||
|
||||
void ArtistInfoFileScanOperation::scan()
|
||||
@@ -72,14 +75,14 @@ namespace lms::scanner
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
||||
_parsedArtistInfo = parseArtistInfo(ifs);
|
||||
if (_parsedArtistInfo->name.empty())
|
||||
{
|
||||
_parsedArtistInfo->name = getFilePath().parent_path().filename();
|
||||
LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'");
|
||||
}
|
||||
}
|
||||
catch (const metadata::ArtistInfoParseException& e)
|
||||
catch (const ArtistInfoParseException& e)
|
||||
{
|
||||
addError<ArtistInfoFileScanError>(getFilePath());
|
||||
}
|
||||
@@ -121,7 +124,7 @@ namespace lms::scanner
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||
|
||||
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||
const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) };
|
||||
artistInfo.modify()->setArtist(artist);
|
||||
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID());
|
||||
@@ -150,7 +153,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
|
||||
{
|
||||
return metadata::getSupportedArtistInfoFiles();
|
||||
return getSupportedArtistInfoFiles();
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/ArtistInfo.hpp"
|
||||
#include "ArtistInfoParser.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -77,4 +77,4 @@ namespace lms::metadata
|
||||
|
||||
return artistInfo;
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
#include "types/ArtistInfo.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ArtistInfoParseException : public core::LmsException
|
||||
{
|
||||
public:
|
||||
using core::LmsException::LmsException;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
|
||||
ArtistInfo parseArtistInfo(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
+191
-184
@@ -24,6 +24,11 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/Path.hpp"
|
||||
#include "core/XxHash3.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
@@ -40,36 +45,34 @@
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "scanners/IFileScanOperation.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
for (const metadata::Artist& artistInfo : artists)
|
||||
for (const Artist& artist : artists)
|
||||
{
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) };
|
||||
db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) };
|
||||
|
||||
const bool matchedUsingMbid{ artistInfo.mbid.has_value() && artist->getMBID() == artistInfo.mbid };
|
||||
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, artist, linkType, role, matchedUsingMbid) };
|
||||
link.modify()->setArtistName(artistInfo.name);
|
||||
if (artistInfo.sortName)
|
||||
link.modify()->setArtistSortName(*artistInfo.sortName);
|
||||
const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
|
||||
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, dbArtist, linkType, role, matchedUsingMbid) };
|
||||
link.modify()->setArtistName(artist.name);
|
||||
if (artist.sortName)
|
||||
link.modify()->setArtistSortName(*artist.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
constexpr std::string_view noRole{};
|
||||
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
|
||||
@@ -102,128 +105,128 @@ namespace lms::scanner
|
||||
return label;
|
||||
}
|
||||
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getSortName() != releaseInfo.sortName)
|
||||
release.modify()->setSortName(releaseInfo.sortName);
|
||||
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
||||
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
if (release->isCompilation() != releaseInfo.isCompilation)
|
||||
release.modify()->setCompilation(releaseInfo.isCompilation);
|
||||
if (release->getBarcode() != releaseInfo.barcode)
|
||||
release.modify()->setBarcode(releaseInfo.barcode);
|
||||
if (release->getComment() != releaseInfo.comment)
|
||||
release.modify()->setComment(releaseInfo.comment);
|
||||
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
||||
if (dbRelease->getName() != release.name)
|
||||
dbRelease.modify()->setName(release.name);
|
||||
if (dbRelease->getSortName() != release.sortName)
|
||||
dbRelease.modify()->setSortName(release.sortName);
|
||||
if (dbRelease->getGroupMBID() != release.groupMBID)
|
||||
dbRelease.modify()->setGroupMBID(release.groupMBID);
|
||||
if (dbRelease->getTotalDisc() != release.mediumCount)
|
||||
dbRelease.modify()->setTotalDisc(release.mediumCount);
|
||||
if (dbRelease->getArtistDisplayName() != release.artistDisplayName)
|
||||
dbRelease.modify()->setArtistDisplayName(release.artistDisplayName);
|
||||
if (dbRelease->isCompilation() != release.isCompilation)
|
||||
dbRelease.modify()->setCompilation(release.isCompilation);
|
||||
if (dbRelease->getBarcode() != release.barcode)
|
||||
dbRelease.modify()->setBarcode(release.barcode);
|
||||
if (dbRelease->getComment() != release.comment)
|
||||
dbRelease.modify()->setComment(release.comment);
|
||||
if (dbRelease->getReleaseTypeNames() != release.releaseTypes)
|
||||
{
|
||||
release.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
||||
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
dbRelease.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : release.releaseTypes)
|
||||
dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
}
|
||||
if (release->getCountryNames() != releaseInfo.countries)
|
||||
if (dbRelease->getCountryNames() != release.countries)
|
||||
{
|
||||
release.modify()->clearCountries();
|
||||
for (std::string_view country : releaseInfo.countries)
|
||||
release.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
dbRelease.modify()->clearCountries();
|
||||
for (std::string_view country : release.countries)
|
||||
dbRelease.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
}
|
||||
if (release->getLabelNames() != releaseInfo.labels)
|
||||
if (dbRelease->getLabelNames() != release.labels)
|
||||
{
|
||||
release.modify()->clearLabels();
|
||||
for (std::string_view label : releaseInfo.labels)
|
||||
release.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
dbRelease.modify()->clearLabels();
|
||||
for (std::string_view label : release.labels)
|
||||
dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare release level info
|
||||
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||
bool isReleaseMatching(const db::Release::pointer& dbCandidateRelease, const Release& release)
|
||||
{
|
||||
// TODO: add more criterias?
|
||||
return candidateRelease->getName() == releaseInfo.name
|
||||
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
||||
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
||||
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
||||
return dbCandidateRelease->getName() == release.name
|
||||
&& dbCandidateRelease->getSortName() == release.sortName
|
||||
&& dbCandidateRelease->getTotalDisc() == release.mediumCount
|
||||
&& dbCandidateRelease->isCompilation() == release.isCompilation
|
||||
&& dbCandidateRelease->getLabelNames() == release.labels
|
||||
&& dbCandidateRelease->getBarcode() == release.barcode;
|
||||
}
|
||||
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory)
|
||||
{
|
||||
db::Release::pointer release;
|
||||
db::Release::pointer dbRelease;
|
||||
|
||||
// First try to get by MBID: fastest, safest
|
||||
if (releaseInfo.mbid)
|
||||
if (release.mbid)
|
||||
{
|
||||
release = db::Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
dbRelease = db::Release::find(session, *release.mbid);
|
||||
if (!dbRelease)
|
||||
dbRelease = session.create<db::Release>(release.name, release.mbid);
|
||||
}
|
||||
else if (releaseInfo.name.empty())
|
||||
else if (release.name.empty())
|
||||
{
|
||||
// No release name (only mbid) -> nothing to do
|
||||
return release;
|
||||
return dbRelease;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
if (!dbRelease && release.mediumCount && *release.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setParentDirectory(parentDirectoryId);
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
params.setName(release.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
if (dbRelease)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
if (dbCandidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
if (!isReleaseMatching(dbCandidateRelease, release))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
dbRelease = dbCandidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
// Lastly try in the current directory: we do this at last to have
|
||||
// opportunities to merge releases in case of migration / rescan
|
||||
if (!release)
|
||||
if (!dbRelease)
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setDirectory(currentDirectory->getId());
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
params.setName(release.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
if (dbRelease)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
if (dbCandidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
if (!isReleaseMatching(dbCandidateRelease, release))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
dbRelease = dbCandidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name);
|
||||
if (!dbRelease)
|
||||
dbRelease = session.create<db::Release>(release.name);
|
||||
|
||||
updateReleaseIfNeeded(session, release, releaseInfo);
|
||||
return release;
|
||||
updateReleaseIfNeeded(session, dbRelease, release);
|
||||
return dbRelease;
|
||||
}
|
||||
|
||||
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
|
||||
db::Medium::pointer getOrCreateMedium(db::Session& session, const Medium& medium, const db::Release::pointer& release)
|
||||
{
|
||||
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
|
||||
if (!dbMedium)
|
||||
@@ -243,7 +246,7 @@ namespace lms::scanner
|
||||
return dbMedium;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
|
||||
@@ -274,69 +277,69 @@ namespace lms::scanner
|
||||
return clusters;
|
||||
}
|
||||
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const Lyrics& lyrics)
|
||||
{
|
||||
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
||||
db::TrackLyrics::pointer dbLyrics{ session.create<db::TrackLyrics>() };
|
||||
|
||||
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
||||
lyrics.modify()->setOffset(lyricsInfo.offset);
|
||||
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
||||
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
||||
if (!lyricsInfo.synchronizedLines.empty())
|
||||
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
||||
dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx");
|
||||
dbLyrics.modify()->setOffset(lyrics.offset);
|
||||
dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist);
|
||||
dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle);
|
||||
if (!lyrics.synchronizedLines.empty())
|
||||
dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines);
|
||||
else
|
||||
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
||||
dbLyrics.modify()->setUnsynchronizedLines(lyrics.unsynchronizedLines);
|
||||
|
||||
return lyrics;
|
||||
return dbLyrics;
|
||||
}
|
||||
|
||||
db::ImageType convertImageType(metadata::Image::Type type)
|
||||
db::ImageType convertImageType(audio::Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case metadata::Image::Type::Unknown:
|
||||
case audio::Image::Type::Unknown:
|
||||
return db::ImageType::Unknown;
|
||||
case metadata::Image::Type::Other:
|
||||
case audio::Image::Type::Other:
|
||||
return db::ImageType::Other;
|
||||
case metadata::Image::Type::FileIcon:
|
||||
case audio::Image::Type::FileIcon:
|
||||
return db::ImageType::FileIcon;
|
||||
case metadata::Image::Type::OtherFileIcon:
|
||||
case audio::Image::Type::OtherFileIcon:
|
||||
return db::ImageType::OtherFileIcon;
|
||||
case metadata::Image::Type::FrontCover:
|
||||
case audio::Image::Type::FrontCover:
|
||||
return db::ImageType::FrontCover;
|
||||
case metadata::Image::Type::BackCover:
|
||||
case audio::Image::Type::BackCover:
|
||||
return db::ImageType::BackCover;
|
||||
case metadata::Image::Type::LeafletPage:
|
||||
case audio::Image::Type::LeafletPage:
|
||||
return db::ImageType::LeafletPage;
|
||||
case metadata::Image::Type::Media:
|
||||
case audio::Image::Type::Media:
|
||||
return db::ImageType::Media;
|
||||
case metadata::Image::Type::LeadArtist:
|
||||
case audio::Image::Type::LeadArtist:
|
||||
return db::ImageType::LeadArtist;
|
||||
case metadata::Image::Type::Artist:
|
||||
case audio::Image::Type::Artist:
|
||||
return db::ImageType::Artist;
|
||||
case metadata::Image::Type::Conductor:
|
||||
case audio::Image::Type::Conductor:
|
||||
return db::ImageType::Conductor;
|
||||
case metadata::Image::Type::Band:
|
||||
case audio::Image::Type::Band:
|
||||
return db::ImageType::Band;
|
||||
case metadata::Image::Type::Composer:
|
||||
case audio::Image::Type::Composer:
|
||||
return db::ImageType::Composer;
|
||||
case metadata::Image::Type::Lyricist:
|
||||
case audio::Image::Type::Lyricist:
|
||||
return db::ImageType::Lyricist;
|
||||
case metadata::Image::Type::RecordingLocation:
|
||||
case audio::Image::Type::RecordingLocation:
|
||||
return db::ImageType::RecordingLocation;
|
||||
case metadata::Image::Type::DuringRecording:
|
||||
case audio::Image::Type::DuringRecording:
|
||||
return db::ImageType::DuringRecording;
|
||||
case metadata::Image::Type::DuringPerformance:
|
||||
case audio::Image::Type::DuringPerformance:
|
||||
return db::ImageType::DuringPerformance;
|
||||
case metadata::Image::Type::MovieScreenCapture:
|
||||
case audio::Image::Type::MovieScreenCapture:
|
||||
return db::ImageType::MovieScreenCapture;
|
||||
case metadata::Image::Type::ColouredFish:
|
||||
case audio::Image::Type::ColouredFish:
|
||||
return db::ImageType::ColouredFish;
|
||||
case metadata::Image::Type::Illustration:
|
||||
case audio::Image::Type::Illustration:
|
||||
return db::ImageType::Illustration;
|
||||
case metadata::Image::Type::BandLogo:
|
||||
case audio::Image::Type::BandLogo:
|
||||
return db::ImageType::BandLogo;
|
||||
case metadata::Image::Type::PublisherLogo:
|
||||
case audio::Image::Type::PublisherLogo:
|
||||
return db::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
@@ -361,10 +364,10 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& dbTrack, const ImageInfo& imageInfo)
|
||||
{
|
||||
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(dbTrack, image) };
|
||||
imageLink.modify()->setIndex(imageInfo.index);
|
||||
imageLink.modify()->setType(convertImageType(imageInfo.type));
|
||||
imageLink.modify()->setDescription(imageInfo.description);
|
||||
@@ -372,35 +375,35 @@ namespace lms::scanner
|
||||
return imageLink;
|
||||
}
|
||||
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span<const ImageInfo> images)
|
||||
{
|
||||
track.modify()->clearEmbeddedImageLinks();
|
||||
dbTrack.modify()->clearEmbeddedImageLinks();
|
||||
for (const ImageInfo& imageInfo : images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
||||
track.modify()->addEmbeddedImageLink(link);
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) };
|
||||
dbTrack.modify()->addEmbeddedImageLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
||||
db::Advisory getAdvisory(std::optional<Track::Advisory> advisory)
|
||||
{
|
||||
if (!advisory)
|
||||
return db::Advisory::UnSet;
|
||||
|
||||
switch (advisory.value())
|
||||
{
|
||||
case metadata::Track::Advisory::Clean:
|
||||
case Track::Advisory::Clean:
|
||||
return db::Advisory::Clean;
|
||||
case metadata::Track::Advisory::Explicit:
|
||||
case Track::Advisory::Explicit:
|
||||
return db::Advisory::Explicit;
|
||||
case metadata::Track::Advisory::Unknown:
|
||||
case Track::Advisory::Unknown:
|
||||
return db::Advisory::Unknown;
|
||||
}
|
||||
|
||||
return db::Advisory::UnSet;
|
||||
}
|
||||
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
// Add as many fields as possible to limit errors
|
||||
@@ -436,9 +439,9 @@ namespace lms::scanner
|
||||
return res;
|
||||
}
|
||||
|
||||
void fillInArtistsWithMbid(std::span<const metadata::Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (const metadata::Artist& artist : artists)
|
||||
for (const Artist& artist : artists)
|
||||
{
|
||||
if (artist.mbid.has_value())
|
||||
{
|
||||
@@ -448,9 +451,9 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
void fillInMbids(std::span<metadata::Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (metadata::Artist& artist : artists)
|
||||
for (Artist& artist : artists)
|
||||
{
|
||||
if (!artist.mbid)
|
||||
{
|
||||
@@ -461,7 +464,7 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
void fillMissingMbids(metadata::Track& track)
|
||||
void fillMissingMbids(Track& track)
|
||||
{
|
||||
// first pass: collect all artists that have mbids
|
||||
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
|
||||
@@ -485,9 +488,10 @@ namespace lms::scanner
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser)
|
||||
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions)
|
||||
: FileScanOperationBase{ std::move(fileToScan), db, settings }
|
||||
, _parser{ parser }
|
||||
, _metadataParser{ metadataParser }
|
||||
, _parserOptions{ parserOptions }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -495,17 +499,20 @@ namespace lms::scanner
|
||||
|
||||
void AudioFileScanOperation::scan()
|
||||
{
|
||||
std::unique_ptr<metadata::Track> track;
|
||||
|
||||
try
|
||||
{
|
||||
_parsedTrack = _parser.parseMetaData(getFilePath());
|
||||
auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) };
|
||||
|
||||
_file.emplace();
|
||||
|
||||
_file->audioProperties = audioFileInfo->getAudioProperties();
|
||||
_file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader());
|
||||
|
||||
// We fill missing artist mbids with mbids found on other artist roles
|
||||
fillMissingMbids(*_parsedTrack);
|
||||
fillMissingMbids(_file->track);
|
||||
|
||||
std::size_t index{};
|
||||
_parser.parseImages(getFilePath(), [&](const metadata::Image& image) {
|
||||
audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) {
|
||||
try
|
||||
{
|
||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||
@@ -522,7 +529,7 @@ namespace lms::scanner
|
||||
info.description = image.description;
|
||||
info.properties = properties;
|
||||
|
||||
_parsedImages.push_back(std::move(info));
|
||||
_file->images.push_back(std::move(info));
|
||||
}
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
@@ -532,15 +539,15 @@ namespace lms::scanner
|
||||
index++;
|
||||
});
|
||||
}
|
||||
catch (const metadata::AudioFileNoAudioPropertiesException&)
|
||||
catch (const audio::AudioFileNoAudioPropertiesException&)
|
||||
{
|
||||
addError<NoAudioTrackFoundError>(getFilePath());
|
||||
}
|
||||
catch (const metadata::IOException& e)
|
||||
catch (const audio::IOException& e)
|
||||
{
|
||||
addError<IOScanError>(getFilePath(), e.getErrorCode());
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
catch (const audio::Exception& e)
|
||||
{
|
||||
addError<AudioFileScanError>(getFilePath());
|
||||
}
|
||||
@@ -552,7 +559,7 @@ namespace lms::scanner
|
||||
|
||||
db::Session& dbSession{ getDb().getTLSSession() };
|
||||
db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) };
|
||||
if (!_parsedTrack)
|
||||
if (!_file)
|
||||
{
|
||||
if (track)
|
||||
{
|
||||
@@ -562,9 +569,9 @@ namespace lms::scanner
|
||||
return OperationResult::Skipped;
|
||||
}
|
||||
|
||||
if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
|
||||
if (_file->track.mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
|
||||
{
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_file->track.mbid) };
|
||||
|
||||
// find for an existing track MBID as the file may have just been moved
|
||||
if (!track && duplicateTracks.size() == 1)
|
||||
@@ -616,7 +623,7 @@ namespace lms::scanner
|
||||
if (!track)
|
||||
{
|
||||
// maybe the file just moved?
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize());
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize());
|
||||
if (track)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath());
|
||||
@@ -625,7 +632,7 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
// We estimate this is an audio file if the duration is not null
|
||||
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
if (_file->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
{
|
||||
addError<BadAudioDurationError>(getFilePath());
|
||||
|
||||
@@ -639,8 +646,8 @@ namespace lms::scanner
|
||||
|
||||
// ***** Title
|
||||
std::string title;
|
||||
if (!_parsedTrack->title.empty())
|
||||
title = _parsedTrack->title;
|
||||
if (!_file->track.title.empty())
|
||||
title = _file->track.title;
|
||||
else
|
||||
{
|
||||
// TODO parse file name to guess track etc.
|
||||
@@ -665,18 +672,18 @@ namespace lms::scanner
|
||||
track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
|
||||
|
||||
// Audio properties
|
||||
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
|
||||
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
|
||||
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||
track.modify()->setBitrate(_file->audioProperties.bitrate ? *_file->audioProperties.bitrate : 0);
|
||||
track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0);
|
||||
track.modify()->setChannelCount(_file->audioProperties.channelCount ? *_file->audioProperties.channelCount : 0);
|
||||
track.modify()->setDuration(_file->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_file->audioProperties.sampleRate ? *_file->audioProperties.sampleRate : 0);
|
||||
|
||||
track.modify()->setFileSize(getFileSize());
|
||||
track.modify()->setLastWriteTime(getLastWriteTime());
|
||||
|
||||
if (_parsedTrack->encodingTime.isValid())
|
||||
if (_file->track.encodingTime.isValid())
|
||||
{
|
||||
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
|
||||
const core::PartialDateTime& encodingTime{ _file->track.encodingTime };
|
||||
Wt::WDate date;
|
||||
Wt::WTime time;
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
|
||||
@@ -696,61 +703,61 @@ namespace lms::scanner
|
||||
track.modify()->clearArtistLinks();
|
||||
|
||||
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
|
||||
if (_file->track.medium && _file->track.medium->release)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback);
|
||||
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _file->track.remixerArtists, allowFallback);
|
||||
|
||||
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||
for (const auto& [role, performers] : _file->track.performerArtists)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
|
||||
|
||||
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
if (_file->track.medium && _file->track.medium->release)
|
||||
{
|
||||
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
|
||||
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) };
|
||||
assert(release);
|
||||
track.modify()->setRelease(release);
|
||||
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
|
||||
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
|
||||
}
|
||||
else
|
||||
{
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setMedium({});
|
||||
}
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||
track.modify()->setDate(_parsedTrack->date);
|
||||
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear });
|
||||
track.modify()->setTrackNumber(_file->track.position);
|
||||
track.modify()->setDate(_file->track.date);
|
||||
track.modify()->setOriginalDate(_file->track.originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _file->track.originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear });
|
||||
|
||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
|
||||
track.modify()->setDate(_parsedTrack->originalDate);
|
||||
if (!_file->track.date.isValid() && _file->track.originalDate.isValid())
|
||||
track.modify()->setDate(_file->track.originalDate);
|
||||
|
||||
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
|
||||
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||
track.modify()->setRecordingMBID(_file->track.recordingMBID);
|
||||
track.modify()->setTrackMBID(_file->track.mbid);
|
||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||
track.modify()->setCopyright(_file->track.copyright);
|
||||
track.modify()->setCopyrightURL(_file->track.copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_file->track.advisory));
|
||||
track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setReplayGain(_file->track.replayGain);
|
||||
track.modify()->setArtistDisplayName(_file->track.artistDisplayName);
|
||||
|
||||
track.modify()->clearEmbeddedLyrics();
|
||||
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
|
||||
for (const Lyrics& lyricsInfo : _file->track.lyrics)
|
||||
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
|
||||
|
||||
updateEmbeddedImages(dbSession, track, _parsedImages);
|
||||
updateEmbeddedImages(dbSession, track, _file->images);
|
||||
|
||||
if (added)
|
||||
{
|
||||
+22
-17
@@ -19,33 +19,32 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "image/Types.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "FileToScan.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/FileToScan.hpp"
|
||||
#include "scanners/IFileScanOperation.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class IDb;
|
||||
} // namespace lms::db
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class IAudioFileParser;
|
||||
} // namespace lms::metadata
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class TrackMetadataParser;
|
||||
|
||||
struct ImageInfo
|
||||
{
|
||||
std::size_t index;
|
||||
metadata::Image::Type type{ metadata::Image::Type::Unknown };
|
||||
audio::Image::Type type{ audio::Image::Type::Unknown };
|
||||
std::uint64_t hash{};
|
||||
std::size_t size{};
|
||||
image::ImageProperties properties;
|
||||
@@ -56,7 +55,7 @@ namespace lms::scanner
|
||||
class AudioFileScanOperation : public FileScanOperationBase
|
||||
{
|
||||
public:
|
||||
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser);
|
||||
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions);
|
||||
~AudioFileScanOperation() override;
|
||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||
@@ -66,9 +65,15 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
metadata::IAudioFileParser& _parser;
|
||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||
std::vector<ImageInfo> _parsedImages;
|
||||
};
|
||||
const TrackMetadataParser& _metadataParser;
|
||||
const audio::ParserOptions& _parserOptions;
|
||||
|
||||
struct AudioFileInfo
|
||||
{
|
||||
audio::AudioProperties audioProperties;
|
||||
Track track;
|
||||
std::vector<ImageInfo> images;
|
||||
};
|
||||
std::optional<AudioFileInfo> _file;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
+25
-14
@@ -21,53 +21,64 @@
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "AudioFileScanOperation.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/audiofile/AudioFileScanOperation.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
metadata::ParserReadStyle getParserReadStyle()
|
||||
audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||
|
||||
if (readStyle == "fast")
|
||||
return metadata::ParserReadStyle::Fast;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Fast;
|
||||
if (readStyle == "average")
|
||||
return metadata::ParserReadStyle::Average;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Average;
|
||||
if (readStyle == "accurate")
|
||||
return metadata::ParserReadStyle::Accurate;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Accurate;
|
||||
|
||||
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||
}
|
||||
|
||||
metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings)
|
||||
TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings)
|
||||
{
|
||||
metadata::AudioFileParserParameters params;
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.userExtraTags = settings.extraTags;
|
||||
params.artistTagDelimiters = settings.artistTagDelimiters;
|
||||
params.defaultTagDelimiters = settings.defaultTagDelimiters;
|
||||
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
|
||||
params.backend = metadata::ParserBackend::TagLib;
|
||||
params.readStyle = getParserReadStyle();
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
audio::ParserOptions createAudioFileParserOptions()
|
||||
{
|
||||
audio::ParserOptions options;
|
||||
options.readStyle = getParserReadStyle();
|
||||
options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
|
||||
: _db{ db }
|
||||
, _settings{ settings }
|
||||
, _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib
|
||||
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
|
||||
, _parserOptions{ createAudioFileParserOptions() }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,7 +96,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return _metadataParser->getSupportedExtensions();
|
||||
return audio::getSupportedExtensions(_parserOptions.parser);
|
||||
}
|
||||
|
||||
bool AudioFileScanner::needsScan(const FileToScan& file) const
|
||||
@@ -101,6 +112,6 @@ namespace lms::scanner
|
||||
|
||||
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||
{
|
||||
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
|
||||
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
+6
-2
@@ -19,7 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms
|
||||
{
|
||||
@@ -55,6 +58,7 @@ namespace lms::scanner
|
||||
|
||||
db::IDb& _db;
|
||||
const ScannerSettings& _settings;
|
||||
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
|
||||
const TrackMetadataParser _trackMetadataParser;
|
||||
const audio::ParserOptions _parserOptions;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
+36
-118
@@ -17,30 +17,26 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileParser.hpp"
|
||||
#include "TrackMetadataParser.hpp"
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "avformat/AvFormatImageReader.hpp"
|
||||
#include "avformat/AvFormatTagReader.hpp"
|
||||
#include "avformat/Utils.hpp"
|
||||
#include "taglib/TagLibImageReader.hpp"
|
||||
#include "taglib/TagLibTagReader.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, ITagReader::TagValueVisitor visitor)
|
||||
void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, audio::ITagReader::TagValueVisitor visitor)
|
||||
{
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
auto visitTagIfNonEmpty{ [&](std::string_view tag) {
|
||||
@@ -76,11 +72,11 @@ namespace lms::metadata
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters, const WhiteList* whitelist = nullptr)
|
||||
std::vector<T> getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes, std::span<const std::string> tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
for (const TagType tagType : tagTypes)
|
||||
for (const audio::TagType tagType : tagTypes)
|
||||
{
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
@@ -150,7 +146,7 @@ namespace lms::metadata
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T> getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes)
|
||||
std::optional<T> getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes)
|
||||
{
|
||||
std::optional<T> res;
|
||||
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
|
||||
@@ -161,45 +157,38 @@ namespace lms::metadata
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span<const std::string> tagDelimiters)
|
||||
std::vector<T> getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span<const std::string> tagDelimiters)
|
||||
{
|
||||
return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T> getTagValueAs(const ITagReader& tagReader, TagType tagType)
|
||||
std::optional<T> getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType)
|
||||
{
|
||||
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
|
||||
}
|
||||
|
||||
std::vector<Lyrics> getLyrics(const ITagReader& tagReader)
|
||||
std::vector<Lyrics> getLyrics(const audio::ITagReader& tagReader)
|
||||
{
|
||||
std::vector<Lyrics> res;
|
||||
|
||||
tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) {
|
||||
std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?)
|
||||
try
|
||||
{
|
||||
Lyrics lyrics{ parseLyrics(iss) };
|
||||
if (lyrics.language.empty())
|
||||
lyrics.language = language;
|
||||
|
||||
res.emplace_back(std::move(lyrics));
|
||||
}
|
||||
catch (const LyricsException& e)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() });
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist> getArtists(const ITagReader& tagReader,
|
||||
std::initializer_list<TagType> artistTagNames,
|
||||
std::initializer_list<TagType> artistSortTagNames,
|
||||
std::initializer_list<TagType> artistMBIDTagNames,
|
||||
const AudioFileParserParameters& params)
|
||||
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
|
||||
std::initializer_list<audio::TagType> artistTagNames,
|
||||
std::initializer_list<audio::TagType> artistSortTagNames,
|
||||
std::initializer_list<audio::TagType> artistMBIDTagNames,
|
||||
const TrackMetadataParser::Parameters& params)
|
||||
{
|
||||
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
if (artistNames.empty())
|
||||
@@ -224,7 +213,7 @@ namespace lms::metadata
|
||||
return artists;
|
||||
}
|
||||
|
||||
PerformerContainer getPerformerArtists(const ITagReader& tagReader)
|
||||
PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
|
||||
@@ -298,9 +287,9 @@ namespace lms::metadata
|
||||
return artistDisplayName;
|
||||
}
|
||||
|
||||
std::optional<Track::Advisory> getAdvisory(const ITagReader& tagReader)
|
||||
std::optional<Track::Advisory> getAdvisory(const audio::ITagReader& tagReader)
|
||||
{
|
||||
if (const auto value{ getTagValueAs<int>(tagReader, TagType::Advisory) })
|
||||
if (const auto value{ getTagValueAs<int>(tagReader, audio::TagType::Advisory) })
|
||||
{
|
||||
switch (*value)
|
||||
{
|
||||
@@ -318,92 +307,24 @@ namespace lms::metadata
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
|
||||
{
|
||||
return std::make_unique<AudioFileParser>(params);
|
||||
}
|
||||
|
||||
AudioFileParser::AudioFileParser(const AudioFileParserParameters& params)
|
||||
TrackMetadataParser::TrackMetadataParser(const Parameters& params)
|
||||
: _params{ params }
|
||||
{
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle));
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
LMS_LOG(METADATA, INFO, "Using AvFormat parser");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
|
||||
TrackMetadataParser::~TrackMetadataParser() = default;
|
||||
|
||||
Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
return taglib::utils::getSupportedExtensions();
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
return avformat::utils::getSupportedExtensions();
|
||||
break;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
|
||||
{
|
||||
std::unique_ptr<ITagReader> tagReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
tagReader = std::make_unique<taglib::TagLibTagReader>(p, _params.readStyle, _params.debug);
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
tagReader = std::make_unique<avformat::AvFormatTagReader>(p, _params.debug);
|
||||
break;
|
||||
}
|
||||
if (!tagReader)
|
||||
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||
|
||||
return parseMetaData(*tagReader);
|
||||
}
|
||||
|
||||
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const
|
||||
{
|
||||
std::unique_ptr<IImageReader> imageReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
imageReader = std::make_unique<taglib::TagLibImageReader>(p);
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
imageReader = std::make_unique<avformat::AvFormatImageReader>(p);
|
||||
break;
|
||||
}
|
||||
if (!imageReader)
|
||||
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||
|
||||
parseImages(*imageReader, std::move(visitor));
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
|
||||
{
|
||||
auto track{ std::make_unique<Track>() };
|
||||
|
||||
track->audioProperties = tagReader.getAudioProperties();
|
||||
processTags(tagReader, *track);
|
||||
|
||||
Track track;
|
||||
processTags(tagReader, track);
|
||||
return track;
|
||||
}
|
||||
|
||||
void AudioFileParser::processTags(const ITagReader& tagReader, Track& track) const
|
||||
void TrackMetadataParser::processTags(const audio::ITagReader& tagReader, Track& track) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
|
||||
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
||||
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
||||
@@ -469,8 +390,10 @@ namespace lms::metadata
|
||||
track.originalYear = track.originalDate.getYear();
|
||||
}
|
||||
|
||||
std::optional<Medium> AudioFileParser::getMedium(const ITagReader& tagReader) const
|
||||
std::optional<Medium> TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::optional<Medium> medium;
|
||||
medium.emplace();
|
||||
|
||||
@@ -499,8 +422,10 @@ namespace lms::metadata
|
||||
return medium;
|
||||
}
|
||||
|
||||
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader) const
|
||||
std::optional<Release> TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::optional<Release> release;
|
||||
|
||||
auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) };
|
||||
@@ -536,11 +461,4 @@ namespace lms::metadata
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor)
|
||||
{
|
||||
reader.visitImages([&](const Image& image) {
|
||||
visitor(image);
|
||||
});
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class TrackMetadataParser
|
||||
{
|
||||
public:
|
||||
struct SortByLengthDesc
|
||||
{
|
||||
bool operator()(const std::string& a, const std::string& b) const
|
||||
{
|
||||
if (a.length() != b.length())
|
||||
return a.length() > b.length();
|
||||
return a < b; // Break ties using lexicographical order
|
||||
}
|
||||
};
|
||||
|
||||
using WhiteList = std::set<std::string, SortByLengthDesc>;
|
||||
struct Parameters
|
||||
{
|
||||
std::vector<std::string> artistTagDelimiters;
|
||||
WhiteList artistsToNotSplit;
|
||||
std::vector<std::string> defaultTagDelimiters;
|
||||
std::vector<std::string> userExtraTags;
|
||||
};
|
||||
|
||||
TrackMetadataParser(const Parameters& params = {});
|
||||
~TrackMetadataParser();
|
||||
TrackMetadataParser(const TrackMetadataParser&) = delete;
|
||||
TrackMetadataParser& operator=(const TrackMetadataParser&) = delete;
|
||||
|
||||
Track parseTrackMetaData(const audio::ITagReader& reader) const;
|
||||
|
||||
private:
|
||||
void processTags(const audio::ITagReader& reader, Track& track) const;
|
||||
|
||||
std::optional<Medium> getMedium(const audio::ITagReader& tagReader) const;
|
||||
std::optional<Release> getRelease(const audio::ITagReader& tagReader) const;
|
||||
|
||||
const Parameters _params;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
+2
-19
@@ -24,9 +24,7 @@
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::metadata::utils
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr)
|
||||
{
|
||||
@@ -96,21 +94,6 @@ namespace lms::metadata::utils
|
||||
return result * sign;
|
||||
}
|
||||
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast:
|
||||
return "fast";
|
||||
case ParserReadStyle::Average:
|
||||
return "average";
|
||||
case ParserReadStyle::Accurate:
|
||||
return "accurate";
|
||||
}
|
||||
|
||||
throw Exception{ "Unknown read style" };
|
||||
}
|
||||
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
@@ -155,4 +138,4 @@ namespace lms::metadata::utils
|
||||
|
||||
return PerformerArtist{ Artist{ artistName }, std::string{ role } };
|
||||
}
|
||||
} // namespace lms::metadata::utils
|
||||
} // namespace lms::scanner::utils
|
||||
+3
-4
@@ -25,13 +25,12 @@
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "metadata/Types.hpp"
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::metadata::utils
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr);
|
||||
std::optional<int> parseYear(std::string_view yearStr);
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||
|
||||
struct PerformerArtist
|
||||
{
|
||||
@@ -41,4 +40,4 @@ namespace lms::metadata::utils
|
||||
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
} // namespace lms::metadata::utils
|
||||
} // namespace lms::scanner::utils
|
||||
+9
-14
@@ -22,17 +22,19 @@
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -48,12 +50,10 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::Lyrics> _parsedLyrics;
|
||||
std::optional<Lyrics> _parsedLyrics;
|
||||
};
|
||||
|
||||
void LyricsFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
@@ -64,12 +64,7 @@ namespace lms::scanner
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedLyrics = metadata::parseLyrics(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<LyricsFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedLyrics = parseLyrics(ifs);
|
||||
}
|
||||
|
||||
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
|
||||
@@ -140,7 +135,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedLyricsFileExtensions();
|
||||
return getSupportedLyricsFileExtensions();
|
||||
}
|
||||
|
||||
bool LyricsFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "LyricsParser.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <regex>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
|
||||
{
|
||||
@@ -227,4 +227,4 @@ namespace lms::metadata
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
|
||||
Lyrics parseLyrics(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
+8
-15
@@ -27,12 +27,12 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/PlayList.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/playlist/PlayListParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -51,12 +51,10 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::PlayList> _parsedPlayList;
|
||||
std::optional<PlayList> _parsedPlayList;
|
||||
};
|
||||
|
||||
void PlayListFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
@@ -67,12 +65,7 @@ namespace lms::scanner
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedPlayList = metadata::parsePlayList(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<PlayListFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedPlayList = parsePlayList(ifs);
|
||||
}
|
||||
|
||||
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
|
||||
@@ -139,7 +132,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedPlayListFileExtensions();
|
||||
return getSupportedPlayListFileExtensions();
|
||||
}
|
||||
|
||||
bool PlayListFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
+3
-3
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "metadata/PlayList.hpp"
|
||||
#include "PlayListParser.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
|
||||
{
|
||||
@@ -102,4 +102,4 @@ namespace lms::metadata
|
||||
|
||||
return playlist;
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "types/PlayList.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
|
||||
PlayList parsePlayList(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
@@ -30,11 +30,11 @@
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -53,7 +53,7 @@ namespace lms::scanner
|
||||
{
|
||||
assert(!link->isArtistMBIDMatched());
|
||||
|
||||
metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
|
||||
Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
|
||||
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, DEBUG, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast<int>(link->getType()) << " from " << link->getArtist() << " to " << newArtist);
|
||||
@@ -66,7 +66,7 @@ namespace lms::scanner
|
||||
{
|
||||
assert(!artistInfo->isMBIDMatched());
|
||||
|
||||
const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
|
||||
Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist);
|
||||
|
||||
|
||||
+2
-16
@@ -19,17 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
// See:
|
||||
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
|
||||
@@ -44,13 +39,4 @@ namespace lms::metadata
|
||||
std::string disambiguation; // mb
|
||||
std::string biography;
|
||||
};
|
||||
|
||||
class ArtistInfoParseException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
|
||||
ArtistInfo parseArtistInfo(std::istream& is);
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
+2
-16
@@ -20,16 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct Lyrics
|
||||
{
|
||||
@@ -42,13 +37,4 @@ namespace lms::metadata
|
||||
std::map<std::chrono::milliseconds, std::string> synchronizedLines;
|
||||
std::vector<std::string> unsynchronizedLines;
|
||||
};
|
||||
|
||||
class LyricsException : public metadata::Exception
|
||||
{
|
||||
public:
|
||||
using metadata::Exception::Exception;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
|
||||
Lyrics parseLyrics(std::istream& is);
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
+2
-7
@@ -20,19 +20,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct PlayList
|
||||
{
|
||||
std::string name;
|
||||
std::vector<std::filesystem::path> files;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
|
||||
PlayList parsePlayList(std::istream& is);
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
+4
-108
@@ -19,11 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -31,9 +28,9 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "Lyrics.hpp"
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::vector<std::string> /* values */>;
|
||||
|
||||
@@ -94,15 +91,6 @@ namespace lms::metadata
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioProperties
|
||||
{
|
||||
std::size_t bitrate{};
|
||||
std::size_t bitsPerSample{};
|
||||
std::size_t channelCount{};
|
||||
std::chrono::milliseconds duration{};
|
||||
std::size_t sampleRate{};
|
||||
};
|
||||
|
||||
struct Track
|
||||
{
|
||||
enum class Advisory
|
||||
@@ -111,7 +99,7 @@ namespace lms::metadata
|
||||
Explicit,
|
||||
Clean,
|
||||
};
|
||||
AudioProperties audioProperties;
|
||||
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> recordingMBID;
|
||||
std::string title;
|
||||
@@ -143,96 +131,4 @@ namespace lms::metadata
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
|
||||
struct Image
|
||||
{
|
||||
// See TagLib types (based on ID3v2 APIC types)
|
||||
enum class Type
|
||||
{
|
||||
// No information
|
||||
Unknown,
|
||||
// A type not enumerated below
|
||||
Other,
|
||||
// 32x32 PNG image that should be used as the file icon
|
||||
FileIcon,
|
||||
// File icon of a different size or format
|
||||
OtherFileIcon,
|
||||
// Front cover image of the album
|
||||
FrontCover,
|
||||
// Back cover image of the album
|
||||
BackCover,
|
||||
// Inside leaflet page of the album
|
||||
LeafletPage,
|
||||
// Image from the album itself
|
||||
Media,
|
||||
// Picture of the lead artist or soloist
|
||||
LeadArtist,
|
||||
// Picture of the artist or performer
|
||||
Artist,
|
||||
// Picture of the conductor
|
||||
Conductor,
|
||||
// Picture of the band or orchestra
|
||||
Band,
|
||||
// Picture of the composer
|
||||
Composer,
|
||||
// Picture of the lyricist or text writer
|
||||
Lyricist,
|
||||
// Picture of the recording location or studio
|
||||
RecordingLocation,
|
||||
// Picture of the artists during recording
|
||||
DuringRecording,
|
||||
// Picture of the artists during performance
|
||||
DuringPerformance,
|
||||
// Picture from a movie or video related to the track
|
||||
MovieScreenCapture,
|
||||
// Picture of a large, coloured fish
|
||||
ColouredFish,
|
||||
// Illustration related to the track
|
||||
Illustration,
|
||||
// Logo of the band or performer
|
||||
BandLogo,
|
||||
// Logo of the publisher (record company)
|
||||
PublisherLogo
|
||||
};
|
||||
|
||||
Type type{ Type::Unknown };
|
||||
std::string mimeType{ "application/octet-stream" };
|
||||
std::string description;
|
||||
std::span<const std::byte> data;
|
||||
};
|
||||
|
||||
enum class ParserBackend
|
||||
{
|
||||
TagLib,
|
||||
AvFormat,
|
||||
};
|
||||
|
||||
enum class ParserReadStyle
|
||||
{
|
||||
Fast,
|
||||
Average,
|
||||
Accurate,
|
||||
};
|
||||
|
||||
struct SortByLengthDesc
|
||||
{
|
||||
bool operator()(const std::string& a, const std::string& b) const
|
||||
{
|
||||
if (a.length() != b.length())
|
||||
return a.length() > b.length();
|
||||
return a < b; // Break ties using lexicographical order
|
||||
}
|
||||
};
|
||||
|
||||
using WhiteList = std::set<std::string, SortByLengthDesc>;
|
||||
struct AudioFileParserParameters
|
||||
{
|
||||
ParserBackend backend{ ParserBackend::TagLib };
|
||||
ParserReadStyle readStyle{ ParserReadStyle::Average };
|
||||
std::vector<std::string> artistTagDelimiters;
|
||||
WhiteList artistsToNotSplit;
|
||||
std::vector<std::string> defaultTagDelimiters;
|
||||
std::vector<std::string> userExtraTags;
|
||||
bool debug{};
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::scanner
|
||||
+3
-3
@@ -20,9 +20,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "metadata/ArtistInfo.hpp"
|
||||
#include "scanners/artistinfo/ArtistInfoParser.hpp"
|
||||
|
||||
namespace lms::metadata::tests
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
TEST(ArtistInfo, basic)
|
||||
{
|
||||
@@ -112,4 +112,4 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl
|
||||
ASSERT_EQ(artistInfo.sortName, "Artist, My");
|
||||
ASSERT_EQ(artistInfo.disambiguation, "My Artist");
|
||||
}
|
||||
} // namespace lms::metadata::tests
|
||||
} // namespace lms::scanner::tests
|
||||
+3
-3
@@ -20,9 +20,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "scanners/audiofile/Utils.hpp"
|
||||
|
||||
namespace lms::metadata::utils::tests
|
||||
namespace lms::scanner::utils::tests
|
||||
{
|
||||
TEST(MetaData, parseDate)
|
||||
{
|
||||
@@ -149,4 +149,4 @@ namespace lms::metadata::utils::tests
|
||||
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
} // namespace lms::metadata::utils::tests
|
||||
} // namespace lms::scanner::utils::tests
|
||||
@@ -0,0 +1,25 @@
|
||||
include(GoogleTest)
|
||||
|
||||
add_executable(test-scanner
|
||||
ArtistInfo.cpp
|
||||
AudioFileUtils.cpp
|
||||
Lyrics.cpp
|
||||
PlayList.cpp
|
||||
Scanner.cpp
|
||||
TrackMetadataParser.cpp
|
||||
)
|
||||
|
||||
target_include_directories(test-scanner PRIVATE
|
||||
../impl
|
||||
)
|
||||
|
||||
target_link_libraries(test-scanner PRIVATE
|
||||
lmsscanner
|
||||
lmsaudio
|
||||
GTest::GTest
|
||||
)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
gtest_discover_tests(test-scanner)
|
||||
endif()
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
namespace lms::metadata::tests
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -397,4 +397,4 @@ I, I just woke up from a dream
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[3], "I, I just woke up from a dream");
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[4], "");
|
||||
}
|
||||
} // namespace lms::metadata::tests
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -21,11 +21,11 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <sstream>
|
||||
|
||||
#include "metadata/PlayList.hpp"
|
||||
#include "scanners/playlist/PlayListParser.hpp"
|
||||
|
||||
namespace lms::metadata::tests
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
TEST(PlayList, basic)
|
||||
TEST(Scanner, playlist)
|
||||
{
|
||||
std::istringstream is{ R"(#EXTM3U
|
||||
#PLAYLIST:My super playlist
|
||||
@@ -53,7 +53,7 @@ one to be/../one to be/normalized/foo.mp3)" };
|
||||
EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3");
|
||||
}
|
||||
|
||||
TEST(PlayList, UTF8_bom)
|
||||
TEST(Scanner, playlist_UTF8_bom)
|
||||
{
|
||||
const unsigned char content[] = { 0xEF, 0xBB, 0xBF, '#', 'E', 'X', 'T', 'M', '3', 'U', '\r', '\n', '\r', '\n', '.', '.', '/', 't', 'e', 's', 't', '.', 'm', 'p', '3', '\r', '\n' };
|
||||
std::istringstream is{ std::string(reinterpret_cast<const char*>(content), sizeof(content)) };
|
||||
@@ -64,4 +64,4 @@ one to be/../one to be/normalized/foo.mp3)" };
|
||||
EXPECT_EQ(playlist.files[0], "../test.mp3");
|
||||
}
|
||||
|
||||
} // namespace lms::metadata::tests
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
+9
-17
@@ -21,22 +21,14 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ITagReader.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace lms::metadata::tests
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
class TestTagReader : public ITagReader
|
||||
class TestTagReader : public audio::ITagReader
|
||||
{
|
||||
public:
|
||||
static constexpr AudioProperties audioProperties{
|
||||
.bitrate = 128000,
|
||||
.bitsPerSample = 16,
|
||||
.channelCount = 2,
|
||||
.duration = std::chrono::seconds{ 180 },
|
||||
.sampleRate = 44000,
|
||||
};
|
||||
|
||||
using Tags = std::unordered_map<TagType, std::vector<std::string_view>>;
|
||||
using Tags = std::unordered_map<audio::TagType, std::vector<std::string_view>>;
|
||||
using Performers = std::unordered_map<std::string_view /*role*/, std::vector<std::string_view> /*names*/>;
|
||||
using ExtraUserTags = std::unordered_map<std::string_view, std::vector<std::string_view>>;
|
||||
using LyricsTags = std::unordered_map<std::string_view /*language*/, std::string_view /*contents*/>;
|
||||
@@ -63,7 +55,7 @@ namespace lms::metadata::tests
|
||||
_lyricsTags = std::move(lyricsTags);
|
||||
}
|
||||
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override
|
||||
void visitTagValues(audio::TagType tag, TagValueVisitor visitor) const override
|
||||
{
|
||||
auto itValues{ _tags.find(tag) };
|
||||
if (itValues != std::cend(_tags))
|
||||
@@ -97,8 +89,6 @@ namespace lms::metadata::tests
|
||||
visitor(language, lyrics);
|
||||
}
|
||||
|
||||
const AudioProperties& getAudioProperties() const override { return audioProperties; }
|
||||
|
||||
private:
|
||||
const Tags _tags;
|
||||
Performers _performers;
|
||||
@@ -106,8 +96,10 @@ namespace lms::metadata::tests
|
||||
LyricsTags _lyricsTags;
|
||||
};
|
||||
|
||||
inline std::unique_ptr<ITagReader> createDefaultPopulatedTestTagReader()
|
||||
inline std::unique_ptr<audio::ITagReader> createDefaultPopulatedTestTagReader()
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::unique_ptr<TestTagReader> testTags{ std::make_unique<TestTagReader>(
|
||||
TestTagReader::Tags{
|
||||
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
|
||||
@@ -168,4 +160,4 @@ namespace lms::metadata::tests
|
||||
|
||||
return testTags;
|
||||
}
|
||||
} // namespace lms::metadata::tests
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -0,0 +1,916 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
#include "TestTagReader.hpp"
|
||||
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
TEST(TrackMetadataParser, generalTest)
|
||||
{
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
|
||||
|
||||
TrackMetadataParser parser{ params };
|
||||
std::unique_ptr<audio::ITagReader> testTags{ createDefaultPopulatedTestTagReader() };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(*testTags) };
|
||||
|
||||
EXPECT_EQ(track.acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
|
||||
ASSERT_TRUE(track.advisory.has_value());
|
||||
EXPECT_EQ(track.advisory.value(), Track::Advisory::Clean);
|
||||
EXPECT_EQ(track.artistDisplayName, "MyArtist1 & MyArtist2");
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "MyArtist1");
|
||||
EXPECT_EQ(track.artists[0].sortName, "MyArtists1SortName");
|
||||
EXPECT_EQ(track.artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae"));
|
||||
EXPECT_EQ(track.artists[1].name, "MyArtist2");
|
||||
EXPECT_EQ(track.artists[1].sortName, "MyArtists2SortName");
|
||||
EXPECT_EQ(track.artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
|
||||
ASSERT_EQ(track.comments.size(), 2);
|
||||
EXPECT_EQ(track.comments[0], "Comment1");
|
||||
EXPECT_EQ(track.comments[1], "Comment2");
|
||||
ASSERT_EQ(track.composerArtists.size(), 2);
|
||||
EXPECT_EQ(track.composerArtists[0].name, "MyComposer1");
|
||||
EXPECT_EQ(track.composerArtists[0].sortName, "MyComposerSortOrder1");
|
||||
EXPECT_EQ(track.composerArtists[1].name, "MyComposer2");
|
||||
EXPECT_EQ(track.composerArtists[1].sortName, "MyComposerSortOrder2");
|
||||
ASSERT_EQ(track.conductorArtists.size(), 2);
|
||||
EXPECT_EQ(track.conductorArtists[0].name, "MyConductor1");
|
||||
EXPECT_EQ(track.conductorArtists[1].name, "MyConductor2");
|
||||
EXPECT_EQ(track.copyright, "MyCopyright");
|
||||
EXPECT_EQ(track.copyrightURL, "MyCopyrightURL");
|
||||
ASSERT_TRUE(track.date.isValid());
|
||||
EXPECT_EQ(track.date.getYear(), 2020);
|
||||
EXPECT_EQ(track.date.getMonth(), 3);
|
||||
EXPECT_EQ(track.date.getDay(), 4);
|
||||
ASSERT_EQ(track.genres.size(), 2);
|
||||
EXPECT_EQ(track.genres[0], "Genre1");
|
||||
EXPECT_EQ(track.genres[1], "Genre2");
|
||||
ASSERT_EQ(track.groupings.size(), 2);
|
||||
EXPECT_EQ(track.groupings[0], "Grouping1");
|
||||
EXPECT_EQ(track.groupings[1], "Grouping2");
|
||||
ASSERT_EQ(track.languages.size(), 2);
|
||||
EXPECT_EQ(track.languages[0], "Language1");
|
||||
EXPECT_EQ(track.languages[1], "Language2");
|
||||
ASSERT_EQ(track.lyricistArtists.size(), 2);
|
||||
EXPECT_EQ(track.lyricistArtists[0].name, "MyLyricist1");
|
||||
EXPECT_EQ(track.lyricistArtists[1].name, "MyLyricist2");
|
||||
ASSERT_EQ(track.lyrics.size(), 1);
|
||||
EXPECT_EQ(track.lyrics.front().language, "eng");
|
||||
ASSERT_EQ(track.lyrics.front().synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 }));
|
||||
EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line");
|
||||
ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 }));
|
||||
EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line");
|
||||
ASSERT_TRUE(track.mbid.has_value());
|
||||
EXPECT_EQ(track.mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a"));
|
||||
ASSERT_EQ(track.mixerArtists.size(), 2);
|
||||
EXPECT_EQ(track.mixerArtists[0].name, "MyMixer1");
|
||||
EXPECT_EQ(track.mixerArtists[1].name, "MyMixer2");
|
||||
ASSERT_EQ(track.moods.size(), 2);
|
||||
EXPECT_EQ(track.moods[0], "Mood1");
|
||||
EXPECT_EQ(track.moods[1], "Mood2");
|
||||
ASSERT_TRUE(track.originalDate.isValid());
|
||||
EXPECT_EQ(track.originalDate.getYear(), 2019);
|
||||
EXPECT_EQ(track.originalDate.getMonth(), 2);
|
||||
EXPECT_EQ(track.originalDate.getDay(), 3);
|
||||
ASSERT_TRUE(track.originalYear.has_value());
|
||||
EXPECT_EQ(track.originalYear.value(), 2019);
|
||||
ASSERT_TRUE(track.performerArtists.contains("Rolea"));
|
||||
ASSERT_EQ(track.performerArtists.at("Rolea").size(), 2);
|
||||
EXPECT_EQ(track.performerArtists.at("Rolea")[0].name, "MyPerformer1ForRoleA");
|
||||
EXPECT_EQ(track.performerArtists.at("Rolea")[1].name, "MyPerformer2ForRoleA");
|
||||
ASSERT_EQ(track.performerArtists.at("Roleb").size(), 2);
|
||||
EXPECT_EQ(track.performerArtists.at("Roleb")[0].name, "MyPerformer1ForRoleB");
|
||||
EXPECT_EQ(track.performerArtists.at("Roleb")[1].name, "MyPerformer2ForRoleB");
|
||||
ASSERT_TRUE(track.position.has_value());
|
||||
EXPECT_EQ(track.position.value(), 7);
|
||||
ASSERT_EQ(track.producerArtists.size(), 2);
|
||||
EXPECT_EQ(track.producerArtists[0].name, "MyProducer1");
|
||||
EXPECT_EQ(track.producerArtists[1].name, "MyProducer2");
|
||||
ASSERT_TRUE(track.recordingMBID.has_value());
|
||||
EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
|
||||
ASSERT_TRUE(track.replayGain.has_value());
|
||||
EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33);
|
||||
ASSERT_EQ(track.remixerArtists.size(), 2);
|
||||
EXPECT_EQ(track.remixerArtists[0].name, "MyRemixer1");
|
||||
EXPECT_EQ(track.remixerArtists[1].name, "MyRemixer2");
|
||||
EXPECT_EQ(track.title, "MyTitle");
|
||||
ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A").size(), 2);
|
||||
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[0], "MyTagValue1ForTagA");
|
||||
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[1], "MyTagValue2ForTagA");
|
||||
ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B").size(), 2);
|
||||
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[0], "MyTagValue1ForTagB");
|
||||
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[1], "MyTagValue2ForTagB");
|
||||
|
||||
// Medium
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
EXPECT_EQ(track.medium->media, "CD");
|
||||
EXPECT_EQ(track.medium->name, "MySubtitle");
|
||||
ASSERT_TRUE(track.medium->position.has_value());
|
||||
EXPECT_EQ(track.medium->position.value(), 2);
|
||||
ASSERT_TRUE(track.medium->replayGain.has_value());
|
||||
EXPECT_FLOAT_EQ(track.medium->replayGain.value(), -0.5);
|
||||
ASSERT_TRUE(track.medium->trackCount.has_value());
|
||||
EXPECT_EQ(track.medium->trackCount.value(), 12);
|
||||
|
||||
// Release
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
const Release& release{ track.medium->release.value() };
|
||||
EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
|
||||
ASSERT_EQ(release.artists.size(), 2);
|
||||
EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1");
|
||||
EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName");
|
||||
EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
|
||||
EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2");
|
||||
EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName");
|
||||
EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
|
||||
EXPECT_TRUE(release.isCompilation);
|
||||
EXPECT_EQ(release.barcode, "MyBarcode");
|
||||
ASSERT_EQ(release.labels.size(), 2);
|
||||
EXPECT_EQ(release.labels[0], "Label1");
|
||||
EXPECT_EQ(release.labels[1], "Label2");
|
||||
ASSERT_TRUE(release.mbid.has_value());
|
||||
EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
|
||||
EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
|
||||
EXPECT_EQ(release.mediumCount, 3);
|
||||
EXPECT_EQ(release.name, "MyAlbum");
|
||||
EXPECT_EQ(release.sortName, "MyAlbumSortName");
|
||||
EXPECT_EQ(release.comment, "MyAlbumComment");
|
||||
ASSERT_EQ(release.countries.size(), 2);
|
||||
EXPECT_EQ(release.countries[0], "MyCountry1");
|
||||
EXPECT_EQ(release.countries[1], "MyCountry2");
|
||||
{
|
||||
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
|
||||
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, trim)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.genres.size(), 3);
|
||||
EXPECT_EQ(track.genres[0], "Genre1");
|
||||
EXPECT_EQ(track.genres[1], "Genre2");
|
||||
EXPECT_EQ(track.genres[2], "Genre3");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimiters)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
||||
{ audio::TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
|
||||
{ audio::TagType::Genre, { "Genre1 ; Genre2" } },
|
||||
{ audio::TagType::Language, { " Lang1/Lang2 / Lang3" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.defaultTagDelimiters = { " ; ", "/" };
|
||||
params.artistTagDelimiters = { " / ", " feat. " };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 3);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artists[2].name, "Artist3");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit
|
||||
ASSERT_EQ(track.genres.size(), 2);
|
||||
EXPECT_EQ(track.genres[0], "Genre1");
|
||||
EXPECT_EQ(track.genres[1], "Genre2");
|
||||
ASSERT_EQ(track.languages.size(), 3);
|
||||
EXPECT_EQ(track.languages[0], "Lang1");
|
||||
EXPECT_EQ(track.languages[1], "Lang2");
|
||||
EXPECT_EQ(track.languages[2], "Lang3");
|
||||
|
||||
// Medium
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
|
||||
// Release
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
EXPECT_EQ(track.medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "AlbumArtist1");
|
||||
EXPECT_EQ(track.medium->release->artists[1].name, "AlbumArtist2");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { " AC/DC " } },
|
||||
{ audio::TagType::Artist, { "AC/DC " } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC");
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
EXPECT_EQ(track.medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 1);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "AC/DC");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_artists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "AC/DC and MyArtist" } },
|
||||
{ audio::TagType::Artists, { "AC/DC", "MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { " AC/DC " };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_first)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "AC/DC;MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_middle)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 3);
|
||||
EXPECT_EQ(track.artists[0].name, "MyArtist1");
|
||||
EXPECT_EQ(track.artists[1].name, "AC/DC");
|
||||
EXPECT_EQ(track.artists[2].name, "MyArtist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_last)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { ";", "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_longest_first)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { ";", "/" };
|
||||
params.artistsToNotSplit = { "AC", "DC", "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track.artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_begin)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].name, "AC/DC; MyArtist");
|
||||
EXPECT_EQ(track.artistDisplayName, "AC/DC; MyArtist");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_middle)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].name, "MyArtist1; AC/DC ; MyArtist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "MyArtist1; AC/DC ; MyArtist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_end)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { " MyArtist; AC/DC " } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].name, "MyArtist; AC/DC");
|
||||
EXPECT_EQ(track.artistDisplayName, "MyArtist; AC/DC");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimiters_foundInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1; Artist2" } },
|
||||
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TrackMetadataParser parser{ params };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimiters_foundInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 feat. Artist2" } },
|
||||
{ audio::TagType::Artists, { "Artist1; Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TrackMetadataParser parser{ params };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1 feat. Artist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimiters_notUsed)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
|
||||
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "; " };
|
||||
TrackMetadataParser parser{ params };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimiters_onlyInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { " & " };
|
||||
TrackMetadataParser parser{ params };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, customDelimitersUsedForArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artists, { "Artist1 & Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { " & " };
|
||||
TrackMetadataParser parser{ params };
|
||||
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, noArtistInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 0);
|
||||
EXPECT_EQ(track.artistDisplayName, "");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, singleArtistInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ audio::TagType::Artists, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artists!
|
||||
{ audio::TagType::Artist, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in Artist!
|
||||
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInArtistsWithEndDelimiter)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 & (CV. Artist2)" } },
|
||||
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1 & (CV. Artist2)");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, singleArtistInAlbumArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtist!
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtists, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium);
|
||||
ASSERT_TRUE(track.medium->release);
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 1);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInAlbumArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtists!
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium);
|
||||
ASSERT_TRUE(track.medium->release);
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInAlbumArtists_displayName)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { "Artist1 & Artist2" } },
|
||||
{ audio::TagType::AlbumArtists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium);
|
||||
ASSERT_TRUE(track.medium->release);
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInAlbumArtists)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
// nothing in AlbumArtist!
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtists, { "Artist1", "Artist2" } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium);
|
||||
ASSERT_TRUE(track.medium->release);
|
||||
ASSERT_EQ(track.medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
|
||||
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
|
||||
{ audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[0].mbid, std::nullopt);
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artists[1].mbid, std::nullopt);
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "Artist1 / Artist2" } },
|
||||
{ audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { " / " };
|
||||
TrackMetadataParser parser{ params };
|
||||
const Track track{ parser.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 2);
|
||||
EXPECT_EQ(track.artists[0].name, "Artist1");
|
||||
EXPECT_EQ(track.artists[0].mbid, std::nullopt);
|
||||
EXPECT_EQ(track.artists[1].name, "Artist2");
|
||||
EXPECT_EQ(track.artists[1].mbid, std::nullopt);
|
||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, release_sortNameFallback)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
// No AlbumSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
EXPECT_EQ(track.medium->release->sortName, "MyAlbum");
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, artist_sortNameFallback)
|
||||
{
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "MyArtist" } },
|
||||
{ audio::TagType::ArtistSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "MyArtist" } },
|
||||
{ audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "MyArtist" } },
|
||||
{ audio::TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } },
|
||||
{ audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.artists.size(), 1);
|
||||
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, albumartist_sortNameFallback)
|
||||
{
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ audio::TagType::AlbumArtistSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
|
||||
const auto& artists{ track.medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
{ audio::TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
|
||||
const auto& artists{ track.medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Album, { "MyAlbum" } },
|
||||
|
||||
{ audio::TagType::AlbumArtist, { "MyArtist" } },
|
||||
{ audio::TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } },
|
||||
{ audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
|
||||
// No ArtistSortOrder
|
||||
}
|
||||
};
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_TRUE(track.medium.has_value());
|
||||
ASSERT_TRUE(track.medium->release.has_value());
|
||||
|
||||
const auto& artists{ track.medium->release->artists };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, advisory)
|
||||
{
|
||||
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Advisory, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
|
||||
if (track.advisory.has_value())
|
||||
{
|
||||
EXPECT_EQ(track.advisory.value(), expectedValue);
|
||||
}
|
||||
};
|
||||
|
||||
doTest("0", Track::Advisory::Unknown);
|
||||
doTest("1", Track::Advisory::Explicit);
|
||||
doTest("4", Track::Advisory::Explicit);
|
||||
doTest("2", Track::Advisory::Clean);
|
||||
doTest("", std::nullopt);
|
||||
doTest("3", std::nullopt);
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, encodingTime)
|
||||
{
|
||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::EncodingTime, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.encodingTime, expectedValue) << "Value = '" << value << "'";
|
||||
};
|
||||
|
||||
doTest("", core::PartialDateTime{});
|
||||
doTest("foo", core::PartialDateTime{});
|
||||
doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 });
|
||||
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
}
|
||||
|
||||
TEST(TrackMetadataParser, date)
|
||||
{
|
||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Date, { value } },
|
||||
}
|
||||
};
|
||||
|
||||
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track.date, expectedValue) << "Value = '" << value << "'";
|
||||
};
|
||||
|
||||
doTest("", core::PartialDateTime{});
|
||||
doTest("foo", core::PartialDateTime{});
|
||||
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020-01", core::PartialDateTime{ 2020, 1 });
|
||||
doTest("2020", core::PartialDateTime{ 2020 });
|
||||
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
||||
doTest("2020/01", core::PartialDateTime{ 2020, 1 });
|
||||
doTest("2020", core::PartialDateTime{ 2020 });
|
||||
}
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -1,6 +1,6 @@
|
||||
add_library(lmstranscoding STATIC
|
||||
impl/TranscodingResourceHandler.cpp
|
||||
impl/TranscodingService.cpp
|
||||
impl/TranscodeResourceHandler.cpp
|
||||
impl/TranscodeService.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmstranscoding INTERFACE
|
||||
@@ -13,7 +13,7 @@ target_include_directories(lmstranscoding PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmstranscoding PRIVATE
|
||||
lmsav
|
||||
lmsaudio
|
||||
)
|
||||
|
||||
target_link_libraries(lmstranscoding PUBLIC
|
||||
|
||||
+9
-13
@@ -17,42 +17,38 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TranscodingResourceHandler.hpp"
|
||||
#include "TranscodeResourceHandler.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "av/ITranscoder.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/ITranscoder.hpp"
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
std::unique_ptr<core::IResourceHandler> createResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, bool estimateContentLength)
|
||||
{
|
||||
return std::make_unique<TranscodingResourceHandler>(inputParameters, outputParameters, estimateContentLength);
|
||||
}
|
||||
|
||||
// TODO set some nice HTTP return code
|
||||
|
||||
TranscodingResourceHandler::TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional<std::size_t> estimatedContentLength)
|
||||
ResourceHandler::ResourceHandler(const audio::TranscodeParameters& parameters, std::optional<std::size_t> estimatedContentLength)
|
||||
: _estimatedContentLength{ estimatedContentLength }
|
||||
{
|
||||
try
|
||||
{
|
||||
_transcoder = av::createTranscoder(inputParameters, outputParameters);
|
||||
_transcoder = createTranscoder(parameters);
|
||||
|
||||
if (_estimatedContentLength)
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
|
||||
else
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length");
|
||||
}
|
||||
catch (av::Exception& e)
|
||||
catch (audio::Exception& e)
|
||||
{
|
||||
LMS_LOG(TRANSCODING, ERROR, "Failed to create transcoder: " << e.what());
|
||||
}
|
||||
}
|
||||
|
||||
TranscodingResourceHandler::~TranscodingResourceHandler() = default;
|
||||
ResourceHandler::~ResourceHandler() = default;
|
||||
|
||||
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
Wt::Http::ResponseContinuation* ResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
{
|
||||
if (!_transcoder)
|
||||
{
|
||||
+7
-7
@@ -23,19 +23,19 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "av/ITranscoder.hpp"
|
||||
#include "audio/ITranscoder.hpp"
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
class TranscodingResourceHandler final : public core::IResourceHandler
|
||||
class ResourceHandler final : public core::IResourceHandler
|
||||
{
|
||||
public:
|
||||
TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional<std::size_t> estimatedContentLength);
|
||||
~TranscodingResourceHandler() override;
|
||||
ResourceHandler(const audio::TranscodeParameters& parameters, std::optional<std::size_t> estimatedContentLength);
|
||||
~ResourceHandler() override;
|
||||
|
||||
TranscodingResourceHandler(const TranscodingResourceHandler&) = delete;
|
||||
TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete;
|
||||
ResourceHandler(const ResourceHandler&) = delete;
|
||||
ResourceHandler& operator=(const ResourceHandler&) = delete;
|
||||
|
||||
private:
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
@@ -46,6 +46,6 @@ namespace lms::transcoding
|
||||
std::array<std::byte, _chunkSize> _buffer;
|
||||
std::size_t _bytesReadyCount{};
|
||||
std::size_t _totalServedByteCount{};
|
||||
std::unique_ptr<av::ITranscoder> _transcoder;
|
||||
std::unique_ptr<audio::ITranscoder> _transcoder;
|
||||
};
|
||||
} // namespace lms::transcoding
|
||||
+13
-22
@@ -17,24 +17,20 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TranscodingService.hpp"
|
||||
#include "TranscodeService.hpp"
|
||||
|
||||
#include "av/ITranscoder.hpp"
|
||||
#include "audio/ITranscoder.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "TranscodingResourceHandler.hpp"
|
||||
#include "TranscodeResourceHandler.hpp"
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
namespace
|
||||
{
|
||||
av::OutputParameters toAv(const OutputParameters& out)
|
||||
{
|
||||
return { .format = static_cast<lms::av::OutputFormat>(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata };
|
||||
}
|
||||
|
||||
std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration)
|
||||
{
|
||||
const std::size_t estimatedContentLength{ static_cast<size_t>((bitrate / 8 * duration.count()) / 1000) };
|
||||
@@ -42,40 +38,35 @@ namespace lms::transcoding
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<ITranscodingService> createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager)
|
||||
std::unique_ptr<ITranscodeService> createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager)
|
||||
{
|
||||
return std::make_unique<TranscodingService>(db, childProcessManager);
|
||||
return std::make_unique<TranscodeService>(db, childProcessManager);
|
||||
}
|
||||
|
||||
TranscodingService::TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager)
|
||||
TranscodeService::TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager)
|
||||
: _db{ db }
|
||||
, _childProcessManager(childProcessManager)
|
||||
{
|
||||
LMS_LOG(TRANSCODING, INFO, "Service started!");
|
||||
}
|
||||
|
||||
TranscodingService::~TranscodingService()
|
||||
TranscodeService::~TranscodeService()
|
||||
{
|
||||
LMS_LOG(TRANSCODING, INFO, "Service stopped!");
|
||||
}
|
||||
|
||||
std::unique_ptr<core::IResourceHandler> TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
|
||||
std::unique_ptr<core::IResourceHandler> TranscodeService::createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength)
|
||||
{
|
||||
av::InputParameters avInputParams;
|
||||
std::optional<std::size_t> estimatedContentLength;
|
||||
|
||||
avInputParams.file = inputParameters.filePath;
|
||||
avInputParams.offset = inputParameters.offset;
|
||||
avInputParams.streamIndex = inputParameters.streamIndex;
|
||||
|
||||
if (estimateContentLength)
|
||||
{
|
||||
if (inputParameters.offset < inputParameters.duration)
|
||||
estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, inputParameters.duration - inputParameters.offset);
|
||||
if (parameters.inputParameters.offset < parameters.inputParameters.duration)
|
||||
estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset);
|
||||
else
|
||||
LMS_LOG(TRANSCODING, WARNING, "Offset " << inputParameters.offset << " is greater than audio file duration " << inputParameters.duration << ": not estimating content length");
|
||||
LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.duration << ": not estimating content length");
|
||||
}
|
||||
|
||||
return std::make_unique<TranscodingResourceHandler>(avInputParams, toAv(outputParameters), estimatedContentLength);
|
||||
return std::make_unique<transcoding::ResourceHandler>(parameters, estimatedContentLength);
|
||||
}
|
||||
} // namespace lms::transcoding
|
||||
+7
-7
@@ -19,21 +19,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/transcoding/ITranscodingService.hpp"
|
||||
#include "services/transcoding/ITranscodeService.hpp"
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
class TranscodingService : public ITranscodingService
|
||||
class TranscodeService : public ITranscodeService
|
||||
{
|
||||
public:
|
||||
explicit TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager);
|
||||
~TranscodingService() override;
|
||||
explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager);
|
||||
~TranscodeService() override;
|
||||
|
||||
TranscodingService(const TranscodingService&) = delete;
|
||||
TranscodingService& operator=(const TranscodingService&) = delete;
|
||||
TranscodeService(const TranscodeService&) = delete;
|
||||
TranscodeService& operator=(const TranscodeService&) = delete;
|
||||
|
||||
private:
|
||||
std::unique_ptr<core::IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override;
|
||||
std::unique_ptr<core::IResourceHandler> createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) override;
|
||||
|
||||
db::IDb& _db;
|
||||
core::IChildProcessManager& _childProcessManager;
|
||||
+5
-10
@@ -19,18 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::transcoding
|
||||
{
|
||||
class IImageReader
|
||||
class Exception : public core::LmsException
|
||||
{
|
||||
public:
|
||||
virtual ~IImageReader() = default;
|
||||
|
||||
using ImageVisitor = std::function<void(const Image& image)>;
|
||||
virtual void visitImages(ImageVisitor visitor) const = 0;
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::transcoding
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio/TranscodeTypes.hpp"
|
||||
|
||||
namespace lms
|
||||
{
|
||||
namespace core
|
||||
{
|
||||
class IChildProcessManager;
|
||||
class IResourceHandler;
|
||||
} // namespace core
|
||||
|
||||
namespace db
|
||||
{
|
||||
class IDb;
|
||||
}
|
||||
} // namespace lms
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
class ITranscodeService
|
||||
{
|
||||
public:
|
||||
virtual ~ITranscodeService() = default;
|
||||
|
||||
// virtual std::unique_ptr<IAudioFileInfo> parseAudioFileInfo(const std::filesystem::path& p) const = 0;
|
||||
|
||||
virtual std::unique_ptr<core::IResourceHandler> createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength = false) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<ITranscodeService> createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager);
|
||||
} // namespace lms::transcoding
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace lms
|
||||
{
|
||||
namespace core
|
||||
{
|
||||
class IChildProcessManager;
|
||||
class IResourceHandler;
|
||||
} // namespace core
|
||||
|
||||
namespace db
|
||||
{
|
||||
class IDb;
|
||||
}
|
||||
} // namespace lms
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
struct InputParameters
|
||||
{
|
||||
std::filesystem::path filePath;
|
||||
std::chrono::milliseconds duration{}; // Duration of the audio file
|
||||
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
|
||||
std::optional<std::size_t> streamIndex; // Index of the stream to be transcoded (select the "best" audio stream if not set)
|
||||
};
|
||||
|
||||
enum class OutputFormat
|
||||
{
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
struct OutputParameters
|
||||
{
|
||||
OutputFormat format;
|
||||
std::size_t bitrate{ 128'000 };
|
||||
bool stripMetadata{ true };
|
||||
};
|
||||
|
||||
class ITranscodingService
|
||||
{
|
||||
public:
|
||||
virtual ~ITranscodingService() = default;
|
||||
|
||||
virtual std::unique_ptr<core::IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<ITranscodingService> createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager);
|
||||
} // namespace lms::transcoding
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user