Bring more diversity in the radio mode. fixes #54

This commit is contained in:
emeric
2022-10-19 13:56:32 +02:00
parent b85553af34
commit c816b0db45
21 changed files with 750 additions and 104 deletions
@@ -4,6 +4,10 @@ add_library(lmsrecommendation SHARED
impl/features/FeaturesEngineCache.cpp
impl/features/FeaturesEngine.cpp
impl/features/FeaturesDefs.cpp
impl/playlist-constraints/ConsecutiveArtists.cpp
impl/playlist-constraints/ConsecutiveReleases.cpp
impl/playlist-constraints/DuplicateTracks.cpp
impl/PlaylistGeneratorService.cpp
impl/RecommendationService.cpp
)
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2022 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 "PlaylistGeneratorService.hpp"
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "playlist-constraints/ConsecutiveArtists.hpp"
#include "playlist-constraints/ConsecutiveReleases.hpp"
#include "playlist-constraints/DuplicateTracks.hpp"
#include "utils/Logger.hpp"
namespace Recommendation
{
using namespace Database;
std::unique_ptr<IPlaylistGeneratorService>
createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
{
return std::make_unique<PlaylistGeneratorService>(db, recommendationService);
}
PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _db {db}
, _recommendationService {recommendationService}
{
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveArtists>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::ConsecutiveReleases>(_db));
_constraints.push_back(std::make_unique<PlaylistGeneratorConstraint::DuplicateTracks>());
}
std::vector<TrackId>
PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
// supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints
const std::vector<TrackId> startingTracks {getTracksFromTrackList(tracklistId)};
std::vector<TrackId> finalResult = startingTracks;
finalResult.reserve(startingTracks.size() + maxCount);
std::vector<float> scores;
for (std::size_t i {}; i < maxCount; ++i)
{
if (similarTracks.empty())
break;
scores.resize(similarTracks.size(), {});
// select the similar track that has the best score
for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex)
{
using namespace Database::Debug;
finalResult.push_back(similarTracks[trackIndex]);
scores[trackIndex] = 0;
for (const auto& constraint : _constraints)
scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1);
finalResult.pop_back();
// early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar)
if (scores[trackIndex] < 0.01)
break;
}
// get the best score
const std::size_t bestScoreIndex {static_cast<std::size_t>(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores))))};
finalResult.push_back(similarTracks[bestScoreIndex]);
similarTracks.erase(std::begin(similarTracks) + bestScoreIndex);
}
// for now, just get some more similar tracks
return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult));
}
TrackContainer
PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const
{
TrackContainer tracks;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createSharedTransaction()};
Track::FindParameters params;
params.setTrackList(tracklistId);
params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false);
for (const TrackId trackId : Track::find(dbSession, params).results)
tracks.push_back(trackId);
return tracks;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2022 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 "services/recommendation/IPlaylistGeneratorService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "playlist-constraints/IConstraint.hpp"
namespace Recommendation
{
class PlaylistGeneratorService : public IPlaylistGeneratorService
{
public:
PlaylistGeneratorService(Database::Db& db, Recommendation::IRecommendationService& recommendationService);
private:
TrackContainer extendPlaylist(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer getTracksFromTrackList(Database::TrackListId tracklistId) const;
Database::Db& _db;
Recommendation::IRecommendationService& _recommendationService;
std::vector<std::unique_ptr<PlaylistGeneratorConstraint::IConstraint>> _constraints;
};
} // namespace Radio
@@ -59,7 +59,7 @@ namespace Recommendation
}
TrackContainer
RecommendationService::findSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const
{
TrackContainer res;
@@ -56,7 +56,7 @@ namespace Recommendation
void load(bool forceReload, const ProgressCallback& progressCallback) override;
void cancelLoad() override;
TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer findSimilarTracks(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;
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2022 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 "ConsecutiveArtists.hpp"
#include <algorithm>
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
namespace
{
std::size_t
countCommonArtists(const ArtistContainer& artists1, const ArtistContainer& artists2)
{
ArtistContainer intersection;
std::set_intersection(std::cbegin(artists1), std::cend(artists1),
std::cbegin(artists2), std::cend(artists2),
std::back_inserter(intersection));
return intersection.size();
}
}
ConsecutiveArtists::ConsecutiveArtists(Database::Db& db)
: _db {db}
{}
float
ConsecutiveArtists::computeScore(const std::vector<Database::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
const ArtistContainer artists {getArtists(trackIds[trackIndex])};
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
float score {};
for (std::size_t i {1}; i < rangeSize; ++i)
{
if (trackIndex >= i)
score += countCommonArtists(artists, getArtists(trackIds[trackIndex - i])) / static_cast<float>(i);
if (trackIndex + i < trackIds.size())
score += countCommonArtists(artists, getArtists(trackIds[trackIndex + i])) / static_cast<float>(i);
}
return score;
}
ArtistContainer
ConsecutiveArtists::getArtists(Database::TrackId trackId)
{
using namespace Database;
ArtistContainer res;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createSharedTransaction()};
const Track::pointer track {Track::find(dbSession, trackId)};
if (!track)
return res;
res = track->getArtistIds({});
std::sort(std::begin(res), std::end(res));
return res;
}
} // namespace Recommendation
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2022 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 "IConstraint.hpp"
#include "services/database/ReleaseId.hpp"
namespace Database
{
class Db;
}
namespace Recommendation::PlaylistGeneratorConstraint
{
class ConsecutiveArtists : public IConstraint
{
public:
ConsecutiveArtists(Database::Db& db);
private:
float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) override;
ArtistContainer getArtists(Database::TrackId trackId);
Database::Db& _db;
};
} // namespace Recommendation::PlaylistGeneratorConstraint
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2022 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 "ConsecutiveReleases.hpp"
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
ConsecutiveReleases::ConsecutiveReleases(Database::Db& db)
: _db {db}
{}
float
ConsecutiveReleases::computeScore(const std::vector<Database::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
const Database::ReleaseId releaseId {getReleaseId(trackIds[trackIndex])};
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
float score {};
for (std::size_t i {1}; i < rangeSize; ++i)
{
if ((trackIndex >= i) && getReleaseId(trackIds[trackIndex - i]) == releaseId)
score += (1.f / static_cast<float>(i));
if ((trackIndex + i < trackIds.size()) && getReleaseId(trackIds[trackIndex + i]) == releaseId)
score += (1.f / static_cast<float>(i));
}
return score;
}
Database::ReleaseId
ConsecutiveReleases::getReleaseId(Database::TrackId trackId)
{
using namespace Database;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createSharedTransaction()};
const Track::pointer track {Track::find(dbSession, trackId)};
if (!track)
return {};
const Release::pointer release {track->getRelease()};
if (!release)
return {};
return release->getId();
}
} // namespace Recommendation
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2022 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 "IConstraint.hpp"
#include "services/database/ReleaseId.hpp"
namespace Database
{
class Db;
}
namespace Recommendation::PlaylistGeneratorConstraint
{
class ConsecutiveReleases : public IConstraint
{
public:
ConsecutiveReleases(Database::Db& db);
private:
float computeScore(const std::vector<Database::TrackId>& trackIds, std::size_t trackIndex) override;
Database::ReleaseId getReleaseId(Database::TrackId trackId);
Database::Db& _db;
};
} // namespace Recommendation
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 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 "DuplicateTracks.hpp"
#include <algorithm>
namespace Recommendation::PlaylistGeneratorConstraint
{
float
DuplicateTracks::computeScore(const std::vector<Database::TrackId>& trackIds, std::size_t trackIndex)
{
const auto count {std::count(std::cbegin(trackIds), std::cend(trackIds), trackIds[trackIndex])};
return count == 1 ? 0 : 1000;
}
} // namespace Recommendation
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2022 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 "IConstraint.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
class DuplicateTracks : public IConstraint
{
private:
float computeScore(const std::vector<Database::TrackId>& trackIds, std::size_t trackIndex) override;
};
} // namespace Recommendation::PlaylistGeneratorConstraints
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2022 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 <vector>
#include "services/recommendation/Types.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
class IConstraint
{
public:
virtual ~IConstraint() = default;
// compute the score of the track at index trackIndex
// 0: best
// 1: worst
// > 1 : violation
virtual float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) = 0;
};
} // namespace Recommendation
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2019 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 <memory>
#include "services/database/TrackListId.hpp"
#include "services/database/Types.hpp"
#include "services/recommendation/Types.hpp"
namespace Database
{
class Db;
}
namespace Recommendation
{
class IRecommendationService;
class IPlaylistGeneratorService
{
public:
// extend an existing playlist with similar tracks (but use playlist contraints)
virtual TrackContainer extendPlaylist(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
};
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(Database::Db& db, IRecommendationService& recommandationService);
} // ns Recommendation
@@ -40,13 +40,13 @@ namespace Recommendation
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void cancelLoad() = 0; // wait for cancel done
virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(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<IRecommendationService> createRecommendationService(Database::Db& db);
} // ns Recommendation