Significantly improved scanner performance (now have a dedicated pool of threads for scanning file metadata)
This commit is contained in:
+4
-1
@@ -33,7 +33,7 @@ approot = "/usr/share/lms/approot";
|
|||||||
# Location for deployment (See README if you want to deploy on a non root path)
|
# Location for deployment (See README if you want to deploy on a non root path)
|
||||||
deploy-path = "/";
|
deploy-path = "/";
|
||||||
|
|
||||||
# Number of threads to be used to dispatch http requests (0 means auto detect)
|
# Number of threads to be used to dispatch http requests (0 means number of logical CPUs)
|
||||||
http-server-thread-count = 0;
|
http-server-thread-count = 0;
|
||||||
|
|
||||||
# ListenBrainz root API
|
# ListenBrainz root API
|
||||||
@@ -98,3 +98,6 @@ scanner-skip-duplicate-mbid = false;
|
|||||||
|
|
||||||
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
|
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
|
||||||
scanner-parser-read-style = "average";
|
scanner-parser-read-style = "average";
|
||||||
|
|
||||||
|
# Number of threads to use for scanning file metadata (0 means number of logical CPUs / 2)
|
||||||
|
scanner-metadata-thread-count = 0;
|
||||||
@@ -215,22 +215,94 @@ namespace Scanner
|
|||||||
|
|
||||||
throw LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
throw LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t getScanMetaDataThreadCount()
|
||||||
|
{
|
||||||
|
std::size_t threadCount{ Service<IConfig>::get()->getULong("scanner-metadata-thread-count", 0) };
|
||||||
|
|
||||||
|
if (threadCount == 0)
|
||||||
|
threadCount = std::max<std::size_t>(std::thread::hardware_concurrency() / 2, 1);
|
||||||
|
|
||||||
|
return threadCount;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
ScanStepScanFiles::MetadataScanQueue::MetadataScanQueue(MetaData::IParser& parser, std::size_t threadCount)
|
||||||
|
: _metadataParser{ parser }
|
||||||
|
, _scanContextRunner{ _scanContext, threadCount }
|
||||||
|
{}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path path)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
_ongoingScanCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scanContext.post([=, this]
|
||||||
|
{
|
||||||
|
std::optional<MetaData::Track> trackMetaData{ _metadataParser.parse(path) };
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
|
||||||
|
_scanResults.emplace_back(std::make_unique<MetaDataScanResult>(path, std::move(trackMetaData)));
|
||||||
|
_ongoingScanCount -= 1;
|
||||||
|
}
|
||||||
|
_condVar.notify_all();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ScanStepScanFiles::MetadataScanQueue::getResultsCount() const
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
return _scanResults.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ScanStepScanFiles::MetadataScanQueue::popResults(std::vector<std::unique_ptr<MetaDataScanResult>>& results, std::size_t maxCount)
|
||||||
|
{
|
||||||
|
results.clear();
|
||||||
|
results.reserve(maxCount);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
|
||||||
|
while (results.size() < maxCount && !_scanResults.empty())
|
||||||
|
{
|
||||||
|
results.push_back(std::move(_scanResults.front()));
|
||||||
|
_scanResults.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::MetadataScanQueue::wait(std::size_t maxScanRequestCount)
|
||||||
|
{
|
||||||
|
std::unique_lock lock{ _mutex };
|
||||||
|
_condVar.wait(lock, [=, this] {return _ongoingScanCount <= maxScanRequestCount;});
|
||||||
|
}
|
||||||
|
|
||||||
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
||||||
: ScanStepBase{ initParams }
|
: ScanStepBase{ initParams }
|
||||||
, _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib
|
, _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib
|
||||||
|
, _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount() }
|
||||||
{
|
{
|
||||||
|
LMS_LOG(DBUPDATER, INFO, "Using " << _metadataScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepScanFiles::process(ScanContext& context)
|
void ScanStepScanFiles::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
|
const std::size_t scanQueueMaxScanRequestCount{ 20 * _metadataScanQueue.getThreadCount() };
|
||||||
|
const std::size_t processMetaDataBatchSize{ 10 };
|
||||||
|
|
||||||
{
|
{
|
||||||
std::vector<std::string> tagsToParse{ _extraTagsToParse };
|
std::vector<std::string> tagsToParse{ _extraTagsToParse };
|
||||||
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
|
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
|
||||||
_metadataParser->setUserExtraTags(tagsToParse);
|
_metadataParser->setUserExtraTags(tagsToParse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<MetaDataScanResult>> scanResults;
|
||||||
context.currentStepStats.totalElems = context.stats.filesScanned;
|
context.currentStepStats.totalElems = context.stats.filesScanned;
|
||||||
|
|
||||||
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
@@ -247,22 +319,32 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
||||||
{
|
{
|
||||||
scanAudioFile(path, context, mediaLibrary);
|
if (checkFileNeedScan(context, path, mediaLibrary))
|
||||||
|
_metadataScanQueue.pushScanRequest(path);
|
||||||
|
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
|
|
||||||
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
|
|
||||||
if ((context.currentStepStats.processedElems % 1'000) == 0)
|
|
||||||
_db.getTLSSession().optimize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (_metadataScanQueue.getResultsCount() > processMetaDataBatchSize)
|
||||||
|
{
|
||||||
|
_metadataScanQueue.popResults(scanResults, processMetaDataBatchSize);
|
||||||
|
processMetaDataScanResults(context, scanResults, mediaLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
_metadataScanQueue.wait(scanQueueMaxScanRequestCount);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, &excludeDirFileName);
|
}, &excludeDirFileName);
|
||||||
|
|
||||||
|
_metadataScanQueue.wait();
|
||||||
|
|
||||||
|
while (_metadataScanQueue.popResults(scanResults, processMetaDataBatchSize) > 0)
|
||||||
|
processMetaDataScanResults(context, scanResults, mediaLibrary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
bool ScanStepScanFiles::checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
ScanStats& stats{ context.stats };
|
||||||
Wt::WDateTime lastWriteTime;
|
Wt::WDateTime lastWriteTime;
|
||||||
@@ -274,7 +356,7 @@ namespace Scanner
|
|||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, e.what());
|
LMS_LOG(DBUPDATER, ERROR, e.what());
|
||||||
stats.skips++;
|
stats.skips++;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needUpdateLibrary{};
|
bool needUpdateLibrary{};
|
||||||
@@ -296,7 +378,7 @@ namespace Scanner
|
|||||||
if (trackMediaLibrary && trackMediaLibrary->getId() == libraryInfo.id)
|
if (trackMediaLibrary && trackMediaLibrary->getId() == libraryInfo.id)
|
||||||
{
|
{
|
||||||
stats.skips++;
|
stats.skips++;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
needUpdateLibrary = true;
|
needUpdateLibrary = true;
|
||||||
@@ -312,26 +394,60 @@ namespace Scanner
|
|||||||
assert(track);
|
assert(track);
|
||||||
track.modify()->setMediaLibrary(Database::MediaLibrary::find(dbSession, libraryInfo.id)); // may be null, will be handled in the next scan anyway
|
track.modify()->setMediaLibrary(Database::MediaLibrary::find(dbSession, libraryInfo.id)); // may be null, will be handled in the next scan anyway
|
||||||
stats.updates++;
|
stats.updates++;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<MetaData::Track> trackInfo{ _metadataParser->parse(file) };
|
return true; // need to scan
|
||||||
if (!trackInfo)
|
}
|
||||||
{
|
|
||||||
context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.scans++;
|
|
||||||
|
|
||||||
|
void ScanStepScanFiles::processMetaDataScanResults(ScanContext& context, std::span<const std::unique_ptr<MetaDataScanResult>> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
||||||
|
{
|
||||||
Database::Session& dbSession{ _db.getTLSSession() };
|
Database::Session& dbSession{ _db.getTLSSession() };
|
||||||
auto transaction{ dbSession.createWriteTransaction() };
|
auto transaction{ dbSession.createWriteTransaction() };
|
||||||
|
|
||||||
|
for (const auto& scanResult : scanResults)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (scanResult->trackMetaData)
|
||||||
|
{
|
||||||
|
context.stats.scans++;
|
||||||
|
|
||||||
|
processFileMetaData(context, scanResult->path, *scanResult->trackMetaData, libraryInfo);
|
||||||
|
|
||||||
|
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
|
||||||
|
if ((context.stats.scans % 1'000) == 0)
|
||||||
|
_db.getTLSSession().optimize();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.stats.errors.emplace_back(scanResult->path, ScanErrorType::CannotParseFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::processFileMetaData(ScanContext& context, const std::filesystem::path& file, const MetaData::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
Wt::WDateTime lastWriteTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lastWriteTime = PathUtils::getLastWriteTime(file);
|
||||||
|
}
|
||||||
|
catch (LmsException& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, e.what());
|
||||||
|
stats.skips++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::Session& dbSession{ _db.getTLSSession() };
|
||||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||||
|
|
||||||
if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID))
|
if (trackMetadata.mbid && (!track || _settings.skipDuplicateMBID))
|
||||||
{
|
{
|
||||||
std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackInfo->mbid) };
|
std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackMetadata.mbid) };
|
||||||
|
|
||||||
// find for existing MBIDs as the file may have just been moved
|
// find for existing MBIDs as the file may have just been moved
|
||||||
if (!track && duplicateTracks.size() == 1)
|
if (!track && duplicateTracks.size() == 1)
|
||||||
@@ -378,7 +494,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We estimate this is an audio file if the duration is not null
|
// We estimate this is an audio file if the duration is not null
|
||||||
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
if (trackMetadata.duration == std::chrono::milliseconds::zero())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
||||||
|
|
||||||
@@ -394,8 +510,8 @@ namespace Scanner
|
|||||||
|
|
||||||
// ***** Title
|
// ***** Title
|
||||||
std::string title;
|
std::string title;
|
||||||
if (!trackInfo->title.empty())
|
if (!trackMetadata.title.empty())
|
||||||
title = trackInfo->title;
|
title = trackMetadata.title;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO parse file name guess track etc.
|
// TODO parse file name guess track etc.
|
||||||
@@ -424,78 +540,78 @@ namespace Scanner
|
|||||||
track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this
|
track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this
|
||||||
track.modify()->clearArtistLinks();
|
track.modify()->clearArtistLinks();
|
||||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||||
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false))
|
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata.artists, false))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
||||||
|
|
||||||
if (trackInfo->medium && trackInfo->medium->release)
|
if (trackMetadata.medium && trackMetadata.medium->release)
|
||||||
{
|
{
|
||||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false))
|
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackMetadata.medium->release->artists, false))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||||
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true))
|
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackMetadata.conductorArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
||||||
|
|
||||||
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true))
|
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackMetadata.composerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
|
||||||
|
|
||||||
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true))
|
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackMetadata.lyricistArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
||||||
|
|
||||||
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true))
|
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackMetadata.mixerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
||||||
|
|
||||||
for (const auto& [role, performers] : trackInfo->performerArtists)
|
for (const auto& [role, performers] : trackMetadata.performerArtists)
|
||||||
{
|
{
|
||||||
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true))
|
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackMetadata.producerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
|
||||||
|
|
||||||
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true))
|
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackMetadata.remixerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
||||||
|
|
||||||
track.modify()->setScanVersion(_settings.scanVersion);
|
track.modify()->setScanVersion(_settings.scanVersion);
|
||||||
if (trackInfo->medium && trackInfo->medium->release)
|
if (trackMetadata.medium && trackMetadata.medium->release)
|
||||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release, file.parent_path()));
|
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata.medium->release, file.parent_path()));
|
||||||
else
|
else
|
||||||
track.modify()->setRelease({});
|
track.modify()->setRelease({});
|
||||||
track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt);
|
track.modify()->setTotalTrack(trackMetadata.medium ? trackMetadata.medium->trackCount : std::nullopt);
|
||||||
track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt);
|
track.modify()->setReleaseReplayGain(trackMetadata.medium ? trackMetadata.medium->replayGain : std::nullopt);
|
||||||
track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : "");
|
track.modify()->setDiscSubtitle(trackMetadata.medium ? trackMetadata.medium->name : "");
|
||||||
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->userExtraTags));
|
track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata.userExtraTags));
|
||||||
track.modify()->setLastWriteTime(lastWriteTime);
|
track.modify()->setLastWriteTime(lastWriteTime);
|
||||||
track.modify()->setName(title);
|
track.modify()->setName(title);
|
||||||
track.modify()->setDuration(trackInfo->duration);
|
track.modify()->setDuration(trackMetadata.duration);
|
||||||
track.modify()->setBitrate(trackInfo->bitrate);
|
track.modify()->setBitrate(trackMetadata.bitrate);
|
||||||
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
||||||
track.modify()->setTrackNumber(trackInfo->position);
|
track.modify()->setTrackNumber(trackMetadata.position);
|
||||||
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
|
track.modify()->setDiscNumber(trackMetadata.medium ? trackMetadata.medium->position : std::nullopt);
|
||||||
track.modify()->setDate(trackInfo->date);
|
track.modify()->setDate(trackMetadata.date);
|
||||||
track.modify()->setYear(trackInfo->year);
|
track.modify()->setYear(trackMetadata.year);
|
||||||
track.modify()->setOriginalDate(trackInfo->originalDate);
|
track.modify()->setOriginalDate(trackMetadata.originalDate);
|
||||||
track.modify()->setOriginalYear(trackInfo->originalYear);
|
track.modify()->setOriginalYear(trackMetadata.originalYear);
|
||||||
|
|
||||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||||
if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid())
|
if (!trackMetadata.date.isValid() && trackMetadata.originalDate.isValid())
|
||||||
track.modify()->setDate(trackInfo->originalDate);
|
track.modify()->setDate(trackMetadata.originalDate);
|
||||||
|
|
||||||
// If a file has an OriginalYear but no Year, set it to ease filtering
|
// If a file has an OriginalYear but no Year, set it to ease filtering
|
||||||
if (!trackInfo->year && trackInfo->originalYear)
|
if (!trackMetadata.year && trackMetadata.originalYear)
|
||||||
track.modify()->setYear(trackInfo->originalYear);
|
track.modify()->setYear(trackMetadata.originalYear);
|
||||||
|
|
||||||
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
track.modify()->setRecordingMBID(trackMetadata.recordingMBID);
|
||||||
track.modify()->setTrackMBID(trackInfo->mbid);
|
track.modify()->setTrackMBID(trackMetadata.mbid);
|
||||||
if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
|
if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
|
||||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||||
track.modify()->setHasCover(trackInfo->hasCover);
|
track.modify()->setHasCover(trackMetadata.hasCover);
|
||||||
track.modify()->setCopyright(trackInfo->copyright);
|
track.modify()->setCopyright(trackMetadata.copyright);
|
||||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
track.modify()->setCopyrightURL(trackMetadata.copyrightURL);
|
||||||
track.modify()->setTrackReplayGain(trackInfo->replayGain);
|
track.modify()->setTrackReplayGain(trackMetadata.replayGain);
|
||||||
track.modify()->setArtistDisplayName(trackInfo->artistDisplayName);
|
track.modify()->setArtistDisplayName(trackMetadata.artistDisplayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <mutex>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
|
#include "utils/IOContextRunner.hpp"
|
||||||
#include "ScanStepBase.hpp"
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
@@ -38,9 +43,44 @@ namespace Scanner
|
|||||||
std::string_view getStepName() const override { return "Scanning files"; }
|
std::string_view getStepName() const override { return "Scanning files"; }
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
void scanAudioFile(const std::filesystem::path& file, ScanContext& context, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
|
struct MetaDataScanResult
|
||||||
|
{
|
||||||
|
std::filesystem::path path;
|
||||||
|
std::optional<MetaData::Track> trackMetaData;
|
||||||
|
};
|
||||||
|
void processMetaDataScanResults(ScanContext& context, std::span<const std::unique_ptr<MetaDataScanResult>> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
|
void processFileMetaData(ScanContext& context, const std::filesystem::path& file, const MetaData::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
|
|
||||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||||
const std::vector<std::string> _extraTagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
|
const std::vector<std::string> _extraTagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
|
||||||
|
|
||||||
|
class MetadataScanQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MetadataScanQueue(MetaData::IParser& parser, std::size_t threadCount);
|
||||||
|
|
||||||
|
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
|
||||||
|
|
||||||
|
void pushScanRequest(const std::filesystem::path path);
|
||||||
|
|
||||||
|
std::size_t getResultsCount() const;
|
||||||
|
size_t popResults(std::vector<std::unique_ptr<MetaDataScanResult>>& results, std::size_t maxCount);
|
||||||
|
|
||||||
|
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
|
||||||
|
|
||||||
|
private:
|
||||||
|
MetaData::IParser& _metadataParser;
|
||||||
|
boost::asio::io_context _scanContext;
|
||||||
|
IOContextRunner _scanContextRunner;
|
||||||
|
|
||||||
|
mutable std::mutex _mutex ;
|
||||||
|
std::size_t _ongoingScanCount{};
|
||||||
|
std::deque<std::unique_ptr<MetaDataScanResult>> _scanResults;
|
||||||
|
std::condition_variable _condVar;
|
||||||
|
};
|
||||||
|
MetadataScanQueue _metadataScanQueue;
|
||||||
|
|
||||||
|
std::deque<std::unique_ptr<MetaDataScanResult>> _metaDataScanResults;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,40 +24,45 @@
|
|||||||
#include "utils/ILogger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount)
|
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount)
|
||||||
: _ioService {ioService}
|
: _ioService{ ioService }
|
||||||
, _work {ioService}
|
, _work{ ioService }
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
||||||
for (std::size_t i {}; i < threadCount; ++i)
|
for (std::size_t i{}; i < threadCount; ++i)
|
||||||
{
|
{
|
||||||
_threads.emplace_back([&]
|
_threads.emplace_back([&]
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ioService.run();
|
_ioService.run();
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, FATAL, "Exception caught in IO context: " << e.what());
|
LMS_LOG(UTILS, FATAL, "Exception caught in IO context: " << e.what());
|
||||||
std::abort();
|
std::abort();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
IOContextRunner::stop()
|
IOContextRunner::stop()
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
||||||
_work.reset();
|
_work.reset();
|
||||||
_ioService.stop();
|
_ioService.stop();
|
||||||
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t IOContextRunner::getThreadCount() const
|
||||||
|
{
|
||||||
|
return _threads.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
IOContextRunner::~IOContextRunner()
|
IOContextRunner::~IOContextRunner()
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
for (std::thread& t : _threads)
|
for (std::thread& t : _threads)
|
||||||
t.join();
|
t.join();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,18 @@
|
|||||||
|
|
||||||
class IOContextRunner
|
class IOContextRunner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount);
|
IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount);
|
||||||
~IOContextRunner();
|
~IOContextRunner();
|
||||||
|
|
||||||
IOContextRunner(const IOContextRunner&) = delete;
|
void stop();
|
||||||
IOContextRunner(IOContextRunner&&) = delete;
|
std::size_t getThreadCount() const;
|
||||||
IOContextRunner& operator=(const IOContextRunner&) = delete;
|
|
||||||
IOContextRunner& operator=(IOContextRunner&&) = delete;
|
|
||||||
|
|
||||||
void stop();
|
private:
|
||||||
|
IOContextRunner(const IOContextRunner&) = delete;
|
||||||
|
IOContextRunner& operator=(const IOContextRunner&) = delete;
|
||||||
|
|
||||||
private:
|
boost::asio::io_service& _ioService;
|
||||||
boost::asio::io_service& _ioService;
|
std::optional<boost::asio::io_service::work> _work;
|
||||||
std::optional<boost::asio::io_service::work> _work;
|
std::vector<std::thread> _threads;
|
||||||
std::vector<std::thread> _threads;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user