Restored recommendations based on acoustic similarities (using musicnn), fixes #301

This commit is contained in:
emeric
2026-06-02 08:32:43 +02:00
parent 1524106124
commit eb7f65878f
227 changed files with 10324 additions and 4673 deletions
@@ -153,6 +153,8 @@ namespace lms::api::subsonic
{ "/getSimilarSongs", { handleGetSimilarSongsRequest } },
{ "/getSimilarSongs2", { handleGetSimilarSongs2Request } },
{ "/getTopSongs", { handleGetTopSongs } },
{ "/getSonicSimilarTracks", { handleGetSonicSimilarTracksRequest } },
{ "/findSonicPath", { handleFindSonicPathRequest } },
// Album/song lists
{ "/getAlbumList", { handleGetAlbumListRequest } },
+89 -8
View File
@@ -19,6 +19,9 @@
#include "Browsing.hpp"
#include <algorithm>
#include <array>
#include "core/ILogger.hpp"
#include "core/Random.hpp"
#include "core/Service.hpp"
@@ -109,7 +112,12 @@ namespace lms::api::subsonic
{
// API says: "Returns a random collection of songs from the given artist and similar artists"
const std::size_t similarArtistCount{ count / 5 };
std::vector<ArtistId> artistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist }, similarArtistCount) };
const recommendation::ArtistResults similarArtists{ core::Service<recommendation::IRecommendationService>::get()->findSimilarArtists(artistId, { TrackArtistLinkType::Artist }, similarArtistCount) };
std::vector<ArtistId> artistIds;
artistIds.reserve(similarArtists.size() + 1);
std::transform(std::cbegin(similarArtists), std::cend(similarArtists), std::back_inserter(artistIds), [](const auto& result) {
return result.id;
});
artistIds.push_back(artistId);
const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 };
@@ -140,7 +148,12 @@ namespace lms::api::subsonic
// API says: "Returns a random collection of songs from the given artist and similar artists"
// so let's extend this for release
const std::size_t similarReleaseCount{ count / 5 };
std::vector<ReleaseId> releaseIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarReleases(releaseId, similarReleaseCount) };
const recommendation::ReleaseResults similarReleases{ core::Service<recommendation::IRecommendationService>::get()->findSimilarReleases(releaseId, similarReleaseCount) };
std::vector<ReleaseId> releaseIds;
releaseIds.reserve(similarReleases.size() + 1);
std::transform(std::cbegin(similarReleases), std::cend(similarReleases), std::back_inserter(releaseIds), [](const auto& result) {
return result.id;
});
releaseIds.push_back(releaseId);
const std::size_t meanTrackCountPerRelease{ (count / releaseIds.size()) + 1 };
@@ -168,7 +181,14 @@ namespace lms::api::subsonic
std::vector<TrackId> findSimilarSongs(RequestContext& /*context*/, TrackId trackId, std::size_t count)
{
return core::Service<recommendation::IRecommendationService>::get()->findSimilarTracks({ trackId }, count);
const std::array<TrackId, 1> trackIdSpan{ trackId };
const recommendation::TrackResults similarTracks{ core::Service<recommendation::IRecommendationService>::get()->findSimilarTracks(trackIdSpan, count) };
std::vector<TrackId> trackIds;
trackIds.reserve(similarTracks.size());
std::transform(std::cbegin(similarTracks), std::cend(similarTracks), std::back_inserter(trackIds), [](const auto& result) {
return result.id;
});
return trackIds;
}
Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
@@ -565,16 +585,16 @@ namespace lms::api::subsonic
});
}
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist }, count) };
auto similarArtists{ core::Service<recommendation::IRecommendationService>::get()->findSimilarArtists(id, { TrackArtistLinkType::Artist }, count) };
{
auto transaction{ context.getDbSession().createReadTransaction() };
for (const ArtistId similarArtistId : similarArtistsId)
for (const auto& similarArtist : similarArtists)
{
const Artist::pointer similarArtist{ Artist::find(context.getDbSession(), similarArtistId) };
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist));
const Artist::pointer artist{ Artist::find(context.getDbSession(), similarArtist.id) };
if (artist)
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, artist));
}
}
@@ -653,4 +673,65 @@ namespace lms::api::subsonic
return response;
}
Response handleGetSonicSimilarTracksRequest(RequestContext& context)
{
// Mandatory params
const auto trackId{ getMandatoryParameterAs<TrackId>(context.getParameters(), "id") };
// Optional params
std::size_t count{ getParameterAs<std::size_t>(context.getParameters(), "count").value_or(10) };
if (count > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize };
const auto similarTracks{ core::Service<recommendation::IRecommendationService>::get()->findSimilarTracks(std::span{ &trackId, 1 }, count) };
auto transaction{ context.getDbSession().createReadTransaction() };
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
for (const auto& similarTrack : similarTracks)
{
const Track::pointer track{ Track::find(context.getDbSession(), similarTrack.id) };
if (track)
{
Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") };
sonicMatchNode.setAttribute("similarity", 1.0F - similarTrack.distance);
sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser()));
}
}
return response;
}
Response handleFindSonicPathRequest(RequestContext& context)
{
// Mandatory params
const auto startTrackId{ getMandatoryParameterAs<TrackId>(context.getParameters(), "startSongId") };
const auto endTrackId{ getMandatoryParameterAs<TrackId>(context.getParameters(), "endSongId") };
// Optional params
std::size_t count{ getParameterAs<std::size_t>(context.getParameters(), "count").value_or(25) };
if (count > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize };
const auto pathTracks{ core::Service<recommendation::IRecommendationService>::get()->findTrackSimilarityPath(startTrackId, endTrackId, count) };
auto transaction{ context.getDbSession().createReadTransaction() };
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
for (const auto& pathTrack : pathTracks)
{
const Track::pointer track{ Track::find(context.getDbSession(), pathTrack.id) };
if (track)
{
Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") };
sonicMatchNode.setAttribute("similarity", 1.0F - pathTrack.distance);
sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser()));
}
}
return response;
}
} // namespace lms::api::subsonic
@@ -38,4 +38,6 @@ namespace lms::api::subsonic
Response handleGetSimilarSongsRequest(RequestContext& context);
Response handleGetSimilarSongs2Request(RequestContext& context);
Response handleGetTopSongs(RequestContext& context);
Response handleGetSonicSimilarTracksRequest(RequestContext& context);
Response handleFindSonicPathRequest(RequestContext& context);
} // namespace lms::api::subsonic
@@ -58,6 +58,7 @@ namespace lms::api::subsonic
Extension{ "songLyrics", 1 },
Extension{ "transcodeOffset", 1 },
Extension{ "transcoding", 1 },
Extension{ "sonicSimilarity", 1 },
};
for (const Extension& extension : extensions)
@@ -17,6 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <limits>
#include <sstream>
#include <gtest/gtest.h>
@@ -113,4 +114,23 @@ namespace lms::api::subsonic::tests
EXPECT_EQ(oss.str(), expected);
}
TEST(SubsonicResponse, jsonNaNAndInfinity)
{
Response response{ Response::createOkResponse(defaultServerProtocolVersion) };
Response::Node& node{ response.createNode("MyMath") };
node.setAttribute("finite", 1.25F);
node.setAttribute("nan", std::numeric_limits<float>::quiet_NaN());
node.setAttribute("negInf", -std::numeric_limits<float>::infinity());
node.setAttribute("posInf", std::numeric_limits<float>::infinity());
std::ostringstream oss;
response.write(oss, ResponseFormat::json);
std::string expected{ R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"${VERSION}","status":"ok","type":"lms","version":"1.16.1","MyMath":{"finite":1.25,"nan":null,"negInf":null,"posInf":null}}})" };
expected = core::stringUtils::replaceInString(expected, "${VERSION}", core::getVersion());
EXPECT_EQ(oss.str(), expected);
}
} // namespace lms::api::subsonic::tests