Finished clean on recommendation engines

This commit is contained in:
emeric
2021-09-30 19:02:49 +02:00
parent 4e9cf0d77f
commit 6baf0d313d
29 changed files with 648 additions and 634 deletions
+7
View File
@@ -66,6 +66,13 @@ Artist::getById(Session& session, ArtistId id)
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
}
bool
Artist::exists(Session& session, ArtistId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
}
Artist::pointer
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
+7
View File
@@ -114,6 +114,13 @@ Release::getById(Session& session, ReleaseId id)
.resultValue();
}
bool
Release::exists(Session& session, ReleaseId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
}
Release::pointer
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
+8
View File
@@ -156,6 +156,14 @@ Track::getById(Session& session, TrackId id)
.resultValue();
}
bool
Track::exists(Session& session, TrackId id)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
}
std::vector<Track::pointer>
Track::getByRecordingMBID(Session& session, const UUID& mbid)
{
@@ -58,8 +58,9 @@ class Artist : public Object<Artist, ArtistId>
// Accessors
static pointer getByMBID(Session& session, const UUID& MBID);
static pointer getById(Session& session, ArtistId id);
static bool exists(Session& session, ArtistId id);
static std::vector<pointer> getByName(Session& session, const std::string& name); // exact match on name field
static std::vector<pointer> getByClusters(Session& session,
static std::vector<pointer> getByClusters(Session& session,
const std::vector<ClusterId>& clusters, // at least one track that belongs to these clusters
SortMethod sortMethod
);
@@ -50,6 +50,7 @@ class Release : public Object<Release, ReleaseId>
static pointer getByMBID(Session& session, const UUID& MBID);
static std::vector<pointer> getByName(Session& session, const std::string& name);
static pointer getById(Session& session, ReleaseId id);
static bool exists(Session& session, ReleaseId id);
static std::vector<pointer> getAllOrphans(Session& session); // no track related
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
static std::vector<ReleaseId> getAllIds(Session& session);
+4 -3
View File
@@ -56,9 +56,10 @@ class Track : public Object<Track, TrackId>
Track(const std::filesystem::path& p);
// Find utility functions
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, TrackId id);
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, TrackId id);
static bool exists(Session& session, TrackId id);
static std::vector<pointer> getByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> getSimilarTracks(Session& session,
const std::vector<TrackId>& trackIds,
+9
View File
@@ -23,6 +23,13 @@ using namespace Database;
TEST_F(DatabaseFixture, SingleArtist)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(Artist::exists(session, 35));
EXPECT_FALSE(Artist::exists(session, 0));
EXPECT_FALSE(Artist::exists(session, 1));
}
ScopedArtist artist {session, "MyArtist"};
{
@@ -31,6 +38,8 @@ TEST_F(DatabaseFixture, SingleArtist)
EXPECT_TRUE(artist.get());
EXPECT_FALSE(!artist.get());
EXPECT_EQ(artist.get()->getId(), artist.getId());
EXPECT_TRUE(Artist::exists(session, artist.getId()));
}
{
+9
View File
@@ -23,11 +23,20 @@ using namespace Database;
TEST_F(DatabaseFixture, SingleRelease)
{
{
auto transaction {session.createSharedTransaction()};
EXPECT_FALSE(Release::exists(session, 0));
EXPECT_FALSE(Release::exists(session, 1));
}
ScopedRelease release {session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_TRUE(Release::exists(session, release.getId()));
auto releases {Release::getAllOrphans(session)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId());
+5
View File
@@ -28,6 +28,7 @@ TEST_F(DatabaseFixture, SingleTrack)
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(Track::getCount(session), 0);
EXPECT_FALSE(Track::exists(session, 0));
}
ScopedTrack track {session, "MyTrackFile"};
@@ -37,6 +38,10 @@ TEST_F(DatabaseFixture, SingleTrack)
EXPECT_EQ(Track::getAll(session).size(), 1);
EXPECT_EQ(Track::getCount(session), 1);
EXPECT_TRUE(Track::exists(session, track.getId()));
auto myTrack {Track::getById(session, track.getId())};
ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId());
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
add_library(lmsrecommendation SHARED
impl/clusters/ClustersClassifier.cpp
impl/clusters/ClustersEngine.cpp
impl/features/FeaturesEngineCache.cpp
impl/features/FeaturesEngine.cpp
impl/features/FeaturesDefs.cpp
@@ -21,10 +21,14 @@
#include <memory>
namespace Recommendation
namespace Database
{
class IClassifier;
std::unique_ptr<IClassifier> createClustersClassifier();
class Db;
}
namespace Recommendation
{
class IEngine;
std::unique_ptr<IEngine> createClustersEngine(Database::Db& db);
}
+216 -247
View File
@@ -22,7 +22,7 @@
#include <unordered_map>
#include <vector>
#include "ClustersClassifierCreator.hpp"
#include "ClustersEngineCreator.hpp"
#include "FeaturesEngineCreator.hpp"
#include "database/Db.hpp"
@@ -31,260 +31,229 @@
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Recommendation {
static
std::unique_ptr<IClassifier>
createClassifier(ClassifierType type)
namespace Recommendation
{
switch (type)
static
std::string_view
engineTypeToString(EngineType engineType)
{
case ClassifierType::Clusters:
return createClustersClassifier();
break;
case ClassifierType::Features:
return createFeaturesEngine();
break;
}
return {};
}
std::unique_ptr<IEngine>
createEngine(Database::Db& db)
{
return std::make_unique<Engine>(db);
}
Engine::Engine(Database::Db& db)
: _db {db}
{
}
Engine::TrackContainer
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount)
{
TrackContainer res;
std::shared_lock lock {_classifiersMutex};
for (const auto& classifierName : _classifierPriorities)
{
auto itClassifier {_classifiers.find(classifierName)};
if (itClassifier == std::cend(_classifiers))
continue;
res = itClassifier->second->getSimilarTracksFromTrackList(session, trackListId, maxCount);
if (!res.empty())
break;
}
return res;
}
Engine::TrackContainer
Engine::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount)
{
TrackContainer res;
std::shared_lock lock {_classifiersMutex};
for (ClassifierType classifierType : _classifierPriorities)
{
auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
const IClassifier& classifier {*itClassifier->second};
res = classifier.getSimilarTracks(dbSession, trackIds, maxCount);
if (!res.empty())
switch (engineType)
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'";
break;
case EngineType::Clusters: return "clusters";
case EngineType::Features: return "features";
}
throw LmsException {"Internal error"};
}
std::unique_ptr<IEngine>
createEngine(Database::Db& db)
{
return std::make_unique<Engine>(db);
}
Engine::Engine(Database::Db& db)
: _db {db}
{
}
Engine::TrackContainer
Engine::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
{
TrackContainer res;
std::shared_lock lock {_enginesMutex};
for (const auto& engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
res = itEngine->second->getSimilarTracksFromTrackList(trackListId, maxCount);
if (!res.empty())
break;
}
return res;
}
Engine::TrackContainer
Engine::getSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
{
TrackContainer res;
std::shared_lock lock {_enginesMutex};
for (EngineType engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
const IEngine& engine {*itEngine->second};
res = engine.getSimilarTracks(trackIds, maxCount);
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'";
break;
}
}
return res;
}
Engine::ReleaseContainer
Engine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
{
ReleaseContainer res;
std::shared_lock lock {_enginesMutex};
for (EngineType engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
const IEngine& engine {*itEngine->second};
res = engine.getSimilarReleases(releaseId, maxCount);
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using engine '" << engineTypeToString(engineType) << "'";
break;
}
LMS_LOG(RECOMMENDATION, DEBUG) << "No result using engine '" << engineTypeToString(engineType) << "'";
}
return res;
}
Engine::ArtistContainer
Engine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
ArtistContainer res;
std::shared_lock lock {_enginesMutex};
for (EngineType engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
const IEngine& engine {*itEngine->second};
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using engine '" << engineTypeToString(engineType) << "'";
return res;
}
}
return res;
}
static
Database::ScanSettings::RecommendationEngineType
getRecommendationEngineType(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
return Database::ScanSettings::get(session)->getRecommendationEngineType();
}
void
Engine::load(bool forceReload, const ProgressCallback& progressCallback)
{
using namespace Database;
LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
EngineContainer enginesToLoad;
{
std::unique_lock controlLock {_controlMutex};
{
std::unique_lock lock {_enginesMutex};
_engines.clear();
}
switch (getRecommendationEngineType(_db.getTLSSession()))
{
case ScanSettings::RecommendationEngineType::Clusters:
_enginePriorities = {EngineType::Clusters};
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
break;
case ScanSettings::RecommendationEngineType::Features:
_enginePriorities = {EngineType::Features, EngineType::Clusters};
// not same order since clusters is faster to load
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db));
break;
}
assert(_pendingEngines.empty());
for (auto& [engineType, engine] : enginesToLoad)
_pendingEngines.push_back(engine.get());
}
for (auto& [engineType, engine] : enginesToLoad)
loadPendingEngine(engineType, std::move(engine), forceReload, progressCallback);
_pendingEnginesCondvar.notify_all();
LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!";
}
void
Engine::loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback)
{
if (!_loadCancelled)
{
LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "'...";
auto progress {[&](const IEngine::Progress& progress)
{
progressCallback(progress);
}};
engine->load(forceReload, progressCallback ? progress : IEngine::ProgressCallback {});
{
std::scoped_lock lock {_controlMutex};
_pendingEngines.erase(std::find(std::begin(_pendingEngines), std::end(_pendingEngines), engine.get()));
}
LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "': " << (_loadCancelled ? "aborted" : "complete");
}
if (!_loadCancelled)
{
std::unique_lock lock {_enginesMutex};
_engines.emplace(engineType, std::move(engine));
}
}
return res;
}
Engine::ReleaseContainer
Engine::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount)
{
ReleaseContainer res;
std::shared_lock lock {_classifiersMutex};
for (ClassifierType classifierType : _classifierPriorities)
void
Engine::cancelLoad()
{
auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading...";
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 '" << classifier.getName() << "'";
break;
}
std::unique_lock controlLock {_controlMutex};
assert(!_loadCancelled);
_loadCancelled = true;
LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingEngines.size() << " pending engines!";
for (IEngine* engine : _pendingEngines)
engine->requestCancelLoad();
_pendingEnginesCondvar.wait(controlLock, [this] {return _pendingEngines.empty();});
_loadCancelled = false;
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE";
}
return res;
}
Engine::ArtistContainer
Engine::getSimilarArtists(Database::Session& dbSession, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount)
{
ArtistContainer res;
std::shared_lock lock {_classifiersMutex};
for (ClassifierType classifierType : _classifierPriorities)
{
auto itClassifier {_classifiers.find(classifierType)};
if (itClassifier == std::cend(_classifiers))
continue;
const IClassifier& classifier {*itClassifier->second};
res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount);
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'";
return res;
}
}
return res;
}
static
Database::ScanSettings::RecommendationEngineType
getRecommendationEngineType(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
return Database::ScanSettings::get(session)->getRecommendationEngineType();
}
void
Engine::load(bool forceReload, const ProgressCallback& progressCallback)
{
using namespace Database;
LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
struct ClassifierWithType
{
ClassifierType type;
std::unique_ptr<IClassifier> classifier;
};
std::vector<ClassifierWithType> classifiers;
auto addClassifier {[&](ClassifierType type)
{
classifiers.emplace_back(ClassifierWithType {type, createClassifier(type)});
}};
switch (getRecommendationEngineType(_db.getTLSSession()))
{
case ScanSettings::RecommendationEngineType::Clusters:
setClassifierPriorities({ClassifierType::Clusters});
addClassifier(ClassifierType::Clusters);
break;
case ScanSettings::RecommendationEngineType::Features:
setClassifierPriorities({ClassifierType::Features, ClassifierType::Clusters});
// not same order since clusters is faster to load
addClassifier(ClassifierType::Clusters);
addClassifier(ClassifierType::Features);
break;
}
assert(_pendingClassifiers.empty());
clearClassifiers();
{
std::scoped_lock lock {_controlMutex};
std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)),
[](auto& classifier) { return classifier.classifier.get(); });
}
for (ClassifierWithType& classifier : classifiers)
loadClassifier(std::move(classifier.classifier), classifier.type, forceReload, progressCallback);
LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!";
}
void
Engine::setClassifierPriorities(const std::vector<ClassifierType>& classifierPriorities)
{
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
_classifierPriorities = classifierPriorities;
}
void
Engine::clearClassifiers()
{
std::unique_lock lock {_classifiersMutex};
_classifiers.clear();
}
void
Engine::loadClassifier(std::unique_ptr<IClassifier> classifier,
ClassifierType classifierType,
bool forceReload,
const ProgressCallback& progressCallback)
{
IClassifier* rawClassifier {classifier.get()};
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};
_classifiers.emplace(classifierType, std::move(classifier));
}
{
std::scoped_lock lock {_controlMutex};
_pendingClassifiers.erase(rawClassifier);
}
_pendingClassifiersCondvar.notify_one();
}
void
Engine::cancelLoad()
{
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->requestCancelLoad();
_pendingClassifiersCondvar.wait(lock, [this] {return _pendingClassifiers.empty();});
_loadCancelled = false;
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE";
}
} // ns Similarity
+20 -21
View File
@@ -20,13 +20,12 @@
#pragma once
#include <condition_variable>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "recommendation/IEngine.hpp"
#include "IClassifier.hpp"
namespace Database
{
@@ -35,7 +34,7 @@ namespace Database
namespace Recommendation
{
enum class ClassifierType
enum class EngineType
{
Clusters,
Features,
@@ -57,29 +56,29 @@ namespace Recommendation
void cancelLoad() override;
void requestCancelLoad() override {};
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override;
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) override;
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) override;
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) override;
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
void clearClassifiers();
void loadClassifier(std::unique_ptr<IClassifier> classifier, ClassifierType classifierType, bool forceReload, const ProgressCallback& progressCallback);
void setEnginePriorities(const std::vector<EngineType>& engineTypes);
void clearEngines();
void loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback);
Database::Db& _db;
std::mutex _controlMutex;
bool _loadCancelled {};
std::condition_variable _pendingClassifiersCondvar;
std::unordered_set<IClassifier*> _pendingClassifiers;
std::mutex _controlMutex;
bool _loadCancelled {};
std::shared_mutex _classifiersMutex;
using ClassifierContainer = std::unordered_map<ClassifierType, std::unique_ptr<IClassifier>>;
ClassifierContainer _classifiers;
std::vector<ClassifierType> _classifierPriorities; // ordered by priority
using EngineContainer = std::unordered_map<EngineType, std::unique_ptr<IEngine>>;
EngineContainer _engines;
mutable std::shared_mutex _enginesMutex;
std::vector<IEngine*> _pendingEngines;
std::shared_mutex _pendingEnginesMutex;
std::condition_variable _pendingEnginesCondvar;
std::vector<EngineType> _enginePriorities; // ordered by priority
};
} // ns Recommendation
@@ -20,10 +20,15 @@
#pragma once
#include <memory>
#include "IClassifier.hpp"
#include "recommendation/IEngine.hpp"
namespace Database
{
class Db;
}
namespace Recommendation
{
std::unique_ptr<IClassifier> createFeaturesEngine();
std::unique_ptr<IEngine> createFeaturesEngine(Database::Db& db);
}
@@ -1,65 +0,0 @@
/*
* Copyright (C) 2020 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 <functional>
#include <string_view>
#include <vector>
#include "database/Types.hpp"
#include "recommendation/IRecommendation.hpp"
#include "utils/EnumSet.hpp"
namespace Database
{
class Session;
}
namespace Recommendation
{
class IClassifier : public IRecommendation
{
public:
virtual ~IClassifier() = default;
virtual std::string_view getName() const = 0;
struct Progress
{
std::size_t totalElems {};
std::size_t processedElems {};
};
using ProgressCallback = std::function<void(const Progress&)>;
virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0;
virtual void requestCancelLoad() = 0;
template <typename IdType>
using ResultContainer = std::vector<IdType>;
virtual ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
} // ns Recommendation
@@ -1,103 +0,0 @@
/*
* 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/>.
*/
#include "ClustersClassifier.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackList.hpp"
namespace Recommendation {
std::unique_ptr<IClassifier> createClustersClassifier()
{
return std::make_unique<ClusterClassifier>();
}
IClassifier::ResultContainer<Database::TrackId>
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
{
ResultContainer<Database::TrackId> res;
auto transaction {dbSession.createSharedTransaction()};
const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)};
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
return res;
}
IClassifier::ResultContainer<Database::TrackId>
ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const
{
ResultContainer<Database::TrackId> res;
auto transaction {session.createSharedTransaction()};
const Database::TrackList::pointer trackList {Database::TrackList::getById(session, tracklistId)};
if (!trackList)
return res;
const auto tracks {trackList->getSimilarTracks(0, maxCount)};
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
return res;
}
IClassifier::ResultContainer<Database::ReleaseId>
ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) const
{
ResultContainer<Database::ReleaseId> res;
auto transaction {dbSession.createSharedTransaction()};
auto release {Database::Release::getById(dbSession, releaseId)};
if (!release)
return res;
const auto releases {release->getSimilarReleases(0, maxCount)};
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
return res;
}
IClassifier::ResultContainer<Database::ArtistId>
ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> artistLinkTypes,
std::size_t maxCount) const
{
ResultContainer<Database::ArtistId> res;
auto transaction {dbSession.createSharedTransaction()};
auto artist {Database::Artist::getById(dbSession, artistId)};
if (!artist)
return res;
const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); });
return res;
}
} // namespace Recommendation
@@ -1,53 +0,0 @@
/*
* 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 "IClassifier.hpp"
namespace Recommendation
{
class ClusterClassifier : public IClassifier
{
public:
ClusterClassifier() = default;
ClusterClassifier(const ClusterClassifier&) = delete;
ClusterClassifier(ClusterClassifier&&) = delete;
ClusterClassifier& operator=(const ClusterClassifier&) = delete;
ClusterClassifier& operator=(ClusterClassifier&&) = delete;
private:
std::string_view getName() const override { return "Clusters"; }
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
void requestCancelLoad() override {}
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override;
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const override;
};
} // namespace Recommendation
@@ -0,0 +1,119 @@
/*
* 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/>.
*/
#include "ClustersEngine.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackList.hpp"
namespace Recommendation {
std::unique_ptr<IEngine> createClustersEngine(Database::Db& db)
{
return std::make_unique<ClusterEngine>(db);
}
IEngine::TrackContainer
ClusterEngine::getSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
{
Database::Session& dbSession {_db.getTLSSession()};
TrackContainer res;
{
auto transaction {dbSession.createSharedTransaction()};
const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)};
res.reserve(tracks.size());
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
}
return res;
}
IEngine::ResultContainer<Database::TrackId>
ClusterEngine::getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const
{
Database::Session& dbSession {_db.getTLSSession()};
TrackContainer res;
{
auto transaction {dbSession.createSharedTransaction()};
const Database::TrackList::pointer trackList {Database::TrackList::getById(dbSession, tracklistId)};
if (!trackList)
return res;
const auto tracks {trackList->getSimilarTracks(0, maxCount)};
res.reserve(tracks.size());
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
}
return res;
}
IEngine::ResultContainer<Database::ReleaseId>
ClusterEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
{
Database::Session& dbSession {_db.getTLSSession()};
ReleaseContainer res;
{
auto transaction {dbSession.createSharedTransaction()};
auto release {Database::Release::getById(dbSession, releaseId)};
if (!release)
return res;
const auto releases {release->getSimilarReleases(0, maxCount)};
res.reserve(releases.size());
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
}
return res;
}
IEngine::ResultContainer<Database::ArtistId>
ClusterEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
{
Database::Session& dbSession {_db.getTLSSession()};
ResultContainer<Database::ArtistId> res;
{
auto transaction {dbSession.createSharedTransaction()};
auto artist {Database::Artist::getById(dbSession, artistId)};
if (!artist)
return res;
const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
res.reserve(artists.size());
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); });
}
return res;
}
} // namespace Recommendation
@@ -0,0 +1,51 @@
/*
* 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 "recommendation/IEngine.hpp"
namespace Recommendation
{
class ClusterEngine : public IEngine
{
public:
ClusterEngine(Database::Db& db) : _db {db} {}
ClusterEngine(const ClusterEngine&) = delete;
ClusterEngine(ClusterEngine&&) = delete;
ClusterEngine& operator=(const ClusterEngine&) = delete;
ClusterEngine& operator=(ClusterEngine&&) = delete;
private:
void load(bool, const ProgressCallback&) override {}
void requestCancelLoad() override {}
void cancelLoad() {}
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
Database::Db& _db;
};
} // namespace Recommendation
@@ -22,6 +22,7 @@
#include <numeric>
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
@@ -35,9 +36,9 @@
namespace Recommendation {
std::unique_ptr<IClassifier> createFeaturesEngine()
std::unique_ptr<IEngine> createFeaturesEngine(Database::Db& db)
{
return std::make_unique<FeaturesEngine>();
return std::make_unique<FeaturesEngine>(db);
}
const FeatureSettingsMap&
@@ -127,8 +128,8 @@ getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t
return weights;
}
bool
FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
void
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
@@ -141,6 +142,8 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
Database::Session& session {_db.getTLSSession()};
std::vector<Database::TrackId> trackIds;
{
auto transaction {session.createSharedTransaction()};
@@ -160,7 +163,7 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
for (Database::TrackId trackId : trackIds)
{
if (_loadCancelled)
return false;
return;
std::optional<FeatureValuesMap> featureValuesMap;
@@ -184,7 +187,7 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
if (samples.empty())
{
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
return false;
return;
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
@@ -219,15 +222,13 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
if (_loadCancelled)
return false;
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
TrackPositions trackPositions;
for (std::size_t i {}; i < samples.size(); ++i)
{
if (_loadCancelled)
return false;
return;
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
@@ -236,23 +237,25 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
return load(session, std::move(network), std::move(trackPositions));
load(std::move(network), std::move(trackPositions));
}
bool
FeaturesEngine::loadFromCache(Database::Session& session, const FeaturesEngineCache& cache)
void
FeaturesEngine::loadFromCache(FeaturesEngineCache cache)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
return load(session, std::move(cache._network), cache._trackPositions);
load(std::move(cache._network), cache._trackPositions);
}
IClassifier::ResultContainer<Database::TrackId>
FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) const
IEngine::TrackContainer
FeaturesEngine::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
{
const std::vector<Database::TrackId> trackIds {[&]
const TrackContainer trackIds {[&]
{
std::vector<Database::TrackId> res;
TrackContainer res;
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
@@ -263,14 +266,16 @@ FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Databa
return res;
}()};
return getSimilarTracks(session, trackIds, maxCount);
return getSimilarTracks(trackIds, maxCount);
}
std::vector<Database::TrackId>
FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
IEngine::TrackContainer
FeaturesEngine::getSimilarTracks(const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
{
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
Database::Session& session {_db.getTLSSession()};
{
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
auto transaction {session.createSharedTransaction()};
@@ -278,18 +283,21 @@ FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector<D
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
[&](Database::TrackId trackId)
{
return Database::Track::getById(session, trackId); // TODO exists
return !Database::Track::exists(session, trackId);
}), std::end(similarTrackIds));
}
return similarTrackIds;
}
std::vector<Database::ReleaseId>
FeaturesEngine::getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const
IEngine::ReleaseContainer
FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds {getSimilarObjects<Database::ReleaseId>({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
Database::Session& session {_db.getTLSSession()};
if (!similarReleaseIds.empty())
{
// Report only existing ids
auto transaction {session.createSharedTransaction()};
@@ -297,7 +305,7 @@ FeaturesEngine::getSimilarReleases(Database::Session& session, Database::Release
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
[&](Database::ReleaseId releaseId)
{
return Database::Release::getById(session, releaseId); // TODO exists
return !Database::Release::exists(session, releaseId);
}), std::end(similarReleaseIds));
}
@@ -305,10 +313,7 @@ FeaturesEngine::getSimilarReleases(Database::Session& session, Database::Release
}
std::vector<Database::ArtistId>
FeaturesEngine::getSimilarArtists(Database::Session& session,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const
FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
{
@@ -333,15 +338,16 @@ FeaturesEngine::getSimilarArtists(Database::Session& session,
std::vector<Database::ArtistId> res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
Database::Session& session {_db.getTLSSession()};
{
// Report only existing ids
auto transaction {session.createSharedTransaction()};
res.erase(std::remove_if(std::begin(res), std::end(res),
[&](Database::ArtistId artistId)
{
return Database::Artist::getById(session, artistId); // TODO exists
}), std::end(res));
[&](Database::ArtistId artistId)
{
return !Database::Artist::exists(session, artistId);
}), std::end(res));
}
while (res.size() > maxCount)
@@ -356,29 +362,25 @@ FeaturesEngine::toCache() const
return FeaturesEngineCache {*_network, _trackPositions};
}
bool
FeaturesEngine::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
void
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
{
if (forceReload)
{
FeaturesEngineCache::invalidate();
}
else
else if (const std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
{
const std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()};
if (cache)
return loadFromCache(session, *cache);
loadFromCache(*cache);
return;
}
TrainSettings trainSettings;
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
const bool res {loadFromTraining(session, trainSettings, progressCallback)};
if (res)
loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled)
toCache().write();
return res;
}
void
@@ -388,10 +390,8 @@ FeaturesEngine::requestCancelLoad()
_loadCancelled = true;
}
bool
FeaturesEngine::load(Database::Session& session,
SOM::Network network,
const TrackPositions& trackPositions)
void
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
{
using namespace Database;
@@ -406,10 +406,12 @@ FeaturesEngine::load(Database::Session& session,
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
Database::Session& session {_db.getTLSSession()};
for (const auto& [trackId, positions] : trackPositions)
{
if (_loadCancelled)
return false;
return;
auto transaction {session.createSharedTransaction()};
@@ -445,11 +447,9 @@ FeaturesEngine::load(Database::Session& session,
}
}
_network = std::make_unique<SOM::Network>(std::move(network));
_network = std::make_unique<SOM::Network>(network);
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
return true;
}
} // ns Recommendation
@@ -26,12 +26,12 @@
#include <string>
#include <vector>
#include "recommendation/IEngine.hpp"
#include "som/DataNormalizer.hpp"
#include "som/Network.hpp"
#include "utils/Utils.hpp"
#include "FeaturesEngineCache.hpp"
#include "FeaturesDefs.hpp"
#include "IClassifier.hpp"
namespace Database
{
@@ -42,10 +42,11 @@ namespace Recommendation {
using FeatureWeight = double;
class FeaturesEngine : public IClassifier
class FeaturesEngine : public IEngine
{
public:
FeaturesEngine() = default;
FeaturesEngine(Database::Db& db) : _db {db} {}
FeaturesEngine(const FeaturesEngine&) = delete;
FeaturesEngine(FeaturesEngine&&) = delete;
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
@@ -59,21 +60,16 @@ class FeaturesEngine : public IClassifier
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
private:
std::string_view getName() const override { return "Features"; }
bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override;
void load(bool forceReload, const ProgressCallback& progressCallback) override;
void requestCancelLoad() override;
void cancelLoad() override {}
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override;
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
Database::ArtistId artistId,
EnumSet<Database::TrackArtistLinkType> linkTypes,
std::size_t maxCount) const override;
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
bool loadFromCache(Database::Session& session, const FeaturesEngineCache& cache);
void loadFromCache(FeaturesEngineCache cache);
// Use training (may be very slow)
struct TrainSettings
@@ -82,7 +78,7 @@ class FeaturesEngine : public IClassifier
float sampleCountPerNeuron {4};
FeatureSettingsMap featureSettingsMap;
};
bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
void loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
template <typename IdType>
using ObjectPositions = std::unordered_map<IdType, std::vector<SOM::Position>>;
@@ -97,7 +93,7 @@ class FeaturesEngine : public IClassifier
using ReleaseMatrix = ObjectMatrix<Database::ReleaseId>;
using TrackMatrix = ObjectMatrix<Database::TrackId>;
bool load(Database::Session& session, SOM::Network network, const TrackPositions& tracksPosition);
void load(const SOM::Network& network, const TrackPositions& tracksPosition);
FeaturesEngineCache toCache() const;
@@ -113,6 +109,7 @@ class FeaturesEngine : public IClassifier
const ObjectPositions<IdType>& objectPositions,
std::size_t maxCount) const;
Database::Db& _db;
bool _loadCancelled {};
std::unique_ptr<SOM::Network> _network;
double _networkRefVectorsDistanceMedian {};
@@ -21,13 +21,13 @@
#include <functional>
#include <memory>
#include <string_view>
#include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database
{
class Db;
class Session;
}
namespace Recommendation
@@ -44,7 +44,8 @@ namespace Recommendation
};
using ProgressCallback = std::function<void(const Progress&)>;
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void cancelLoad() = 0;
virtual void cancelLoad() = 0; // wait for cancel done
virtual void requestCancelLoad() = 0;
template <typename IdType>
using ResultContainer = std::vector<IdType>;
@@ -53,13 +54,10 @@ namespace Recommendation
using ReleaseContainer = ResultContainer<Database::ReleaseId>;
using TrackContainer = ResultContainer<Database::TrackId>;
virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0;
virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) = 0;
virtual ReleaseContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) = 0;
virtual ArtistContainer getSimilarArtists(Database::Session& session, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) = 0;
protected:
virtual void requestCancelLoad() = 0;
virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
std::unique_ptr<IEngine> createEngine(Database::Db& db);
@@ -1,38 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include "database/Types.hpp"
#include "utils/EnumSet.hpp"
namespace Database
{
class Db;
class Session;
}
namespace Recommendation
{
class IRecommendation
{
};
} // ns Recommendation
@@ -0,0 +1,92 @@
/*
* 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/>.
*/
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp>
#include <Wt/Dbo/Dbo.h>
#include "SendQueue.hpp"
namespace Database
{
class Db;
class Session;
class User;
}
namespace Scrobbling::ListenBrainz
{
class ListensSynchronizer
{
public:
FeedbackSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue);
// void updateFeedback(const TimedListen& listen);
private:
struct UserContext
{
UserContext(Database::UserId id) : userId {id} {}
UserContext(const UserContext&) = delete;
UserContext(UserContext&&) = delete;
UserContext& operator=(const UserContext&) = delete;
UserContext& operator=(UserContext&&) = delete;
const Database::UserId userId;
bool fetching {};
std::optional<std::size_t> listenCount {};
// resetted at each fetch
std::string listenBrainzUserName; // need to be resolved first
Wt::WDateTime maxDateTime;
std::size_t fetchedListenCount{};
std::size_t matchedListenCount{};
std::size_t importedListenCount{};
};
UserContext& getUserContext(Database::UserId userId);
bool isFetching() const;
void scheduleGetListens(std::chrono::seconds fromNow);
void startGetListens();
void startGetListens(UserContext& context);
void onGetListensEnded(UserContext& context);
void enqueValidateToken(UserContext& context);
void enqueGetListenCount(UserContext& context);
void enqueGetListens(UserContext& context);
std::optional<SendQueue::RequestData> createValidateTokenRequestData(Database::UserId userId);
std::optional<SendQueue::RequestData> createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime);
void processGetListensResponse(std::string_view body, UserContext& context);
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand {_ioContext};
Database::Db& _db;
SendQueue& _sendQueue;
boost::asio::steady_timer _getListensTimer {_ioContext};
std::unordered_map<Database::UserId, UserContext> _userContexts;
const std::size_t _maxSyncFeedbackCount;
const std::chrono::hours _syncFeedbackPeriod;
};
} // Scrobbling::ListenBrainz
+2 -8
View File
@@ -896,10 +896,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
}
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
id,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
count)};
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count)};
{
auto transaction {context.dbSession.createSharedTransaction()};
@@ -1135,10 +1132,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
artistId,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
5)};
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
auto transaction {context.dbSession.createSharedTransaction()};
+1 -1
View File
@@ -520,7 +520,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
void
PlayQueue::enqueueRadioTracks()
{
const auto similarTrackIds {Service<Recommendation::IEngine>::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)};
const auto similarTrackIds {Service<Recommendation::IEngine>::get()->getSimilarTracksFromTrackList(_tracklistId, 3)};
std::vector<Database::TrackId> trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds));
Random::shuffleContainer(trackToAddIds);
+1 -4
View File
@@ -102,10 +102,7 @@ Artist::refreshView()
if (!artistId)
throw ArtistNotFoundException {};
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(),
*artistId,
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
5)};
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(*artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+1 -1
View File
@@ -101,7 +101,7 @@ Release::refreshView()
if (!releaseId)
throw ReleaseNotFoundException {};
auto similarReleasesIds {Service<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)};
auto similarReleasesIds {Service<Recommendation::IEngine>::get()->getSimilarReleases(*releaseId, 6)};
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
@@ -66,7 +66,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng
};
std::cout << "Processing track '" << trackToString(trackId) << std::endl;
for (Database::TrackId similarTrackId : engine.getSimilarTracks(session, {trackId}, maxSimilarityCount))
for (Database::TrackId similarTrackId : engine.getSimilarTracks({trackId}, maxSimilarityCount))
std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl;
}
}
@@ -84,7 +84,7 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e
std::cout << "*** Releases ***" << std::endl;
for (Database::ReleaseId releaseId : releaseIds)
{
auto releaseToString = [&](Database::ReleaseId releaseId)
auto releaseToString = [&](Database::ReleaseId releaseId) -> std::string
{
auto transaction {session.createSharedTransaction()};
@@ -93,7 +93,7 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e
};
std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl;
for (Database::ReleaseId similarReleaseId : engine.getSimilarReleases(session, releaseId, maxSimilarityCount))
for (Database::ReleaseId similarReleaseId : engine.getSimilarReleases(releaseId, maxSimilarityCount))
std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl;
}
}
@@ -120,7 +120,7 @@ dumpArtistsRecommendation(Database::Session session, Recommendation::IEngine& en
};
std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl;
for (Database::ArtistId similarArtistId : engine.getSimilarArtists(session, artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount))
for (Database::ArtistId similarArtistId : engine.getSimilarArtists(artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount))
{
std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl;
}