diff --git a/approot/messages.xml b/approot/messages.xml
index dad967c1..2bbb4907 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -59,7 +59,7 @@
Cannot read file
-Force scan now
+Force full rescan now
Get report
Last scan
Not available
@@ -73,9 +73,10 @@
Not scheduled
Scheduled on {1}
Scanning: step {1}/{2}
-Checking for missing files... {1}%
+Checking files... {1}%
Discovering files: {1} files
Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...
+Reloading similarity engine: {1}%...
Scanning files: {1}/{2} files ({3}%)...
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 955938ae..0b66924a 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -59,7 +59,7 @@
Impossible de lire le fichier
-Lancer un scan forcé
+Forcer un rescan complet
Rapport
Dernier scan
Non disponible
@@ -73,10 +73,11 @@
Non planifié
Planifié le {1}
En cours de scan : étape {1}/{2}
-Vérification des fichiers supprimés... {1}%
-Découverte des fichiers: {1} fichiers
-Récupération des métadonnées AcousticBrainz: {1}/{2} fichiers ({3}%)...
-Scan des fichiers: {1}/{2} fichiers ({3}%)...
+Vérification des fichiers... {1}%
+Découverte des fichiers : {1} fichiers
+Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...
+Rechargement du moteur de recommandation : {1}%...
+Scan des fichiers : {1}/{2} fichiers ({3}%)...
Ajouter
diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt
index 35243f50..b33e36c3 100644
--- a/src/libs/recommendation/CMakeLists.txt
+++ b/src/libs/recommendation/CMakeLists.txt
@@ -22,5 +22,7 @@ target_link_libraries(lmsrecommendation PRIVATE
wt
)
+target_include_directories(lmsrecommendation PRIVATE impl)
+
install(TARGETS lmsrecommendation DESTINATION lib)
diff --git a/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp b/src/libs/recommendation/impl/ClustersClassifierCreator.hpp
similarity index 100%
rename from src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp
rename to src/libs/recommendation/impl/ClustersClassifierCreator.hpp
diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp
index 26caf31b..397a7cda 100644
--- a/src/libs/recommendation/impl/Engine.cpp
+++ b/src/libs/recommendation/impl/Engine.cpp
@@ -19,16 +19,39 @@
#include "Engine.hpp"
-#include "recommendation/ClustersClassifierCreator.hpp"
-#include "recommendation/FeaturesClassifierCreator.hpp"
+#include
+#include
+#include "ClustersClassifierCreator.hpp"
+#include "FeaturesClassifierCreator.hpp"
+
+#include "database/Db.hpp"
+#include "database/Session.hpp"
#include "database/ScanSettings.hpp"
-#include "database/TrackList.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Recommendation {
+
+static
+std::unique_ptr
+createClassifier(ClassifierType type)
+{
+ switch (type)
+ {
+ case ClassifierType::Clusters:
+ return createClustersClassifier();
+ break;
+
+ case ClassifierType::Features:
+ return createFeaturesClassifier();
+ break;
+ }
+
+ return {};
+}
+
std::unique_ptr
createEngine(Database::Db& db)
{
@@ -36,65 +59,16 @@ createEngine(Database::Db& db)
}
Engine::Engine(Database::Db& db)
-: _dbSession {db}
+: _db {db}
{
- start();
}
-Engine::~Engine()
-{
- stop();
-}
-
-void
-Engine::start()
-{
- assert(!_running);
- _running = true;
- _ioService.start();
-}
-
-void
-Engine::stop()
-{
- assert(_running);
- _running = false;
-
- cancelPendingClassifiers();
-
- _ioService.stop();
-}
-
-void
-Engine::requestLoad()
-{
- requestReloadInternal(false);
-}
-
-void
-Engine::requestReload()
-{
- requestReloadInternal(true);
-}
-
-void
-Engine::requestReloadInternal(bool databaseChanged)
-{
- LMS_LOG(RECOMMENDATION, DEBUG) << "Reload requested...";
-
- _ioService.post([=]()
- {
- reload(databaseChanged);
- });
-}
-
-std::vector
+std::unordered_set
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount)
{
+ std::unordered_set res;
+
std::shared_lock lock {_classifiersMutex};
-
- std::vector res;
-
for (const auto& classifierName : _classifierPriorities)
{
auto itClassifier {_classifiers.find(classifierName)};
@@ -109,23 +83,23 @@ Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdTy
return res;
}
-std::vector
+std::unordered_set
Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount)
{
+ std::unordered_set res;
+
std::shared_lock lock {_classifiersMutex};
-
- std::vector res;
-
- for (const auto& classifierName : _classifierPriorities)
+ for (ClassifierType classifierType : _classifierPriorities)
{
- auto itClassifier {_classifiers.find(classifierName)};
+ auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
- res = itClassifier->second->getSimilarTracks(dbSession, trackIds, maxCount);
+ const IClassifier& classifier {*itClassifier->second};
+ res = classifier.getSimilarTracks(dbSession, trackIds, maxCount);
if (!res.empty())
{
- LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifierName << "'";
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'";
break;
}
}
@@ -133,23 +107,23 @@ Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<
return res;
}
-std::vector
+std::unordered_set
Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount)
{
+ std::unordered_set res;
+
std::shared_lock lock {_classifiersMutex};
-
- std::vector res;
-
- for (const auto& classifierName : _classifierPriorities)
+ for (ClassifierType classifierType : _classifierPriorities)
{
- auto itClassifier {_classifiers.find(classifierName)};
+ auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
- res = itClassifier->second->getSimilarReleases(dbSession, releaseId, maxCount);
+ const IClassifier& classifier {*itClassifier->second};
+ res = classifier.getSimilarReleases(dbSession, releaseId, maxCount);
if (!res.empty())
{
- LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifierName << "'";
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifier.getName() << "'";
break;
}
}
@@ -157,23 +131,23 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
return res;
}
-std::vector
+std::unordered_set
Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount)
{
+ std::unordered_set res;
+
std::shared_lock lock {_classifiersMutex};
-
- std::vector res;
-
- for (const auto& classifierName : _classifierPriorities)
+ for (ClassifierType classifierType : _classifierPriorities)
{
- auto itClassifier {_classifiers.find(classifierName)};
+ auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
- res = itClassifier->second->getSimilarArtists(dbSession, artistId, maxCount);
+ const IClassifier& classifier {*itClassifier->second};
+ res = classifier.getSimilarArtists(dbSession, artistId, maxCount);
if (!res.empty())
{
- LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifierName << "'";
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'";
return res;
}
}
@@ -181,107 +155,132 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI
return res;
}
-void
-Engine::reload(bool databaseChanged)
+static
+Database::ScanSettings::RecommendationEngineType
+getRecommendationEngineType(Database::Session& session)
{
- using namespace Database;
+ auto transaction {session.createSharedTransaction()};
- LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
-
- const ScanSettings::RecommendationEngineType engineType {[&]()
- {
- auto transaction {_dbSession.createSharedTransaction()};
-
- return ScanSettings::get(_dbSession)->getRecommendationEngineType();
- }()};
-
- clearClassifiers();
-
- switch (engineType)
- {
- case ScanSettings::RecommendationEngineType::Features:
- {
- auto clustersClassifier {createClustersClassifier()};
- auto featuresClassifier {createFeaturesClassifier()};
-
- setClassifierPriorities({featuresClassifier->getName(), clustersClassifier->getName()});
-
- initAndAddClassifier(std::move(clustersClassifier), databaseChanged); // init first since faster
- initAndAddClassifier(std::move(featuresClassifier), databaseChanged);
- break;
- }
-
- case ScanSettings::RecommendationEngineType::Clusters:
- auto clustersClassifier {createClustersClassifier()};
-
- setClassifierPriorities({clustersClassifier->getName()});
-
- initAndAddClassifier(std::move(clustersClassifier), databaseChanged);
- break;
- }
-
- LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines reloaded!";
-
- _sigReloaded.emit();
+ return Database::ScanSettings::get(session)->getRecommendationEngineType();
}
void
-Engine::setClassifierPriorities(std::initializer_list classifierPriorities)
+Engine::load(bool forceReload, const ProgressCallback& progressCallback)
+{
+ using namespace Database;
+
+ static const std::unordered_map> classifierMappings
+ {
+ {ScanSettings::RecommendationEngineType::Features, {ClassifierType::Clusters, ClassifierType::Features}},
+ {ScanSettings::RecommendationEngineType::Clusters, {ClassifierType::Clusters}},
+ };
+
+ LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
+
+ const ScanSettings::RecommendationEngineType engineType {getRecommendationEngineType(_db.getTLSSession())};
+
+ assert(_pendingClassifiers.empty());
+ clearClassifiers();
+
+ auto itClassifierTypes {classifierMappings.find(engineType)};
+ assert(itClassifierTypes != std::cend(classifierMappings));
+ const std::vector& classifierTypes {itClassifierTypes->second};
+
+ setClassifierPriorities(classifierTypes);
+
+ std::vector> classifiers;
+ for (ClassifierType type : classifierTypes)
+ classifiers.emplace_back(createClassifier(type));
+
+ {
+ std::scoped_lock lock {_controlMutex};
+
+ std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)),
+ [](auto& classifier) { return classifier.get(); });
+ }
+
+ for (std::size_t i {}; i < classifiers.size(); ++i)
+ loadClassifier(std::move(classifiers[i]), classifierTypes[i], forceReload, progressCallback);
+
+ LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!";
+}
+
+void
+Engine::setClassifierPriorities(const std::vector& classifierPriorities)
{
std::unique_lock lock {_classifiersMutex};
- _classifierPriorities.clear();
- std::transform(std::cbegin(classifierPriorities), std::cend(classifierPriorities), std::back_inserter(_classifierPriorities), [](std::string_view name) { return std::string {name}; });
+ _classifierPriorities = classifierPriorities;
}
void
Engine::clearClassifiers()
{
- std::unique_lock lock {_classifiersMutex};
+ std::unique_lock lock {_classifiersMutex};
_classifiers.clear();
}
void
-Engine::initAndAddClassifier(std::unique_ptr classifier, bool databaseChanged)
+Engine::loadClassifier(std::unique_ptr classifier,
+ ClassifierType classifierType,
+ bool forceReload,
+ const ProgressCallback& progressCallback)
{
- PendingClassifierHandler pendingClassifier {*this, *classifier.get()};
+ IClassifier* rawClassifier {classifier.get()};
- LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'...";
- bool res {classifier->init(_dbSession, databaseChanged)};
- LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE");
+ bool res {};
+ if (!_loadCancelled)
+ {
+ LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'...";
+
+ auto progress {[&](IClassifier::Progress progress)
+ {
+ progressCallback(Progress {progress.processedElems, progress.totalElems});
+ }};
+
+ res = classifier->load(_db.getTLSSession(), forceReload, progressCallback ? progress : IClassifier::ProgressCallback {});
+
+ LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE");
+ }
if (res)
{
- std::unique_lock lock {_classifiersMutex};
+ std::unique_lock lock {_classifiersMutex};
- _classifiers.emplace(classifier->getName(), std::move(classifier));
+ _classifiers.emplace(classifierType, std::move(classifier));
}
+
+ {
+ std::scoped_lock lock {_controlMutex};
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "About to erase. _pendingClassifiers size = " << _pendingClassifiers.size();
+ _pendingClassifiers.erase(rawClassifier);
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Erased. _pendingClassifiers size = " << _pendingClassifiers.size();
+ }
+
+ _pendingClassifiersCondvar.notify_one();
+
}
void
-Engine::cancelPendingClassifiers()
+Engine::cancelLoad()
{
- std::unique_lock lock {_classifiersMutex};
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading...";
+
+ std::unique_lock lock {_controlMutex};
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingClassifiers.size() << " pending classifiers!";
+
+ _loadCancelled = true;
for (IClassifier* classifier : _pendingClassifiers)
- classifier->requestCancelInit();
-}
+ classifier->requestCancelLoad();
-void
-Engine::addPendingClassifier(IClassifier& classifier)
-{
- std::unique_lock lock {_classifiersMutex};
+ _pendingClassifiersCondvar.wait(lock, [this] {return _pendingClassifiers.empty();});
+ _loadCancelled = false;
- _pendingClassifiers.insert(&classifier);
-}
-
-void
-Engine::removePendingClassifier(IClassifier& classifier)
-{
- std::unique_lock lock {_classifiersMutex};
-
- _pendingClassifiers.erase(&classifier);
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE";
}
} // ns Similarity
diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp
index fd2f91fb..dc516863 100644
--- a/src/libs/recommendation/impl/Engine.hpp
+++ b/src/libs/recommendation/impl/Engine.hpp
@@ -19,23 +19,33 @@
#pragma once
-#include