diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index b97e9ad9..cb2fad61 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -520,5 +520,33 @@ Track::getClusterGroups(const std::vector& clusterTypes, s return res; } +namespace Debug +{ + std::ostream& + operator<<(std::ostream& os, const TrackInfo& trackInfo) + { + auto transaction {trackInfo.session.createSharedTransaction()}; + + const Track::pointer track {Track::find(trackInfo.session, trackInfo.trackId)}; + if (track) + { + os << track->getName(); + + if (const Release::pointer release {track->getRelease()}) + os << " [" << release->getName() << "]"; + for (auto artist : track->getArtists({TrackArtistLinkType::Artist})) + os << " - " << artist->getName(); + for (auto cluster : track->getClusters()) + os << " {" + cluster->getType()->getName() << "-" << cluster->getName() << "}"; + } + else + { + os << "*unknown*"; + } + + return os; + } +} + } // namespace Database diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index 88f33085..d7f211e2 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -21,10 +21,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -163,8 +165,8 @@ class Track : public Object std::optional getReleaseReplayGain() const { return _releaseReplayGain; } // no artistLinkTypes means get all - std::vector> getArtists(EnumSet artistLinkTypes) const; - std::vector getArtistIds(EnumSet artistLinkTypes) const; + std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all + std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all std::vector> getArtistLinks() const; ObjectPtr getRelease() const { return _release; } std::vector> getClusters() const; @@ -237,6 +239,16 @@ class Track : public Object Wt::Dbo::collection> _clusters; }; +namespace Debug +{ + struct TrackInfo + { + Session& session; + TrackId trackId; + }; + std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); +} + } // namespace database diff --git a/src/libs/services/recommendation/CMakeLists.txt b/src/libs/services/recommendation/CMakeLists.txt index a001854e..8244679b 100644 --- a/src/libs/services/recommendation/CMakeLists.txt +++ b/src/libs/services/recommendation/CMakeLists.txt @@ -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 ) diff --git a/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp b/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp new file mode 100644 index 00000000..b136e9b4 --- /dev/null +++ b/src/libs/services/recommendation/impl/PlaylistGeneratorService.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 . + */ + +#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 + createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) + { + return std::make_unique(db, recommendationService); + } + + PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) + : _db {db} + , _recommendationService {recommendationService} + { + _constraints.push_back(std::make_unique(_db)); + _constraints.push_back(std::make_unique(_db)); + _constraints.push_back(std::make_unique()); + } + + std::vector + 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 similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints + + const std::vector startingTracks {getTracksFromTrackList(tracklistId)}; + + std::vector finalResult = startingTracks; + finalResult.reserve(startingTracks.size() + maxCount); + + std::vector 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::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; + } +} diff --git a/src/libs/services/recommendation/impl/PlaylistGeneratorService.hpp b/src/libs/services/recommendation/impl/PlaylistGeneratorService.hpp new file mode 100644 index 00000000..8dd9ee03 --- /dev/null +++ b/src/libs/services/recommendation/impl/PlaylistGeneratorService.hpp @@ -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 . + */ + +#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> _constraints; + }; +} // namespace Radio diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp index 67445959..dd302c6b 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.cpp +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -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; diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp index 25e6b5d4..11b10cfc 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.hpp +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -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& tracksId, std::size_t maxCount) const override; ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; diff --git a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp new file mode 100644 index 00000000..ff45b7b5 --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp @@ -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 . + */ + +#include "ConsecutiveArtists.hpp" + +#include + +#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& 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(i); + + if (trackIndex + i < trackIds.size()) + score += countCommonArtists(artists, getArtists(trackIds[trackIndex + i])) / static_cast(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 + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.hpp b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.hpp new file mode 100644 index 00000000..34bcd0ab --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.hpp @@ -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 . + */ + +#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 + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.cpp b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.cpp new file mode 100644 index 00000000..d97482ca --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.cpp @@ -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 . + */ + +#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& 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(i)); + + if ((trackIndex + i < trackIds.size()) && getReleaseId(trackIds[trackIndex + i]) == releaseId) + score += (1.f / static_cast(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 + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.hpp b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.hpp new file mode 100644 index 00000000..3ce4922a --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveReleases.hpp @@ -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 . + */ + +#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& trackIds, std::size_t trackIndex) override; + + Database::ReleaseId getReleaseId(Database::TrackId trackId); + + Database::Db& _db; + }; +} // namespace Recommendation + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.cpp b/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.cpp new file mode 100644 index 00000000..e315c831 --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.cpp @@ -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 . + */ + +#include "DuplicateTracks.hpp" + +#include + +namespace Recommendation::PlaylistGeneratorConstraint +{ + float + DuplicateTracks::computeScore(const std::vector& 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 + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.hpp b/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.hpp new file mode 100644 index 00000000..a5f3c821 --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/DuplicateTracks.hpp @@ -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 . + */ + +#pragma once + +#include "IConstraint.hpp" + +namespace Recommendation::PlaylistGeneratorConstraint +{ + class DuplicateTracks : public IConstraint + { + private: + float computeScore(const std::vector& trackIds, std::size_t trackIndex) override; + }; +} // namespace Recommendation::PlaylistGeneratorConstraints + diff --git a/src/libs/services/recommendation/impl/playlist-constraints/IConstraint.hpp b/src/libs/services/recommendation/impl/playlist-constraints/IConstraint.hpp new file mode 100644 index 00000000..be24b393 --- /dev/null +++ b/src/libs/services/recommendation/impl/playlist-constraints/IConstraint.hpp @@ -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 . + */ + +#pragma once + +#include + +#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 diff --git a/src/libs/services/recommendation/include/services/recommendation/IPlaylistGeneratorService.hpp b/src/libs/services/recommendation/include/services/recommendation/IPlaylistGeneratorService.hpp new file mode 100644 index 00000000..4c87e2cb --- /dev/null +++ b/src/libs/services/recommendation/include/services/recommendation/IPlaylistGeneratorService.hpp @@ -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 . + */ + +#pragma once + +#include +#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 createPlaylistGeneratorService(Database::Db& db, IRecommendationService& recommandationService); +} // ns Recommendation + diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp index 45704ac9..a9ebba14 100644 --- a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -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& 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 linkTypes, std::size_t maxCount) const = 0; + }; std::unique_ptr createRecommendationService(Database::Db& db); - } // ns Recommendation diff --git a/src/lms/main.cpp b/src/lms/main.cpp index d7ecd162..185fc208 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -32,6 +32,7 @@ #include "services/cover/ICoverService.hpp" #include "services/database/Db.hpp" #include "services/database/Session.hpp" +#include "services/recommendation/IPlaylistGeneratorService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -258,6 +259,7 @@ int main(int argc, char* argv[]) Image::init(argv[0]); Service coverService {Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")}; Service recommendationService {Recommendation::createRecommendationService(database)}; + Service playlistGeneratorService {Recommendation::createPlaylistGeneratorService(database, *recommendationService.get())}; Service scannerService {Scanner::createScannerService(database, *recommendationService)}; scannerService->getEvents().scanComplete.connect([&] diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index c9992c5c..e5108f36 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -133,6 +133,12 @@ LmsApplication::instance() return reinterpret_cast(Wt::WApplication::instance()); } +Database::Db& +LmsApplication::getDb() +{ + return _db; +} + Database::Session& LmsApplication::getDbSession() { diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 35a58747..83d07dfb 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -59,9 +59,9 @@ class LmsApplication : public Wt::WApplication static std::unique_ptr create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager); static LmsApplication* instance(); - // Session application data std::shared_ptr getCoverResource() { return _coverResource; } + Database::Db& getDb(); Database::Session& getDbSession(); // always thread safe Database::ObjectPtr getUser(); diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 9731b291..4b37bb1d 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -35,7 +35,7 @@ #include "services/database/TrackList.hpp" #include "services/database/User.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "services/recommendation/IRecommendationService.hpp" +#include "services/recommendation/IPlaylistGeneratorService.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" #include "utils/Service.hpp" @@ -118,6 +118,8 @@ namespace PlayQueue::PlayQueue() : Template {Wt::WString::tr("Lms.PlayQueue.template")} { + initTrackLists(); + addFunction("id", &Wt::WTemplate::Functions::id); addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -146,13 +148,13 @@ PlayQueue::PlayQueue() { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - Database::TrackList::pointer trackList {getTrackList()}; - auto entries {trackList->getEntries()}; + Database::TrackList::pointer queue {getQueue()}; + auto entries {queue->getEntries()}; Random::shuffleContainer(entries); - getTrackList().modify()->clear(); - for (const auto& entry : entries) - LmsApp->getDbSession().create(entry->getTrack(), trackList); + queue.modify()->clear(); + for (const Database::TrackListEntry::pointer& entry : entries) + LmsApp->getDbSession().create(entry->getTrack(), queue); } _entriesContainer->clear(); addSome(); @@ -175,71 +177,59 @@ PlayQueue::PlayQueue() _radioBtn = bindNew("radio-btn"); _radioBtn->clicked().connect([=] { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + { + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - if (!LmsApp->getUser()->isDemo()) - LmsApp->getUser().modify()->setRadio(isRadioModeSet()); + if (!LmsApp->getUser()->isDemo()) + LmsApp->getUser().modify()->setRadio(isRadioModeSet()); + } + if (isRadioModeSet()) + enqueueRadioTracksIfNeeded(); }); + + bool isRadioModeSet {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (LmsApp->getUser()->isRadioSet()) - _radioBtn->setCheckState(Wt::CheckState::Checked); + isRadioModeSet = LmsApp->getUser()->isRadioSet(); + } + if (isRadioModeSet) + { + _radioBtn->setCheckState(Wt::CheckState::Checked); + enqueueRadioTracksIfNeeded(); } _nbTracks = bindNew("track-count"); _duration = bindNew("duration"); + LmsApp->getMediaPlayer().settingsLoaded.connect([=] + { + if (_mediaPlayerSettingsLoaded) + return; + + _mediaPlayerSettingsLoaded = true; + + std::size_t trackPos {}; + + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + trackPos = LmsApp->getUser()->getCurPlayingTrackPos(); + } + + loadTrack(trackPos, false); + }); + LmsApp->preQuit().connect([=] { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; if (LmsApp->getUser()->isDemo()) { - LMS_LOG(UI, DEBUG) << "Removing tracklist id " << _tracklistId.toString(); - auto tracklist = Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); - if (tracklist) - tracklist.remove(); + LMS_LOG(UI, DEBUG) << "Removing queue (tracklist id " << _queueId.toString() << ")"; + if (Database::TrackList::pointer queue {getQueue()}) + queue.remove(); } }); - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - Database::TrackList::pointer trackList; - - if (!LmsApp->getUser()->isDemo()) - { - LmsApp->getMediaPlayer().settingsLoaded.connect([=] - { - if (_mediaPlayerSettingsLoaded) - return; - - _mediaPlayerSettingsLoaded = true; - - std::size_t trackPos {}; - - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - trackPos = LmsApp->getUser()->getCurPlayingTrackPos(); - } - - loadTrack(trackPos, false); - }); - - static const std::string queuedListName {"__queued_tracks__"}; - trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackListType::Internal, LmsApp->getUserId()); - if (!trackList) - trackList = LmsApp->getDbSession().create(queuedListName, Database::TrackListType::Internal, false, LmsApp->getUser()); - } - else - { - static const std::string currentPlayQueueName {"__current__playqueue__"}; - trackList = LmsApp->getDbSession().create(currentPlayQueueName, Database::TrackListType::Internal, false, LmsApp->getUser()); - } - - _tracklistId = trackList->getId(); - } - updateInfo(); addSome(); } @@ -257,16 +247,16 @@ PlayQueue::isRadioModeSet() const } Database::TrackList::pointer -PlayQueue::getTrackList() const +PlayQueue::getQueue() const { - return Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); + return Database::TrackList::find(LmsApp->getDbSession(), _queueId); } bool PlayQueue::isFull() const { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - return getTrackList()->getCount() == getCapacity(); + return getQueue()->getCount() == getCapacity(); } void @@ -274,7 +264,7 @@ PlayQueue::clearTracks() { { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - getTrackList().modify()->clear(); + getQueue().modify()->clear(); } _entriesContainer->clear(); @@ -295,17 +285,16 @@ PlayQueue::loadTrack(std::size_t pos, bool play) updateCurrentTrack(false); Database::TrackId trackId {}; - bool addRadioTrack {}; std::optional replayGain {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::TrackList::pointer tracklist {getTrackList()}; + const Database::TrackList::pointer queue {getQueue()}; // If out of range, stop playing - if (pos >= tracklist->getCount()) + if (pos >= queue->getCount()) { - if (!isRepeatAllSet() || tracklist->getCount() == 0) + if (!isRepeatAllSet() || queue->getCount() == 0) { stop(); return; @@ -314,12 +303,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play) pos = 0; } - // If last and radio mode, fill the next song - if (isRadioModeSet() && pos == tracklist->getCount() - 1) - addRadioTrack = true; - _trackPos = pos; - auto track = tracklist->getEntry(*_trackPos)->getTrack(); + const Database::Track::pointer track {queue->getEntry(*_trackPos)->getTrack()}; trackId = track->getId(); @@ -329,11 +314,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play) LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); } - if (addRadioTrack) - enqueueRadioTracks(); - + enqueueRadioTracksIfNeeded(); updateCurrentTrack(true); - trackSelected.emit(trackId, play, replayGain ? *replayGain : 0); } @@ -361,14 +343,39 @@ PlayQueue::playNext() loadTrack(*_trackPos + 1, true); } +void +PlayQueue::initTrackLists() +{ + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + + Database::TrackList::pointer queue; + Database::TrackList::pointer radioStartingTracks; + + if (!LmsApp->getUser()->isDemo()) + { + static const std::string queueName {"__queued_tracks__"}; + queue = Database::TrackList::find(LmsApp->getDbSession(), queueName, Database::TrackListType::Internal, LmsApp->getUserId()); + if (!queue) + queue = LmsApp->getDbSession().create(queueName, Database::TrackListType::Internal, false, LmsApp->getUser()); + } + else + { + static const std::string queueName {"__temp_queue__"}; + queue = LmsApp->getDbSession().create(queueName, Database::TrackListType::Internal, false, LmsApp->getUser()); + } + + _queueId = queue->getId(); +} + void PlayQueue::updateInfo() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto trackCount {getTrackList()->getCount()}; + const Database::TrackList::pointer queue {getQueue()}; + const auto trackCount {queue->getCount()}; _nbTracks->setText(Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount)); - _duration->setText(Utils::durationToString(getTrackList()->getDuration())); + _duration->setText(Utils::durationToString(queue->getDuration())); } void @@ -392,9 +399,10 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - auto tracklist {getTrackList()}; + Database::TrackList::pointer queue {getQueue()}; + const std::size_t queueSize {queue->getCount()}; - std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > getCapacity() ? getCapacity() - tracklist->getCount() : trackIds.size()}; + std::size_t nbTracksToEnqueue {queueSize + trackIds.size() > getCapacity() ? getCapacity() - queueSize : trackIds.size()}; for (const Database::TrackId trackId : trackIds) { Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; @@ -404,7 +412,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) if (nbTracksQueued == nbTracksToEnqueue) break; - LmsApp->getDbSession().create(track, tracklist); + LmsApp->getDbSession().create(track, queue); nbTracksQueued++; } } @@ -468,13 +476,12 @@ PlayQueue::addSome() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - auto tracklist = getTrackList(); - - auto tracklistEntries = tracklist->getEntries(_entriesContainer->getCount(), _batchSize); + const Database::TrackList::pointer queue {getQueue()}; + const auto tracklistEntries {queue->getEntries(_entriesContainer->getCount(), _batchSize)}; for (const Database::TrackListEntry::pointer& tracklistEntry : tracklistEntries) addEntry(tracklistEntry); - _entriesContainer->setHasMore(_entriesContainer->getCount() < tracklist->getCount()); + _entriesContainer->setHasMore(_entriesContainer->getCount() < queue->getCount()); } void @@ -566,14 +573,32 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) ->setLink(Wt::WLink {std::make_unique(trackId)}); } +void +PlayQueue::enqueueRadioTracksIfNeeded() +{ + if (!isRadioModeSet()) + return; + + bool addTracks {}; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::TrackList::pointer queue {getQueue()}; + + // If out of range, stop playing + if (_trackPos >= queue->getCount() - 1) + addTracks = true; + } + + if (addTracks) + enqueueRadioTracks(); +} + void PlayQueue::enqueueRadioTracks() { - const auto similarTrackIds {Service::get()->findSimilarTracksFromTrackList(_tracklistId, 3)}; - - std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); - Random::shuffleContainer(trackToAddIds); - enqueueTracks(trackToAddIds); + std::vector trackIds = Service::get()->extendPlaylist(_queueId, 15); + enqueueTracks(trackIds); } std::optional @@ -602,9 +627,9 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) case MediaPlayer::Settings::ReplayGain::Mode::Auto: { - const auto trackList {getTrackList()}; - const auto prevEntry {pos > 0 ? trackList->getEntry(pos - 1) : Database::TrackListEntry::pointer {}}; - const auto nextEntry {trackList->getEntry(pos + 1)}; + const Database::TrackList::pointer queue {getQueue()}; + const Database::TrackListEntry::pointer prevEntry {pos > 0 ? queue->getEntry(pos - 1) : Database::TrackListEntry::pointer {}}; + const Database::TrackListEntry::pointer nextEntry {queue->getEntry(pos + 1)}; const Database::Track::pointer prevTrack {prevEntry ? prevEntry->getTrack() : Database::Track::pointer {}}; const Database::Track::pointer nextTrack {nextEntry ? nextEntry->getTrack() : Database::Track::pointer {}}; @@ -747,12 +772,11 @@ PlayQueue::exportToTrackList(Database::TrackListId trackListId) trackList.modify()->clear(); Track::FindParameters params; - params.setTrackList(_tracklistId); + params.setTrackList(_queueId); params.setDistinct(false); params.setSortMethod(TrackSortMethod::TrackList); const auto tracks {Track::find(session, params)}; - for (const TrackId trackId : tracks.results) session.create(Track::find(session, trackId), trackList); } diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index f6e21076..bef41f68 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -32,10 +32,6 @@ #include "common/Template.hpp" -namespace Similarity -{ - class Finder; -} namespace Database { @@ -73,14 +69,17 @@ class PlayQueue : public Template constexpr std::size_t getCapacity() const { return _capacity; } private: + void initTrackLists(); + void notifyAddedTracks(std::size_t nbAddedTracks) const; - Database::ObjectPtr getTrackList() const; + Database::ObjectPtr getQueue() const; bool isFull() const; void clearTracks(); std::size_t enqueueTracks(const std::vector& trackIds); void addSome(); void addEntry(const Database::ObjectPtr& entry); + void enqueueRadioTracksIfNeeded(); void enqueueRadioTracks(); void updateInfo(); void updateCurrentTrack(bool selected); @@ -90,8 +89,6 @@ class PlayQueue : public Template void loadTrack(std::size_t pos, bool play); void stop(); - void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); - void addRadioTrackFromClusters(); std::optional getReplayGain(std::size_t pos, const Database::ObjectPtr& track) const; void saveAsTrackList(); @@ -102,7 +99,7 @@ class PlayQueue : public Template static inline constexpr std::size_t _batchSize {12}; bool _mediaPlayerSettingsLoaded {}; - Database::TrackListId _tracklistId {}; + Database::TrackListId _queueId {}; InfiniteScrollingContainer* _entriesContainer {}; Wt::WText* _nbTracks {}; Wt::WText* _duration {};