Finished clean on recommendation engines
This commit is contained in:
@@ -66,6 +66,13 @@ Artist::getById(Session& session, ArtistId id)
|
|||||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
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::pointer
|
||||||
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ Release::getById(Session& session, ReleaseId id)
|
|||||||
.resultValue();
|
.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::pointer
|
||||||
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -156,6 +156,14 @@ Track::getById(Session& session, TrackId id)
|
|||||||
.resultValue();
|
.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>
|
std::vector<Track::pointer>
|
||||||
Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ class Artist : public Object<Artist, ArtistId>
|
|||||||
// Accessors
|
// Accessors
|
||||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||||
static pointer getById(Session& session, ArtistId id);
|
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> 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
|
const std::vector<ClusterId>& clusters, // at least one track that belongs to these clusters
|
||||||
SortMethod sortMethod
|
SortMethod sortMethod
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class Release : public Object<Release, ReleaseId>
|
|||||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||||
static pointer getById(Session& session, ReleaseId id);
|
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> getAllOrphans(Session& session); // no track related
|
||||||
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
static std::vector<ReleaseId> getAllIds(Session& session);
|
static std::vector<ReleaseId> getAllIds(Session& session);
|
||||||
|
|||||||
@@ -56,9 +56,10 @@ class Track : public Object<Track, TrackId>
|
|||||||
Track(const std::filesystem::path& p);
|
Track(const std::filesystem::path& p);
|
||||||
|
|
||||||
// Find utility functions
|
// Find utility functions
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
||||||
static pointer getById(Session& session, TrackId id);
|
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> getByRecordingMBID(Session& session, const UUID& MBID);
|
||||||
static std::vector<pointer> getSimilarTracks(Session& session,
|
static std::vector<pointer> getSimilarTracks(Session& session,
|
||||||
const std::vector<TrackId>& trackIds,
|
const std::vector<TrackId>& trackIds,
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ using namespace Database;
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleArtist)
|
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"};
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -31,6 +38,8 @@ TEST_F(DatabaseFixture, SingleArtist)
|
|||||||
EXPECT_TRUE(artist.get());
|
EXPECT_TRUE(artist.get());
|
||||||
EXPECT_FALSE(!artist.get());
|
EXPECT_FALSE(!artist.get());
|
||||||
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
||||||
|
|
||||||
|
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,11 +23,20 @@ using namespace Database;
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleRelease)
|
TEST_F(DatabaseFixture, SingleRelease)
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_FALSE(Release::exists(session, 0));
|
||||||
|
EXPECT_FALSE(Release::exists(session, 1));
|
||||||
|
}
|
||||||
|
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_TRUE(Release::exists(session, release.getId()));
|
||||||
|
|
||||||
auto releases {Release::getAllOrphans(session)};
|
auto releases {Release::getAllOrphans(session)};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ TEST_F(DatabaseFixture, SingleTrack)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_EQ(Track::getCount(session), 0);
|
EXPECT_EQ(Track::getCount(session), 0);
|
||||||
|
EXPECT_FALSE(Track::exists(session, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedTrack track {session, "MyTrackFile"};
|
ScopedTrack track {session, "MyTrackFile"};
|
||||||
@@ -37,6 +38,10 @@ TEST_F(DatabaseFixture, SingleTrack)
|
|||||||
|
|
||||||
EXPECT_EQ(Track::getAll(session).size(), 1);
|
EXPECT_EQ(Track::getAll(session).size(), 1);
|
||||||
EXPECT_EQ(Track::getCount(session), 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,6 +1,6 @@
|
|||||||
|
|
||||||
add_library(lmsrecommendation SHARED
|
add_library(lmsrecommendation SHARED
|
||||||
impl/clusters/ClustersClassifier.cpp
|
impl/clusters/ClustersEngine.cpp
|
||||||
impl/features/FeaturesEngineCache.cpp
|
impl/features/FeaturesEngineCache.cpp
|
||||||
impl/features/FeaturesEngine.cpp
|
impl/features/FeaturesEngine.cpp
|
||||||
impl/features/FeaturesDefs.cpp
|
impl/features/FeaturesDefs.cpp
|
||||||
|
|||||||
+8
-4
@@ -21,10 +21,14 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace Recommendation
|
namespace Database
|
||||||
{
|
{
|
||||||
class IClassifier;
|
class Db;
|
||||||
|
}
|
||||||
std::unique_ptr<IClassifier> createClustersClassifier();
|
|
||||||
|
namespace Recommendation
|
||||||
|
{
|
||||||
|
class IEngine;
|
||||||
|
std::unique_ptr<IEngine> createClustersEngine(Database::Db& db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "ClustersClassifierCreator.hpp"
|
#include "ClustersEngineCreator.hpp"
|
||||||
#include "FeaturesEngineCreator.hpp"
|
#include "FeaturesEngineCreator.hpp"
|
||||||
|
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
@@ -31,260 +31,229 @@
|
|||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
namespace Recommendation {
|
namespace Recommendation
|
||||||
|
|
||||||
|
|
||||||
static
|
|
||||||
std::unique_ptr<IClassifier>
|
|
||||||
createClassifier(ClassifierType type)
|
|
||||||
{
|
{
|
||||||
switch (type)
|
|
||||||
|
static
|
||||||
|
std::string_view
|
||||||
|
engineTypeToString(EngineType engineType)
|
||||||
{
|
{
|
||||||
case ClassifierType::Clusters:
|
switch (engineType)
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'";
|
case EngineType::Clusters: return "clusters";
|
||||||
break;
|
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;
|
void
|
||||||
}
|
Engine::cancelLoad()
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
auto itClassifier {_classifiers.find(classifierType)};
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading...";
|
||||||
if (itClassifier == std::cend(_classifiers))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const IClassifier& classifier {*itClassifier->second};
|
std::unique_lock controlLock {_controlMutex};
|
||||||
res = classifier.getSimilarReleases(dbSession, releaseId, maxCount);
|
|
||||||
if (!res.empty())
|
assert(!_loadCancelled);
|
||||||
{
|
_loadCancelled = true;
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifier.getName() << "'";
|
|
||||||
break;
|
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
|
} // ns Similarity
|
||||||
|
|||||||
@@ -20,13 +20,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "recommendation/IEngine.hpp"
|
#include "recommendation/IEngine.hpp"
|
||||||
#include "IClassifier.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -35,7 +34,7 @@ namespace Database
|
|||||||
|
|
||||||
namespace Recommendation
|
namespace Recommendation
|
||||||
{
|
{
|
||||||
enum class ClassifierType
|
enum class EngineType
|
||||||
{
|
{
|
||||||
Clusters,
|
Clusters,
|
||||||
Features,
|
Features,
|
||||||
@@ -57,29 +56,29 @@ namespace Recommendation
|
|||||||
void cancelLoad() override;
|
void cancelLoad() override;
|
||||||
void requestCancelLoad() override {};
|
void requestCancelLoad() override {};
|
||||||
|
|
||||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override;
|
TrackContainer getSimilarTracksFromTrackList(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) override;
|
TrackContainer getSimilarTracks(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) override;
|
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||||
Database::ArtistId artistId,
|
|
||||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
|
||||||
std::size_t maxCount) override;
|
|
||||||
|
|
||||||
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
|
void setEnginePriorities(const std::vector<EngineType>& engineTypes);
|
||||||
void clearClassifiers();
|
void clearEngines();
|
||||||
void loadClassifier(std::unique_ptr<IClassifier> classifier, ClassifierType classifierType, bool forceReload, const ProgressCallback& progressCallback);
|
void loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback);
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
|
|
||||||
std::mutex _controlMutex;
|
std::mutex _controlMutex;
|
||||||
bool _loadCancelled {};
|
bool _loadCancelled {};
|
||||||
std::condition_variable _pendingClassifiersCondvar;
|
|
||||||
std::unordered_set<IClassifier*> _pendingClassifiers;
|
|
||||||
|
|
||||||
std::shared_mutex _classifiersMutex;
|
using EngineContainer = std::unordered_map<EngineType, std::unique_ptr<IEngine>>;
|
||||||
using ClassifierContainer = std::unordered_map<ClassifierType, std::unique_ptr<IClassifier>>;
|
EngineContainer _engines;
|
||||||
ClassifierContainer _classifiers;
|
mutable std::shared_mutex _enginesMutex;
|
||||||
std::vector<ClassifierType> _classifierPriorities; // ordered by priority
|
|
||||||
|
std::vector<IEngine*> _pendingEngines;
|
||||||
|
std::shared_mutex _pendingEnginesMutex;
|
||||||
|
std::condition_variable _pendingEnginesCondvar;
|
||||||
|
|
||||||
|
std::vector<EngineType> _enginePriorities; // ordered by priority
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ns Recommendation
|
} // ns Recommendation
|
||||||
|
|||||||
@@ -20,10 +20,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "IClassifier.hpp"
|
#include "recommendation/IEngine.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Recommendation
|
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 <numeric>
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
@@ -35,9 +36,9 @@
|
|||||||
|
|
||||||
namespace Recommendation {
|
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&
|
const FeatureSettingsMap&
|
||||||
@@ -127,8 +128,8 @@ getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t
|
|||||||
return weights;
|
return weights;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void
|
||||||
FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
|
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;
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
||||||
|
|
||||||
|
Database::Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
std::vector<Database::TrackId> trackIds;
|
std::vector<Database::TrackId> trackIds;
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
@@ -160,7 +163,7 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
|
|||||||
for (Database::TrackId trackId : trackIds)
|
for (Database::TrackId trackId : trackIds)
|
||||||
{
|
{
|
||||||
if (_loadCancelled)
|
if (_loadCancelled)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
std::optional<FeatureValuesMap> featureValuesMap;
|
std::optional<FeatureValuesMap> featureValuesMap;
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
|
|||||||
if (samples.empty())
|
if (samples.empty())
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
|
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
|
||||||
@@ -219,15 +222,13 @@ FeaturesEngine::loadFromTraining(Database::Session& session, const TrainSettings
|
|||||||
[this] { return _loadCancelled; });
|
[this] { return _loadCancelled; });
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
|
||||||
|
|
||||||
if (_loadCancelled)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
||||||
TrackPositions trackPositions;
|
TrackPositions trackPositions;
|
||||||
for (std::size_t i {}; i < samples.size(); ++i)
|
for (std::size_t i {}; i < samples.size(); ++i)
|
||||||
{
|
{
|
||||||
if (_loadCancelled)
|
if (_loadCancelled)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
|
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";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
|
||||||
|
|
||||||
return load(session, std::move(network), std::move(trackPositions));
|
load(std::move(network), std::move(trackPositions));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void
|
||||||
FeaturesEngine::loadFromCache(Database::Session& session, const FeaturesEngineCache& cache)
|
FeaturesEngine::loadFromCache(FeaturesEngineCache cache)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from 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>
|
IEngine::TrackContainer
|
||||||
FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount) const
|
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()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
@@ -263,14 +266,16 @@ FeaturesEngine::getSimilarTracksFromTrackList(Database::Session& session, Databa
|
|||||||
return res;
|
return res;
|
||||||
}()};
|
}()};
|
||||||
|
|
||||||
return getSimilarTracks(session, trackIds, maxCount);
|
return getSimilarTracks(trackIds, maxCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::TrackId>
|
IEngine::TrackContainer
|
||||||
FeaturesEngine::getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
|
FeaturesEngine::getSimilarTracks(const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
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)
|
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||||
auto transaction {session.createSharedTransaction()};
|
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),
|
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||||
[&](Database::TrackId trackId)
|
[&](Database::TrackId trackId)
|
||||||
{
|
{
|
||||||
return Database::Track::getById(session, trackId); // TODO exists
|
return !Database::Track::exists(session, trackId);
|
||||||
}), std::end(similarTrackIds));
|
}), std::end(similarTrackIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
return similarTrackIds;
|
return similarTrackIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::ReleaseId>
|
IEngine::ReleaseContainer
|
||||||
FeaturesEngine::getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const
|
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
|
// Report only existing ids
|
||||||
auto transaction {session.createSharedTransaction()};
|
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),
|
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||||
[&](Database::ReleaseId releaseId)
|
[&](Database::ReleaseId releaseId)
|
||||||
{
|
{
|
||||||
return Database::Release::getById(session, releaseId); // TODO exists
|
return !Database::Release::exists(session, releaseId);
|
||||||
}), std::end(similarReleaseIds));
|
}), std::end(similarReleaseIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,10 +313,7 @@ FeaturesEngine::getSimilarReleases(Database::Session& session, Database::Release
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::ArtistId>
|
std::vector<Database::ArtistId>
|
||||||
FeaturesEngine::getSimilarArtists(Database::Session& session,
|
FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||||
Database::ArtistId artistId,
|
|
||||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
|
||||||
std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
|
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));
|
std::vector<Database::ArtistId> res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||||
|
|
||||||
|
Database::Session& session {_db.getTLSSession()};
|
||||||
{
|
{
|
||||||
// Report only existing ids
|
// Report only existing ids
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||||
[&](Database::ArtistId artistId)
|
[&](Database::ArtistId artistId)
|
||||||
{
|
{
|
||||||
return Database::Artist::getById(session, artistId); // TODO exists
|
return !Database::Artist::exists(session, artistId);
|
||||||
}), std::end(res));
|
}), std::end(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
while (res.size() > maxCount)
|
while (res.size() > maxCount)
|
||||||
@@ -356,29 +362,25 @@ FeaturesEngine::toCache() const
|
|||||||
return FeaturesEngineCache {*_network, _trackPositions};
|
return FeaturesEngineCache {*_network, _trackPositions};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void
|
||||||
FeaturesEngine::load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback)
|
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
|
||||||
{
|
{
|
||||||
if (forceReload)
|
if (forceReload)
|
||||||
|
|
||||||
{
|
{
|
||||||
FeaturesEngineCache::invalidate();
|
FeaturesEngineCache::invalidate();
|
||||||
}
|
}
|
||||||
else
|
else if (const std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
|
||||||
{
|
{
|
||||||
const std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()};
|
loadFromCache(*cache);
|
||||||
if (cache)
|
return;
|
||||||
return loadFromCache(session, *cache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrainSettings trainSettings;
|
TrainSettings trainSettings;
|
||||||
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
||||||
|
|
||||||
const bool res {loadFromTraining(session, trainSettings, progressCallback)};
|
loadFromTraining(trainSettings, progressCallback);
|
||||||
if (res)
|
if (!_loadCancelled)
|
||||||
toCache().write();
|
toCache().write();
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -388,10 +390,8 @@ FeaturesEngine::requestCancelLoad()
|
|||||||
_loadCancelled = true;
|
_loadCancelled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void
|
||||||
FeaturesEngine::load(Database::Session& session,
|
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
|
||||||
SOM::Network network,
|
|
||||||
const TrackPositions& trackPositions)
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
@@ -406,10 +406,12 @@ FeaturesEngine::load(Database::Session& session,
|
|||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
||||||
|
|
||||||
|
Database::Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
for (const auto& [trackId, positions] : trackPositions)
|
for (const auto& [trackId, positions] : trackPositions)
|
||||||
{
|
{
|
||||||
if (_loadCancelled)
|
if (_loadCancelled)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
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!";
|
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // ns Recommendation
|
} // ns Recommendation
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "recommendation/IEngine.hpp"
|
||||||
#include "som/DataNormalizer.hpp"
|
#include "som/DataNormalizer.hpp"
|
||||||
#include "som/Network.hpp"
|
#include "som/Network.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
#include "FeaturesEngineCache.hpp"
|
#include "FeaturesEngineCache.hpp"
|
||||||
#include "FeaturesDefs.hpp"
|
#include "FeaturesDefs.hpp"
|
||||||
#include "IClassifier.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -42,10 +42,11 @@ namespace Recommendation {
|
|||||||
|
|
||||||
using FeatureWeight = double;
|
using FeatureWeight = double;
|
||||||
|
|
||||||
class FeaturesEngine : public IClassifier
|
class FeaturesEngine : public IEngine
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FeaturesEngine() = default;
|
FeaturesEngine(Database::Db& db) : _db {db} {}
|
||||||
|
|
||||||
FeaturesEngine(const FeaturesEngine&) = delete;
|
FeaturesEngine(const FeaturesEngine&) = delete;
|
||||||
FeaturesEngine(FeaturesEngine&&) = delete;
|
FeaturesEngine(FeaturesEngine&&) = delete;
|
||||||
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
|
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
|
||||||
@@ -59,21 +60,16 @@ class FeaturesEngine : public IClassifier
|
|||||||
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||||
std::string_view getName() const override { return "Features"; }
|
|
||||||
|
|
||||||
bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) override;
|
|
||||||
void requestCancelLoad() override;
|
void requestCancelLoad() override;
|
||||||
|
void cancelLoad() override {}
|
||||||
|
|
||||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
TrackContainer getSimilarTracksFromTrackList(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;
|
TrackContainer getSimilarTracks(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;
|
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||||
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)
|
// Use training (may be very slow)
|
||||||
struct TrainSettings
|
struct TrainSettings
|
||||||
@@ -82,7 +78,7 @@ class FeaturesEngine : public IClassifier
|
|||||||
float sampleCountPerNeuron {4};
|
float sampleCountPerNeuron {4};
|
||||||
FeatureSettingsMap featureSettingsMap;
|
FeatureSettingsMap featureSettingsMap;
|
||||||
};
|
};
|
||||||
bool loadFromTraining(Database::Session& session, const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
|
void loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
|
||||||
|
|
||||||
template <typename IdType>
|
template <typename IdType>
|
||||||
using ObjectPositions = std::unordered_map<IdType, std::vector<SOM::Position>>;
|
using ObjectPositions = std::unordered_map<IdType, std::vector<SOM::Position>>;
|
||||||
@@ -97,7 +93,7 @@ class FeaturesEngine : public IClassifier
|
|||||||
using ReleaseMatrix = ObjectMatrix<Database::ReleaseId>;
|
using ReleaseMatrix = ObjectMatrix<Database::ReleaseId>;
|
||||||
using TrackMatrix = ObjectMatrix<Database::TrackId>;
|
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;
|
FeaturesEngineCache toCache() const;
|
||||||
|
|
||||||
@@ -113,6 +109,7 @@ class FeaturesEngine : public IClassifier
|
|||||||
const ObjectPositions<IdType>& objectPositions,
|
const ObjectPositions<IdType>& objectPositions,
|
||||||
std::size_t maxCount) const;
|
std::size_t maxCount) const;
|
||||||
|
|
||||||
|
Database::Db& _db;
|
||||||
bool _loadCancelled {};
|
bool _loadCancelled {};
|
||||||
std::unique_ptr<SOM::Network> _network;
|
std::unique_ptr<SOM::Network> _network;
|
||||||
double _networkRefVectorsDistanceMedian {};
|
double _networkRefVectorsDistanceMedian {};
|
||||||
|
|||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
class Session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Recommendation
|
namespace Recommendation
|
||||||
@@ -44,7 +44,8 @@ namespace Recommendation
|
|||||||
};
|
};
|
||||||
using ProgressCallback = std::function<void(const Progress&)>;
|
using ProgressCallback = std::function<void(const Progress&)>;
|
||||||
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
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>
|
template <typename IdType>
|
||||||
using ResultContainer = std::vector<IdType>;
|
using ResultContainer = std::vector<IdType>;
|
||||||
@@ -53,13 +54,10 @@ namespace Recommendation
|
|||||||
using ReleaseContainer = ResultContainer<Database::ReleaseId>;
|
using ReleaseContainer = ResultContainer<Database::ReleaseId>;
|
||||||
using TrackContainer = ResultContainer<Database::TrackId>;
|
using TrackContainer = ResultContainer<Database::TrackId>;
|
||||||
|
|
||||||
virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0;
|
virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
||||||
virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) = 0;
|
virtual TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
||||||
virtual ReleaseContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) = 0;
|
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
||||||
virtual ArtistContainer getSimilarArtists(Database::Session& session, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) = 0;
|
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void requestCancelLoad() = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IEngine> createEngine(Database::Db& db);
|
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
|
||||||
|
|
||||||
@@ -896,10 +896,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
|||||||
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
|
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count)};
|
||||||
id,
|
|
||||||
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
|
|
||||||
count)};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
@@ -1135,10 +1132,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
|||||||
// Optional params
|
// Optional params
|
||||||
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
|
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
|
||||||
|
|
||||||
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession,
|
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
|
||||||
artistId,
|
|
||||||
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
|
|
||||||
5)};
|
|
||||||
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
|||||||
void
|
void
|
||||||
PlayQueue::enqueueRadioTracks()
|
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));
|
std::vector<Database::TrackId> trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds));
|
||||||
Random::shuffleContainer(trackToAddIds);
|
Random::shuffleContainer(trackToAddIds);
|
||||||
|
|||||||
@@ -102,10 +102,7 @@ Artist::refreshView()
|
|||||||
if (!artistId)
|
if (!artistId)
|
||||||
throw ArtistNotFoundException {};
|
throw ArtistNotFoundException {};
|
||||||
|
|
||||||
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(),
|
const auto similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(*artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, 5)};
|
||||||
*artistId,
|
|
||||||
{TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist},
|
|
||||||
5)};
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Release::refreshView()
|
|||||||
if (!releaseId)
|
if (!releaseId)
|
||||||
throw ReleaseNotFoundException {};
|
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()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IEngine& eng
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::cout << "Processing track '" << trackToString(trackId) << std::endl;
|
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;
|
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;
|
std::cout << "*** Releases ***" << std::endl;
|
||||||
for (Database::ReleaseId releaseId : releaseIds)
|
for (Database::ReleaseId releaseId : releaseIds)
|
||||||
{
|
{
|
||||||
auto releaseToString = [&](Database::ReleaseId releaseId)
|
auto releaseToString = [&](Database::ReleaseId releaseId) -> std::string
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ dumpReleasesRecommendation(Database::Session session, Recommendation::IEngine& e
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl;
|
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;
|
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;
|
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;
|
std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user