From 980709dbcf6770092cf4042d146eefa226d5b26c Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 13 Jul 2024 14:22:19 +0200 Subject: [PATCH] Can now browse by directorie in the Subsonic API, ref #474 --- src/libs/database/impl/Directory.cpp | 38 +- src/libs/database/impl/Release.cpp | 6 +- src/libs/database/impl/Track.cpp | 3 + .../database/include/database/Directory.hpp | 34 +- .../database/include/database/Release.hpp | 7 + src/libs/database/include/database/Track.hpp | 7 + src/libs/database/test/Directory.cpp | 108 ++++- src/libs/subsonic/impl/SubsonicId.cpp | 21 + src/libs/subsonic/impl/SubsonicId.hpp | 5 + src/libs/subsonic/impl/SubsonicResource.cpp | 2 +- .../impl/entrypoints/AlbumSongLists.cpp | 8 +- .../subsonic/impl/entrypoints/Browsing.cpp | 406 +++++++++++------- .../impl/entrypoints/MediaAnnotation.cpp | 35 +- .../subsonic/impl/entrypoints/Searching.cpp | 57 ++- src/libs/subsonic/impl/responses/Album.cpp | 55 +-- src/libs/subsonic/impl/responses/Album.hpp | 3 +- src/libs/subsonic/impl/responses/Artist.cpp | 14 +- src/libs/subsonic/impl/responses/Artist.hpp | 3 +- src/libs/subsonic/impl/responses/Song.cpp | 23 +- src/libs/subsonic/impl/responses/Song.hpp | 2 +- 20 files changed, 613 insertions(+), 224 deletions(-) diff --git a/src/libs/database/impl/Directory.cpp b/src/libs/database/impl/Directory.cpp index 8b9ba07a..0b20237f 100644 --- a/src/libs/database/impl/Directory.cpp +++ b/src/libs/database/impl/Directory.cpp @@ -33,10 +33,25 @@ namespace lms::db { auto query{ session.getDboSession()->query>("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()) { - query.join("track t ON t.directory_id = d.id") - .join("artist a ON a.id = t_a_l.artist_id") + query.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") .where("a.id = ?") .bind(params.artist); @@ -57,10 +72,11 @@ namespace lms::db } 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; } } // namespace @@ -108,10 +124,16 @@ namespace lms::db }); } + RangeResults Directory::find(Session& session, const FindParameters& params) + { + auto query{ createQuery(session, params) }; + return utils::execRangeQuery(query, params.range); + } + void Directory::find(Session& session, const FindParameters& params, const std::function& func) { 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); }); } @@ -131,6 +153,12 @@ namespace lms::db return utils::execRangeQuery(query, range); } + RangeResults Directory::findRootDirectories(Session& session, std::optional range) + { + auto query{ session.getDboSession()->query>("SELECT d from directory d").where("d.parent_directory_id IS NULL") }; + return utils::execRangeQuery(query, range); + } + void Directory::setAbsolutePath(const std::filesystem::path& p) { assert(p.is_absolute()); diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index b7322ff4..a597de86 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -53,7 +53,8 @@ namespace lms::db || params.dateRange || params.artist.isValid() || params.clusters.size() == 1 - || params.mediaLibrary.isValid()) + || params.mediaLibrary.isValid() + || params.directory.isValid()) { query.join("track t ON t.release_id = r.id"); } @@ -61,6 +62,9 @@ namespace lms::db if (params.mediaLibrary.isValid()) 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()) { query.join("release_release_type r_r_t ON r_r_t.release_id = r.id"); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 80c10c18..a8652d3e 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -154,6 +154,9 @@ namespace lms::db if (params.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.mediaLibrary); + if (params.directory.isValid()) + query.where("t.directory_id = ?").bind(params.directory); + switch (params.sortMethod) { case TrackSortMethod::None: diff --git a/src/libs/database/include/database/Directory.hpp b/src/libs/database/include/database/Directory.hpp index 51564cf6..7cecbc69 100644 --- a/src/libs/database/include/database/Directory.hpp +++ b/src/libs/database/include/database/Directory.hpp @@ -21,12 +21,16 @@ #include #include +#include +#include +#include #include #include "core/EnumSet.hpp" #include "database/ArtistId.hpp" #include "database/DirectoryId.hpp" +#include "database/ReleaseId.hpp" #include "database/Object.hpp" #include "database/Types.hpp" @@ -42,20 +46,44 @@ namespace lms::db struct FindParameters { std::optional range; - ArtistId artist; // only tracks that involve this artist + std::vector 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 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 = _range; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) + { + keywords = _keywords; + return *this; + } FindParameters& setArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; 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 @@ -63,13 +91,15 @@ namespace lms::db static pointer find(Session& session, DirectoryId id); static pointer find(Session& session, const std::filesystem::path& path); static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function& func); + static RangeResults find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& parameters, const std::function& func); static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static RangeResults findRootDirectories(Session& session, std::optional range = std::nullopt); // getters const std::filesystem::path& getAbsolutePath() const { return _absolutePath; } std::string_view getName() const { return _name; } - ObjectPtr getParent() const { return _parent; } + ObjectPtr getParentDirectory() const { return _parent; } // setters void setAbsolutePath(const std::filesystem::path& p); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 7af4e2bc..371df127 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -33,6 +33,7 @@ #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" +#include "database/DirectoryId.hpp" #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" @@ -96,6 +97,7 @@ namespace lms::db core::EnumSet excludedTrackArtistLinkTypes; // but not for these link types 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 + DirectoryId directory; // if set, tracks in this directory FindParameters& setClusters(std::span _clusters) { @@ -150,6 +152,11 @@ namespace lms::db mediaLibrary = _mediaLibrary; return *this; } + FindParameters& setDirectory(DirectoryId _directory) + { + directory = _directory; + return *this; + } }; Release() = default; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 19829e66..f06d9659 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -37,6 +37,7 @@ #include "core/UUID.hpp" #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" +#include "database/DirectoryId.hpp" #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" @@ -81,6 +82,7 @@ namespace lms::db std::optional trackNumber; // matching this track number std::optional discNumber; // matching this disc number MediaLibraryId mediaLibrary; // If set, tracks in this library + DirectoryId directory; // if set, tracks in this directory FindParameters& setClusters(std::span _clusters) { @@ -165,6 +167,11 @@ namespace lms::db mediaLibrary = _mediaLibrary; return *this; } + FindParameters& setDirectory(DirectoryId _directory) + { + directory = _directory; + return *this; + } }; struct PathResult diff --git a/src/libs/database/test/Directory.cpp b/src/libs/database/test/Directory.cpp index da7152ed..9e3be53f 100644 --- a/src/libs/database/test/Directory.cpp +++ b/src/libs/database/test/Directory.cpp @@ -107,23 +107,35 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto dir{ child->getParent() }; + auto dir{ child->getParentDirectory() }; EXPECT_EQ(dir, Directory::pointer{}); } { auto transaction{ session.createWriteTransaction() }; - child.get().modify()->setParent(parent.lockAndGet()); + child.get().modify()->setParent(parent.get()); } { auto transaction{ session.createReadTransaction() }; - auto dir{ child->getParent() }; + auto dir{ child->getParentDirectory() }; ASSERT_NE(dir, Directory::pointer{}); 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) @@ -141,7 +153,7 @@ namespace lms::db::tests { 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()); } } + + 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 \ No newline at end of file diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index 9ec69508..b1fda5af 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -31,6 +31,11 @@ namespace lms::api::subsonic return "ar-" + id.toString(); } + std::string idToString(db::DirectoryId id) + { + return "dir-" + id.toString(); + } + std::string idToString(db::MediaLibraryId id) { // No need to prefix as this is only used at well known places @@ -76,6 +81,22 @@ namespace lms::core::stringUtils return std::nullopt; } + template<> + std::optional readAs(std::string_view str) + { + std::vector 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(values[1]) }) + return db::DirectoryId{ *value }; + + return std::nullopt; + } + template<> std::optional readAs(std::string_view str) { diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index d7a88efa..331708a5 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -21,6 +21,7 @@ #include "core/String.hpp" #include "database/ArtistId.hpp" +#include "database/DirectoryId.hpp" #include "database/MediaLibraryId.hpp" #include "database/ReleaseId.hpp" #include "database/TrackId.hpp" @@ -33,6 +34,7 @@ namespace lms::api::subsonic }; std::string idToString(db::ArtistId id); + std::string idToString(db::DirectoryId id); std::string idToString(db::MediaLibraryId id); std::string idToString(db::ReleaseId id); std::string idToString(db::TrackId id); @@ -49,6 +51,9 @@ namespace lms::core::stringUtils template<> std::optional readAs(std::string_view str); + template<> + std::optional readAs(std::string_view str); + template<> std::optional readAs(std::string_view str); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 311c420f..6bf949f2 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -168,7 +168,7 @@ namespace lms::api::subsonic { "/getAlbum", { handleGetAlbumRequest } }, { "/getSong", { handleGetSongRequest } }, { "/getVideos", { handleNotImplemented } }, - { "/getArtistInfo", { handleGetArtistInfoRequest } }, + { "/getArtistInfo", { handleNotImplemented } }, { "/getArtistInfo2", { handleGetArtistInfo2Request } }, { "/getAlbumInfo", { handleNotImplemented } }, { "/getAlbumInfo2", { handleNotImplemented } }, diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 59d6966c..916086be 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -169,7 +169,7 @@ namespace lms::api::subsonic for (const ReleaseId releaseId : releases.results) { 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; @@ -187,6 +187,8 @@ namespace lms::api::subsonic feedback::IFeedbackService& feedbackService{ *core::Service::get() }; + // We don't support starring directories + if (id3) { feedback::IFeedbackService::ArtistFindParameters artistFindParams; artistFindParams.setUser(context.user->getId()); @@ -194,7 +196,7 @@ namespace lms::api::subsonic for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) { 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) { 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) diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 660a8a80..f7221b46 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -24,11 +24,13 @@ #include "core/Service.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" +#include "database/Directory.hpp" #include "database/MediaLibrary.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -48,123 +50,58 @@ namespace lms::api::subsonic namespace { - Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) + std::vector getRootDirectories(Session& session, MediaLibraryId libraryId) { - // Mandatory params - ArtistId id{ getMandatoryParameterAs(context.parameters, "id") }; - - // Optional params - std::size_t count{ getParameterAs(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" }) }; + std::vector res; + 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 (!artist) - throw RequestedDataNotFoundError{}; - - std::optional artistMBID{ artist->getMBID() }; - if (artistMBID) - artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); + if (Directory::pointer rootDirectory{ Directory::find(session, library->getPath()) }) + res.push_back(rootDirectory); + } + else + { + res = Directory::findRootDirectories(session).results; } - auto similarArtistsId{ core::Service::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, context.user, id3)); - } - } - - return response; + return res; } - Response handleGetArtistsRequestCommon(RequestContext& context, bool id3) + struct IndexComparator { - // Optional params - const MediaLibraryId mediaLibrary{ getParameterAs(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; + constexpr bool operator()(char lhs, char rhs) const { - auto transaction{ context.dbSession.createReadTransaction() }; + if (lhs == '#' && std::isalpha(rhs)) + return false; + if (rhs == '#' && std::isalpha(lhs)) + return true; - 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; - } + return lhs < rhs; } - parameters.setMediaLibrary(mediaLibrary); + }; - // This endpoint does not scale: make sort lived transactions in order not to block the whole application + using IndexMap = std::map, 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 - LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists..."); - std::map> artistsSortedByFirstChar; - std::size_t currentArtistOffset{ 0 }; - constexpr std::size_t batchSize{ 100 }; - bool hasMoreArtists{ true }; - while (hasMoreArtists) - { - auto transaction{ context.dbSession.createReadTransaction() }; + Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) { + const std::string_view name{ directory->getName() }; + assert(!name.empty()); - 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 (name.empty() || !std::isalpha(name[0])) + sortChar = '#'; + else + sortChar = std::toupper(name[0]); - 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, context.user, id3)); - } - } - - return response; + res[sortChar].push_back(directory); + }); } std::vector findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count) @@ -266,6 +203,21 @@ namespace lms::api::subsonic 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 Response handleGetMusicFoldersRequest(RequestContext& context) @@ -286,64 +238,112 @@ namespace lms::api::subsonic Response handleGetIndexesRequest(RequestContext& context) { - return handleGetArtistsRequestCommon(context, false /* no id3 */); + // Optional params + const MediaLibraryId mediaLibrary{ getParameterAs(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 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) { // Mandatory params - const auto artistId{ getParameterAs(context.parameters, "id") }; - const auto releaseId{ getParameterAs(context.parameters, "id") }; - const auto root{ getParameterAs(context.parameters, "id") }; - - if (!root && !artistId && !releaseId) - throw BadParameterGenericError{ "id" }; + const auto directoryId{ getMandatoryParameterAs(context.parameters, "id") }; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& directoryNode{ response.createNode("directory") }; auto transaction{ context.dbSession.createReadTransaction() }; - if (root) - { - directoryNode.setAttribute("id", idToString(RootId{})); - directoryNode.setAttribute("name", "Music"); + const Directory::pointer directory{ Directory::find(context.dbSession, directoryId) }; + if (!directory) + throw RequestedDataNotFoundError{}; - // TODO: this does not scale when a lot of artists are present - Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName), [&](const Artist::pointer& artist) { - directoryNode.addArrayChild("child", createArtistNode(context, artist, context.user, false /* no id3 */)); + if (const Release::pointer release{ getReleaseFromDirectory(context.dbSession, directoryId) }) + { + directoryNode.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), release->getId())); + if (const Wt::WDateTime dateTime{ core::Service::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) }; - 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) { + Track::find(context.dbSession, params, [&](const Track::pointer& track) { directoryNode.addArrayChild("child", createSongNode(context, track, context.user)); }); } - else - throw BadParameterGenericError{ "id" }; return response; } @@ -370,7 +370,82 @@ namespace lms::api::subsonic Response handleGetArtistsRequest(RequestContext& context) { - return handleGetArtistsRequestCommon(context, true /* id3 */); + // Optional params + const MediaLibraryId mediaLibrary{ getParameterAs(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> 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) @@ -385,11 +460,11 @@ namespace lms::api::subsonic throw RequestedDataNotFoundError{}; 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())) }; 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)); @@ -408,11 +483,11 @@ namespace lms::api::subsonic throw RequestedDataNotFoundError{}; 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)) }; 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)); @@ -436,14 +511,43 @@ namespace lms::api::subsonic return response; } - Response handleGetArtistInfoRequest(RequestContext& context) - { - return handleGetArtistInfoRequestCommon(context, false /* no id3 */); - } - Response handleGetArtistInfo2Request(RequestContext& context) { - return handleGetArtistInfoRequestCommon(context, true /* id3 */); + // Mandatory params + ArtistId id{ getMandatoryParameterAs(context.parameters, "id") }; + + // Optional params + std::size_t count{ getParameterAs(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 artistMBID{ artist->getMBID() }; + if (artistMBID) + artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); + } + + auto similarArtistsId{ core::Service::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) diff --git a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp index 2abdd212..b8a156c1 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp @@ -23,7 +23,9 @@ #include "core/Service.hpp" #include "database/ArtistId.hpp" +#include "database/Release.hpp" #include "database/ReleaseId.hpp" +#include "database/Session.hpp" #include "database/TrackId.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -43,25 +45,50 @@ namespace lms::api::subsonic std::vector artistIds; std::vector releaseIds; std::vector trackIds; + std::vector directoryIds; }; StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters) { StarParameters res; - // TODO handle parameters for legacy file browsing + // id could be either a trackId or a directory id + res.directoryIds = getMultiParametersAs(parameters, "id"); res.trackIds = getMultiParametersAs(parameters, "id"); res.artistIds = getMultiParametersAs(parameters, "artistId"); res.releaseIds = getMultiParametersAs(parameters, "albumId"); 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 Response handleStarRequest(RequestContext& context) { StarParameters params{ getStarParameters(context.parameters) }; + for (const DirectoryId id : params.directoryIds) + { + if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid()) + core::Service::get()->star(context.user->getId(), releaseId); + } + for (const ArtistId id : params.artistIds) core::Service::get()->star(context.user->getId(), id); @@ -78,6 +105,12 @@ namespace lms::api::subsonic { StarParameters params{ getStarParameters(context.parameters) }; + for (const DirectoryId id : params.directoryIds) + { + if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid()) + core::Service::get()->unstar(context.user->getId(), releaseId); + } + for (const ArtistId id : params.artistIds) core::Service::get()->unstar(context.user->getId(), id); diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index 008f3bdf..a5087945 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -25,6 +25,7 @@ #include "core/Random.hpp" #include "database/Artist.hpp" +#include "database/Directory.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" @@ -113,7 +114,36 @@ namespace lms::api::subsonic _ongoingScans[scanInfo] = { now, lastRetrievedId }; } - void findRequestedArtists(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + void findRequestedArtistDirectories(RequestContext& context, const std::vector& 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(context.parameters, "artistCount").value_or(20) }; + if (artistCount == 0) + return; + + if (artistCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; + + const std::size_t artistOffset{ getParameterAs(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& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode) { static ScanTracker currentScansInProgress; @@ -135,7 +165,7 @@ namespace lms::api::subsonic params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods 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(); }); } }; @@ -158,7 +188,7 @@ namespace lms::api::subsonic { Artist::find( context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) { - searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + searchResultNode.addArrayChild("artist", createArtistNode(context, artist)); }, mediaLibrary); lastRetrievedId = cachedLastRetrievedId; @@ -176,7 +206,7 @@ namespace lms::api::subsonic } } - void findRequestedAlbums(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + void findRequestedAlbums(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode) { static ScanTracker currentScansInProgress; @@ -199,7 +229,7 @@ namespace lms::api::subsonic params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods 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(); }); } }; @@ -222,7 +252,7 @@ namespace lms::api::subsonic { Release::find( context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) { - searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); + searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3)); }, mediaLibrary); lastRetrievedId = cachedLastRetrievedId; @@ -240,7 +270,7 @@ namespace lms::api::subsonic } } - void findRequestedTracks(RequestContext& context, const std::vector& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode) + void findRequestedTracks(RequestContext& context, bool id3, const std::vector& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode) { static ScanTracker currentScansInProgress; @@ -263,7 +293,7 @@ namespace lms::api::subsonic params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods 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(); }); } }; @@ -286,7 +316,7 @@ namespace lms::api::subsonic { Track::find( context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { - searchResultNode.addArrayChild("song", createSongNode(context, track, user)); + searchResultNode.addArrayChild("song", createSongNode(context, track, id3)); }, mediaLibrary); lastRetrievedId = cachedLastRetrievedId; @@ -329,9 +359,12 @@ namespace lms::api::subsonic auto transaction{ context.dbSession.createReadTransaction() }; - findRequestedArtists(context, id3, keywords, mediaLibrary, context.user, searchResultNode); - findRequestedAlbums(context, id3, keywords, mediaLibrary, context.user, searchResultNode); - findRequestedTracks(context, keywords, mediaLibrary, context.user, searchResultNode); + if (id3) + findRequestedArtists(context, keywords, mediaLibrary, searchResultNode); + else + findRequestedArtistDirectories(context, keywords, mediaLibrary, searchResultNode); + findRequestedAlbums(context, id3, keywords, mediaLibrary, searchResultNode); + findRequestedTracks(context, id3, keywords, mediaLibrary, searchResultNode); return response; } diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 546a19e7..88d00e43 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -24,6 +24,7 @@ #include "core/String.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" +#include "database/Directory.hpp" #include "database/Release.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -39,7 +40,7 @@ namespace lms::api::subsonic { 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"); @@ -47,21 +48,38 @@ namespace lms::api::subsonic if (id3) { + albumNode.setAttribute("id", idToString(release->getId())); albumNode.setAttribute("name", release->getName()); albumNode.setAttribute("songCount", release->getTrackCount()); - albumNode.setAttribute( - "duration", std::chrono::duration_cast( - release->getDuration()) - .count()); + albumNode.setAttribute("duration", std::chrono::duration_cast(release->getDuration()).count()); } 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("created", core::stringUtils::toISO8601String(release->getLastWritten())); - albumNode.setAttribute("id", idToString(release->getId())); albumNode.setAttribute("coverArt", idToString(release->getId())); if (const auto year{ release->getYear() }) albumNode.setAttribute("year", *year); @@ -70,11 +88,7 @@ namespace lms::api::subsonic if (artists.empty()) artists = release->getArtists(); - if (artists.empty() && !id3) - { - albumNode.setAttribute("parent", idToString(RootId{})); - } - else if (!artists.empty()) + if (!artists.empty()) { if (!release->getArtistDisplayName().empty()) albumNode.setAttribute("artist", release->getArtistDisplayName()); @@ -83,16 +97,11 @@ namespace lms::api::subsonic if (artists.size() == 1) { - albumNode.setAttribute(id3 ? Response::Node::Key{ "artistId" } : Response::Node::Key{ "parent" }, idToString(artists.front()->getId())); - } - else - { - if (!id3) - albumNode.setAttribute("parent", idToString(RootId{})); + albumNode.setAttribute("artistId", idToString(artists.front()->getId())); } } - albumNode.setAttribute("playCount", core::Service::get()->getCount(user->getId(), release->getId())); + albumNode.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), release->getId())); // Report the first GENRE for this track const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; @@ -103,7 +112,7 @@ namespace lms::api::subsonic albumNode.setAttribute("genre", clusters.front().front()->getName()); } - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid()) albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); if (!context.enableOpenSubsonic) @@ -111,12 +120,10 @@ namespace lms::api::subsonic // OpenSubsonic specific fields (must always be set) albumNode.setAttribute("sortName", release->getSortName()); - - if (!id3) - albumNode.setAttribute("mediaType", "album"); + albumNode.setAttribute("mediaType", "album"); { - const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(user->getId(), release->getId()) }; + const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.user->getId(), release->getId()) }; albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" }); } diff --git a/src/libs/subsonic/impl/responses/Album.hpp b/src/libs/subsonic/impl/responses/Album.hpp index a9508a05..d324fe24 100644 --- a/src/libs/subsonic/impl/responses/Album.hpp +++ b/src/libs/subsonic/impl/responses/Album.hpp @@ -25,6 +25,7 @@ namespace lms::db { + class Directory; class Release; class User; class Session; @@ -32,5 +33,5 @@ namespace lms::db namespace lms::api::subsonic { - Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr& release, const db::ObjectPtr& user, bool id3); + Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr& release, bool id3, const db::ObjectPtr& directory = {}); } \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 734b02cd..77481fba 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -85,7 +85,7 @@ namespace lms::api::subsonic } } // 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"); @@ -96,20 +96,16 @@ namespace lms::api::subsonic if (const db::Image::pointer artistImage{ artist->getImage() }) 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::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), artist->getId()) }; dateTime.isValid()) artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); // OpenSubsonic specific fields (must always be set) if (context.enableOpenSubsonic) { - if (!id3) - artistNode.setAttribute("mediaType", "artist"); + artistNode.setAttribute("mediaType", "artist"); { std::optional mbid{ artist->getMBID() }; diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp index b7ebc516..3a8a5687 100644 --- a/src/libs/subsonic/impl/responses/Artist.hpp +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -41,6 +41,7 @@ namespace lms::api::subsonic std::string joinArtistNames(const std::vector>& artists); std::string_view toString(db::TrackArtistLinkType type); } // namespace utils - Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr& artist, const db::ObjectPtr& user, bool id3); + + Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr& artist); Response::Node createArtistNode(const db::ObjectPtr& artist); // only minimal info } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index cc4e0365..9ff70831 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -27,6 +27,7 @@ #include "core/String.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" +#include "database/Directory.hpp" #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" @@ -67,14 +68,20 @@ namespace lms::api::subsonic } } // 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"); 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("isDir", false); trackResponse.setAttribute("title", track->getName()); if (track->getTrackNumber()) trackResponse.setAttribute("track", *track->getTrackNumber()); @@ -82,7 +89,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("discNumber", *track->getDiscNumber()); if (track->getYear()) trackResponse.setAttribute("year", *track->getYear()); - trackResponse.setAttribute("playCount", core::Service::get()->getCount(user->getId(), track->getId())); + trackResponse.setAttribute("playCount", core::Service::get()->getCount(context.user->getId(), track->getId())); trackResponse.setAttribute("path", track->getRelativeFilePath().string()); 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("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& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; if (!artists.empty()) @@ -117,7 +125,6 @@ namespace lms::api::subsonic { trackResponse.setAttribute("album", release->getName()); trackResponse.setAttribute("albumId", idToString(release->getId())); - trackResponse.setAttribute("parent", idToString(release->getId())); } trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); @@ -126,7 +133,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten())); trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); - if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), track->getId()) }; dateTime.isValid()) trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); // Report the first GENRE for this track @@ -152,7 +159,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("mediaType", "song"); { - const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(user->getId(), track->getId()) }; + const Wt::WDateTime dateTime{ core::Service::get()->getLastListenDateTime(context.user->getId(), track->getId()) }; trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : ""); } diff --git a/src/libs/subsonic/impl/responses/Song.hpp b/src/libs/subsonic/impl/responses/Song.hpp index 1a91549e..5b35227b 100644 --- a/src/libs/subsonic/impl/responses/Song.hpp +++ b/src/libs/subsonic/impl/responses/Song.hpp @@ -32,5 +32,5 @@ namespace lms::db namespace lms::api::subsonic { - Response::Node createSongNode(RequestContext& context, const db::ObjectPtr& track, const db::ObjectPtr& user); + Response::Node createSongNode(RequestContext& context, const db::ObjectPtr& track, bool id3); } \ No newline at end of file