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:
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
namespace lms::scanner::benchmarks
|
||||
{
|
||||
static void BM_Lyrics(benchmark::State& state)
|
||||
{
|
||||
std::istringstream lyricsStream{ R"(
|
||||
[id: rkrzmqos]
|
||||
[ar: Billie Eilish]
|
||||
[al: HIT ME HARD AND SOFT]
|
||||
[ti: WILDFLOWER]
|
||||
[length: 04:21]
|
||||
[00:15.16]Things fall apart and time breaks your heart
|
||||
[00:21.57]I wasn't there, but I know
|
||||
[00:28.01]She was your girl, you showed her the world
|
||||
[00:34.18]You fell out of love and you both let go
|
||||
[00:40.33]She was cryin' on my shoulder, all I could do was hold her
|
||||
[00:46.71]Only made us closer until July
|
||||
[00:53.47]Now I know that you love me, you don't need to remind me
|
||||
[00:59.86]I should put it all behind me, shouldn't I?
|
||||
[01:04.72]But I see her in the back of my mind
|
||||
[01:11.50]All the time
|
||||
[01:17.75]Like a fever, like I'm burning alive
|
||||
[01:24.31]Like a sign
|
||||
[01:32.67]Did I cross the line?
|
||||
[01:37.72]Mm, hm
|
||||
[01:48.97]Well, good things don't last (good things don't last)
|
||||
[01:52.28]And life moves so fast (life moves so fast)
|
||||
[01:55.50]I'd never ask who was better (I'd never ask who was better)
|
||||
[02:01.68]'Cause she couldn't be (she couldn't be)
|
||||
[02:05.20]More different from me (more different)
|
||||
[02:08.53]Happy and free (happy and free) in leather
|
||||
[02:14.51]And I know that you love me (you love me)
|
||||
[02:18.01]You don't need to remind me (remind me)
|
||||
[02:20.88]Wanna put it all behind me, but baby
|
||||
[02:26.41]I see her in the back of my mind (back of my mind)
|
||||
[02:32.66]All the time (all the time)
|
||||
[02:38.95]Feels like a fever (like a fever)
|
||||
[02:42.06]Like I'm burning alive (burning alive)
|
||||
[02:45.66]Like a sign
|
||||
[02:53.68]Did I cross the line?
|
||||
[02:58.03]You say no one knows you so well (oh)
|
||||
[03:02.63]But every time you touch me, I just wonder how she felt
|
||||
[03:08.54]Valentine's Day, cryin' in the hotel
|
||||
[03:14.48]I know you didn't mean to hurt me, so I kept it to myself
|
||||
[03:21.13]And I wonder
|
||||
[03:24.41]Do you see her in the back of your mind?
|
||||
[03:31.13]In my eyes?
|
||||
[03:51.94]You say no one knows you so well
|
||||
[03:56.88]But every time you touch me, I just wonder how she felt
|
||||
[04:03.87]Valentine's Day, cryin' in the hotel
|
||||
[04:09.16]I know you didn't mean to hurt me, so I kept it to myself
|
||||
[04:15.59]
|
||||
)" };
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
lyricsStream.clear();
|
||||
lyricsStream.seekg(0, std::ios::beg);
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(lyricsStream) };
|
||||
assert(lyrics.synchronizedLines.size() == 41);
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK(BM_Lyrics);
|
||||
|
||||
} // 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();
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 <benchmark/benchmark.h>
|
||||
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
#include "TestTagReader.hpp"
|
||||
|
||||
namespace lms::scanner::benchmarks
|
||||
{
|
||||
static void BM_Metadata_parse(benchmark::State& state)
|
||||
{
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
|
||||
|
||||
std::unique_ptr<audio::ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
|
||||
const TrackMetadataParser parser{ params };
|
||||
for (auto _ : state)
|
||||
{
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(*testTags));
|
||||
}
|
||||
}
|
||||
|
||||
static void BM_Metadata_parseArtists(benchmark::State& state)
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
const TrackMetadataParser parser;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
static void BM_Metadata_parseArtists_WithWhitelist(benchmark::State& state)
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
// The list itself is not important, the idea is to have some volume
|
||||
params.artistsToNotSplit = { "AC/DC",
|
||||
"+/-",
|
||||
R"(A/N【eɪ-ɛn)",
|
||||
"Akron/Family",
|
||||
"AM/FM",
|
||||
"Ashes/Dust",
|
||||
"B/B/S/",
|
||||
"BLCK/MRKT/RGNS",
|
||||
"Body/Gate/Head",
|
||||
"Body/Head",
|
||||
"Born/Dead",
|
||||
"Burger/Ink",
|
||||
"case/lang/veirs",
|
||||
"Chicago / London Underground",
|
||||
"Dakota/Dakota",
|
||||
"Dark/Light",
|
||||
"Decades/Failures",
|
||||
"The Denison/Kimball Trio",
|
||||
"D-W/L-SS",
|
||||
"F/i",
|
||||
"Friend / Enemy",
|
||||
"GZA/Genius",
|
||||
"I/O",
|
||||
"I/O3",
|
||||
"In/Humanity",
|
||||
"Love/Lust",
|
||||
"Mirror/Dash",
|
||||
"Model/Actress",
|
||||
"N/N",
|
||||
"Neither/Neither World",
|
||||
"P1/E",
|
||||
"Sick/Tired",
|
||||
"t/e/u/",
|
||||
"tide/edit",
|
||||
"V/Vm",
|
||||
"White/Lichens",
|
||||
"White/Light",
|
||||
"Yamantaka // Sonic Titan" };
|
||||
|
||||
const TrackMetadataParser parser{ params };
|
||||
for (auto _ : state)
|
||||
{
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
static void BM_Metadata_parseArtists_WithoutWhitelist(benchmark::State& state)
|
||||
{
|
||||
const tests::TestTagReader testTags{
|
||||
{
|
||||
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.artistTagDelimiters = { "/", ";" };
|
||||
|
||||
const TrackMetadataParser parser{ params };
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK(BM_Metadata_parse);
|
||||
BENCHMARK(BM_Metadata_parseArtists);
|
||||
BENCHMARK(BM_Metadata_parseArtists_WithWhitelist);
|
||||
BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist);
|
||||
|
||||
} // 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
|
||||
{
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 "ArtistInfoParser.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::optional<std::string_view> getText(const pugi::xml_node& node, const core::LiteralString& tag)
|
||||
{
|
||||
std::optional<std::string_view> res;
|
||||
|
||||
if (const pugi::xml_node child{ node.child(tag.c_str()) })
|
||||
res = std::string_view{ child.child_value() };
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 1> files{ "artist.nfo" };
|
||||
return files;
|
||||
}
|
||||
|
||||
ArtistInfo parseArtistInfo(std::istream& is)
|
||||
{
|
||||
ArtistInfo artistInfo;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result{ doc.load(is) };
|
||||
if (!result)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "Cannot read artist info xml: " << result.description());
|
||||
throw ArtistInfoParseException{ result.description() };
|
||||
}
|
||||
|
||||
const pugi::xml_node artistNode{ doc.child("artist") };
|
||||
if (!artistNode)
|
||||
throw ArtistInfoParseException{ "No <artist> element found in artist info xml" };
|
||||
|
||||
{
|
||||
auto mbid{ getText(artistNode, "musicBrainzArtistID") };
|
||||
if (!mbid.has_value())
|
||||
mbid = getText(artistNode, "musicbrainzartistid"); // lidarr seems to put this in lowercase
|
||||
artistInfo.mbid = core::UUID::fromString(core::stringUtils::stringTrim(mbid.has_value() ? *mbid : ""));
|
||||
}
|
||||
|
||||
artistInfo.name = core::stringUtils::stringTrim(getText(artistNode, "name").value_or(""));
|
||||
artistInfo.sortName = core::stringUtils::stringTrim(getText(artistNode, "sortname").value_or(""));
|
||||
artistInfo.type = core::stringUtils::stringTrim(getText(artistNode, "type").value_or(""));
|
||||
artistInfo.gender = core::stringUtils::stringTrim(getText(artistNode, "gender").value_or(""));
|
||||
artistInfo.disambiguation = core::stringUtils::stringTrim(getText(artistNode, "disambiguation").value_or(""));
|
||||
artistInfo.biography = getText(artistNode, "biography").value_or("");
|
||||
|
||||
return artistInfo;
|
||||
}
|
||||
} // 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
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 "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 "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
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) {
|
||||
tag = core::stringUtils::stringTrim(tag);
|
||||
if (!tag.empty())
|
||||
visitor(tag);
|
||||
} };
|
||||
|
||||
for (std::string_view tagDelimiter : tagDelimiters)
|
||||
{
|
||||
if (value.find(tagDelimiter) != std::string_view::npos)
|
||||
{
|
||||
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
|
||||
visitTagIfNonEmpty(splitTag);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no delimiter found, or no delimiter to be used
|
||||
visitTagIfNonEmpty(value);
|
||||
});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void addTagIfNonEmpty(std::vector<T>& res, std::string_view tag)
|
||||
{
|
||||
if (tag.empty())
|
||||
return;
|
||||
|
||||
if (std::optional<T> val{ core::stringUtils::readAs<T>(tag) })
|
||||
res.emplace_back(std::move(*val));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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 audio::TagType tagType : tagTypes)
|
||||
{
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
|
||||
// short path: no custom delimiter
|
||||
if (tagDelimiters.empty())
|
||||
{
|
||||
addTagIfNonEmpty(res, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Algo:
|
||||
// 1. replace whitelist entries by placeholders
|
||||
// 2. apply delimiters
|
||||
// 3. replace whitelist entries back
|
||||
|
||||
constexpr std::string_view substitutionPrefix{ "__LMS_ENTRY__" };
|
||||
std::unordered_map<std::string, std::string_view> substitutionMap;
|
||||
std::string strToSplit{ value };
|
||||
if (whitelist)
|
||||
{
|
||||
std::size_t counter{};
|
||||
|
||||
for (std::string_view whiteListEntry : *whitelist)
|
||||
{
|
||||
whiteListEntry = core::stringUtils::stringTrim(whiteListEntry);
|
||||
|
||||
const std::string::size_type pos{ strToSplit.find(whiteListEntry) };
|
||||
if (pos == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string substitutionStr{ std::string{ substitutionPrefix } + std::to_string(counter++) };
|
||||
strToSplit.replace(pos, whiteListEntry.size(), substitutionStr);
|
||||
substitutionMap.emplace(std::move(substitutionStr), whiteListEntry);
|
||||
}
|
||||
}
|
||||
|
||||
for (std::string_view strSplit : core::stringUtils::splitString(strToSplit, tagDelimiters))
|
||||
{
|
||||
std::string str{ core::stringUtils::stringTrim(strSplit) };
|
||||
|
||||
while (true)
|
||||
{
|
||||
std::string::size_type prefixPos{ str.find(substitutionPrefix) };
|
||||
if (prefixPos == std::string::npos)
|
||||
break;
|
||||
|
||||
std::string::size_type counterEnd{ prefixPos + substitutionPrefix.size() };
|
||||
while (std::isdigit(str[counterEnd]))
|
||||
counterEnd++;
|
||||
|
||||
std::string substitutionStr{ str.substr(prefixPos, counterEnd - prefixPos) };
|
||||
auto it{ substitutionMap.find(substitutionStr) };
|
||||
if (it != std::cend(substitutionMap))
|
||||
str.replace(prefixPos, counterEnd - prefixPos, it->second);
|
||||
}
|
||||
|
||||
addTagIfNonEmpty(res, str);
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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 */) };
|
||||
if (!values.empty())
|
||||
res = std::move(values.front());
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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 audio::ITagReader& tagReader, audio::TagType tagType)
|
||||
{
|
||||
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
|
||||
}
|
||||
|
||||
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?)
|
||||
Lyrics lyrics{ parseLyrics(iss) };
|
||||
if (lyrics.language.empty())
|
||||
lyrics.language = language;
|
||||
|
||||
res.emplace_back(std::move(lyrics));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
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())
|
||||
return {};
|
||||
|
||||
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, params.defaultTagDelimiters) };
|
||||
|
||||
std::vector<Artist> artists;
|
||||
artists.reserve(artistNames.size());
|
||||
|
||||
for (std::size_t i{}; i < artistNames.size(); ++i)
|
||||
{
|
||||
Artist& artist{ artists.emplace_back(std::move(artistNames[i])) };
|
||||
|
||||
if (artistNames.size() == artistSortNames.size())
|
||||
artist.sortName = std::move(artistSortNames[i]);
|
||||
if (artistNames.size() == artistMBIDs.size())
|
||||
artist.mbid = std::move(artistMBIDs[i]);
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
|
||||
tagReader.visitPerformerTags([&](std::string_view role, std::string_view name) {
|
||||
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
|
||||
// We consider we may hit both styles for the same track
|
||||
if (role.empty())
|
||||
{
|
||||
// "PERFORMER" "artist (role)"
|
||||
utils::PerformerArtist performer{ utils::extractPerformerAndRole(name) };
|
||||
core::stringUtils::capitalize(performer.role);
|
||||
performers[performer.role].push_back(std::move(performer.artist));
|
||||
}
|
||||
else
|
||||
{
|
||||
// "PERFORMER:role", "artist" (MP3)
|
||||
std::string roleCapitalized{ core::stringUtils::stringToLower(role) };
|
||||
core::stringUtils::capitalize(roleCapitalized);
|
||||
performers[roleCapitalized].push_back(Artist{ name });
|
||||
}
|
||||
});
|
||||
|
||||
return performers;
|
||||
}
|
||||
|
||||
bool strIsMatchingArtistNames(std::string_view str, std::span<const std::string_view> artistNames)
|
||||
{
|
||||
std::string_view::size_type currentOffset{};
|
||||
|
||||
for (const std::string_view artistName : artistNames)
|
||||
{
|
||||
std::string_view::size_type newPos{ str.find(artistName, currentOffset) };
|
||||
if (newPos == std::string_view::npos)
|
||||
return false;
|
||||
|
||||
currentOffset = newPos + artistName.size();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool strIsContainingAny(std::string_view str, std::span<const std::string> subStrs)
|
||||
{
|
||||
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
|
||||
}
|
||||
|
||||
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string>& artistTag, std::span<const std::string> artistTagDelimiters)
|
||||
{
|
||||
std::string artistDisplayName;
|
||||
|
||||
if (artists.size() == 1)
|
||||
artistDisplayName = artists.front().name;
|
||||
else if (artists.size() > 1)
|
||||
{
|
||||
std::vector<std::string_view> artistNames;
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
|
||||
|
||||
// Picard use case: if we manage to match all artists in the "artist" tag (considered single-valued), and if no custom delimiter is hit, we use it as the display name
|
||||
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
||||
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
||||
{
|
||||
// Limitation: this test does not take the whitelist into account
|
||||
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
||||
artistDisplayName = *artistTag;
|
||||
}
|
||||
|
||||
if (artistDisplayName.empty())
|
||||
artistDisplayName = core::stringUtils::joinStrings(artistNames, ", ");
|
||||
}
|
||||
|
||||
return artistDisplayName;
|
||||
}
|
||||
|
||||
std::optional<Track::Advisory> getAdvisory(const audio::ITagReader& tagReader)
|
||||
{
|
||||
if (const auto value{ getTagValueAs<int>(tagReader, audio::TagType::Advisory) })
|
||||
{
|
||||
switch (*value)
|
||||
{
|
||||
case 1:
|
||||
case 4:
|
||||
return Track::Advisory::Explicit;
|
||||
case 2:
|
||||
return Track::Advisory::Clean;
|
||||
case 0:
|
||||
return Track::Advisory::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TrackMetadataParser::TrackMetadataParser(const Parameters& params)
|
||||
: _params{ params }
|
||||
{
|
||||
}
|
||||
|
||||
TrackMetadataParser::~TrackMetadataParser() = default;
|
||||
|
||||
Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
Track track;
|
||||
processTags(tagReader, track);
|
||||
return track;
|
||||
}
|
||||
|
||||
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);
|
||||
track.acoustID = getTagValueAs<core::UUID>(tagReader, TagType::AcoustID);
|
||||
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
|
||||
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::Date) })
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid())
|
||||
track.date = date;
|
||||
}
|
||||
if (const auto dateStr = getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseDate))
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid())
|
||||
track.originalDate = date;
|
||||
}
|
||||
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseYear) })
|
||||
track.originalYear = utils::parseYear(*dateStr);
|
||||
|
||||
if (const auto encodingTimeStr{ getTagValueAs<std::string>(tagReader, TagType::EncodingTime) })
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*encodingTimeStr) }; date.isValid())
|
||||
track.encodingTime = date;
|
||||
}
|
||||
|
||||
track.advisory = getAdvisory(tagReader);
|
||||
|
||||
track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics
|
||||
track.comments = getTagValuesAs<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
|
||||
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
|
||||
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
||||
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
||||
|
||||
for (const std::string& userExtraTag : _params.userExtraTags)
|
||||
{
|
||||
visitTagValues(tagReader, userExtraTag, _params.defaultTagDelimiters, [&](std::string_view value) {
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
if (!value.empty())
|
||||
track.userExtraTags[userExtraTag].push_back(std::string{ value });
|
||||
});
|
||||
}
|
||||
|
||||
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _params.defaultTagDelimiters);
|
||||
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _params.defaultTagDelimiters);
|
||||
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _params.defaultTagDelimiters);
|
||||
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _params.defaultTagDelimiters);
|
||||
|
||||
std::vector<std::string_view> artistDelimiters{};
|
||||
|
||||
track.medium = getMedium(tagReader);
|
||||
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistsSortOrder, TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _params);
|
||||
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _params.artistTagDelimiters);
|
||||
|
||||
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, { TagType::MusicBrainzConductorID }, _params);
|
||||
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, { TagType::MusicBrainzComposerID }, _params);
|
||||
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, { TagType::MusicBrainzLyricistID }, _params);
|
||||
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, { TagType::MusicBrainzMixerID }, _params);
|
||||
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, { TagType::MusicBrainzProducerID }, _params);
|
||||
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, { TagType::MusicBrainzRemixerID }, _params);
|
||||
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
||||
|
||||
// If a file has originalDate but no originalYear, set it
|
||||
if (!track.originalYear)
|
||||
track.originalYear = track.originalDate.getYear();
|
||||
}
|
||||
|
||||
std::optional<Medium> TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::optional<Medium> medium;
|
||||
medium.emplace();
|
||||
|
||||
medium->media = getTagValueAs<std::string>(tagReader, TagType::Media).value_or("");
|
||||
medium->name = getTagValueAs<std::string>(tagReader, TagType::DiscSubtitle).value_or("");
|
||||
medium->trackCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalTracks);
|
||||
if (!medium->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::TrackNumber) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ core::stringUtils::splitString(*value, '/') };
|
||||
if (strings.size() == 2)
|
||||
medium->trackCount = core::stringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
// Expecting 'Number[/Total]'
|
||||
medium->position = getTagValueAs<std::size_t>(tagReader, TagType::DiscNumber);
|
||||
medium->release = getRelease(tagReader);
|
||||
medium->replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainAlbumGain);
|
||||
|
||||
if (medium->isDefault())
|
||||
medium.reset();
|
||||
|
||||
return medium;
|
||||
}
|
||||
|
||||
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) };
|
||||
if (!releaseName)
|
||||
return release;
|
||||
|
||||
release.emplace();
|
||||
release->name = std::move(*releaseName);
|
||||
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or(release->name);
|
||||
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _params);
|
||||
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters);
|
||||
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
||||
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
||||
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
||||
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
|
||||
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
|
||||
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _params.defaultTagDelimiters);
|
||||
release->comment = getTagValueAs<std::string>(tagReader, TagType::AlbumComment).value_or("");
|
||||
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _params.defaultTagDelimiters);
|
||||
if (!release->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as "position/count"
|
||||
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::DiscNumber) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ core::stringUtils::splitString(*value, '/') };
|
||||
if (strings.size() == 2)
|
||||
release->mediumCount = core::stringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
release->releaseTypes = getTagValuesAs<std::string>(tagReader, TagType::ReleaseType, _params.defaultTagDelimiters);
|
||||
|
||||
return release;
|
||||
}
|
||||
} // 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
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "Utils.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]{
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
};
|
||||
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm{};
|
||||
tm.tm_mon = -1;
|
||||
tm.tm_mday = -1;
|
||||
|
||||
std::istringstream ss{ std::string{ dateStr } }; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
if (tm.tm_mday <= 0 || tm.tm_mon < 0)
|
||||
continue;
|
||||
|
||||
const Wt::WDate res{
|
||||
tm.tm_year + 1900, // tm.tm_year: years since 1900
|
||||
tm.tm_mon + 1, // tm.tm_mon: months since January – [00, 11]
|
||||
tm.tm_mday // tm.tm_mday: day of the month – [1, 31]
|
||||
};
|
||||
if (!res.isValid())
|
||||
continue;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<int> parseYear(std::string_view yearStr)
|
||||
{
|
||||
// limit to first 4 digit, accept leading '-'
|
||||
if (yearStr.empty())
|
||||
return std::nullopt;
|
||||
|
||||
int sign;
|
||||
if (yearStr.front() == '-')
|
||||
{
|
||||
sign = -1;
|
||||
yearStr.remove_prefix(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sign = 1;
|
||||
}
|
||||
|
||||
if (yearStr.empty() || !std::isdigit(yearStr.front()))
|
||||
return std::nullopt;
|
||||
|
||||
int result{};
|
||||
for (std::size_t i{}; i < yearStr.size() && i < 4; ++i)
|
||||
{
|
||||
if (!std::isdigit(yearStr[i]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
result = result * 10 + (yearStr[i] - '0');
|
||||
}
|
||||
|
||||
return result * sign;
|
||||
}
|
||||
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
std::string_view role;
|
||||
|
||||
std::size_t roleBegin{};
|
||||
std::size_t roleEnd{};
|
||||
std::size_t count{};
|
||||
|
||||
for (std::size_t i{}; i < entry.size(); ++i)
|
||||
{
|
||||
std::size_t currentIndex{ entry.size() - i - 1 };
|
||||
const char c{ entry[currentIndex] };
|
||||
|
||||
if (std::isspace(c))
|
||||
continue;
|
||||
|
||||
if (c == ')')
|
||||
{
|
||||
if (count++ == 0)
|
||||
roleEnd = currentIndex;
|
||||
}
|
||||
else if (c == '(')
|
||||
{
|
||||
if (count == 0)
|
||||
break;
|
||||
|
||||
if (--count == 0)
|
||||
{
|
||||
roleBegin = currentIndex + 1;
|
||||
role = core::stringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
|
||||
artistName = core::stringUtils::stringTrim(entry.substr(0, currentIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (count == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!roleEnd || !roleBegin)
|
||||
artistName = core::stringUtils::stringTrim(entry);
|
||||
|
||||
return PerformerArtist{ Artist{ artistName }, std::string{ role } };
|
||||
}
|
||||
} // namespace lms::scanner::utils
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 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 <string_view>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr);
|
||||
std::optional<int> parseYear(std::string_view yearStr);
|
||||
|
||||
struct PerformerArtist
|
||||
{
|
||||
Artist artist;
|
||||
std::string role;
|
||||
};
|
||||
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
} // namespace lms::scanner::utils
|
||||
+15
-20
@@ -22,17 +22,19 @@
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -48,28 +50,21 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::Lyrics> _parsedLyrics;
|
||||
std::optional<Lyrics> _parsedLyrics;
|
||||
};
|
||||
|
||||
void LyricsFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedLyrics = metadata::parseLyrics(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<LyricsFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedLyrics = parseLyrics(ifs);
|
||||
}
|
||||
|
||||
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
|
||||
@@ -140,7 +135,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedLyricsFileExtensions();
|
||||
return getSupportedLyricsFileExtensions();
|
||||
}
|
||||
|
||||
bool LyricsFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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 "LyricsParser.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <regex>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 2> fileExtensions{ ".lrc", ".txt" }; // TODO handle ".elrc"
|
||||
return fileExtensions;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// Parse a single line with a tag like [ar: Artist] and set the appropriate fields in the Lyrics object
|
||||
bool parseTag(std::string_view line, Lyrics& lyrics)
|
||||
{
|
||||
if (line.empty())
|
||||
return false;
|
||||
|
||||
if (line.front() != '[' || line.back() != ']') // consider lines are trimmed
|
||||
return false;
|
||||
|
||||
const auto separator{ line.find(':') };
|
||||
if (separator == std::string_view::npos)
|
||||
return false;
|
||||
|
||||
const std::string_view tagType{ core::stringUtils::stringTrim(line.substr(1, separator - 1)) };
|
||||
const std::string_view tagValue{ core::stringUtils::stringTrim(line.substr(separator + 1, line.size() - separator - 2)) };
|
||||
|
||||
if (tagType.empty())
|
||||
return false;
|
||||
|
||||
// check for timestamps
|
||||
if (std::any_of(tagType.begin(), tagType.end(), [](char c) { return std::isdigit(c); }))
|
||||
return false;
|
||||
|
||||
if (tagType == "ar")
|
||||
{
|
||||
lyrics.displayArtist = tagValue;
|
||||
}
|
||||
else if (tagType == "al")
|
||||
{
|
||||
lyrics.displayAlbum = tagValue;
|
||||
}
|
||||
else if (tagType == "ti")
|
||||
{
|
||||
lyrics.displayTitle = tagValue;
|
||||
}
|
||||
else if (tagType == "la")
|
||||
{
|
||||
lyrics.language = tagValue;
|
||||
}
|
||||
else if (tagType == "offset")
|
||||
{
|
||||
if (const auto value{ core::stringUtils::readAs<int>(tagValue) })
|
||||
lyrics.offset = std::chrono::milliseconds{ *value };
|
||||
}
|
||||
// not interrested by other tags like 'duration', 'id', etc.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse timestamps from a line, update the associated times in milliseconds and return the remaining line
|
||||
std::string_view extractTimestamps(std::string_view line, std::vector<std::chrono::milliseconds>& timestamps)
|
||||
{
|
||||
timestamps.clear();
|
||||
static const std::regex timeTagRegex{ R"(\[(?:(\d{1,2}):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?\])" };
|
||||
std::cregex_iterator regexIt(line.begin(), line.end(), timeTagRegex);
|
||||
std::cregex_iterator regexEnd;
|
||||
std::string_view::size_type offset{};
|
||||
|
||||
while (regexIt != regexEnd)
|
||||
{
|
||||
std::cmatch match{ *regexIt };
|
||||
int hour{ match[1].matched ? std::stoi(match[1].str()) : 0 };
|
||||
int minute{ std::stoi(match[2].str()) };
|
||||
int second{ std::stoi(match[3].str()) };
|
||||
int fractional{ match[4].matched ? std::stoi(match[4].str()) : 0 };
|
||||
|
||||
std::chrono::milliseconds currentTimestamp{ std::chrono::hours{ hour } + std::chrono::minutes{ minute } + std::chrono::seconds{ second } };
|
||||
|
||||
if (match[4].length() == 2) // Centiseconds
|
||||
{
|
||||
currentTimestamp += std::chrono::milliseconds{ fractional * 10 };
|
||||
}
|
||||
else // Milliseconds
|
||||
{
|
||||
currentTimestamp += std::chrono::milliseconds{ fractional };
|
||||
}
|
||||
|
||||
offset = match[0].second - line.data();
|
||||
timestamps.push_back(currentTimestamp);
|
||||
++regexIt;
|
||||
}
|
||||
|
||||
return line.substr(offset);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Main function to parse lyrics from an input stream
|
||||
Lyrics parseLyrics(std::istream& is)
|
||||
{
|
||||
Lyrics lyrics;
|
||||
|
||||
enum class State
|
||||
{
|
||||
None,
|
||||
SynchronizedLyrics,
|
||||
UnsynchronizedLyrics,
|
||||
};
|
||||
State currentState{ State::None };
|
||||
|
||||
std::vector<std::chrono::milliseconds> lastTimestamps;
|
||||
std::vector<std::chrono::milliseconds> timestamps;
|
||||
std::string accumulatedLyrics;
|
||||
|
||||
auto applyAccumulatedLyrics = [&](bool skipTrailingEmptyLines = false) {
|
||||
if (lastTimestamps.empty())
|
||||
return;
|
||||
|
||||
if (skipTrailingEmptyLines)
|
||||
accumulatedLyrics.resize(core::stringUtils::stringTrimEnd(accumulatedLyrics, " \t\r\n").size());
|
||||
|
||||
if (accumulatedLyrics.empty())
|
||||
return;
|
||||
|
||||
for (std::chrono::milliseconds timestamp : lastTimestamps)
|
||||
{
|
||||
std::string& synchronizedLine{ lyrics.synchronizedLines.find(timestamp)->second };
|
||||
synchronizedLine += accumulatedLyrics;
|
||||
}
|
||||
accumulatedLyrics.clear();
|
||||
};
|
||||
|
||||
bool firstLine{ true };
|
||||
std::string line;
|
||||
while (std::getline(is, line))
|
||||
{
|
||||
// Remove potential UTF8 BOM
|
||||
if (firstLine)
|
||||
{
|
||||
firstLine = false;
|
||||
constexpr std::string_view utf8BOM{ "\xEF\xBB\xBF" };
|
||||
if (line.starts_with(utf8BOM))
|
||||
line.erase(0, utf8BOM.size());
|
||||
}
|
||||
|
||||
std::string_view trimmedLine{ core::stringUtils::stringTrimEnd(line) };
|
||||
|
||||
// Skip comments
|
||||
if (!trimmedLine.empty() && trimmedLine.front() == '#')
|
||||
continue;
|
||||
|
||||
// Skip empty lines before actual lyrics
|
||||
if (currentState == State::None && trimmedLine.empty())
|
||||
continue;
|
||||
|
||||
if (parseTag(trimmedLine, lyrics))
|
||||
continue;
|
||||
|
||||
const std::string_view lyricsText{ extractTimestamps(trimmedLine, timestamps) };
|
||||
|
||||
// If there are timestamps, add as synchronized lyrics
|
||||
if (!timestamps.empty())
|
||||
{
|
||||
if (currentState == State::UnsynchronizedLyrics)
|
||||
lyrics.unsynchronizedLines.clear(); // choice: discard all lyrics parsed so far
|
||||
|
||||
currentState = State::SynchronizedLyrics;
|
||||
|
||||
applyAccumulatedLyrics();
|
||||
for (std::chrono::milliseconds timestamp : timestamps)
|
||||
{
|
||||
auto itLine{ lyrics.synchronizedLines.find(timestamp) };
|
||||
if (itLine != std::cend(lyrics.synchronizedLines))
|
||||
{
|
||||
itLine->second.push_back('\n');
|
||||
itLine->second.append(lyricsText);
|
||||
}
|
||||
else
|
||||
lyrics.synchronizedLines.emplace(timestamp, lyricsText);
|
||||
}
|
||||
|
||||
lastTimestamps = timestamps;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!lastTimestamps.empty())
|
||||
{
|
||||
accumulatedLyrics += '\n';
|
||||
accumulatedLyrics += trimmedLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(currentState != State::SynchronizedLyrics); // should be handled
|
||||
currentState = State::UnsynchronizedLyrics;
|
||||
|
||||
lyrics.unsynchronizedLines.push_back(std::string{ trimmedLine });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentState == State::SynchronizedLyrics)
|
||||
applyAccumulatedLyrics(true);
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
|
||||
Lyrics parseLyrics(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
+14
-21
@@ -27,12 +27,12 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/PlayList.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/playlist/PlayListParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -51,28 +51,21 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::PlayList> _parsedPlayList;
|
||||
std::optional<PlayList> _parsedPlayList;
|
||||
};
|
||||
|
||||
void PlayListFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedPlayList = metadata::parsePlayList(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<PlayListFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedPlayList = parsePlayList(ifs);
|
||||
}
|
||||
|
||||
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
|
||||
@@ -139,7 +132,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedPlayListFileExtensions();
|
||||
return getSupportedPlayListFileExtensions();
|
||||
}
|
||||
|
||||
bool PlayListFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 "PlayListParser.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 2> fileExtensions{ ".m3u", ".m3u8" };
|
||||
return fileExtensions;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Comment
|
||||
{
|
||||
std::string_view directive;
|
||||
std::string_view parameter;
|
||||
};
|
||||
|
||||
std::optional<Comment> parseComment(std::string_view line)
|
||||
{
|
||||
if (line.empty() || line.front() != '#')
|
||||
return std::nullopt;
|
||||
|
||||
Comment comment;
|
||||
const std::string_view::size_type parameterSeparator{ line.find(':') };
|
||||
if (parameterSeparator == std::string_view::npos)
|
||||
{
|
||||
comment.directive = line;
|
||||
}
|
||||
else
|
||||
{
|
||||
comment.directive = line.substr(0, parameterSeparator + 1);
|
||||
comment.parameter = line.substr(parameterSeparator + 1);
|
||||
};
|
||||
|
||||
return comment;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PlayList parsePlayList(std::istream& is)
|
||||
{
|
||||
bool firstLine{ true };
|
||||
PlayList playlist;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(is, line))
|
||||
{
|
||||
// Remove potential UTF8 BOM
|
||||
if (firstLine)
|
||||
{
|
||||
firstLine = false;
|
||||
constexpr std::string_view utf8BOM{ "\xEF\xBB\xBF" };
|
||||
if (line.starts_with(utf8BOM))
|
||||
line.erase(0, utf8BOM.size());
|
||||
}
|
||||
|
||||
const std::string_view trimmedLine{ core::stringUtils::stringTrim(line) };
|
||||
if (trimmedLine.empty())
|
||||
continue;
|
||||
|
||||
// Don't enforce #EXTM3U as first line: be permissive
|
||||
if (const std::optional<Comment> comment{ parseComment(trimmedLine) })
|
||||
{
|
||||
if (comment->directive == "#PLAYLIST:")
|
||||
playlist.name = comment->parameter;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter out URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
|
||||
// Consider an entry with a ':' is actually an url, as filenames are not supposed to have ':' on windows
|
||||
if (trimmedLine.find(':') != std::string_view::npos)
|
||||
continue;
|
||||
|
||||
const std::filesystem::path path{ std::cbegin(trimmedLine), std::cend(trimmedLine) };
|
||||
playlist.files.emplace_back(path.lexically_normal());
|
||||
}
|
||||
|
||||
return playlist;
|
||||
}
|
||||
} // 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);
|
||||
|
||||
|
||||
@@ -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 <optional>
|
||||
#include <string>
|
||||
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
// See:
|
||||
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
|
||||
// - for the definition of some mb fields: https://musicbrainz.org/doc/Artist
|
||||
struct ArtistInfo
|
||||
{
|
||||
std::string name;
|
||||
std::optional<core::UUID> mbid;
|
||||
std::string sortName; // mb
|
||||
std::string type; // mb
|
||||
std::string gender; // mb
|
||||
std::string disambiguation; // mb
|
||||
std::string biography;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct Lyrics
|
||||
{
|
||||
std::string language;
|
||||
std::chrono::milliseconds offset{};
|
||||
std::string displayArtist;
|
||||
std::string displayAlbum;
|
||||
std::string displayTitle;
|
||||
|
||||
std::map<std::chrono::milliseconds, std::string> synchronizedLines;
|
||||
std::vector<std::string> unsynchronizedLines;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct PlayList
|
||||
{
|
||||
std::string name;
|
||||
std::vector<std::filesystem::path> files;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::vector<std::string> /* values */>;
|
||||
|
||||
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
|
||||
|
||||
struct Artist
|
||||
{
|
||||
std::optional<core::UUID> mbid;
|
||||
std::string name;
|
||||
std::optional<std::string> sortName;
|
||||
|
||||
Artist(std::string_view _name)
|
||||
: name{ _name } {}
|
||||
Artist(std::optional<core::UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName)
|
||||
: mbid{ std::move(_mbid) }
|
||||
, name{ _name }
|
||||
, sortName{ std::move(_sortName) } {}
|
||||
|
||||
auto operator<=>(const Artist&) const = default;
|
||||
};
|
||||
|
||||
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
||||
|
||||
struct Release
|
||||
{
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> groupMBID;
|
||||
std::string name;
|
||||
std::string sortName;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
std::optional<std::size_t> mediumCount;
|
||||
std::vector<std::string> labels;
|
||||
std::vector<std::string> releaseTypes;
|
||||
bool isCompilation{};
|
||||
std::string barcode;
|
||||
std::string comment;
|
||||
std::vector<std::string> countries;
|
||||
|
||||
auto operator<=>(const Release&) const = default;
|
||||
};
|
||||
|
||||
struct Medium
|
||||
{
|
||||
std::string media; // CD, etc.
|
||||
std::string name;
|
||||
std::optional<Release> release;
|
||||
std::optional<std::size_t> position; // in release
|
||||
std::optional<std::size_t> trackCount;
|
||||
std::optional<float> replayGain;
|
||||
|
||||
auto operator<=>(const Medium&) const = default;
|
||||
|
||||
bool isDefault() const
|
||||
{
|
||||
static const Medium defaultMedium;
|
||||
return *this == defaultMedium;
|
||||
}
|
||||
};
|
||||
|
||||
struct Track
|
||||
{
|
||||
enum class Advisory
|
||||
{
|
||||
Unknown,
|
||||
Explicit,
|
||||
Clean,
|
||||
};
|
||||
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> recordingMBID;
|
||||
std::string title;
|
||||
std::optional<Medium> medium;
|
||||
std::optional<std::size_t> position; // in medium
|
||||
std::vector<std::string> groupings;
|
||||
std::vector<std::string> genres;
|
||||
std::vector<std::string> moods;
|
||||
std::vector<std::string> languages;
|
||||
Tags userExtraTags;
|
||||
core::PartialDateTime date;
|
||||
std::optional<int> originalYear;
|
||||
core::PartialDateTime originalDate;
|
||||
std::optional<Advisory> advisory;
|
||||
core::PartialDateTime encodingTime;
|
||||
std::optional<core::UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::vector<std::string> comments;
|
||||
std::vector<Lyrics> lyrics;
|
||||
std::optional<float> replayGain;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> conductorArtists;
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
PerformerContainer performerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
#include "scanners/artistinfo/ArtistInfoParser.hpp"
|
||||
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
TEST(ArtistInfo, basic)
|
||||
{
|
||||
std::istringstream is{ R"(<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<artist>
|
||||
<name>Tim Taylor</name>
|
||||
<musicBrainzArtistID>38811c52-85e3-4e2e-3319-ab7d9f2cfa5b</musicBrainzArtistID>
|
||||
<sortname>Taylor, Tim</sortname>
|
||||
<disambiguation>Timothy Taylor</disambiguation>
|
||||
<biography>DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.
|
||||
|
||||
He moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh.</biography>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/mG5pml7VOsld5ix_X_GNlY-wiN6axOjpFD4eZBkszL0/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTAwLmpwZWc.jpeg">https://i.discogs.com/zY8kWeJfDfWgDDJZ44uYARjNEzDLLqRiXk23LUlik-c/rs:fit/g:sm/q:90/h:800/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTAwLmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/AOk30C5KRPIub0DW8Q_3NP-PhtE0l3caXV1_r0lP3ao/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTEyMTg2NTk2MC5q/cGc.jpeg">https://i.discogs.com/7Do2Xbok8HnWJEjcW6b0u9hyYMpNleGY3HRIEhNlxlM/rs:fit/g:sm/q:90/h:387/w:281/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTEyMTg2NTk2MC5q/cGc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/6YyKIXiD5wNTDVzS7JxpnipMUQ5UoJmtQgzhgV0-RB0/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi05/NDc3LmpwZWc.jpeg">https://i.discogs.com/fOeq1muY2Cu-gAJZGo5yK0AHIS1PJ1rcWqu8p_e3opY/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi05/NDc3LmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/Ubw6Imsd8FUqoQnAb3VVbIqnh1b8VJDKAclTe2X7cXw/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS05/NzQ3LmpwZWc.jpeg">https://i.discogs.com/vhMFP7ICq7VyJcZaGim2X0x4nfKNGXkk7U5u153owfs/rs:fit/g:sm/q:90/h:540/w:364/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS05/NzQ3LmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/gXRlEh7W0awsBO3Cndww_n46JNLycyI6EOWajsBOU0A/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS03/NDYwLmpwZWc.jpeg">https://i.discogs.com/wsJi9gfDDaoamUjsxFm0R02VAllhW4iaFsCnwVfouO4/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS03/NDYwLmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/QbSO414VlPwlRLcBAvhe6NFxCcsdy1rQkAmCzQ8Xe_o/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy02/MTg5LmpwZWc.jpeg">https://i.discogs.com/fjy0PGAGHsHIXex5HqMDitJI0Yh3MesiPL6ZOyko4bk/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy02/MTg5LmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/8rHEak0VmPSeBRQ8kTg7xwARlg0-yqJtlLCjUiWd75c/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi0x/NDgzLmpwZWc.jpeg">https://i.discogs.com/k79VVA9du3LW57naLLjXLlWesxbtygfstnwrHp6Ku84/rs:fit/g:sm/q:90/h:450/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi0x/NDgzLmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/KtxVQl-q2BzNmIP0hk-Ip8AKwCYZZP0-excxgmMMi68/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTY1LmpwZWc.jpeg">https://i.discogs.com/TLjVejJmVWFkQuAhXndIV0Ovt-1GJ5mHE5NWr3MNXGk/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTY1LmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/Qf4r5w-aRA9ysSo53rnI0E-xDM8XaB7R4CGiHwla_a8/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy0y/Mjc5LmpwZWc.jpeg">https://i.discogs.com/Y8V1WqvgdSmcIsgC2CAq_VNhfaWTX9gWJi9bQ6c0Vno/rs:fit/g:sm/q:90/h:398/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy0y/Mjc5LmpwZWc.jpeg</thumb>
|
||||
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/8a-6X1gRPL0h4PqUBwCffauybMNwz8JYJ81E6JyjnpY/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy00/MzcyLmpwZWc.jpeg">https://i.discogs.com/OD2sPGIfSZGnrT6JyfKmlO7kuX4ZadJhP-iNaEzbbuE/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy00/MzcyLmpwZWc.jpeg</thumb>
|
||||
<genre clear="true">Acid House / Hardcore / Techno / Acid / Breakbeat / Minimal / Tech House / Tribal</genre>
|
||||
<album>
|
||||
<title>The Penguin / Scissorhands</title>
|
||||
<year>1996</year>
|
||||
</album>
|
||||
<album>
|
||||
<title>The Minneapolis Sessions (2016 Reissue)</title>
|
||||
<year>1997</year>
|
||||
</album>
|
||||
<album>
|
||||
<title>Over The Hill</title>
|
||||
<year>2001</year>
|
||||
</album>
|
||||
<album>
|
||||
<title>Over The Hill Remixes</title>
|
||||
<year>2001</year>
|
||||
</album>
|
||||
<album>
|
||||
<title>Pleasure Unit</title>
|
||||
<year>2016</year>
|
||||
</album>
|
||||
</artist>)" };
|
||||
|
||||
const ArtistInfo artistInfo{ parseArtistInfo(is) };
|
||||
|
||||
EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
|
||||
EXPECT_EQ(artistInfo.name, "Tim Taylor");
|
||||
ASSERT_EQ(artistInfo.sortName, "Taylor, Tim");
|
||||
ASSERT_EQ(artistInfo.disambiguation, "Timothy Taylor");
|
||||
ASSERT_EQ(artistInfo.biography, "DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.\r\n\r\nHe moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh.");
|
||||
}
|
||||
|
||||
TEST(ArtistInfo, basic_musicbrainzartistid)
|
||||
{
|
||||
std::istringstream is{ R"(<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<artist>
|
||||
<name>Tim Taylor</name>
|
||||
<musicbrainzartistid>38811c52-85e3-4e2e-3319-ab7d9f2cfa5b</musicbrainzartistid>
|
||||
<sortname>Taylor, Tim</sortname>
|
||||
<disambiguation>Timothy Taylor</disambiguation>
|
||||
</artist>)" };
|
||||
|
||||
const ArtistInfo artistInfo{ parseArtistInfo(is) };
|
||||
|
||||
EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
|
||||
EXPECT_EQ(artistInfo.name, "Tim Taylor");
|
||||
ASSERT_EQ(artistInfo.sortName, "Taylor, Tim");
|
||||
ASSERT_EQ(artistInfo.disambiguation, "Timothy Taylor");
|
||||
}
|
||||
|
||||
TEST(ArtistInfo, trim)
|
||||
{
|
||||
std::istringstream is{ R"(<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<artist>
|
||||
<name> My Artist </name>
|
||||
<musicBrainzArtistID> 38811c52-85e3-4e2e-3319-ab7d9f2cfa5b </musicBrainzArtistID>
|
||||
<sortname> Artist, My </sortname>
|
||||
<disambiguation> My Artist </disambiguation>
|
||||
</artist>)" };
|
||||
|
||||
const ArtistInfo artistInfo{ parseArtistInfo(is) };
|
||||
|
||||
EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
|
||||
EXPECT_EQ(artistInfo.name, "My Artist");
|
||||
ASSERT_EQ(artistInfo.sortName, "Artist, My");
|
||||
ASSERT_EQ(artistInfo.disambiguation, "My Artist");
|
||||
}
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -0,0 +1,152 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "scanners/audiofile/Utils.hpp"
|
||||
|
||||
namespace lms::scanner::utils::tests
|
||||
{
|
||||
TEST(MetaData, parseDate)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
Wt::WDate result;
|
||||
} testCases[]{
|
||||
{ "1995-05-09", Wt::WDate{ 1995, 5, 9 } },
|
||||
{ "1995-01-01", Wt::WDate{ 1995, 1, 1 } },
|
||||
{ "1900-01-01", Wt::WDate{ 1900, 1, 1 } },
|
||||
{ "1899-01-01", Wt::WDate{ 1899, 1, 1 } },
|
||||
{ "1899-12-31", Wt::WDate{ 1899, 12, 31 } },
|
||||
{ "1899-11-30", Wt::WDate{ 1899, 11, 30 } },
|
||||
{ "1500-11-30", Wt::WDate{ 1500, 11, 30 } },
|
||||
{ "1000-11-30", Wt::WDate{ 1000, 11, 30 } },
|
||||
{ "1899-11-31", Wt::WDate{} }, // invalid day
|
||||
{ "1899-11-00", Wt::WDate{} }, // invalid day
|
||||
{ "1899-13-01", Wt::WDate{} }, // invalid month
|
||||
{ "1899-00-01", Wt::WDate{} }, // invalid month
|
||||
{ "1899-11", Wt::WDate{} }, // missing day
|
||||
{ "1899", Wt::WDate{} }, // missing month and days
|
||||
{ "1600", Wt::WDate{} }, // missing month and days
|
||||
{ "1995/05/09", Wt::WDate{ 1995, 5, 9 } },
|
||||
{ "1995/01/01", Wt::WDate{ 1995, 1, 1 } },
|
||||
{ "1900/01/01", Wt::WDate{ 1900, 1, 1 } },
|
||||
{ "1899/01/01", Wt::WDate{ 1899, 1, 1 } },
|
||||
{ "1899/12/31", Wt::WDate{ 1899, 12, 31 } },
|
||||
{ "1899/11/30", Wt::WDate{ 1899, 11, 30 } },
|
||||
{ "1500/11/30", Wt::WDate{ 1500, 11, 30 } },
|
||||
{ "1000/11/30", Wt::WDate{ 1000, 11, 30 } },
|
||||
{ "1899/11/31", Wt::WDate{} }, // invalid day
|
||||
{ "1899/11/00", Wt::WDate{} }, // invalid day
|
||||
{ "1899/13/01", Wt::WDate{} }, // invalid month
|
||||
{ "1899/00/01", Wt::WDate{} }, // invalid month
|
||||
{ "1899/11", Wt::WDate{} }, // missing day
|
||||
{ "1899", Wt::WDate{} }, // missing month and days
|
||||
{ "1600", Wt::WDate{} }, // missing month and days
|
||||
{ "1995/05-09", Wt::WDate{} }, // invalid mixup separators
|
||||
{ "1995-05/09", Wt::WDate{} }, // invalid mixup separators
|
||||
};
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
const Wt::WDate parsed{ parseDate(testCase.str) };
|
||||
|
||||
EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MetaData, parseYear)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
std::optional<int> result;
|
||||
} testCases[]{
|
||||
{ "1995-05-09", 1995 },
|
||||
{ "1995", 1995 },
|
||||
{ "-0", 0 },
|
||||
{ "0", 0 },
|
||||
{ "00", 0 },
|
||||
{ "05", 5 },
|
||||
{ "050", 50 },
|
||||
{ "00005", 0 },
|
||||
{ "-50", -50 },
|
||||
{ "-", std::nullopt },
|
||||
{ "", std::nullopt },
|
||||
{ "a", std::nullopt },
|
||||
{ "1a", 1 },
|
||||
{ "12a", 12 },
|
||||
{ "123a", 123 },
|
||||
{ "1234a", 1234 },
|
||||
{ "19951123", 1995 },
|
||||
{ "199511", 1995 },
|
||||
};
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
const std::optional<int> parsed{ parseYear(testCase.str) };
|
||||
EXPECT_EQ(parsed, testCase.result) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MetaData, extractPerformerAndRole)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
std::string expectedArtistName;
|
||||
std::string expectedRole;
|
||||
} testCases[]{
|
||||
{ "", "", "" },
|
||||
{ "(myrole)", "", "myrole" },
|
||||
{ "(my role)", "", "my role" },
|
||||
{ " ( my role ) ", "", "my role" },
|
||||
{ " (()) ", "", "()" },
|
||||
{ ")", ")", "" },
|
||||
{ "(", "(", "" },
|
||||
{ "artist name (my role)", "artist name", "my role" },
|
||||
{ "artist name ()", "artist name", "" },
|
||||
{ "artist name ( )", "artist name", "" },
|
||||
{ "artist (subname) name", "artist (subname) name", "" },
|
||||
{ " artist name ( my role )", "artist name", "my role" },
|
||||
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
|
||||
{ "artist name", "artist name", "" },
|
||||
{ " artist name ", "artist name", "" },
|
||||
{ "artist name (", "artist name (", "" },
|
||||
{ "artist name )", "artist name )", "" },
|
||||
{ "artist name (()", "artist name (", "" },
|
||||
{ "artist name (())", "artist name", "()" },
|
||||
{ "artist name ( () )", "artist name", "()" },
|
||||
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
|
||||
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
|
||||
};
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
PerformerArtist performer{ extractPerformerAndRole(testCase.str) };
|
||||
|
||||
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
} // 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()
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(Lyrics, synchronized)
|
||||
{
|
||||
std::istringstream is{ R"([id: dqsxdkbu]
|
||||
[ar: Lady Gaga]
|
||||
[al: Lady Gaga]
|
||||
[ti: Die With A Smile]
|
||||
[la: eng]
|
||||
[length: 04:12]
|
||||
[offset: -34]
|
||||
[00:03.30]Ooh, ooh
|
||||
[00:06.75]
|
||||
[00:09.16]I, I just woke up from a dream)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.displayArtist, "Lady Gaga");
|
||||
EXPECT_EQ(lyrics.displayAlbum, "Lady Gaga");
|
||||
EXPECT_EQ(lyrics.displayTitle, "Die With A Smile");
|
||||
EXPECT_EQ(lyrics.language, "eng");
|
||||
EXPECT_EQ(lyrics.offset, -34ms);
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 3);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, tagInMidleOfLyrics)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
[id: dqsxdkbu]
|
||||
[00:09.16]I, I just woke up from a dream)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, tagsWithSpaces)
|
||||
{
|
||||
std::istringstream is{ R"([al: dqsxdkbu ]
|
||||
[00:09.16]I, I just woke up from a dream)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
EXPECT_EQ(lyrics.displayAlbum, "dqsxdkbu");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, tagIDsWithSpaces)
|
||||
{
|
||||
std::istringstream is{ R"([ al : dqsxdkbu ]
|
||||
[00:09.16]I, I just woke up from a dream)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
EXPECT_EQ(lyrics.displayAlbum, "dqsxdkbu");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, tagAtTheEndOfLyrics)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
[00:09.16]I, I just woke up from a dream
|
||||
[id: dqsxdkbu])" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, skipEmptyBeginLines)
|
||||
{
|
||||
std::istringstream is{ R"(
|
||||
|
||||
|
||||
[00:03.30]Ooh, ooh)" };
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
}
|
||||
|
||||
TEST(Lyrics, skipEmptyEndLines)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
|
||||
)" };
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
}
|
||||
|
||||
TEST(Lyrics, skipLeadingUnsynchronizedLyrics)
|
||||
{
|
||||
std::istringstream is{ R"(
|
||||
Some unsynchronized lyrics
|
||||
[00:03.30]Ooh, ooh)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
}
|
||||
|
||||
TEST(Lyrics, skipComments)
|
||||
{
|
||||
std::istringstream is{ R"(###
|
||||
[00:03.30]Ooh, ooh
|
||||
## just dance
|
||||
[00:09.16]I, I just woke up from a dream
|
||||
##end)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_notags)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_withTimestampsDelimiters)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh ] [])" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh ] []");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_timestampFormats)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]First line
|
||||
[00:01.301]in milliseconds
|
||||
[0:02.301]leading with only one digit
|
||||
[61:01.30]more than 60 minutes
|
||||
[02:01:01.30]With hours
|
||||
[3:01:01.30]With hours with only one digit)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 6);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(1s + 301ms));
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(2s + 301ms));
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(61min + 1s + 300ms));
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(2h + 1min + 1s + 300ms));
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3h + 1min + 1s + 300ms));
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_keepBlankLinesExceptEOF)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
|
||||
|
||||
[00:06.75]Foo
|
||||
)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh\n\n");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "Foo");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_blankLinesEnd)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
SecondLine
|
||||
Even a third line!!
|
||||
[00:06.75]Foo
|
||||
)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh\nSecondLine\n Even a third line!!");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "Foo");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_emptyLines)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
[00:03.30]
|
||||
[00:06.75]
|
||||
[00:06.75]Foo
|
||||
)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 2);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh\n");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "\nFoo");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_multitimestamps)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30][00:09.16] [00:15.16]Ooh, ooh
|
||||
[00:06.75]I, I just woke up from a dream)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 4);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "I, I just woke up from a dream");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(15s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(15s + 160ms)->second, "Ooh, ooh");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_multitimestamps_blank)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||
[00:06.75]
|
||||
[00:09.16]I, I just woke up from a dream
|
||||
[00:10.16]
|
||||
|
||||
)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 4);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(10s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(10s + 160ms)->second, "");
|
||||
}
|
||||
|
||||
TEST(Lyrics, synchronized_multitimestamps_multilines)
|
||||
{
|
||||
std::istringstream is{ R"([00:03.30][00:09.16]Ooh, ooh
|
||||
Second line
|
||||
Third line
|
||||
|
||||
Fifth line after an empty one...
|
||||
[00:06.75]I, I just woke up from a dream
|
||||
Cool)" };
|
||||
|
||||
const Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||
ASSERT_EQ(lyrics.synchronizedLines.size(), 3);
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh\nSecond line\n Third line\n\nFifth line after an empty one...");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(6s + 750ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(6s + 750ms)->second, "I, I just woke up from a dream\nCool");
|
||||
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "Ooh, ooh\nSecond line\n Third line\n\nFifth line after an empty one...");
|
||||
}
|
||||
|
||||
TEST(Lyrics, unsynchronized)
|
||||
{
|
||||
std::istringstream is{ R"([id: dqsxdkbu]
|
||||
[ar: Lady Gaga]
|
||||
[al: Lady Gaga]
|
||||
[ti: Die With A Smile]
|
||||
[length: 04:12]
|
||||
[offset: -34]
|
||||
Ooh, ooh
|
||||
|
||||
|
||||
I, I just woke up from a dream
|
||||
|
||||
)" };
|
||||
|
||||
Lyrics lyrics{ parseLyrics(is) };
|
||||
|
||||
EXPECT_EQ(lyrics.displayArtist, "Lady Gaga");
|
||||
EXPECT_EQ(lyrics.displayAlbum, "Lady Gaga");
|
||||
EXPECT_EQ(lyrics.displayTitle, "Die With A Smile");
|
||||
EXPECT_EQ(lyrics.offset, -34ms);
|
||||
ASSERT_EQ(lyrics.unsynchronizedLines.size(), 5);
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[0], "Ooh, ooh");
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[1], "");
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[2], "");
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[3], "I, I just woke up from a dream");
|
||||
EXPECT_EQ(lyrics.unsynchronizedLines[4], "");
|
||||
}
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
#include <sstream>
|
||||
|
||||
#include "scanners/playlist/PlayListParser.hpp"
|
||||
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
TEST(Scanner, playlist)
|
||||
{
|
||||
std::istringstream is{ R"(#EXTM3U
|
||||
#PLAYLIST:My super playlist
|
||||
01-Foo.mp3
|
||||
|
||||
|
||||
|
||||
#EXTINF:263,Alice in Chains - Don't Follow
|
||||
02-FooBar.mp3
|
||||
#EXTALB:Album Title (2009)
|
||||
03-Bar.mp3
|
||||
/this is/a test with a long/path and some spaces/foo.mp3
|
||||
and another one/with relative path/foo.mp3
|
||||
one to be/../one to be/normalized/foo.mp3)" };
|
||||
|
||||
const PlayList playlist{ parsePlayList(is) };
|
||||
|
||||
EXPECT_EQ(playlist.name, "My super playlist");
|
||||
ASSERT_EQ(playlist.files.size(), 6);
|
||||
EXPECT_EQ(playlist.files[0], "01-Foo.mp3");
|
||||
EXPECT_EQ(playlist.files[1], "02-FooBar.mp3");
|
||||
EXPECT_EQ(playlist.files[2], "03-Bar.mp3");
|
||||
EXPECT_EQ(playlist.files[3], "/this is/a test with a long/path and some spaces/foo.mp3");
|
||||
EXPECT_EQ(playlist.files[4], "and another one/with relative path/foo.mp3");
|
||||
EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3");
|
||||
}
|
||||
|
||||
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)) };
|
||||
|
||||
const PlayList playlist{ parsePlayList(is) };
|
||||
EXPECT_EQ(playlist.name, "");
|
||||
ASSERT_EQ(playlist.files.size(), 1);
|
||||
EXPECT_EQ(playlist.files[0], "../test.mp3");
|
||||
}
|
||||
|
||||
} // namespace lms::scanner::tests
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace lms::scanner::tests
|
||||
{
|
||||
class TestTagReader : public audio::ITagReader
|
||||
{
|
||||
public:
|
||||
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*/>;
|
||||
TestTagReader(Tags&& tags)
|
||||
: _tags{ std::move(tags) }
|
||||
{
|
||||
}
|
||||
~TestTagReader() override = default;
|
||||
TestTagReader(const TestTagReader&) = delete;
|
||||
TestTagReader& operator=(const TestTagReader&) = delete;
|
||||
|
||||
void setPerformersTags(Performers&& performers)
|
||||
{
|
||||
_performers = std::move(performers);
|
||||
}
|
||||
|
||||
void setExtraUserTags(ExtraUserTags&& extraUserTags)
|
||||
{
|
||||
_extraUserTags = std::move(extraUserTags);
|
||||
}
|
||||
|
||||
void setLyricsTags(LyricsTags&& lyricsTags)
|
||||
{
|
||||
_lyricsTags = std::move(lyricsTags);
|
||||
}
|
||||
|
||||
void visitTagValues(audio::TagType tag, TagValueVisitor visitor) const override
|
||||
{
|
||||
auto itValues{ _tags.find(tag) };
|
||||
if (itValues != std::cend(_tags))
|
||||
{
|
||||
for (std::string_view value : itValues->second)
|
||||
visitor(value);
|
||||
}
|
||||
}
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override
|
||||
{
|
||||
auto itValues{ _extraUserTags.find(tag) };
|
||||
if (itValues == std::cend(_extraUserTags))
|
||||
return;
|
||||
|
||||
for (std::string_view value : itValues->second)
|
||||
visitor(value);
|
||||
}
|
||||
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override
|
||||
{
|
||||
for (const auto& [role, names] : _performers)
|
||||
{
|
||||
for (const auto& name : names)
|
||||
visitor(role, name);
|
||||
}
|
||||
}
|
||||
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override
|
||||
{
|
||||
for (const auto& [language, lyrics] : _lyricsTags)
|
||||
visitor(language, lyrics);
|
||||
}
|
||||
|
||||
private:
|
||||
const Tags _tags;
|
||||
Performers _performers;
|
||||
ExtraUserTags _extraUserTags;
|
||||
LyricsTags _lyricsTags;
|
||||
};
|
||||
|
||||
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" } },
|
||||
{ TagType::Advisory, { "2" } },
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumSortOrder, { "MyAlbumSortName" } },
|
||||
{ TagType::Artist, { "MyArtist1 & MyArtist2" } },
|
||||
{ TagType::Artists, { "MyArtist1", "MyArtist2" } },
|
||||
{ TagType::ArtistSortOrder, { "MyArtist1SortName", "MyArtist2SortName" } },
|
||||
{ TagType::ArtistsSortOrder, { "MyArtists1SortName", "MyArtists2SortName" } },
|
||||
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
|
||||
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
|
||||
{ TagType::AlbumArtistSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtists1SortName", "MyAlbumArtists2SortName" } },
|
||||
{ TagType::AlbumComment, { "MyAlbumComment" } },
|
||||
{ TagType::Barcode, { "MyBarcode" } },
|
||||
{ TagType::Comment, { "Comment1", "Comment2" } },
|
||||
{ TagType::Compilation, { "1" } },
|
||||
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
|
||||
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
|
||||
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
|
||||
{ TagType::Copyright, { "MyCopyright" } },
|
||||
{ TagType::CopyrightURL, { "MyCopyrightURL" } },
|
||||
{ TagType::Date, { "2020/03/04" } },
|
||||
{ TagType::DiscNumber, { "2" } },
|
||||
{ TagType::DiscSubtitle, { "MySubtitle" } },
|
||||
{ TagType::Genre, { "Genre1", "Genre2" } },
|
||||
{ TagType::Grouping, { "Grouping1", "Grouping2" } },
|
||||
{ TagType::Media, { "CD" } },
|
||||
{ TagType::Mixer, { "MyMixer1", "MyMixer2" } },
|
||||
{ TagType::Mood, { "Mood1", "Mood2" } },
|
||||
{ TagType::MusicBrainzArtistID, { "9d2e0c8c-8c5e-4372-a061-590955eaeaae", "5e2cf87f-c8d7-4504-8a86-954dc0840229" } },
|
||||
{ TagType::MusicBrainzTrackID, { "0afb190a-6735-46df-a16d-199f48206e4a" } },
|
||||
{ TagType::MusicBrainzReleaseArtistID, { "6fbf097c-1487-43e8-874b-50dd074398a7", "5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1" } },
|
||||
{ TagType::MusicBrainzReleaseID, { "3fa39992-b786-4585-a70e-85d5cc15ef69" } },
|
||||
{ TagType::MusicBrainzReleaseGroupID, { "5b1a5a44-8420-4426-9b86-d25dc8d04838" } },
|
||||
{ TagType::MusicBrainzRecordingID, { "bd3fc666-89de-4ac8-93f6-2dbf028ad8d5" } },
|
||||
{ TagType::Producer, { "MyProducer1", "MyProducer2" } },
|
||||
{ TagType::Remixer, { "MyRemixer1", "MyRemixer2" } },
|
||||
{ TagType::RecordLabel, { "Label1", "Label2" } },
|
||||
{ TagType::ReleaseCountry, { "MyCountry1", "MyCountry2" } },
|
||||
{ TagType::Language, { "Language1", "Language2" } },
|
||||
{ TagType::Lyricist, { "MyLyricist1", "MyLyricist2" } },
|
||||
{ TagType::OriginalReleaseDate, { "2019/02/03" } },
|
||||
{ TagType::ReleaseType, { "Album", "Compilation" } },
|
||||
{ TagType::ReplayGainTrackGain, { "-0.33" } },
|
||||
{ TagType::ReplayGainAlbumGain, { "-0.5" } },
|
||||
{ TagType::TrackTitle, { "MyTitle" } },
|
||||
{ TagType::TrackNumber, { "7" } },
|
||||
{ TagType::TotalTracks, { "12" } },
|
||||
{ TagType::TotalDiscs, { "3" } },
|
||||
}) };
|
||||
testTags->setExtraUserTags({ { "MY_AWESOME_TAG_A", { "MyTagValue1ForTagA", "MyTagValue2ForTagA" } },
|
||||
{ "MY_AWESOME_TAG_B", { "MyTagValue1ForTagB", "MyTagValue2ForTagB" } } });
|
||||
testTags->setPerformersTags({ { "RoleA", { "MyPerformer1ForRoleA", "MyPerformer2ForRoleA" } },
|
||||
{ "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } });
|
||||
testTags->setLyricsTags({ { "eng", "[00:00.00]First line\n[00:01.00]Second line" } });
|
||||
|
||||
return testTags;
|
||||
}
|
||||
} // 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;
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 "core/Exception.hpp"
|
||||
|
||||
namespace lms::transcoding
|
||||
{
|
||||
class Exception : public core::LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
} // 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
|
||||
Reference in New Issue
Block a user