Can now browse by directorie in the Subsonic API, ref #474
This commit is contained in:
@@ -33,10 +33,25 @@ namespace lms::db
|
||||
{
|
||||
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())
|
||||
{
|
||||
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::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)
|
||||
{
|
||||
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<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)
|
||||
{
|
||||
assert(p.is_absolute());
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#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> 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
|
||||
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 = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
|
||||
{
|
||||
keywords = _keywords;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _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<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 RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// getters
|
||||
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
|
||||
std::string_view getName() const { return _name; }
|
||||
ObjectPtr<Directory> getParent() const { return _parent; }
|
||||
ObjectPtr<Directory> getParentDirectory() const { return _parent; }
|
||||
|
||||
// setters
|
||||
void setAbsolutePath(const std::filesystem::path& p);
|
||||
|
||||
@@ -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<TrackArtistLinkType> 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<const ClusterId> _clusters)
|
||||
{
|
||||
@@ -150,6 +152,11 @@ namespace lms::db
|
||||
mediaLibrary = _mediaLibrary;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDirectory(DirectoryId _directory)
|
||||
{
|
||||
directory = _directory;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Release() = default;
|
||||
|
||||
@@ -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<int> trackNumber; // matching this track number
|
||||
std::optional<int> 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<const ClusterId> _clusters)
|
||||
{
|
||||
@@ -165,6 +167,11 @@ namespace lms::db
|
||||
mediaLibrary = _mediaLibrary;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDirectory(DirectoryId _directory)
|
||||
{
|
||||
directory = _directory;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct PathResult
|
||||
|
||||
@@ -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
|
||||
@@ -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<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<>
|
||||
std::optional<db::MediaLibraryId> readAs(std::string_view str)
|
||||
{
|
||||
|
||||
@@ -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<db::ArtistId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<db::DirectoryId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<db::MediaLibraryId> readAs(std::string_view str);
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ namespace lms::api::subsonic
|
||||
{ "/getAlbum", { handleGetAlbumRequest } },
|
||||
{ "/getSong", { handleGetSongRequest } },
|
||||
{ "/getVideos", { handleNotImplemented } },
|
||||
{ "/getArtistInfo", { handleGetArtistInfoRequest } },
|
||||
{ "/getArtistInfo", { handleNotImplemented } },
|
||||
{ "/getArtistInfo2", { handleGetArtistInfo2Request } },
|
||||
{ "/getAlbumInfo", { handleNotImplemented } },
|
||||
{ "/getAlbumInfo2", { handleNotImplemented } },
|
||||
|
||||
@@ -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<feedback::IFeedbackService>::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)
|
||||
|
||||
@@ -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<Directory::pointer> getRootDirectories(Session& session, MediaLibraryId libraryId)
|
||||
{
|
||||
// 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(id3 ? Response::Node::Key{ "artistInfo2" } : Response::Node::Key{ "artistInfo" }) };
|
||||
std::vector<Directory::pointer> 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<core::UUID> 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<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, context.user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
return res;
|
||||
}
|
||||
|
||||
Response handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
||||
struct IndexComparator
|
||||
{
|
||||
// Optional params
|
||||
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;
|
||||
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<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
|
||||
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() };
|
||||
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<TrackId> 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<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)
|
||||
{
|
||||
// Mandatory params
|
||||
const auto artistId{ getParameterAs<ArtistId>(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" };
|
||||
const auto directoryId{ getMandatoryParameterAs<DirectoryId>(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<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) };
|
||||
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<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)
|
||||
@@ -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<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)
|
||||
|
||||
@@ -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<ArtistId> artistIds;
|
||||
std::vector<ReleaseId> releaseIds;
|
||||
std::vector<TrackId> trackIds;
|
||||
std::vector<DirectoryId> 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<DirectoryId>(parameters, "id");
|
||||
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
|
||||
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
|
||||
res.releaseIds = getMultiParametersAs<ReleaseId>(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<feedback::IFeedbackService>::get()->star(context.user->getId(), releaseId);
|
||||
}
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
core::Service<feedback::IFeedbackService>::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<feedback::IFeedbackService>::get()->unstar(context.user->getId(), releaseId);
|
||||
}
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
||||
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<std::chrono::seconds>(
|
||||
release->getDuration())
|
||||
.count());
|
||||
albumNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(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<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
|
||||
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<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));
|
||||
|
||||
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<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{ "" });
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 = {});
|
||||
}
|
||||
@@ -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<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));
|
||||
|
||||
// OpenSubsonic specific fields (must always be set)
|
||||
if (context.enableOpenSubsonic)
|
||||
{
|
||||
if (!id3)
|
||||
artistNode.setAttribute("mediaType", "artist");
|
||||
artistNode.setAttribute("mediaType", "artist");
|
||||
|
||||
{
|
||||
std::optional<core::UUID> mbid{ artist->getMBID() };
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace lms::api::subsonic
|
||||
std::string joinArtistNames(const std::vector<db::ObjectPtr<db::Artist>>& artists);
|
||||
std::string_view toString(db::TrackArtistLinkType type);
|
||||
} // 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
|
||||
} // namespace lms::api::subsonic
|
||||
@@ -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<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("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<Artist::pointer>& 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<std::chrono::seconds>(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<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));
|
||||
|
||||
// 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<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) : "");
|
||||
}
|
||||
|
||||
|
||||
@@ -32,5 +32,5 @@ namespace lms::db
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user