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:
emeric
2025-11-02 16:23:10 +01:00
parent 8e554b258c
commit b25e3b9b1e
112 changed files with 4005 additions and 3379 deletions
@@ -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
@@ -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
@@ -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
@@ -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)
{
@@ -19,33 +19,32 @@
#pragma once
#include "IFileScanOperation.hpp"
#include <memory>
#include <vector>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "image/Types.hpp"
#include "metadata/Types.hpp"
#include "FileScanOperationBase.hpp"
#include "FileToScan.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/FileToScan.hpp"
#include "scanners/IFileScanOperation.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::db
{
class IDb;
} // namespace lms::db
namespace lms::metadata
{
class IAudioFileParser;
} // namespace lms::metadata
namespace lms::scanner
{
class TrackMetadataParser;
struct ImageInfo
{
std::size_t index;
metadata::Image::Type type{ metadata::Image::Type::Unknown };
audio::Image::Type type{ audio::Image::Type::Unknown };
std::uint64_t hash{};
std::size_t size{};
image::ImageProperties properties;
@@ -56,7 +55,7 @@ namespace lms::scanner
class AudioFileScanOperation : public FileScanOperationBase
{
public:
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser);
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions);
~AudioFileScanOperation() override;
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
@@ -66,9 +65,15 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
metadata::IAudioFileParser& _parser;
std::unique_ptr<metadata::Track> _parsedTrack;
std::vector<ImageInfo> _parsedImages;
};
const TrackMetadataParser& _metadataParser;
const audio::ParserOptions& _parserOptions;
struct AudioFileInfo
{
audio::AudioProperties audioProperties;
Track track;
std::vector<ImageInfo> images;
};
std::optional<AudioFileInfo> _file;
};
} // namespace lms::scanner
@@ -21,53 +21,64 @@
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/Track.hpp"
#include "metadata/IAudioFileParser.hpp"
#include "AudioFileScanOperation.hpp"
#include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "scanners/Utils.hpp"
#include "scanners/audiofile/AudioFileScanOperation.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms::scanner
{
namespace
{
metadata::ParserReadStyle getParserReadStyle()
audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
{
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
if (readStyle == "fast")
return metadata::ParserReadStyle::Fast;
return audio::ParserOptions::AudioPropertiesReadStyle::Fast;
if (readStyle == "average")
return metadata::ParserReadStyle::Average;
return audio::ParserOptions::AudioPropertiesReadStyle::Average;
if (readStyle == "accurate")
return metadata::ParserReadStyle::Accurate;
return audio::ParserOptions::AudioPropertiesReadStyle::Accurate;
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
}
metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings)
TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings)
{
metadata::AudioFileParserParameters params;
TrackMetadataParser::Parameters params;
params.userExtraTags = settings.extraTags;
params.artistTagDelimiters = settings.artistTagDelimiters;
params.defaultTagDelimiters = settings.defaultTagDelimiters;
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
params.backend = metadata::ParserBackend::TagLib;
params.readStyle = getParserReadStyle();
return params;
}
audio::ParserOptions createAudioFileParserOptions()
{
audio::ParserOptions options;
options.readStyle = getParserReadStyle();
options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib
return options;
}
} // namespace
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
: _db{ db }
, _settings{ settings }
, _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
, _parserOptions{ createAudioFileParserOptions() }
{
}
@@ -85,7 +96,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
{
return _metadataParser->getSupportedExtensions();
return audio::getSupportedExtensions(_parserOptions.parser);
}
bool AudioFileScanner::needsScan(const FileToScan& file) const
@@ -101,6 +112,6 @@ namespace lms::scanner
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
{
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
}
} // namespace lms::scanner
@@ -19,7 +19,10 @@
#pragma once
#include "IFileScanner.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "scanners/IFileScanner.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms
{
@@ -55,6 +58,7 @@ namespace lms::scanner
db::IDb& _db;
const ScannerSettings& _settings;
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
const TrackMetadataParser _trackMetadataParser;
const audio::ParserOptions _parserOptions;
};
} // namespace lms::scanner
@@ -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, &params.artistsToNotSplit) };
if (artistNames.empty())
return {};
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters, &params.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
@@ -22,17 +22,19 @@
#include <fstream>
#include <optional>
#include "FileScanOperationBase.hpp"
#include "ScannerSettings.hpp"
#include "core/ILogger.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/TrackLyrics.hpp"
#include "metadata/Lyrics.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "Utils.hpp"
#include "ScannerSettings.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
#include "types/Lyrics.hpp"
namespace lms::scanner
{
@@ -48,28 +50,21 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
std::optional<metadata::Lyrics> _parsedLyrics;
std::optional<Lyrics> _parsedLyrics;
};
void LyricsFileScanOperation::scan()
{
try
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedLyrics = metadata::parseLyrics(ifs);
}
catch (const metadata::Exception& e)
{
addError<LyricsFileScanError>(getFilePath());
}
_parsedLyrics = parseLyrics(ifs);
}
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
@@ -140,7 +135,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedLyricsFileExtensions();
return getSupportedLyricsFileExtensions();
}
bool LyricsFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -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
@@ -27,12 +27,12 @@
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/PlayListFile.hpp"
#include "metadata/Exception.hpp"
#include "metadata/PlayList.hpp"
#include "FileScanOperationBase.hpp"
#include "ScanContext.hpp"
#include "Utils.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/playlist/PlayListParser.hpp"
namespace lms::scanner
{
@@ -51,28 +51,21 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
std::optional<metadata::PlayList> _parsedPlayList;
std::optional<PlayList> _parsedPlayList;
};
void PlayListFileScanOperation::scan()
{
try
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedPlayList = metadata::parsePlayList(ifs);
}
catch (const metadata::Exception& e)
{
addError<PlayListFileScanError>(getFilePath());
}
_parsedPlayList = parsePlayList(ifs);
}
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
@@ -139,7 +132,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedPlayListFileExtensions();
return getSupportedPlayListFileExtensions();
}
bool PlayListFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -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