Can now browse by directorie in the Subsonic API, ref #474

This commit is contained in:
emeric
2024-07-13 14:22:19 +02:00
parent 9292b05732
commit 980709dbcf
20 changed files with 613 additions and 224 deletions
+33 -5
View File
@@ -33,10 +33,25 @@ namespace lms::db
{ {
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };
for (std::string_view keyword : params.keywords)
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
if (params.artist.isValid()
|| params.release.isValid())
{
query.join("track t ON t.directory_id = d.id");
query.groupBy("d.id");
}
if (params.parentDirectory.isValid())
query.where("d.parent_directory_id = ?").bind(params.parentDirectory);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid()) if (params.artist.isValid())
{ {
query.join("track t ON t.directory_id = d.id") query.join("artist a ON a.id = t_a_l.artist_id")
.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id") .join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?") .where("a.id = ?")
.bind(params.artist); .bind(params.artist);
@@ -57,10 +72,11 @@ namespace lms::db
} }
query.where(oss.str()); query.where(oss.str());
} }
query.groupBy("d.id");
} }
if (params.withNoTrack)
query.where("NOT EXISTS (SELECT 1 FROM track t WHERE t.directory_id = d.id)");
return query; return query;
} }
} // namespace } // namespace
@@ -108,10 +124,16 @@ namespace lms::db
}); });
} }
RangeResults<Directory::pointer> Directory::find(Session& session, const FindParameters& params)
{
auto query{ createQuery(session, params) };
return utils::execRangeQuery<Directory::pointer>(query, params.range);
}
void Directory::find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func) void Directory::find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func)
{ {
auto query{ createQuery(session, params) }; auto query{ createQuery(session, params) };
utils::forEachQueryResult(query, [&func](const Directory::pointer& dir) { utils::forEachQueryRangeResult(query, params.range, [&func](const Directory::pointer& dir) {
func(dir); func(dir);
}); });
} }
@@ -131,6 +153,12 @@ namespace lms::db
return utils::execRangeQuery<DirectoryId>(query, range); return utils::execRangeQuery<DirectoryId>(query, range);
} }
RangeResults<Directory::pointer> Directory::findRootDirectories(Session& session, std::optional<Range> range)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.parent_directory_id IS NULL") };
return utils::execRangeQuery<Directory::pointer>(query, range);
}
void Directory::setAbsolutePath(const std::filesystem::path& p) void Directory::setAbsolutePath(const std::filesystem::path& p)
{ {
assert(p.is_absolute()); assert(p.is_absolute());
+5 -1
View File
@@ -53,7 +53,8 @@ namespace lms::db
|| params.dateRange || params.dateRange
|| params.artist.isValid() || params.artist.isValid()
|| params.clusters.size() == 1 || params.clusters.size() == 1
|| params.mediaLibrary.isValid()) || params.mediaLibrary.isValid()
|| params.directory.isValid())
{ {
query.join("track t ON t.release_id = r.id"); query.join("track t ON t.release_id = r.id");
} }
@@ -61,6 +62,9 @@ namespace lms::db
if (params.mediaLibrary.isValid()) if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary); query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
if (!params.releaseType.empty()) if (!params.releaseType.empty())
{ {
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id"); query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
+3
View File
@@ -154,6 +154,9 @@ namespace lms::db
if (params.mediaLibrary.isValid()) if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary); query.where("t.media_library_id = ?").bind(params.mediaLibrary);
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
switch (params.sortMethod) switch (params.sortMethod)
{ {
case TrackSortMethod::None: case TrackSortMethod::None:
@@ -21,12 +21,16 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include <vector>
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp" #include "database/DirectoryId.hpp"
#include "database/ReleaseId.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -42,20 +46,44 @@ namespace lms::db
struct FindParameters struct FindParameters
{ {
std::optional<Range> range; std::optional<Range> range;
ArtistId artist; // only tracks that involve this artist std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
ArtistId artist; // only directory that involve this artist
ReleaseId release; // only releases that involve this artist
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
DirectoryId parentDirectory; // If set, directories that have this parent
bool withNoTrack{}; // If set, directories that do not contain any track
FindParameters& setRange(std::optional<Range> _range) FindParameters& setRange(std::optional<Range> _range)
{ {
range = _range; range = _range;
return *this; return *this;
} }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
{
keywords = _keywords;
return *this;
}
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
{ {
artist = _artist; artist = _artist;
trackArtistLinkTypes = _trackArtistLinkTypes; trackArtistLinkTypes = _trackArtistLinkTypes;
return *this; return *this;
} }
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setParentDirectory(DirectoryId _parentDirectory)
{
parentDirectory = _parentDirectory;
return *this;
}
FindParameters& setWithNoTrack(bool _withNoTrack)
{
withNoTrack = _withNoTrack;
return *this;
}
}; };
// find // find
@@ -63,13 +91,15 @@ namespace lms::db
static pointer find(Session& session, DirectoryId id); static pointer find(Session& session, DirectoryId id);
static pointer find(Session& session, const std::filesystem::path& path); static pointer find(Session& session, const std::filesystem::path& path);
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<Directory::pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
// getters // getters
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; } const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
ObjectPtr<Directory> getParent() const { return _parent; } ObjectPtr<Directory> getParentDirectory() const { return _parent; }
// setters // setters
void setAbsolutePath(const std::filesystem::path& p); void setAbsolutePath(const std::filesystem::path& p);
@@ -33,6 +33,7 @@
#include "core/UUID.hpp" #include "core/UUID.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/ClusterId.hpp" #include "database/ClusterId.hpp"
#include "database/DirectoryId.hpp"
#include "database/MediaLibraryId.hpp" #include "database/MediaLibraryId.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
@@ -96,6 +97,7 @@ namespace lms::db
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
std::string releaseType; // If set, albums that has this release type std::string releaseType; // If set, albums that has this release type
MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library
DirectoryId directory; // if set, tracks in this directory
FindParameters& setClusters(std::span<const ClusterId> _clusters) FindParameters& setClusters(std::span<const ClusterId> _clusters)
{ {
@@ -150,6 +152,11 @@ namespace lms::db
mediaLibrary = _mediaLibrary; mediaLibrary = _mediaLibrary;
return *this; return *this;
} }
FindParameters& setDirectory(DirectoryId _directory)
{
directory = _directory;
return *this;
}
}; };
Release() = default; Release() = default;
@@ -37,6 +37,7 @@
#include "core/UUID.hpp" #include "core/UUID.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/ClusterId.hpp" #include "database/ClusterId.hpp"
#include "database/DirectoryId.hpp"
#include "database/MediaLibraryId.hpp" #include "database/MediaLibraryId.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
@@ -81,6 +82,7 @@ namespace lms::db
std::optional<int> trackNumber; // matching this track number std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number std::optional<int> discNumber; // matching this disc number
MediaLibraryId mediaLibrary; // If set, tracks in this library MediaLibraryId mediaLibrary; // If set, tracks in this library
DirectoryId directory; // if set, tracks in this directory
FindParameters& setClusters(std::span<const ClusterId> _clusters) FindParameters& setClusters(std::span<const ClusterId> _clusters)
{ {
@@ -165,6 +167,11 @@ namespace lms::db
mediaLibrary = _mediaLibrary; mediaLibrary = _mediaLibrary;
return *this; return *this;
} }
FindParameters& setDirectory(DirectoryId _directory)
{
directory = _directory;
return *this;
}
}; };
struct PathResult struct PathResult
+104 -4
View File
@@ -107,23 +107,35 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto dir{ child->getParent() }; auto dir{ child->getParentDirectory() };
EXPECT_EQ(dir, Directory::pointer{}); EXPECT_EQ(dir, Directory::pointer{});
} }
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
child.get().modify()->setParent(parent.lockAndGet()); child.get().modify()->setParent(parent.get());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto dir{ child->getParent() }; auto dir{ child->getParentDirectory() };
ASSERT_NE(dir, Directory::pointer{}); ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getId(), parent.getId()); EXPECT_EQ(dir->getId(), parent.getId());
} }
{
auto transaction{ session.createReadTransaction() };
Directory::pointer foundDir;
Directory::find(session, Directory::FindParameters{}.setParentDirectory(parent->getId()), [&](const Directory::pointer& dir) {
ASSERT_EQ(foundDir, Directory::pointer{});
foundDir = dir;
});
ASSERT_NE(foundDir, Directory::pointer{});
EXPECT_EQ(foundDir->getId(), child.getId());
}
} }
TEST_F(DatabaseFixture, Directory_orphaned) TEST_F(DatabaseFixture, Directory_orphaned)
@@ -141,7 +153,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
child.get().modify()->setParent(parent.lockAndGet()); child.get().modify()->setParent(parent.get());
} }
{ {
@@ -152,4 +164,92 @@ namespace lms::db::tests
EXPECT_EQ(directories.front(), child.getId()); EXPECT_EQ(directories.front(), child.getId());
} }
} }
TEST_F(DatabaseFixture, Directory_findRootDirectories)
{
ScopedDirectory parent1{ session, "/root1" };
ScopedDirectory child{ session, "/root1/child" };
ScopedDirectory parent2{ session, "/root2" };
{
auto transaction{ session.createWriteTransaction() };
child.get().modify()->setParent(parent1.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findRootDirectories(session).results };
ASSERT_EQ(directories.size(), 2);
EXPECT_EQ(directories[0]->getId(), parent1.getId());
EXPECT_EQ(directories[1]->getId(), parent2.getId());
}
}
TEST_F(DatabaseFixture, Directory_findNonTrackDirectories)
{
ScopedDirectory parent{ session, "/root" };
ScopedDirectory child1{ session, "/root/child1" };
ScopedDirectory child2{ session, "/root/child2" };
ScopedTrack track{ session };
{
auto transaction{ session.createWriteTransaction() };
child1.get().modify()->setParent(parent.get());
child2.get().modify()->setParent(parent.get());
}
{
auto transaction{ session.createReadTransaction() };
Directory::FindParameters params;
params.setWithNoTrack(true);
auto res{ Directory::find(session, params).results };
ASSERT_EQ(res.size(), 3);
EXPECT_EQ(res[0]->getId(), parent.getId());
EXPECT_EQ(res[1]->getId(), child1.getId());
EXPECT_EQ(res[2]->getId(), child2.getId());
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setDirectory(child2.get());
}
{
auto transaction{ session.createReadTransaction() };
Directory::FindParameters params;
params.setWithNoTrack(true);
auto res{ Directory::find(session, params).results };
ASSERT_EQ(res.size(), 2);
EXPECT_EQ(res[0]->getId(), parent.getId());
EXPECT_EQ(res[1]->getId(), child1.getId());
}
}
TEST_F(DatabaseFixture, Directory_findWithKeywords)
{
ScopedDirectory parent{ session, "/root" };
ScopedDirectory child1{ session, "/root/foo" };
ScopedDirectory child2{ session, "/root/bar/foo" };
{
auto transaction{ session.createReadTransaction() };
Directory::FindParameters params;
params.setKeywords({ "foo" });
auto res{ Directory::find(session, params).results };
ASSERT_EQ(res.size(), 2);
EXPECT_EQ(res[0]->getId(), child1.getId());
EXPECT_EQ(res[1]->getId(), child2.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+21
View File
@@ -31,6 +31,11 @@ namespace lms::api::subsonic
return "ar-" + id.toString(); return "ar-" + id.toString();
} }
std::string idToString(db::DirectoryId id)
{
return "dir-" + id.toString();
}
std::string idToString(db::MediaLibraryId id) std::string idToString(db::MediaLibraryId id)
{ {
// No need to prefix as this is only used at well known places // No need to prefix as this is only used at well known places
@@ -76,6 +81,22 @@ namespace lms::core::stringUtils
return std::nullopt; return std::nullopt;
} }
template<>
std::optional<db::DirectoryId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
if (values[0] != "dir")
return std::nullopt;
if (const auto value{ core::stringUtils::readAs<db::DirectoryId::ValueType>(values[1]) })
return db::DirectoryId{ *value };
return std::nullopt;
}
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str) std::optional<db::MediaLibraryId> readAs(std::string_view str)
{ {
+5
View File
@@ -21,6 +21,7 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp"
#include "database/MediaLibraryId.hpp" #include "database/MediaLibraryId.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
#include "database/TrackId.hpp" #include "database/TrackId.hpp"
@@ -33,6 +34,7 @@ namespace lms::api::subsonic
}; };
std::string idToString(db::ArtistId id); std::string idToString(db::ArtistId id);
std::string idToString(db::DirectoryId id);
std::string idToString(db::MediaLibraryId id); std::string idToString(db::MediaLibraryId id);
std::string idToString(db::ReleaseId id); std::string idToString(db::ReleaseId id);
std::string idToString(db::TrackId id); std::string idToString(db::TrackId id);
@@ -49,6 +51,9 @@ namespace lms::core::stringUtils
template<> template<>
std::optional<db::ArtistId> readAs(std::string_view str); std::optional<db::ArtistId> readAs(std::string_view str);
template<>
std::optional<db::DirectoryId> readAs(std::string_view str);
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str); std::optional<db::MediaLibraryId> readAs(std::string_view str);
+1 -1
View File
@@ -168,7 +168,7 @@ namespace lms::api::subsonic
{ "/getAlbum", { handleGetAlbumRequest } }, { "/getAlbum", { handleGetAlbumRequest } },
{ "/getSong", { handleGetSongRequest } }, { "/getSong", { handleGetSongRequest } },
{ "/getVideos", { handleNotImplemented } }, { "/getVideos", { handleNotImplemented } },
{ "/getArtistInfo", { handleGetArtistInfoRequest } }, { "/getArtistInfo", { handleNotImplemented } },
{ "/getArtistInfo2", { handleGetArtistInfo2Request } }, { "/getArtistInfo2", { handleGetArtistInfo2Request } },
{ "/getAlbumInfo", { handleNotImplemented } }, { "/getAlbumInfo", { handleNotImplemented } },
{ "/getAlbumInfo2", { handleNotImplemented } }, { "/getAlbumInfo2", { handleNotImplemented } },
@@ -169,7 +169,7 @@ namespace lms::api::subsonic
for (const ReleaseId releaseId : releases.results) for (const ReleaseId releaseId : releases.results)
{ {
const Release::pointer release{ Release::find(context.dbSession, releaseId) }; const Release::pointer release{ Release::find(context.dbSession, releaseId) };
albumListNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3)); albumListNode.addArrayChild("album", createAlbumNode(context, release, id3));
} }
return response; return response;
@@ -187,6 +187,8 @@ namespace lms::api::subsonic
feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() }; feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() };
// We don't support starring directories
if (id3)
{ {
feedback::IFeedbackService::ArtistFindParameters artistFindParams; feedback::IFeedbackService::ArtistFindParameters artistFindParams;
artistFindParams.setUser(context.user->getId()); artistFindParams.setUser(context.user->getId());
@@ -194,7 +196,7 @@ namespace lms::api::subsonic
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
{ {
if (auto artist{ Artist::find(context.dbSession, artistId) }) if (auto artist{ Artist::find(context.dbSession, artistId) })
starredNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3)); starredNode.addArrayChild("artist", createArtistNode(context, artist));
} }
} }
@@ -205,7 +207,7 @@ namespace lms::api::subsonic
for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results) for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
{ {
if (auto release{ Release::find(context.dbSession, releaseId) }) if (auto release{ Release::find(context.dbSession, releaseId) })
starredNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3)); starredNode.addArrayChild("album", createAlbumNode(context, release, id3));
} }
for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results) for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results)
+255 -151
View File
@@ -24,11 +24,13 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.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"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IRecommendationService.hpp" #include "services/recommendation/IRecommendationService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
@@ -48,123 +50,58 @@ namespace lms::api::subsonic
namespace namespace
{ {
Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) std::vector<Directory::pointer> getRootDirectories(Session& session, MediaLibraryId libraryId)
{ {
// Mandatory params std::vector<Directory::pointer> res;
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
// Optional params
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& artistInfoNode{ response.createNode(id3 ? Response::Node::Key{ "artistInfo2" } : Response::Node::Key{ "artistInfo" }) };
if (libraryId.isValid())
{ {
auto transaction{ context.dbSession.createReadTransaction() }; const MediaLibrary::pointer library{ MediaLibrary::find(session, libraryId) };
if (!library)
throw BadParameterGenericError{ "id" };
const Artist::pointer artist{ Artist::find(context.dbSession, id) }; if (Directory::pointer rootDirectory{ Directory::find(session, library->getPath()) })
if (!artist) res.push_back(rootDirectory);
throw RequestedDataNotFoundError{}; }
else
std::optional<core::UUID> artistMBID{ artist->getMBID() }; {
if (artistMBID) res = Directory::findRootDirectories(session).results;
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
} }
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) }; return res;
{
auto transaction{ context.dbSession.createReadTransaction() };
for (const ArtistId similarArtistId : similarArtistsId)
{
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist, context.user, id3));
}
}
return response;
} }
Response handleGetArtistsRequestCommon(RequestContext& context, bool id3) struct IndexComparator
{ {
// Optional params constexpr bool operator()(char lhs, char rhs) const
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& artistsNode{ response.createNode(id3 ? "artists" : "indexes") };
artistsNode.setAttribute("ignoredArticles", "");
artistsNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
Artist::FindParameters parameters;
{ {
auto transaction{ context.dbSession.createReadTransaction() }; if (lhs == '#' && std::isalpha(rhs))
return false;
if (rhs == '#' && std::isalpha(lhs))
return true;
parameters.setSortMethod(ArtistSortMethod::SortName); return lhs < rhs;
switch (context.user->getSubsonicArtistListMode())
{
case SubsonicArtistListMode::AllArtists:
break;
case SubsonicArtistListMode::ReleaseArtists:
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
break;
case SubsonicArtistListMode::TrackArtists:
parameters.setLinkType(TrackArtistLinkType::Artist);
break;
}
} }
parameters.setMediaLibrary(mediaLibrary); };
// This endpoint does not scale: make sort lived transactions in order not to block the whole application using IndexMap = std::map<char, std::vector<Directory::pointer>, IndexComparator>;
void getIndexedChildDirectories(RequestContext& context, const Directory::pointer& parentDirectory, IndexMap& res)
{
Directory::FindParameters params;
params.setParentDirectory(parentDirectory->getId());
// first pass: dispatch the artists by first letter Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) {
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists..."); const std::string_view name{ directory->getName() };
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar; assert(!name.empty());
std::size_t currentArtistOffset{ 0 };
constexpr std::size_t batchSize{ 100 };
bool hasMoreArtists{ true };
while (hasMoreArtists)
{
auto transaction{ context.dbSession.createReadTransaction() };
parameters.setRange(Range{ currentArtistOffset, batchSize }); char sortChar;
const auto artists{ Artist::find(context.dbSession, parameters) }; if (name.empty() || !std::isalpha(name[0]))
for (const Artist::pointer& artist : artists.results) sortChar = '#';
{ else
std::string_view sortName{ artist->getSortName() }; sortChar = std::toupper(name[0]);
char sortChar; res[sortChar].push_back(directory);
if (sortName.empty() || !std::isalpha(sortName[0])) });
sortChar = '?';
else
sortChar = std::toupper(sortName[0]);
artistsSortedByFirstChar[sortChar].push_back(artist->getId());
}
hasMoreArtists = artists.moreResults;
currentArtistOffset += artists.results.size();
}
// second pass: add each artist
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
{
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
indexNode.setAttribute("name", std::string{ sortChar });
for (const ArtistId artistId : artistIds)
{
auto transaction{ context.dbSession.createReadTransaction() };
if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) })
indexNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3));
}
}
return response;
} }
std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count) std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
@@ -266,6 +203,21 @@ namespace lms::api::subsonic
return response; return response;
} }
Release::pointer getReleaseFromDirectory(Session& session, DirectoryId directoryId)
{
auto transaction{ session.createReadTransaction() };
Release::FindParameters params;
params.setDirectory(directoryId);
params.setRange(Range{ 0, 1 }); // only support 1 directory <-> 1 release
Release::pointer res;
Release::find(session, params, [&](const Release::pointer& release) {
res = release;
});
return res;
}
} // namespace } // namespace
Response handleGetMusicFoldersRequest(RequestContext& context) Response handleGetMusicFoldersRequest(RequestContext& context)
@@ -286,64 +238,112 @@ namespace lms::api::subsonic
Response handleGetIndexesRequest(RequestContext& context) Response handleGetIndexesRequest(RequestContext& context)
{ {
return handleGetArtistsRequestCommon(context, false /* no id3 */); // Optional params
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& indexesNode{ response.createNode("indexes") };
indexesNode.setAttribute("ignoredArticles", "");
indexesNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
auto transaction{ context.dbSession.createReadTransaction() };
const std::vector<Directory::pointer> rootDirectories{ getRootDirectories(context.dbSession, mediaLibrary) };
IndexMap indexedDirectories;
for (const Directory::pointer& rootdirectory : rootDirectories)
{
Track::FindParameters params;
params.setDirectory(rootdirectory->getId());
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
indexesNode.addArrayChild("child", createSongNode(context, track, context.user));
});
getIndexedChildDirectories(context, rootdirectory, indexedDirectories);
}
for (const auto& [index, directories] : indexedDirectories)
{
Response::Node& indexNode{ indexesNode.createArrayChild("index") };
indexNode.setAttribute("name", std::string{ index });
for (const Directory::pointer& directory : directories)
{
// Legacy behavior: all sub directories are considered as artists (even if this is just containing an album, or just an intermediary directory)
Response::Node childNode;
childNode.setAttribute("id", idToString(directory->getId()));
childNode.setAttribute("name", directory->getName());
indexNode.addArrayChild("artist", std::move(childNode));
}
}
return response;
} }
Response handleGetMusicDirectoryRequest(RequestContext& context) Response handleGetMusicDirectoryRequest(RequestContext& context)
{ {
// Mandatory params // Mandatory params
const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") }; const auto directoryId{ getMandatoryParameterAs<DirectoryId>(context.parameters, "id") };
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
const auto root{ getParameterAs<RootId>(context.parameters, "id") };
if (!root && !artistId && !releaseId)
throw BadParameterGenericError{ "id" };
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& directoryNode{ response.createNode("directory") }; Response::Node& directoryNode{ response.createNode("directory") };
auto transaction{ context.dbSession.createReadTransaction() }; auto transaction{ context.dbSession.createReadTransaction() };
if (root) const Directory::pointer directory{ Directory::find(context.dbSession, directoryId) };
{ if (!directory)
directoryNode.setAttribute("id", idToString(RootId{})); throw RequestedDataNotFoundError{};
directoryNode.setAttribute("name", "Music");
// TODO: this does not scale when a lot of artists are present if (const Release::pointer release{ getReleaseFromDirectory(context.dbSession, directoryId) })
Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName), [&](const Artist::pointer& artist) { {
directoryNode.addArrayChild("child", createArtistNode(context, artist, context.user, false /* no id3 */)); directoryNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), release->getId()));
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid())
directoryNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
}
directoryNode.setAttribute("id", idToString(directory->getId()));
directoryNode.setAttribute("name", directory->getName());
// Original Subsonic does not report parent if this the parent directory is the root directory
if (const Directory::pointer parentDirectory{ directory->getParentDirectory() })
directoryNode.setAttribute("parent", idToString(parentDirectory->getId()));
// list all sub directories
{
Directory::FindParameters params;
params.setParentDirectory(directory->getId());
Directory::find(context.dbSession, params, [&](const Directory::pointer& subDirectory) {
const Release::pointer release{ getReleaseFromDirectory(context.dbSession, subDirectory->getId()) };
if (release)
{
directoryNode.addArrayChild("child", createAlbumNode(context, release, false, subDirectory));
}
else
{
Response::Node childNode;
childNode.setAttribute("id", idToString(subDirectory->getId()));
childNode.setAttribute("title", subDirectory->getName());
childNode.setAttribute("isDir", true);
childNode.setAttribute("parent", idToString(directory->getId()));
directoryNode.addArrayChild("child", std::move(childNode));
}
}); });
} }
else if (artistId)
// list all tracks
{ {
directoryNode.setAttribute("id", idToString(*artistId)); Track::FindParameters params;
params.setDirectory(directory->getId());
auto artist{ Artist::find(context.dbSession, *artistId) }; Track::find(context.dbSession, params, [&](const Track::pointer& track) {
if (!artist)
throw RequestedDataNotFoundError{};
directoryNode.setAttribute("name", utils::makeNameFilesystemCompatible(artist->getName()));
Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release) {
directoryNode.addArrayChild("child", createAlbumNode(context, release, context.user, false /* no id3 */));
});
}
else if (releaseId)
{
directoryNode.setAttribute("id", idToString(*releaseId));
auto release{ Release::find(context.dbSession, *releaseId) };
if (!release)
throw RequestedDataNotFoundError{};
directoryNode.setAttribute("name", utils::makeNameFilesystemCompatible(release->getName()));
Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track) {
directoryNode.addArrayChild("child", createSongNode(context, track, context.user)); directoryNode.addArrayChild("child", createSongNode(context, track, context.user));
}); });
} }
else
throw BadParameterGenericError{ "id" };
return response; return response;
} }
@@ -370,7 +370,82 @@ namespace lms::api::subsonic
Response handleGetArtistsRequest(RequestContext& context) Response handleGetArtistsRequest(RequestContext& context)
{ {
return handleGetArtistsRequestCommon(context, true /* id3 */); // Optional params
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& artistsNode{ response.createNode("artists") };
artistsNode.setAttribute("ignoredArticles", "");
artistsNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
Artist::FindParameters parameters;
{
auto transaction{ context.dbSession.createReadTransaction() };
parameters.setSortMethod(ArtistSortMethod::SortName);
switch (context.user->getSubsonicArtistListMode())
{
case SubsonicArtistListMode::AllArtists:
break;
case SubsonicArtistListMode::ReleaseArtists:
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
break;
case SubsonicArtistListMode::TrackArtists:
parameters.setLinkType(TrackArtistLinkType::Artist);
break;
}
}
parameters.setMediaLibrary(mediaLibrary);
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
// first pass: dispatch the artists by first letter
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
std::size_t currentArtistOffset{ 0 };
constexpr std::size_t batchSize{ 100 };
bool hasMoreArtists{ true };
while (hasMoreArtists)
{
auto transaction{ context.dbSession.createReadTransaction() };
parameters.setRange(Range{ currentArtistOffset, batchSize });
const auto artists{ Artist::find(context.dbSession, parameters) };
for (const Artist::pointer& artist : artists.results)
{
std::string_view sortName{ artist->getSortName() };
char sortChar;
if (sortName.empty() || !std::isalpha(sortName[0]))
sortChar = '#';
else
sortChar = std::toupper(sortName[0]);
artistsSortedByFirstChar[sortChar].push_back(artist->getId());
}
hasMoreArtists = artists.moreResults;
currentArtistOffset += artists.results.size();
}
// second pass: add each artist
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
{
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
indexNode.setAttribute("name", std::string{ sortChar });
for (const ArtistId artistId : artistIds)
{
auto transaction{ context.dbSession.createReadTransaction() };
if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) })
indexNode.addArrayChild("artist", createArtistNode(context, artist));
}
}
return response;
} }
Response handleGetArtistRequest(RequestContext& context) Response handleGetArtistRequest(RequestContext& context)
@@ -385,11 +460,11 @@ namespace lms::api::subsonic
throw RequestedDataNotFoundError{}; throw RequestedDataNotFoundError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node artistNode{ createArtistNode(context, artist, context.user, true /* id3 */) }; Response::Node artistNode{ createArtistNode(context, artist) };
const auto releases{ Release::find(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) }; const auto releases{ Release::find(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
for (const Release::pointer& release : releases.results) for (const Release::pointer& release : releases.results)
artistNode.addArrayChild("album", createAlbumNode(context, release, context.user, true /* id3 */)); artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */));
response.addNode("artist", std::move(artistNode)); response.addNode("artist", std::move(artistNode));
@@ -408,11 +483,11 @@ namespace lms::api::subsonic
throw RequestedDataNotFoundError{}; throw RequestedDataNotFoundError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node albumNode{ createAlbumNode(context, release, context.user, true /* id3 */) }; Response::Node albumNode{ createAlbumNode(context, release, true /* id3 */) };
const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
for (const Track::pointer& track : tracks.results) for (const Track::pointer& track : tracks.results)
albumNode.addArrayChild("song", createSongNode(context, track, context.user)); albumNode.addArrayChild("song", createSongNode(context, track, true /* id3 */));
response.addNode("album", std::move(albumNode)); response.addNode("album", std::move(albumNode));
@@ -436,14 +511,43 @@ namespace lms::api::subsonic
return response; return response;
} }
Response handleGetArtistInfoRequest(RequestContext& context)
{
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
}
Response handleGetArtistInfo2Request(RequestContext& context) Response handleGetArtistInfo2Request(RequestContext& context)
{ {
return handleGetArtistInfoRequestCommon(context, true /* id3 */); // Mandatory params
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
// Optional params
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& artistInfoNode{ response.createNode(Response::Node::Key{ "artistInfo2" }) };
{
auto transaction{ context.dbSession.createReadTransaction() };
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
if (!artist)
throw RequestedDataNotFoundError{};
std::optional<core::UUID> artistMBID{ artist->getMBID() };
if (artistMBID)
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
}
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) };
{
auto transaction{ context.dbSession.createReadTransaction() };
for (const ArtistId similarArtistId : similarArtistsId)
{
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist));
}
}
return response;
} }
Response handleGetSimilarSongsRequest(RequestContext& context) Response handleGetSimilarSongsRequest(RequestContext& context)
@@ -23,7 +23,9 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/Release.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
#include "database/Session.hpp"
#include "database/TrackId.hpp" #include "database/TrackId.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
@@ -43,25 +45,50 @@ namespace lms::api::subsonic
std::vector<ArtistId> artistIds; std::vector<ArtistId> artistIds;
std::vector<ReleaseId> releaseIds; std::vector<ReleaseId> releaseIds;
std::vector<TrackId> trackIds; std::vector<TrackId> trackIds;
std::vector<DirectoryId> directoryIds;
}; };
StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters) StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters)
{ {
StarParameters res; StarParameters res;
// TODO handle parameters for legacy file browsing // id could be either a trackId or a directory id
res.directoryIds = getMultiParametersAs<DirectoryId>(parameters, "id");
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id"); res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId"); res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId"); res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId");
return res; return res;
} }
ReleaseId getReleaseFromDirectory(Session& session, DirectoryId directory)
{
auto transaction{ session.createReadTransaction() };
Release::FindParameters params;
params.setDirectory(directory);
params.setRange(Range{ 0, 1 }); // consider one directory <-> one release
Release::pointer res;
Release::find(session, params, [&](const Release::pointer& release) {
res = release;
});
return res ? res->getId() : ReleaseId{};
}
} // namespace } // namespace
Response handleStarRequest(RequestContext& context) Response handleStarRequest(RequestContext& context)
{ {
StarParameters params{ getStarParameters(context.parameters) }; StarParameters params{ getStarParameters(context.parameters) };
for (const DirectoryId id : params.directoryIds)
{
if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid())
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), releaseId);
}
for (const ArtistId id : params.artistIds) for (const ArtistId id : params.artistIds)
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id); core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
@@ -78,6 +105,12 @@ namespace lms::api::subsonic
{ {
StarParameters params{ getStarParameters(context.parameters) }; StarParameters params{ getStarParameters(context.parameters) };
for (const DirectoryId id : params.directoryIds)
{
if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid())
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), releaseId);
}
for (const ArtistId id : params.artistIds) for (const ArtistId id : params.artistIds)
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id); core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
@@ -25,6 +25,7 @@
#include "core/Random.hpp" #include "core/Random.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Directory.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"
@@ -113,7 +114,36 @@ namespace lms::api::subsonic
_ongoingScans[scanInfo] = { now, lastRetrievedId }; _ongoingScans[scanInfo] = { now, lastRetrievedId };
} }
void findRequestedArtists(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) void findRequestedArtistDirectories(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
{
// For now, no need to "accelerate" all this
// Find all the directories that match the name and that do not contain any track (considered by the legacy API as artists)
const std::size_t artistCount{ getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20) };
if (artistCount == 0)
return;
if (artistCount > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize };
const std::size_t artistOffset{ getParameterAs<std::size_t>(context.parameters, "artistOffset").value_or(0) };
Directory::FindParameters params;
params.setKeywords(keywords);
params.setRange(Range{artistOffset, artistCount});
params.setWithNoTrack(true);
// TODO media
Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) {
Response::Node childNode;
childNode.setAttribute("id", idToString(directory->getId()));
childNode.setAttribute("name", directory->getName());
childNode.setAttribute("isDir", true);
searchResultNode.addArrayChild("artist", std::move(childNode));
});
}
void findRequestedArtists(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
{ {
static ScanTracker<ArtistId> currentScansInProgress; static ScanTracker<ArtistId> currentScansInProgress;
@@ -135,7 +165,7 @@ namespace lms::api::subsonic
params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods
Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) { Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) {
searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
lastRetrievedId = artist->getId(); lastRetrievedId = artist->getId();
}); });
} }; } };
@@ -158,7 +188,7 @@ namespace lms::api::subsonic
{ {
Artist::find( Artist::find(
context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) { context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) {
searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
}, },
mediaLibrary); mediaLibrary);
lastRetrievedId = cachedLastRetrievedId; lastRetrievedId = cachedLastRetrievedId;
@@ -176,7 +206,7 @@ namespace lms::api::subsonic
} }
} }
void findRequestedAlbums(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) void findRequestedAlbums(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
{ {
static ScanTracker<ReleaseId> currentScansInProgress; static ScanTracker<ReleaseId> currentScansInProgress;
@@ -199,7 +229,7 @@ namespace lms::api::subsonic
params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods
Release::find(context.dbSession, params, [&](const Release::pointer& release) { Release::find(context.dbSession, params, [&](const Release::pointer& release) {
searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
lastRetrievedId = release->getId(); lastRetrievedId = release->getId();
}); });
} }; } };
@@ -222,7 +252,7 @@ namespace lms::api::subsonic
{ {
Release::find( Release::find(
context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) { context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) {
searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
}, },
mediaLibrary); mediaLibrary);
lastRetrievedId = cachedLastRetrievedId; lastRetrievedId = cachedLastRetrievedId;
@@ -240,7 +270,7 @@ namespace lms::api::subsonic
} }
} }
void findRequestedTracks(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) void findRequestedTracks(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
{ {
static ScanTracker<TrackId> currentScansInProgress; static ScanTracker<TrackId> currentScansInProgress;
@@ -263,7 +293,7 @@ namespace lms::api::subsonic
params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods
Track::find(context.dbSession, params, [&](const Track::pointer& track) { Track::find(context.dbSession, params, [&](const Track::pointer& track) {
searchResultNode.addArrayChild("song", createSongNode(context, track, user)); searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
lastRetrievedId = track->getId(); lastRetrievedId = track->getId();
}); });
} }; } };
@@ -286,7 +316,7 @@ namespace lms::api::subsonic
{ {
Track::find( Track::find(
context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) {
searchResultNode.addArrayChild("song", createSongNode(context, track, user)); searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
}, },
mediaLibrary); mediaLibrary);
lastRetrievedId = cachedLastRetrievedId; lastRetrievedId = cachedLastRetrievedId;
@@ -329,9 +359,12 @@ namespace lms::api::subsonic
auto transaction{ context.dbSession.createReadTransaction() }; auto transaction{ context.dbSession.createReadTransaction() };
findRequestedArtists(context, id3, keywords, mediaLibrary, context.user, searchResultNode); if (id3)
findRequestedAlbums(context, id3, keywords, mediaLibrary, context.user, searchResultNode); findRequestedArtists(context, keywords, mediaLibrary, searchResultNode);
findRequestedTracks(context, keywords, mediaLibrary, context.user, searchResultNode); else
findRequestedArtistDirectories(context, keywords, mediaLibrary, searchResultNode);
findRequestedAlbums(context, id3, keywords, mediaLibrary, searchResultNode);
findRequestedTracks(context, id3, keywords, mediaLibrary, searchResultNode);
return response; return response;
} }
+31 -24
View File
@@ -24,6 +24,7 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
@@ -39,7 +40,7 @@ namespace lms::api::subsonic
{ {
using namespace db; using namespace db;
Response::Node createAlbumNode(RequestContext& context, const Release::pointer& release, const User::pointer& user, bool id3) Response::Node createAlbumNode(RequestContext& context, const Release::pointer& release, bool id3, const Directory::pointer& directory)
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateAlbum"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateAlbum");
@@ -47,21 +48,38 @@ namespace lms::api::subsonic
if (id3) if (id3)
{ {
albumNode.setAttribute("id", idToString(release->getId()));
albumNode.setAttribute("name", release->getName()); albumNode.setAttribute("name", release->getName());
albumNode.setAttribute("songCount", release->getTrackCount()); albumNode.setAttribute("songCount", release->getTrackCount());
albumNode.setAttribute( albumNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(release->getDuration()).count());
"duration", std::chrono::duration_cast<std::chrono::seconds>(
release->getDuration())
.count());
} }
else else
{ {
albumNode.setAttribute("title", release->getName()); Directory::pointer directoryToReport{ directory };
if (!directoryToReport)
{
Directory::FindParameters params;
params.setRelease(release->getId());
params.setRange(Range{ 0, 1 }); // only support 1 directory <-> 1 release
Directory::find(context.dbSession, params, [&](const Directory::pointer& foundDirectory) {
directoryToReport = foundDirectory;
});
}
if (directoryToReport)
{
albumNode.setAttribute("title", directoryToReport->getName());
albumNode.setAttribute("id", idToString(directoryToReport->getId()));
if (const Directory::pointer & parentDirectory{ directoryToReport->getParentDirectory() })
albumNode.setAttribute("parent", idToString(parentDirectory->getId()));
}
albumNode.setAttribute("album", release->getName());
albumNode.setAttribute("isDir", true); albumNode.setAttribute("isDir", true);
} }
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten())); albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
albumNode.setAttribute("id", idToString(release->getId()));
albumNode.setAttribute("coverArt", idToString(release->getId())); albumNode.setAttribute("coverArt", idToString(release->getId()));
if (const auto year{ release->getYear() }) if (const auto year{ release->getYear() })
albumNode.setAttribute("year", *year); albumNode.setAttribute("year", *year);
@@ -70,11 +88,7 @@ namespace lms::api::subsonic
if (artists.empty()) if (artists.empty())
artists = release->getArtists(); artists = release->getArtists();
if (artists.empty() && !id3) if (!artists.empty())
{
albumNode.setAttribute("parent", idToString(RootId{}));
}
else if (!artists.empty())
{ {
if (!release->getArtistDisplayName().empty()) if (!release->getArtistDisplayName().empty())
albumNode.setAttribute("artist", release->getArtistDisplayName()); albumNode.setAttribute("artist", release->getArtistDisplayName());
@@ -83,16 +97,11 @@ namespace lms::api::subsonic
if (artists.size() == 1) if (artists.size() == 1)
{ {
albumNode.setAttribute(id3 ? Response::Node::Key{ "artistId" } : Response::Node::Key{ "parent" }, idToString(artists.front()->getId())); albumNode.setAttribute("artistId", idToString(artists.front()->getId()));
}
else
{
if (!id3)
albumNode.setAttribute("parent", idToString(RootId{}));
} }
} }
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(user->getId(), release->getId())); albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), release->getId()));
// Report the first GENRE for this track // Report the first GENRE for this track
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
@@ -103,7 +112,7 @@ namespace lms::api::subsonic
albumNode.setAttribute("genre", clusters.front().front()->getName()); albumNode.setAttribute("genre", clusters.front().front()->getName());
} }
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid()) if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid())
albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
if (!context.enableOpenSubsonic) if (!context.enableOpenSubsonic)
@@ -111,12 +120,10 @@ namespace lms::api::subsonic
// OpenSubsonic specific fields (must always be set) // OpenSubsonic specific fields (must always be set)
albumNode.setAttribute("sortName", release->getSortName()); albumNode.setAttribute("sortName", release->getSortName());
albumNode.setAttribute("mediaType", "album");
if (!id3)
albumNode.setAttribute("mediaType", "album");
{ {
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), release->getId()) }; const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(context.user->getId(), release->getId()) };
albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" }); albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" });
} }
+2 -1
View File
@@ -25,6 +25,7 @@
namespace lms::db namespace lms::db
{ {
class Directory;
class Release; class Release;
class User; class User;
class Session; class Session;
@@ -32,5 +33,5 @@ namespace lms::db
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr<db::Release>& release, const db::ObjectPtr<db::User>& user, bool id3); Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr<db::Release>& release, bool id3, const db::ObjectPtr<db::Directory>& directory = {});
} }
+5 -9
View File
@@ -85,7 +85,7 @@ namespace lms::api::subsonic
} }
} // namespace utils } // namespace utils
Response::Node createArtistNode(RequestContext& context, const Artist::pointer& artist, const User::pointer& user, bool id3) Response::Node createArtistNode(RequestContext& context, const Artist::pointer& artist)
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist");
@@ -96,20 +96,16 @@ namespace lms::api::subsonic
if (const db::Image::pointer artistImage{ artist->getImage() }) if (const db::Image::pointer artistImage{ artist->getImage() })
artistNode.setAttribute("coverArt", idToString(artist->getId())); artistNode.setAttribute("coverArt", idToString(artist->getId()));
if (id3) const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
{ artistNode.setAttribute("albumCount", count);
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
artistNode.setAttribute("albumCount", count);
}
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid()) if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), artist->getId()) }; dateTime.isValid())
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
// OpenSubsonic specific fields (must always be set) // OpenSubsonic specific fields (must always be set)
if (context.enableOpenSubsonic) if (context.enableOpenSubsonic)
{ {
if (!id3) artistNode.setAttribute("mediaType", "artist");
artistNode.setAttribute("mediaType", "artist");
{ {
std::optional<core::UUID> mbid{ artist->getMBID() }; std::optional<core::UUID> mbid{ artist->getMBID() };
+2 -1
View File
@@ -41,6 +41,7 @@ namespace lms::api::subsonic
std::string joinArtistNames(const std::vector<db::ObjectPtr<db::Artist>>& artists); std::string joinArtistNames(const std::vector<db::ObjectPtr<db::Artist>>& artists);
std::string_view toString(db::TrackArtistLinkType type); std::string_view toString(db::TrackArtistLinkType type);
} // namespace utils } // namespace utils
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist, const db::ObjectPtr<db::User>& user, bool id3);
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist);
Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+15 -8
View File
@@ -27,6 +27,7 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackArtistLink.hpp" #include "database/TrackArtistLink.hpp"
@@ -67,14 +68,20 @@ namespace lms::api::subsonic
} }
} // namespace } // namespace
Response::Node createSongNode(RequestContext& context, const Track::pointer& track, const User::pointer& user) Response::Node createSongNode(RequestContext& context, const Track::pointer& track, bool id3)
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
Response::Node trackResponse; Response::Node trackResponse;
if (!id3)
{
if (const auto directory{ track->getDirectory() })
trackResponse.setAttribute("parent", idToString(directory->getId()));
trackResponse.setAttribute("isDir", false);
}
trackResponse.setAttribute("id", idToString(track->getId())); trackResponse.setAttribute("id", idToString(track->getId()));
trackResponse.setAttribute("isDir", false);
trackResponse.setAttribute("title", track->getName()); trackResponse.setAttribute("title", track->getName());
if (track->getTrackNumber()) if (track->getTrackNumber())
trackResponse.setAttribute("track", *track->getTrackNumber()); trackResponse.setAttribute("track", *track->getTrackNumber());
@@ -82,7 +89,7 @@ namespace lms::api::subsonic
trackResponse.setAttribute("discNumber", *track->getDiscNumber()); trackResponse.setAttribute("discNumber", *track->getDiscNumber());
if (track->getYear()) if (track->getYear())
trackResponse.setAttribute("year", *track->getYear()); trackResponse.setAttribute("year", *track->getYear());
trackResponse.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(user->getId(), track->getId())); trackResponse.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), track->getId()));
trackResponse.setAttribute("path", track->getRelativeFilePath().string()); trackResponse.setAttribute("path", track->getRelativeFilePath().string());
trackResponse.setAttribute("size", track->getFileSize()); trackResponse.setAttribute("size", track->getFileSize());
@@ -93,12 +100,13 @@ namespace lms::api::subsonic
} }
{ {
const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodingOutputFormat()) }; const std::string fileSuffix{ formatToSuffix(context.user->getSubsonicDefaultTranscodingOutputFormat()) };
trackResponse.setAttribute("transcodedSuffix", fileSuffix); trackResponse.setAttribute("transcodedSuffix", fileSuffix);
trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix })); trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
} }
trackResponse.setAttribute("coverArt", idToString(track->getId())); if (track->hasCover())
trackResponse.setAttribute("coverArt", idToString(track->getId()));
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
if (!artists.empty()) if (!artists.empty())
@@ -117,7 +125,6 @@ namespace lms::api::subsonic
{ {
trackResponse.setAttribute("album", release->getName()); trackResponse.setAttribute("album", release->getName());
trackResponse.setAttribute("albumId", idToString(release->getId())); trackResponse.setAttribute("albumId", idToString(release->getId()));
trackResponse.setAttribute("parent", idToString(release->getId()));
} }
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()); trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
@@ -126,7 +133,7 @@ namespace lms::api::subsonic
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten())); trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten()));
trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension()));
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), track->getId()) }; dateTime.isValid())
trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
// Report the first GENRE for this track // Report the first GENRE for this track
@@ -152,7 +159,7 @@ namespace lms::api::subsonic
trackResponse.setAttribute("mediaType", "song"); trackResponse.setAttribute("mediaType", "song");
{ {
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), track->getId()) }; const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(context.user->getId(), track->getId()) };
trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : ""); trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : "");
} }
+1 -1
View File
@@ -32,5 +32,5 @@ namespace lms::db
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createSongNode(RequestContext& context, const db::ObjectPtr<db::Track>& track, const db::ObjectPtr<db::User>& user); Response::Node createSongNode(RequestContext& context, const db::ObjectPtr<db::Track>& track, bool id3);
} }