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:
@@ -27,15 +27,16 @@
|
||||
#include "core/IJobScheduler.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/ScanSettings.hpp"
|
||||
|
||||
#include "scanners/ArtistInfoFileScanner.hpp"
|
||||
#include "scanners/AudioFileScanner.hpp"
|
||||
#include "scanners/ImageFileScanner.hpp"
|
||||
#include "scanners/LyricsFileScanner.hpp"
|
||||
#include "scanners/PlayListFileScanner.hpp"
|
||||
#include "scanners/artistinfo/ArtistInfoFileScanner.hpp"
|
||||
#include "scanners/audiofile/AudioFileScanner.hpp"
|
||||
#include "scanners/lyrics/LyricsFileScanner.hpp"
|
||||
#include "scanners/playlist/PlayListFileScanner.hpp"
|
||||
|
||||
#include "steps/ScanStepArtistReconciliation.hpp"
|
||||
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||
|
||||
@@ -21,13 +21,14 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner::helpers
|
||||
{
|
||||
namespace
|
||||
{
|
||||
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
|
||||
db::Artist::pointer createArtist(db::Session& session, const Artist& artistInfo)
|
||||
{
|
||||
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace lms::scanner::helpers
|
||||
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
||||
}
|
||||
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const Artist& artistInfo)
|
||||
{
|
||||
// MBID may be set
|
||||
if (artist->getMBID() != artistInfo.mbid)
|
||||
@@ -68,7 +69,7 @@ namespace lms::scanner::helpers
|
||||
|
||||
} // namespace
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
assert(artistInfo.mbid.has_value());
|
||||
db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) };
|
||||
@@ -99,7 +100,7 @@ namespace lms::scanner::helpers
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
db::Artist::pointer artist;
|
||||
|
||||
@@ -139,7 +140,7 @@ namespace lms::scanner::helpers
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/TaggedType.hpp"
|
||||
|
||||
#include "database/objects/Artist.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct Artist;
|
||||
}
|
||||
@@ -31,8 +32,8 @@ namespace lms::scanner::helpers
|
||||
{
|
||||
using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>;
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
|
||||
} // namespace lms::scanner::helpers
|
||||
+12
-9
@@ -24,19 +24,22 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/ArtistInfo.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "metadata/ArtistInfo.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/artistinfo/ArtistInfoParser.hpp"
|
||||
#include "types/ArtistInfo.hpp"
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -57,7 +60,7 @@ namespace lms::scanner
|
||||
|
||||
std::string getArtistNameFromArtistInfoFilePath();
|
||||
|
||||
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
||||
std::optional<ArtistInfo> _parsedArtistInfo;
|
||||
};
|
||||
|
||||
void ArtistInfoFileScanOperation::scan()
|
||||
@@ -72,14 +75,14 @@ namespace lms::scanner
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
||||
_parsedArtistInfo = parseArtistInfo(ifs);
|
||||
if (_parsedArtistInfo->name.empty())
|
||||
{
|
||||
_parsedArtistInfo->name = getFilePath().parent_path().filename();
|
||||
LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'");
|
||||
}
|
||||
}
|
||||
catch (const metadata::ArtistInfoParseException& e)
|
||||
catch (const ArtistInfoParseException& e)
|
||||
{
|
||||
addError<ArtistInfoFileScanError>(getFilePath());
|
||||
}
|
||||
@@ -121,7 +124,7 @@ namespace lms::scanner
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||
|
||||
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||
const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) };
|
||||
artistInfo.modify()->setArtist(artist);
|
||||
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID());
|
||||
@@ -150,7 +153,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
|
||||
{
|
||||
return metadata::getSupportedArtistInfoFiles();
|
||||
return getSupportedArtistInfoFiles();
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ArtistInfoParser.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::optional<std::string_view> getText(const pugi::xml_node& node, const core::LiteralString& tag)
|
||||
{
|
||||
std::optional<std::string_view> res;
|
||||
|
||||
if (const pugi::xml_node child{ node.child(tag.c_str()) })
|
||||
res = std::string_view{ child.child_value() };
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 1> files{ "artist.nfo" };
|
||||
return files;
|
||||
}
|
||||
|
||||
ArtistInfo parseArtistInfo(std::istream& is)
|
||||
{
|
||||
ArtistInfo artistInfo;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result{ doc.load(is) };
|
||||
if (!result)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "Cannot read artist info xml: " << result.description());
|
||||
throw ArtistInfoParseException{ result.description() };
|
||||
}
|
||||
|
||||
const pugi::xml_node artistNode{ doc.child("artist") };
|
||||
if (!artistNode)
|
||||
throw ArtistInfoParseException{ "No <artist> element found in artist info xml" };
|
||||
|
||||
{
|
||||
auto mbid{ getText(artistNode, "musicBrainzArtistID") };
|
||||
if (!mbid.has_value())
|
||||
mbid = getText(artistNode, "musicbrainzartistid"); // lidarr seems to put this in lowercase
|
||||
artistInfo.mbid = core::UUID::fromString(core::stringUtils::stringTrim(mbid.has_value() ? *mbid : ""));
|
||||
}
|
||||
|
||||
artistInfo.name = core::stringUtils::stringTrim(getText(artistNode, "name").value_or(""));
|
||||
artistInfo.sortName = core::stringUtils::stringTrim(getText(artistNode, "sortname").value_or(""));
|
||||
artistInfo.type = core::stringUtils::stringTrim(getText(artistNode, "type").value_or(""));
|
||||
artistInfo.gender = core::stringUtils::stringTrim(getText(artistNode, "gender").value_or(""));
|
||||
artistInfo.disambiguation = core::stringUtils::stringTrim(getText(artistNode, "disambiguation").value_or(""));
|
||||
artistInfo.biography = getText(artistNode, "biography").value_or("");
|
||||
|
||||
return artistInfo;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
#include "types/ArtistInfo.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ArtistInfoParseException : public core::LmsException
|
||||
{
|
||||
public:
|
||||
using core::LmsException::LmsException;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
|
||||
ArtistInfo parseArtistInfo(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
+191
-184
@@ -24,6 +24,11 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/Path.hpp"
|
||||
#include "core/XxHash3.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
@@ -40,36 +45,34 @@
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "scanners/IFileScanOperation.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
for (const metadata::Artist& artistInfo : artists)
|
||||
for (const Artist& artist : artists)
|
||||
{
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) };
|
||||
db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) };
|
||||
|
||||
const bool matchedUsingMbid{ artistInfo.mbid.has_value() && artist->getMBID() == artistInfo.mbid };
|
||||
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, artist, linkType, role, matchedUsingMbid) };
|
||||
link.modify()->setArtistName(artistInfo.name);
|
||||
if (artistInfo.sortName)
|
||||
link.modify()->setArtistSortName(*artistInfo.sortName);
|
||||
const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
|
||||
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, dbArtist, linkType, role, matchedUsingMbid) };
|
||||
link.modify()->setArtistName(artist.name);
|
||||
if (artist.sortName)
|
||||
link.modify()->setArtistSortName(*artist.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
constexpr std::string_view noRole{};
|
||||
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
|
||||
@@ -102,128 +105,128 @@ namespace lms::scanner
|
||||
return label;
|
||||
}
|
||||
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getSortName() != releaseInfo.sortName)
|
||||
release.modify()->setSortName(releaseInfo.sortName);
|
||||
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
||||
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
if (release->isCompilation() != releaseInfo.isCompilation)
|
||||
release.modify()->setCompilation(releaseInfo.isCompilation);
|
||||
if (release->getBarcode() != releaseInfo.barcode)
|
||||
release.modify()->setBarcode(releaseInfo.barcode);
|
||||
if (release->getComment() != releaseInfo.comment)
|
||||
release.modify()->setComment(releaseInfo.comment);
|
||||
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
||||
if (dbRelease->getName() != release.name)
|
||||
dbRelease.modify()->setName(release.name);
|
||||
if (dbRelease->getSortName() != release.sortName)
|
||||
dbRelease.modify()->setSortName(release.sortName);
|
||||
if (dbRelease->getGroupMBID() != release.groupMBID)
|
||||
dbRelease.modify()->setGroupMBID(release.groupMBID);
|
||||
if (dbRelease->getTotalDisc() != release.mediumCount)
|
||||
dbRelease.modify()->setTotalDisc(release.mediumCount);
|
||||
if (dbRelease->getArtistDisplayName() != release.artistDisplayName)
|
||||
dbRelease.modify()->setArtistDisplayName(release.artistDisplayName);
|
||||
if (dbRelease->isCompilation() != release.isCompilation)
|
||||
dbRelease.modify()->setCompilation(release.isCompilation);
|
||||
if (dbRelease->getBarcode() != release.barcode)
|
||||
dbRelease.modify()->setBarcode(release.barcode);
|
||||
if (dbRelease->getComment() != release.comment)
|
||||
dbRelease.modify()->setComment(release.comment);
|
||||
if (dbRelease->getReleaseTypeNames() != release.releaseTypes)
|
||||
{
|
||||
release.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
||||
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
dbRelease.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : release.releaseTypes)
|
||||
dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
}
|
||||
if (release->getCountryNames() != releaseInfo.countries)
|
||||
if (dbRelease->getCountryNames() != release.countries)
|
||||
{
|
||||
release.modify()->clearCountries();
|
||||
for (std::string_view country : releaseInfo.countries)
|
||||
release.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
dbRelease.modify()->clearCountries();
|
||||
for (std::string_view country : release.countries)
|
||||
dbRelease.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
}
|
||||
if (release->getLabelNames() != releaseInfo.labels)
|
||||
if (dbRelease->getLabelNames() != release.labels)
|
||||
{
|
||||
release.modify()->clearLabels();
|
||||
for (std::string_view label : releaseInfo.labels)
|
||||
release.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
dbRelease.modify()->clearLabels();
|
||||
for (std::string_view label : release.labels)
|
||||
dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare release level info
|
||||
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||
bool isReleaseMatching(const db::Release::pointer& dbCandidateRelease, const Release& release)
|
||||
{
|
||||
// TODO: add more criterias?
|
||||
return candidateRelease->getName() == releaseInfo.name
|
||||
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
||||
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
||||
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
||||
return dbCandidateRelease->getName() == release.name
|
||||
&& dbCandidateRelease->getSortName() == release.sortName
|
||||
&& dbCandidateRelease->getTotalDisc() == release.mediumCount
|
||||
&& dbCandidateRelease->isCompilation() == release.isCompilation
|
||||
&& dbCandidateRelease->getLabelNames() == release.labels
|
||||
&& dbCandidateRelease->getBarcode() == release.barcode;
|
||||
}
|
||||
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory)
|
||||
{
|
||||
db::Release::pointer release;
|
||||
db::Release::pointer dbRelease;
|
||||
|
||||
// First try to get by MBID: fastest, safest
|
||||
if (releaseInfo.mbid)
|
||||
if (release.mbid)
|
||||
{
|
||||
release = db::Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
dbRelease = db::Release::find(session, *release.mbid);
|
||||
if (!dbRelease)
|
||||
dbRelease = session.create<db::Release>(release.name, release.mbid);
|
||||
}
|
||||
else if (releaseInfo.name.empty())
|
||||
else if (release.name.empty())
|
||||
{
|
||||
// No release name (only mbid) -> nothing to do
|
||||
return release;
|
||||
return dbRelease;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
if (!dbRelease && release.mediumCount && *release.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setParentDirectory(parentDirectoryId);
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
params.setName(release.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
if (dbRelease)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
if (dbCandidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
if (!isReleaseMatching(dbCandidateRelease, release))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
dbRelease = dbCandidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
// Lastly try in the current directory: we do this at last to have
|
||||
// opportunities to merge releases in case of migration / rescan
|
||||
if (!release)
|
||||
if (!dbRelease)
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setDirectory(currentDirectory->getId());
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
params.setName(release.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
if (dbRelease)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
if (dbCandidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
if (!isReleaseMatching(dbCandidateRelease, release))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
dbRelease = dbCandidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name);
|
||||
if (!dbRelease)
|
||||
dbRelease = session.create<db::Release>(release.name);
|
||||
|
||||
updateReleaseIfNeeded(session, release, releaseInfo);
|
||||
return release;
|
||||
updateReleaseIfNeeded(session, dbRelease, release);
|
||||
return dbRelease;
|
||||
}
|
||||
|
||||
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
|
||||
db::Medium::pointer getOrCreateMedium(db::Session& session, const Medium& medium, const db::Release::pointer& release)
|
||||
{
|
||||
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
|
||||
if (!dbMedium)
|
||||
@@ -243,7 +246,7 @@ namespace lms::scanner
|
||||
return dbMedium;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
|
||||
@@ -274,69 +277,69 @@ namespace lms::scanner
|
||||
return clusters;
|
||||
}
|
||||
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const Lyrics& lyrics)
|
||||
{
|
||||
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
||||
db::TrackLyrics::pointer dbLyrics{ session.create<db::TrackLyrics>() };
|
||||
|
||||
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
||||
lyrics.modify()->setOffset(lyricsInfo.offset);
|
||||
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
||||
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
||||
if (!lyricsInfo.synchronizedLines.empty())
|
||||
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
||||
dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx");
|
||||
dbLyrics.modify()->setOffset(lyrics.offset);
|
||||
dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist);
|
||||
dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle);
|
||||
if (!lyrics.synchronizedLines.empty())
|
||||
dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines);
|
||||
else
|
||||
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
||||
dbLyrics.modify()->setUnsynchronizedLines(lyrics.unsynchronizedLines);
|
||||
|
||||
return lyrics;
|
||||
return dbLyrics;
|
||||
}
|
||||
|
||||
db::ImageType convertImageType(metadata::Image::Type type)
|
||||
db::ImageType convertImageType(audio::Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case metadata::Image::Type::Unknown:
|
||||
case audio::Image::Type::Unknown:
|
||||
return db::ImageType::Unknown;
|
||||
case metadata::Image::Type::Other:
|
||||
case audio::Image::Type::Other:
|
||||
return db::ImageType::Other;
|
||||
case metadata::Image::Type::FileIcon:
|
||||
case audio::Image::Type::FileIcon:
|
||||
return db::ImageType::FileIcon;
|
||||
case metadata::Image::Type::OtherFileIcon:
|
||||
case audio::Image::Type::OtherFileIcon:
|
||||
return db::ImageType::OtherFileIcon;
|
||||
case metadata::Image::Type::FrontCover:
|
||||
case audio::Image::Type::FrontCover:
|
||||
return db::ImageType::FrontCover;
|
||||
case metadata::Image::Type::BackCover:
|
||||
case audio::Image::Type::BackCover:
|
||||
return db::ImageType::BackCover;
|
||||
case metadata::Image::Type::LeafletPage:
|
||||
case audio::Image::Type::LeafletPage:
|
||||
return db::ImageType::LeafletPage;
|
||||
case metadata::Image::Type::Media:
|
||||
case audio::Image::Type::Media:
|
||||
return db::ImageType::Media;
|
||||
case metadata::Image::Type::LeadArtist:
|
||||
case audio::Image::Type::LeadArtist:
|
||||
return db::ImageType::LeadArtist;
|
||||
case metadata::Image::Type::Artist:
|
||||
case audio::Image::Type::Artist:
|
||||
return db::ImageType::Artist;
|
||||
case metadata::Image::Type::Conductor:
|
||||
case audio::Image::Type::Conductor:
|
||||
return db::ImageType::Conductor;
|
||||
case metadata::Image::Type::Band:
|
||||
case audio::Image::Type::Band:
|
||||
return db::ImageType::Band;
|
||||
case metadata::Image::Type::Composer:
|
||||
case audio::Image::Type::Composer:
|
||||
return db::ImageType::Composer;
|
||||
case metadata::Image::Type::Lyricist:
|
||||
case audio::Image::Type::Lyricist:
|
||||
return db::ImageType::Lyricist;
|
||||
case metadata::Image::Type::RecordingLocation:
|
||||
case audio::Image::Type::RecordingLocation:
|
||||
return db::ImageType::RecordingLocation;
|
||||
case metadata::Image::Type::DuringRecording:
|
||||
case audio::Image::Type::DuringRecording:
|
||||
return db::ImageType::DuringRecording;
|
||||
case metadata::Image::Type::DuringPerformance:
|
||||
case audio::Image::Type::DuringPerformance:
|
||||
return db::ImageType::DuringPerformance;
|
||||
case metadata::Image::Type::MovieScreenCapture:
|
||||
case audio::Image::Type::MovieScreenCapture:
|
||||
return db::ImageType::MovieScreenCapture;
|
||||
case metadata::Image::Type::ColouredFish:
|
||||
case audio::Image::Type::ColouredFish:
|
||||
return db::ImageType::ColouredFish;
|
||||
case metadata::Image::Type::Illustration:
|
||||
case audio::Image::Type::Illustration:
|
||||
return db::ImageType::Illustration;
|
||||
case metadata::Image::Type::BandLogo:
|
||||
case audio::Image::Type::BandLogo:
|
||||
return db::ImageType::BandLogo;
|
||||
case metadata::Image::Type::PublisherLogo:
|
||||
case audio::Image::Type::PublisherLogo:
|
||||
return db::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
@@ -361,10 +364,10 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& dbTrack, const ImageInfo& imageInfo)
|
||||
{
|
||||
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(dbTrack, image) };
|
||||
imageLink.modify()->setIndex(imageInfo.index);
|
||||
imageLink.modify()->setType(convertImageType(imageInfo.type));
|
||||
imageLink.modify()->setDescription(imageInfo.description);
|
||||
@@ -372,35 +375,35 @@ namespace lms::scanner
|
||||
return imageLink;
|
||||
}
|
||||
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span<const ImageInfo> images)
|
||||
{
|
||||
track.modify()->clearEmbeddedImageLinks();
|
||||
dbTrack.modify()->clearEmbeddedImageLinks();
|
||||
for (const ImageInfo& imageInfo : images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
||||
track.modify()->addEmbeddedImageLink(link);
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) };
|
||||
dbTrack.modify()->addEmbeddedImageLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
||||
db::Advisory getAdvisory(std::optional<Track::Advisory> advisory)
|
||||
{
|
||||
if (!advisory)
|
||||
return db::Advisory::UnSet;
|
||||
|
||||
switch (advisory.value())
|
||||
{
|
||||
case metadata::Track::Advisory::Clean:
|
||||
case Track::Advisory::Clean:
|
||||
return db::Advisory::Clean;
|
||||
case metadata::Track::Advisory::Explicit:
|
||||
case Track::Advisory::Explicit:
|
||||
return db::Advisory::Explicit;
|
||||
case metadata::Track::Advisory::Unknown:
|
||||
case Track::Advisory::Unknown:
|
||||
return db::Advisory::Unknown;
|
||||
}
|
||||
|
||||
return db::Advisory::UnSet;
|
||||
}
|
||||
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
// Add as many fields as possible to limit errors
|
||||
@@ -436,9 +439,9 @@ namespace lms::scanner
|
||||
return res;
|
||||
}
|
||||
|
||||
void fillInArtistsWithMbid(std::span<const metadata::Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (const metadata::Artist& artist : artists)
|
||||
for (const Artist& artist : artists)
|
||||
{
|
||||
if (artist.mbid.has_value())
|
||||
{
|
||||
@@ -448,9 +451,9 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
void fillInMbids(std::span<metadata::Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (metadata::Artist& artist : artists)
|
||||
for (Artist& artist : artists)
|
||||
{
|
||||
if (!artist.mbid)
|
||||
{
|
||||
@@ -461,7 +464,7 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
void fillMissingMbids(metadata::Track& track)
|
||||
void fillMissingMbids(Track& track)
|
||||
{
|
||||
// first pass: collect all artists that have mbids
|
||||
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
|
||||
@@ -485,9 +488,10 @@ namespace lms::scanner
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser)
|
||||
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions)
|
||||
: FileScanOperationBase{ std::move(fileToScan), db, settings }
|
||||
, _parser{ parser }
|
||||
, _metadataParser{ metadataParser }
|
||||
, _parserOptions{ parserOptions }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -495,17 +499,20 @@ namespace lms::scanner
|
||||
|
||||
void AudioFileScanOperation::scan()
|
||||
{
|
||||
std::unique_ptr<metadata::Track> track;
|
||||
|
||||
try
|
||||
{
|
||||
_parsedTrack = _parser.parseMetaData(getFilePath());
|
||||
auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) };
|
||||
|
||||
_file.emplace();
|
||||
|
||||
_file->audioProperties = audioFileInfo->getAudioProperties();
|
||||
_file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader());
|
||||
|
||||
// We fill missing artist mbids with mbids found on other artist roles
|
||||
fillMissingMbids(*_parsedTrack);
|
||||
fillMissingMbids(_file->track);
|
||||
|
||||
std::size_t index{};
|
||||
_parser.parseImages(getFilePath(), [&](const metadata::Image& image) {
|
||||
audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) {
|
||||
try
|
||||
{
|
||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||
@@ -522,7 +529,7 @@ namespace lms::scanner
|
||||
info.description = image.description;
|
||||
info.properties = properties;
|
||||
|
||||
_parsedImages.push_back(std::move(info));
|
||||
_file->images.push_back(std::move(info));
|
||||
}
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
@@ -532,15 +539,15 @@ namespace lms::scanner
|
||||
index++;
|
||||
});
|
||||
}
|
||||
catch (const metadata::AudioFileNoAudioPropertiesException&)
|
||||
catch (const audio::AudioFileNoAudioPropertiesException&)
|
||||
{
|
||||
addError<NoAudioTrackFoundError>(getFilePath());
|
||||
}
|
||||
catch (const metadata::IOException& e)
|
||||
catch (const audio::IOException& e)
|
||||
{
|
||||
addError<IOScanError>(getFilePath(), e.getErrorCode());
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
catch (const audio::Exception& e)
|
||||
{
|
||||
addError<AudioFileScanError>(getFilePath());
|
||||
}
|
||||
@@ -552,7 +559,7 @@ namespace lms::scanner
|
||||
|
||||
db::Session& dbSession{ getDb().getTLSSession() };
|
||||
db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) };
|
||||
if (!_parsedTrack)
|
||||
if (!_file)
|
||||
{
|
||||
if (track)
|
||||
{
|
||||
@@ -562,9 +569,9 @@ namespace lms::scanner
|
||||
return OperationResult::Skipped;
|
||||
}
|
||||
|
||||
if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
|
||||
if (_file->track.mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
|
||||
{
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_file->track.mbid) };
|
||||
|
||||
// find for an existing track MBID as the file may have just been moved
|
||||
if (!track && duplicateTracks.size() == 1)
|
||||
@@ -616,7 +623,7 @@ namespace lms::scanner
|
||||
if (!track)
|
||||
{
|
||||
// maybe the file just moved?
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize());
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize());
|
||||
if (track)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath());
|
||||
@@ -625,7 +632,7 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
// We estimate this is an audio file if the duration is not null
|
||||
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
if (_file->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
{
|
||||
addError<BadAudioDurationError>(getFilePath());
|
||||
|
||||
@@ -639,8 +646,8 @@ namespace lms::scanner
|
||||
|
||||
// ***** Title
|
||||
std::string title;
|
||||
if (!_parsedTrack->title.empty())
|
||||
title = _parsedTrack->title;
|
||||
if (!_file->track.title.empty())
|
||||
title = _file->track.title;
|
||||
else
|
||||
{
|
||||
// TODO parse file name to guess track etc.
|
||||
@@ -665,18 +672,18 @@ namespace lms::scanner
|
||||
track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
|
||||
|
||||
// Audio properties
|
||||
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
|
||||
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
|
||||
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||
track.modify()->setBitrate(_file->audioProperties.bitrate ? *_file->audioProperties.bitrate : 0);
|
||||
track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0);
|
||||
track.modify()->setChannelCount(_file->audioProperties.channelCount ? *_file->audioProperties.channelCount : 0);
|
||||
track.modify()->setDuration(_file->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_file->audioProperties.sampleRate ? *_file->audioProperties.sampleRate : 0);
|
||||
|
||||
track.modify()->setFileSize(getFileSize());
|
||||
track.modify()->setLastWriteTime(getLastWriteTime());
|
||||
|
||||
if (_parsedTrack->encodingTime.isValid())
|
||||
if (_file->track.encodingTime.isValid())
|
||||
{
|
||||
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
|
||||
const core::PartialDateTime& encodingTime{ _file->track.encodingTime };
|
||||
Wt::WDate date;
|
||||
Wt::WTime time;
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
|
||||
@@ -696,61 +703,61 @@ namespace lms::scanner
|
||||
track.modify()->clearArtistLinks();
|
||||
|
||||
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
|
||||
if (_file->track.medium && _file->track.medium->release)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback);
|
||||
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _file->track.remixerArtists, allowFallback);
|
||||
|
||||
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||
for (const auto& [role, performers] : _file->track.performerArtists)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
|
||||
|
||||
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
if (_file->track.medium && _file->track.medium->release)
|
||||
{
|
||||
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
|
||||
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) };
|
||||
assert(release);
|
||||
track.modify()->setRelease(release);
|
||||
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
|
||||
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
|
||||
}
|
||||
else
|
||||
{
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setMedium({});
|
||||
}
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||
track.modify()->setDate(_parsedTrack->date);
|
||||
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear });
|
||||
track.modify()->setTrackNumber(_file->track.position);
|
||||
track.modify()->setDate(_file->track.date);
|
||||
track.modify()->setOriginalDate(_file->track.originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _file->track.originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear });
|
||||
|
||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
|
||||
track.modify()->setDate(_parsedTrack->originalDate);
|
||||
if (!_file->track.date.isValid() && _file->track.originalDate.isValid())
|
||||
track.modify()->setDate(_file->track.originalDate);
|
||||
|
||||
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
|
||||
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||
track.modify()->setRecordingMBID(_file->track.recordingMBID);
|
||||
track.modify()->setTrackMBID(_file->track.mbid);
|
||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||
track.modify()->setCopyright(_file->track.copyright);
|
||||
track.modify()->setCopyrightURL(_file->track.copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_file->track.advisory));
|
||||
track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setReplayGain(_file->track.replayGain);
|
||||
track.modify()->setArtistDisplayName(_file->track.artistDisplayName);
|
||||
|
||||
track.modify()->clearEmbeddedLyrics();
|
||||
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
|
||||
for (const Lyrics& lyricsInfo : _file->track.lyrics)
|
||||
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
|
||||
|
||||
updateEmbeddedImages(dbSession, track, _parsedImages);
|
||||
updateEmbeddedImages(dbSession, track, _file->images);
|
||||
|
||||
if (added)
|
||||
{
|
||||
+22
-17
@@ -19,33 +19,32 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "image/Types.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "FileToScan.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/FileToScan.hpp"
|
||||
#include "scanners/IFileScanOperation.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class IDb;
|
||||
} // namespace lms::db
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
class IAudioFileParser;
|
||||
} // namespace lms::metadata
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class TrackMetadataParser;
|
||||
|
||||
struct ImageInfo
|
||||
{
|
||||
std::size_t index;
|
||||
metadata::Image::Type type{ metadata::Image::Type::Unknown };
|
||||
audio::Image::Type type{ audio::Image::Type::Unknown };
|
||||
std::uint64_t hash{};
|
||||
std::size_t size{};
|
||||
image::ImageProperties properties;
|
||||
@@ -56,7 +55,7 @@ namespace lms::scanner
|
||||
class AudioFileScanOperation : public FileScanOperationBase
|
||||
{
|
||||
public:
|
||||
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser);
|
||||
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions);
|
||||
~AudioFileScanOperation() override;
|
||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||
@@ -66,9 +65,15 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
metadata::IAudioFileParser& _parser;
|
||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||
std::vector<ImageInfo> _parsedImages;
|
||||
};
|
||||
const TrackMetadataParser& _metadataParser;
|
||||
const audio::ParserOptions& _parserOptions;
|
||||
|
||||
struct AudioFileInfo
|
||||
{
|
||||
audio::AudioProperties audioProperties;
|
||||
Track track;
|
||||
std::vector<ImageInfo> images;
|
||||
};
|
||||
std::optional<AudioFileInfo> _file;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
+25
-14
@@ -21,53 +21,64 @@
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "AudioFileScanOperation.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/audiofile/AudioFileScanOperation.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
metadata::ParserReadStyle getParserReadStyle()
|
||||
audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||
|
||||
if (readStyle == "fast")
|
||||
return metadata::ParserReadStyle::Fast;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Fast;
|
||||
if (readStyle == "average")
|
||||
return metadata::ParserReadStyle::Average;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Average;
|
||||
if (readStyle == "accurate")
|
||||
return metadata::ParserReadStyle::Accurate;
|
||||
return audio::ParserOptions::AudioPropertiesReadStyle::Accurate;
|
||||
|
||||
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||
}
|
||||
|
||||
metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings)
|
||||
TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings)
|
||||
{
|
||||
metadata::AudioFileParserParameters params;
|
||||
TrackMetadataParser::Parameters params;
|
||||
params.userExtraTags = settings.extraTags;
|
||||
params.artistTagDelimiters = settings.artistTagDelimiters;
|
||||
params.defaultTagDelimiters = settings.defaultTagDelimiters;
|
||||
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
|
||||
params.backend = metadata::ParserBackend::TagLib;
|
||||
params.readStyle = getParserReadStyle();
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
audio::ParserOptions createAudioFileParserOptions()
|
||||
{
|
||||
audio::ParserOptions options;
|
||||
options.readStyle = getParserReadStyle();
|
||||
options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
|
||||
: _db{ db }
|
||||
, _settings{ settings }
|
||||
, _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib
|
||||
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
|
||||
, _parserOptions{ createAudioFileParserOptions() }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,7 +96,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return _metadataParser->getSupportedExtensions();
|
||||
return audio::getSupportedExtensions(_parserOptions.parser);
|
||||
}
|
||||
|
||||
bool AudioFileScanner::needsScan(const FileToScan& file) const
|
||||
@@ -101,6 +112,6 @@ namespace lms::scanner
|
||||
|
||||
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||
{
|
||||
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
|
||||
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
+6
-2
@@ -19,7 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||
|
||||
namespace lms
|
||||
{
|
||||
@@ -55,6 +58,7 @@ namespace lms::scanner
|
||||
|
||||
db::IDb& _db;
|
||||
const ScannerSettings& _settings;
|
||||
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
|
||||
const TrackMetadataParser _trackMetadataParser;
|
||||
const audio::ParserOptions _parserOptions;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackMetadataParser.hpp"
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, audio::ITagReader::TagValueVisitor visitor)
|
||||
{
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
auto visitTagIfNonEmpty{ [&](std::string_view tag) {
|
||||
tag = core::stringUtils::stringTrim(tag);
|
||||
if (!tag.empty())
|
||||
visitor(tag);
|
||||
} };
|
||||
|
||||
for (std::string_view tagDelimiter : tagDelimiters)
|
||||
{
|
||||
if (value.find(tagDelimiter) != std::string_view::npos)
|
||||
{
|
||||
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
|
||||
visitTagIfNonEmpty(splitTag);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no delimiter found, or no delimiter to be used
|
||||
visitTagIfNonEmpty(value);
|
||||
});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void addTagIfNonEmpty(std::vector<T>& res, std::string_view tag)
|
||||
{
|
||||
if (tag.empty())
|
||||
return;
|
||||
|
||||
if (std::optional<T> val{ core::stringUtils::readAs<T>(tag) })
|
||||
res.emplace_back(std::move(*val));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes, std::span<const std::string> tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
for (const audio::TagType tagType : tagTypes)
|
||||
{
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
|
||||
// short path: no custom delimiter
|
||||
if (tagDelimiters.empty())
|
||||
{
|
||||
addTagIfNonEmpty(res, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Algo:
|
||||
// 1. replace whitelist entries by placeholders
|
||||
// 2. apply delimiters
|
||||
// 3. replace whitelist entries back
|
||||
|
||||
constexpr std::string_view substitutionPrefix{ "__LMS_ENTRY__" };
|
||||
std::unordered_map<std::string, std::string_view> substitutionMap;
|
||||
std::string strToSplit{ value };
|
||||
if (whitelist)
|
||||
{
|
||||
std::size_t counter{};
|
||||
|
||||
for (std::string_view whiteListEntry : *whitelist)
|
||||
{
|
||||
whiteListEntry = core::stringUtils::stringTrim(whiteListEntry);
|
||||
|
||||
const std::string::size_type pos{ strToSplit.find(whiteListEntry) };
|
||||
if (pos == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string substitutionStr{ std::string{ substitutionPrefix } + std::to_string(counter++) };
|
||||
strToSplit.replace(pos, whiteListEntry.size(), substitutionStr);
|
||||
substitutionMap.emplace(std::move(substitutionStr), whiteListEntry);
|
||||
}
|
||||
}
|
||||
|
||||
for (std::string_view strSplit : core::stringUtils::splitString(strToSplit, tagDelimiters))
|
||||
{
|
||||
std::string str{ core::stringUtils::stringTrim(strSplit) };
|
||||
|
||||
while (true)
|
||||
{
|
||||
std::string::size_type prefixPos{ str.find(substitutionPrefix) };
|
||||
if (prefixPos == std::string::npos)
|
||||
break;
|
||||
|
||||
std::string::size_type counterEnd{ prefixPos + substitutionPrefix.size() };
|
||||
while (std::isdigit(str[counterEnd]))
|
||||
counterEnd++;
|
||||
|
||||
std::string substitutionStr{ str.substr(prefixPos, counterEnd - prefixPos) };
|
||||
auto it{ substitutionMap.find(substitutionStr) };
|
||||
if (it != std::cend(substitutionMap))
|
||||
str.replace(prefixPos, counterEnd - prefixPos, it->second);
|
||||
}
|
||||
|
||||
addTagIfNonEmpty(res, str);
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T> getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes)
|
||||
{
|
||||
std::optional<T> res;
|
||||
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
|
||||
if (!values.empty())
|
||||
res = std::move(values.front());
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span<const std::string> tagDelimiters)
|
||||
{
|
||||
return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T> getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType)
|
||||
{
|
||||
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
|
||||
}
|
||||
|
||||
std::vector<Lyrics> getLyrics(const audio::ITagReader& tagReader)
|
||||
{
|
||||
std::vector<Lyrics> res;
|
||||
|
||||
tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) {
|
||||
std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?)
|
||||
Lyrics lyrics{ parseLyrics(iss) };
|
||||
if (lyrics.language.empty())
|
||||
lyrics.language = language;
|
||||
|
||||
res.emplace_back(std::move(lyrics));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
|
||||
std::initializer_list<audio::TagType> artistTagNames,
|
||||
std::initializer_list<audio::TagType> artistSortTagNames,
|
||||
std::initializer_list<audio::TagType> artistMBIDTagNames,
|
||||
const TrackMetadataParser::Parameters& params)
|
||||
{
|
||||
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
if (artistNames.empty())
|
||||
return {};
|
||||
|
||||
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, params.defaultTagDelimiters) };
|
||||
|
||||
std::vector<Artist> artists;
|
||||
artists.reserve(artistNames.size());
|
||||
|
||||
for (std::size_t i{}; i < artistNames.size(); ++i)
|
||||
{
|
||||
Artist& artist{ artists.emplace_back(std::move(artistNames[i])) };
|
||||
|
||||
if (artistNames.size() == artistSortNames.size())
|
||||
artist.sortName = std::move(artistSortNames[i]);
|
||||
if (artistNames.size() == artistMBIDs.size())
|
||||
artist.mbid = std::move(artistMBIDs[i]);
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
|
||||
tagReader.visitPerformerTags([&](std::string_view role, std::string_view name) {
|
||||
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
|
||||
// We consider we may hit both styles for the same track
|
||||
if (role.empty())
|
||||
{
|
||||
// "PERFORMER" "artist (role)"
|
||||
utils::PerformerArtist performer{ utils::extractPerformerAndRole(name) };
|
||||
core::stringUtils::capitalize(performer.role);
|
||||
performers[performer.role].push_back(std::move(performer.artist));
|
||||
}
|
||||
else
|
||||
{
|
||||
// "PERFORMER:role", "artist" (MP3)
|
||||
std::string roleCapitalized{ core::stringUtils::stringToLower(role) };
|
||||
core::stringUtils::capitalize(roleCapitalized);
|
||||
performers[roleCapitalized].push_back(Artist{ name });
|
||||
}
|
||||
});
|
||||
|
||||
return performers;
|
||||
}
|
||||
|
||||
bool strIsMatchingArtistNames(std::string_view str, std::span<const std::string_view> artistNames)
|
||||
{
|
||||
std::string_view::size_type currentOffset{};
|
||||
|
||||
for (const std::string_view artistName : artistNames)
|
||||
{
|
||||
std::string_view::size_type newPos{ str.find(artistName, currentOffset) };
|
||||
if (newPos == std::string_view::npos)
|
||||
return false;
|
||||
|
||||
currentOffset = newPos + artistName.size();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool strIsContainingAny(std::string_view str, std::span<const std::string> subStrs)
|
||||
{
|
||||
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
|
||||
}
|
||||
|
||||
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string>& artistTag, std::span<const std::string> artistTagDelimiters)
|
||||
{
|
||||
std::string artistDisplayName;
|
||||
|
||||
if (artists.size() == 1)
|
||||
artistDisplayName = artists.front().name;
|
||||
else if (artists.size() > 1)
|
||||
{
|
||||
std::vector<std::string_view> artistNames;
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
|
||||
|
||||
// Picard use case: if we manage to match all artists in the "artist" tag (considered single-valued), and if no custom delimiter is hit, we use it as the display name
|
||||
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
||||
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
||||
{
|
||||
// Limitation: this test does not take the whitelist into account
|
||||
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
||||
artistDisplayName = *artistTag;
|
||||
}
|
||||
|
||||
if (artistDisplayName.empty())
|
||||
artistDisplayName = core::stringUtils::joinStrings(artistNames, ", ");
|
||||
}
|
||||
|
||||
return artistDisplayName;
|
||||
}
|
||||
|
||||
std::optional<Track::Advisory> getAdvisory(const audio::ITagReader& tagReader)
|
||||
{
|
||||
if (const auto value{ getTagValueAs<int>(tagReader, audio::TagType::Advisory) })
|
||||
{
|
||||
switch (*value)
|
||||
{
|
||||
case 1:
|
||||
case 4:
|
||||
return Track::Advisory::Explicit;
|
||||
case 2:
|
||||
return Track::Advisory::Clean;
|
||||
case 0:
|
||||
return Track::Advisory::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TrackMetadataParser::TrackMetadataParser(const Parameters& params)
|
||||
: _params{ params }
|
||||
{
|
||||
}
|
||||
|
||||
TrackMetadataParser::~TrackMetadataParser() = default;
|
||||
|
||||
Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
Track track;
|
||||
processTags(tagReader, track);
|
||||
return track;
|
||||
}
|
||||
|
||||
void TrackMetadataParser::processTags(const audio::ITagReader& tagReader, Track& track) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
|
||||
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
||||
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
||||
track.acoustID = getTagValueAs<core::UUID>(tagReader, TagType::AcoustID);
|
||||
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
|
||||
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::Date) })
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid())
|
||||
track.date = date;
|
||||
}
|
||||
if (const auto dateStr = getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseDate))
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*dateStr) }; date.isValid())
|
||||
track.originalDate = date;
|
||||
}
|
||||
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseYear) })
|
||||
track.originalYear = utils::parseYear(*dateStr);
|
||||
|
||||
if (const auto encodingTimeStr{ getTagValueAs<std::string>(tagReader, TagType::EncodingTime) })
|
||||
{
|
||||
if (const core::PartialDateTime date{ core::PartialDateTime::fromString(*encodingTimeStr) }; date.isValid())
|
||||
track.encodingTime = date;
|
||||
}
|
||||
|
||||
track.advisory = getAdvisory(tagReader);
|
||||
|
||||
track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics
|
||||
track.comments = getTagValuesAs<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
|
||||
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
|
||||
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
||||
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
||||
|
||||
for (const std::string& userExtraTag : _params.userExtraTags)
|
||||
{
|
||||
visitTagValues(tagReader, userExtraTag, _params.defaultTagDelimiters, [&](std::string_view value) {
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
if (!value.empty())
|
||||
track.userExtraTags[userExtraTag].push_back(std::string{ value });
|
||||
});
|
||||
}
|
||||
|
||||
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _params.defaultTagDelimiters);
|
||||
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _params.defaultTagDelimiters);
|
||||
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _params.defaultTagDelimiters);
|
||||
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _params.defaultTagDelimiters);
|
||||
|
||||
std::vector<std::string_view> artistDelimiters{};
|
||||
|
||||
track.medium = getMedium(tagReader);
|
||||
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistsSortOrder, TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _params);
|
||||
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _params.artistTagDelimiters);
|
||||
|
||||
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, { TagType::MusicBrainzConductorID }, _params);
|
||||
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, { TagType::MusicBrainzComposerID }, _params);
|
||||
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, { TagType::MusicBrainzLyricistID }, _params);
|
||||
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, { TagType::MusicBrainzMixerID }, _params);
|
||||
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, { TagType::MusicBrainzProducerID }, _params);
|
||||
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, { TagType::MusicBrainzRemixerID }, _params);
|
||||
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
||||
|
||||
// If a file has originalDate but no originalYear, set it
|
||||
if (!track.originalYear)
|
||||
track.originalYear = track.originalDate.getYear();
|
||||
}
|
||||
|
||||
std::optional<Medium> TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::optional<Medium> medium;
|
||||
medium.emplace();
|
||||
|
||||
medium->media = getTagValueAs<std::string>(tagReader, TagType::Media).value_or("");
|
||||
medium->name = getTagValueAs<std::string>(tagReader, TagType::DiscSubtitle).value_or("");
|
||||
medium->trackCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalTracks);
|
||||
if (!medium->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::TrackNumber) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ core::stringUtils::splitString(*value, '/') };
|
||||
if (strings.size() == 2)
|
||||
medium->trackCount = core::stringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
// Expecting 'Number[/Total]'
|
||||
medium->position = getTagValueAs<std::size_t>(tagReader, TagType::DiscNumber);
|
||||
medium->release = getRelease(tagReader);
|
||||
medium->replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainAlbumGain);
|
||||
|
||||
if (medium->isDefault())
|
||||
medium.reset();
|
||||
|
||||
return medium;
|
||||
}
|
||||
|
||||
std::optional<Release> TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const
|
||||
{
|
||||
using namespace audio;
|
||||
|
||||
std::optional<Release> release;
|
||||
|
||||
auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) };
|
||||
if (!releaseName)
|
||||
return release;
|
||||
|
||||
release.emplace();
|
||||
release->name = std::move(*releaseName);
|
||||
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or(release->name);
|
||||
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _params);
|
||||
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters);
|
||||
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
||||
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
||||
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
||||
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
|
||||
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
|
||||
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _params.defaultTagDelimiters);
|
||||
release->comment = getTagValueAs<std::string>(tagReader, TagType::AlbumComment).value_or("");
|
||||
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _params.defaultTagDelimiters);
|
||||
if (!release->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as "position/count"
|
||||
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::DiscNumber) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ core::stringUtils::splitString(*value, '/') };
|
||||
if (strings.size() == 2)
|
||||
release->mediumCount = core::stringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
release->releaseTypes = getTagValuesAs<std::string>(tagReader, TagType::ReleaseType, _params.defaultTagDelimiters);
|
||||
|
||||
return release;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class TrackMetadataParser
|
||||
{
|
||||
public:
|
||||
struct SortByLengthDesc
|
||||
{
|
||||
bool operator()(const std::string& a, const std::string& b) const
|
||||
{
|
||||
if (a.length() != b.length())
|
||||
return a.length() > b.length();
|
||||
return a < b; // Break ties using lexicographical order
|
||||
}
|
||||
};
|
||||
|
||||
using WhiteList = std::set<std::string, SortByLengthDesc>;
|
||||
struct Parameters
|
||||
{
|
||||
std::vector<std::string> artistTagDelimiters;
|
||||
WhiteList artistsToNotSplit;
|
||||
std::vector<std::string> defaultTagDelimiters;
|
||||
std::vector<std::string> userExtraTags;
|
||||
};
|
||||
|
||||
TrackMetadataParser(const Parameters& params = {});
|
||||
~TrackMetadataParser();
|
||||
TrackMetadataParser(const TrackMetadataParser&) = delete;
|
||||
TrackMetadataParser& operator=(const TrackMetadataParser&) = delete;
|
||||
|
||||
Track parseTrackMetaData(const audio::ITagReader& reader) const;
|
||||
|
||||
private:
|
||||
void processTags(const audio::ITagReader& reader, Track& track) const;
|
||||
|
||||
std::optional<Medium> getMedium(const audio::ITagReader& tagReader) const;
|
||||
std::optional<Release> getRelease(const audio::ITagReader& tagReader) const;
|
||||
|
||||
const Parameters _params;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]{
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
};
|
||||
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm{};
|
||||
tm.tm_mon = -1;
|
||||
tm.tm_mday = -1;
|
||||
|
||||
std::istringstream ss{ std::string{ dateStr } }; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
if (tm.tm_mday <= 0 || tm.tm_mon < 0)
|
||||
continue;
|
||||
|
||||
const Wt::WDate res{
|
||||
tm.tm_year + 1900, // tm.tm_year: years since 1900
|
||||
tm.tm_mon + 1, // tm.tm_mon: months since January – [00, 11]
|
||||
tm.tm_mday // tm.tm_mday: day of the month – [1, 31]
|
||||
};
|
||||
if (!res.isValid())
|
||||
continue;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<int> parseYear(std::string_view yearStr)
|
||||
{
|
||||
// limit to first 4 digit, accept leading '-'
|
||||
if (yearStr.empty())
|
||||
return std::nullopt;
|
||||
|
||||
int sign;
|
||||
if (yearStr.front() == '-')
|
||||
{
|
||||
sign = -1;
|
||||
yearStr.remove_prefix(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sign = 1;
|
||||
}
|
||||
|
||||
if (yearStr.empty() || !std::isdigit(yearStr.front()))
|
||||
return std::nullopt;
|
||||
|
||||
int result{};
|
||||
for (std::size_t i{}; i < yearStr.size() && i < 4; ++i)
|
||||
{
|
||||
if (!std::isdigit(yearStr[i]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
result = result * 10 + (yearStr[i] - '0');
|
||||
}
|
||||
|
||||
return result * sign;
|
||||
}
|
||||
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
std::string_view role;
|
||||
|
||||
std::size_t roleBegin{};
|
||||
std::size_t roleEnd{};
|
||||
std::size_t count{};
|
||||
|
||||
for (std::size_t i{}; i < entry.size(); ++i)
|
||||
{
|
||||
std::size_t currentIndex{ entry.size() - i - 1 };
|
||||
const char c{ entry[currentIndex] };
|
||||
|
||||
if (std::isspace(c))
|
||||
continue;
|
||||
|
||||
if (c == ')')
|
||||
{
|
||||
if (count++ == 0)
|
||||
roleEnd = currentIndex;
|
||||
}
|
||||
else if (c == '(')
|
||||
{
|
||||
if (count == 0)
|
||||
break;
|
||||
|
||||
if (--count == 0)
|
||||
{
|
||||
roleBegin = currentIndex + 1;
|
||||
role = core::stringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
|
||||
artistName = core::stringUtils::stringTrim(entry.substr(0, currentIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (count == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!roleEnd || !roleBegin)
|
||||
artistName = core::stringUtils::stringTrim(entry);
|
||||
|
||||
return PerformerArtist{ Artist{ artistName }, std::string{ role } };
|
||||
}
|
||||
} // namespace lms::scanner::utils
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner::utils
|
||||
{
|
||||
Wt::WDate parseDate(std::string_view dateStr);
|
||||
std::optional<int> parseYear(std::string_view yearStr);
|
||||
|
||||
struct PerformerArtist
|
||||
{
|
||||
Artist artist;
|
||||
std::string role;
|
||||
};
|
||||
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
} // namespace lms::scanner::utils
|
||||
+15
-20
@@ -22,17 +22,19 @@
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "metadata/Lyrics.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/lyrics/LyricsParser.hpp"
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -48,28 +50,21 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::Lyrics> _parsedLyrics;
|
||||
std::optional<Lyrics> _parsedLyrics;
|
||||
};
|
||||
|
||||
void LyricsFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedLyrics = metadata::parseLyrics(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<LyricsFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedLyrics = parseLyrics(ifs);
|
||||
}
|
||||
|
||||
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
|
||||
@@ -140,7 +135,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedLyricsFileExtensions();
|
||||
return getSupportedLyricsFileExtensions();
|
||||
}
|
||||
|
||||
bool LyricsFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "LyricsParser.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <regex>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 2> fileExtensions{ ".lrc", ".txt" }; // TODO handle ".elrc"
|
||||
return fileExtensions;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// Parse a single line with a tag like [ar: Artist] and set the appropriate fields in the Lyrics object
|
||||
bool parseTag(std::string_view line, Lyrics& lyrics)
|
||||
{
|
||||
if (line.empty())
|
||||
return false;
|
||||
|
||||
if (line.front() != '[' || line.back() != ']') // consider lines are trimmed
|
||||
return false;
|
||||
|
||||
const auto separator{ line.find(':') };
|
||||
if (separator == std::string_view::npos)
|
||||
return false;
|
||||
|
||||
const std::string_view tagType{ core::stringUtils::stringTrim(line.substr(1, separator - 1)) };
|
||||
const std::string_view tagValue{ core::stringUtils::stringTrim(line.substr(separator + 1, line.size() - separator - 2)) };
|
||||
|
||||
if (tagType.empty())
|
||||
return false;
|
||||
|
||||
// check for timestamps
|
||||
if (std::any_of(tagType.begin(), tagType.end(), [](char c) { return std::isdigit(c); }))
|
||||
return false;
|
||||
|
||||
if (tagType == "ar")
|
||||
{
|
||||
lyrics.displayArtist = tagValue;
|
||||
}
|
||||
else if (tagType == "al")
|
||||
{
|
||||
lyrics.displayAlbum = tagValue;
|
||||
}
|
||||
else if (tagType == "ti")
|
||||
{
|
||||
lyrics.displayTitle = tagValue;
|
||||
}
|
||||
else if (tagType == "la")
|
||||
{
|
||||
lyrics.language = tagValue;
|
||||
}
|
||||
else if (tagType == "offset")
|
||||
{
|
||||
if (const auto value{ core::stringUtils::readAs<int>(tagValue) })
|
||||
lyrics.offset = std::chrono::milliseconds{ *value };
|
||||
}
|
||||
// not interrested by other tags like 'duration', 'id', etc.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse timestamps from a line, update the associated times in milliseconds and return the remaining line
|
||||
std::string_view extractTimestamps(std::string_view line, std::vector<std::chrono::milliseconds>& timestamps)
|
||||
{
|
||||
timestamps.clear();
|
||||
static const std::regex timeTagRegex{ R"(\[(?:(\d{1,2}):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?\])" };
|
||||
std::cregex_iterator regexIt(line.begin(), line.end(), timeTagRegex);
|
||||
std::cregex_iterator regexEnd;
|
||||
std::string_view::size_type offset{};
|
||||
|
||||
while (regexIt != regexEnd)
|
||||
{
|
||||
std::cmatch match{ *regexIt };
|
||||
int hour{ match[1].matched ? std::stoi(match[1].str()) : 0 };
|
||||
int minute{ std::stoi(match[2].str()) };
|
||||
int second{ std::stoi(match[3].str()) };
|
||||
int fractional{ match[4].matched ? std::stoi(match[4].str()) : 0 };
|
||||
|
||||
std::chrono::milliseconds currentTimestamp{ std::chrono::hours{ hour } + std::chrono::minutes{ minute } + std::chrono::seconds{ second } };
|
||||
|
||||
if (match[4].length() == 2) // Centiseconds
|
||||
{
|
||||
currentTimestamp += std::chrono::milliseconds{ fractional * 10 };
|
||||
}
|
||||
else // Milliseconds
|
||||
{
|
||||
currentTimestamp += std::chrono::milliseconds{ fractional };
|
||||
}
|
||||
|
||||
offset = match[0].second - line.data();
|
||||
timestamps.push_back(currentTimestamp);
|
||||
++regexIt;
|
||||
}
|
||||
|
||||
return line.substr(offset);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Main function to parse lyrics from an input stream
|
||||
Lyrics parseLyrics(std::istream& is)
|
||||
{
|
||||
Lyrics lyrics;
|
||||
|
||||
enum class State
|
||||
{
|
||||
None,
|
||||
SynchronizedLyrics,
|
||||
UnsynchronizedLyrics,
|
||||
};
|
||||
State currentState{ State::None };
|
||||
|
||||
std::vector<std::chrono::milliseconds> lastTimestamps;
|
||||
std::vector<std::chrono::milliseconds> timestamps;
|
||||
std::string accumulatedLyrics;
|
||||
|
||||
auto applyAccumulatedLyrics = [&](bool skipTrailingEmptyLines = false) {
|
||||
if (lastTimestamps.empty())
|
||||
return;
|
||||
|
||||
if (skipTrailingEmptyLines)
|
||||
accumulatedLyrics.resize(core::stringUtils::stringTrimEnd(accumulatedLyrics, " \t\r\n").size());
|
||||
|
||||
if (accumulatedLyrics.empty())
|
||||
return;
|
||||
|
||||
for (std::chrono::milliseconds timestamp : lastTimestamps)
|
||||
{
|
||||
std::string& synchronizedLine{ lyrics.synchronizedLines.find(timestamp)->second };
|
||||
synchronizedLine += accumulatedLyrics;
|
||||
}
|
||||
accumulatedLyrics.clear();
|
||||
};
|
||||
|
||||
bool firstLine{ true };
|
||||
std::string line;
|
||||
while (std::getline(is, line))
|
||||
{
|
||||
// Remove potential UTF8 BOM
|
||||
if (firstLine)
|
||||
{
|
||||
firstLine = false;
|
||||
constexpr std::string_view utf8BOM{ "\xEF\xBB\xBF" };
|
||||
if (line.starts_with(utf8BOM))
|
||||
line.erase(0, utf8BOM.size());
|
||||
}
|
||||
|
||||
std::string_view trimmedLine{ core::stringUtils::stringTrimEnd(line) };
|
||||
|
||||
// Skip comments
|
||||
if (!trimmedLine.empty() && trimmedLine.front() == '#')
|
||||
continue;
|
||||
|
||||
// Skip empty lines before actual lyrics
|
||||
if (currentState == State::None && trimmedLine.empty())
|
||||
continue;
|
||||
|
||||
if (parseTag(trimmedLine, lyrics))
|
||||
continue;
|
||||
|
||||
const std::string_view lyricsText{ extractTimestamps(trimmedLine, timestamps) };
|
||||
|
||||
// If there are timestamps, add as synchronized lyrics
|
||||
if (!timestamps.empty())
|
||||
{
|
||||
if (currentState == State::UnsynchronizedLyrics)
|
||||
lyrics.unsynchronizedLines.clear(); // choice: discard all lyrics parsed so far
|
||||
|
||||
currentState = State::SynchronizedLyrics;
|
||||
|
||||
applyAccumulatedLyrics();
|
||||
for (std::chrono::milliseconds timestamp : timestamps)
|
||||
{
|
||||
auto itLine{ lyrics.synchronizedLines.find(timestamp) };
|
||||
if (itLine != std::cend(lyrics.synchronizedLines))
|
||||
{
|
||||
itLine->second.push_back('\n');
|
||||
itLine->second.append(lyricsText);
|
||||
}
|
||||
else
|
||||
lyrics.synchronizedLines.emplace(timestamp, lyricsText);
|
||||
}
|
||||
|
||||
lastTimestamps = timestamps;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!lastTimestamps.empty())
|
||||
{
|
||||
accumulatedLyrics += '\n';
|
||||
accumulatedLyrics += trimmedLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(currentState != State::SynchronizedLyrics); // should be handled
|
||||
currentState = State::UnsynchronizedLyrics;
|
||||
|
||||
lyrics.unsynchronizedLines.push_back(std::string{ trimmedLine });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentState == State::SynchronizedLyrics)
|
||||
applyAccumulatedLyrics(true);
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
|
||||
Lyrics parseLyrics(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
+14
-21
@@ -27,12 +27,12 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/PlayList.hpp"
|
||||
|
||||
#include "FileScanOperationBase.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "scanners/FileScanOperationBase.hpp"
|
||||
#include "scanners/Utils.hpp"
|
||||
#include "scanners/playlist/PlayListParser.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -51,28 +51,21 @@ namespace lms::scanner
|
||||
void scan() override;
|
||||
OperationResult processResult() override;
|
||||
|
||||
std::optional<metadata::PlayList> _parsedPlayList;
|
||||
std::optional<PlayList> _parsedPlayList;
|
||||
};
|
||||
|
||||
void PlayListFileScanOperation::scan()
|
||||
{
|
||||
try
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
std::ifstream ifs{ getFilePath() };
|
||||
if (!ifs)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
addError<IOScanError>(getFilePath(), ec);
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedPlayList = metadata::parsePlayList(ifs);
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
addError<PlayListFileScanError>(getFilePath());
|
||||
}
|
||||
_parsedPlayList = parsePlayList(ifs);
|
||||
}
|
||||
|
||||
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
|
||||
@@ -139,7 +132,7 @@ namespace lms::scanner
|
||||
|
||||
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
||||
{
|
||||
return metadata::getSupportedPlayListFileExtensions();
|
||||
return getSupportedPlayListFileExtensions();
|
||||
}
|
||||
|
||||
bool PlayListFileScanner::needsScan(const FileToScan& file) const
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IFileScanner.hpp"
|
||||
#include "scanners/IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PlayListParser.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 2> fileExtensions{ ".m3u", ".m3u8" };
|
||||
return fileExtensions;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Comment
|
||||
{
|
||||
std::string_view directive;
|
||||
std::string_view parameter;
|
||||
};
|
||||
|
||||
std::optional<Comment> parseComment(std::string_view line)
|
||||
{
|
||||
if (line.empty() || line.front() != '#')
|
||||
return std::nullopt;
|
||||
|
||||
Comment comment;
|
||||
const std::string_view::size_type parameterSeparator{ line.find(':') };
|
||||
if (parameterSeparator == std::string_view::npos)
|
||||
{
|
||||
comment.directive = line;
|
||||
}
|
||||
else
|
||||
{
|
||||
comment.directive = line.substr(0, parameterSeparator + 1);
|
||||
comment.parameter = line.substr(parameterSeparator + 1);
|
||||
};
|
||||
|
||||
return comment;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PlayList parsePlayList(std::istream& is)
|
||||
{
|
||||
bool firstLine{ true };
|
||||
PlayList playlist;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(is, line))
|
||||
{
|
||||
// Remove potential UTF8 BOM
|
||||
if (firstLine)
|
||||
{
|
||||
firstLine = false;
|
||||
constexpr std::string_view utf8BOM{ "\xEF\xBB\xBF" };
|
||||
if (line.starts_with(utf8BOM))
|
||||
line.erase(0, utf8BOM.size());
|
||||
}
|
||||
|
||||
const std::string_view trimmedLine{ core::stringUtils::stringTrim(line) };
|
||||
if (trimmedLine.empty())
|
||||
continue;
|
||||
|
||||
// Don't enforce #EXTM3U as first line: be permissive
|
||||
if (const std::optional<Comment> comment{ parseComment(trimmedLine) })
|
||||
{
|
||||
if (comment->directive == "#PLAYLIST:")
|
||||
playlist.name = comment->parameter;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter out URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
|
||||
// Consider an entry with a ':' is actually an url, as filenames are not supposed to have ':' on windows
|
||||
if (trimmedLine.find(':') != std::string_view::npos)
|
||||
continue;
|
||||
|
||||
const std::filesystem::path path{ std::cbegin(trimmedLine), std::cend(trimmedLine) };
|
||||
playlist.files.emplace_back(path.lexically_normal());
|
||||
}
|
||||
|
||||
return playlist;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <span>
|
||||
|
||||
#include "types/PlayList.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
|
||||
PlayList parsePlayList(std::istream& is);
|
||||
} // namespace lms::scanner
|
||||
@@ -30,11 +30,11 @@
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "types/TrackMetadata.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -53,7 +53,7 @@ namespace lms::scanner
|
||||
{
|
||||
assert(!link->isArtistMBIDMatched());
|
||||
|
||||
metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
|
||||
Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
|
||||
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, DEBUG, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast<int>(link->getType()) << " from " << link->getArtist() << " to " << newArtist);
|
||||
@@ -66,7 +66,7 @@ namespace lms::scanner
|
||||
{
|
||||
assert(!artistInfo->isMBIDMatched());
|
||||
|
||||
const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
|
||||
Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist);
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
// See:
|
||||
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
|
||||
// - for the definition of some mb fields: https://musicbrainz.org/doc/Artist
|
||||
struct ArtistInfo
|
||||
{
|
||||
std::string name;
|
||||
std::optional<core::UUID> mbid;
|
||||
std::string sortName; // mb
|
||||
std::string type; // mb
|
||||
std::string gender; // mb
|
||||
std::string disambiguation; // mb
|
||||
std::string biography;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct Lyrics
|
||||
{
|
||||
std::string language;
|
||||
std::chrono::milliseconds offset{};
|
||||
std::string displayArtist;
|
||||
std::string displayAlbum;
|
||||
std::string displayTitle;
|
||||
|
||||
std::map<std::chrono::milliseconds, std::string> synchronizedLines;
|
||||
std::vector<std::string> unsynchronizedLines;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct PlayList
|
||||
{
|
||||
std::string name;
|
||||
std::vector<std::filesystem::path> files;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "types/Lyrics.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::vector<std::string> /* values */>;
|
||||
|
||||
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
|
||||
|
||||
struct Artist
|
||||
{
|
||||
std::optional<core::UUID> mbid;
|
||||
std::string name;
|
||||
std::optional<std::string> sortName;
|
||||
|
||||
Artist(std::string_view _name)
|
||||
: name{ _name } {}
|
||||
Artist(std::optional<core::UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName)
|
||||
: mbid{ std::move(_mbid) }
|
||||
, name{ _name }
|
||||
, sortName{ std::move(_sortName) } {}
|
||||
|
||||
auto operator<=>(const Artist&) const = default;
|
||||
};
|
||||
|
||||
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
||||
|
||||
struct Release
|
||||
{
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> groupMBID;
|
||||
std::string name;
|
||||
std::string sortName;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
std::optional<std::size_t> mediumCount;
|
||||
std::vector<std::string> labels;
|
||||
std::vector<std::string> releaseTypes;
|
||||
bool isCompilation{};
|
||||
std::string barcode;
|
||||
std::string comment;
|
||||
std::vector<std::string> countries;
|
||||
|
||||
auto operator<=>(const Release&) const = default;
|
||||
};
|
||||
|
||||
struct Medium
|
||||
{
|
||||
std::string media; // CD, etc.
|
||||
std::string name;
|
||||
std::optional<Release> release;
|
||||
std::optional<std::size_t> position; // in release
|
||||
std::optional<std::size_t> trackCount;
|
||||
std::optional<float> replayGain;
|
||||
|
||||
auto operator<=>(const Medium&) const = default;
|
||||
|
||||
bool isDefault() const
|
||||
{
|
||||
static const Medium defaultMedium;
|
||||
return *this == defaultMedium;
|
||||
}
|
||||
};
|
||||
|
||||
struct Track
|
||||
{
|
||||
enum class Advisory
|
||||
{
|
||||
Unknown,
|
||||
Explicit,
|
||||
Clean,
|
||||
};
|
||||
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> recordingMBID;
|
||||
std::string title;
|
||||
std::optional<Medium> medium;
|
||||
std::optional<std::size_t> position; // in medium
|
||||
std::vector<std::string> groupings;
|
||||
std::vector<std::string> genres;
|
||||
std::vector<std::string> moods;
|
||||
std::vector<std::string> languages;
|
||||
Tags userExtraTags;
|
||||
core::PartialDateTime date;
|
||||
std::optional<int> originalYear;
|
||||
core::PartialDateTime originalDate;
|
||||
std::optional<Advisory> advisory;
|
||||
core::PartialDateTime encodingTime;
|
||||
std::optional<core::UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::vector<std::string> comments;
|
||||
std::vector<Lyrics> lyrics;
|
||||
std::optional<float> replayGain;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> conductorArtists;
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
PerformerContainer performerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
Reference in New Issue
Block a user