Subsonic API: added bookmark support. Fixes #24
This commit is contained in:
@@ -46,6 +46,8 @@ lms_SOURCES = \
|
||||
$(srcdir)/database/SqlQuery.hpp \
|
||||
$(srcdir)/database/Track.cpp \
|
||||
$(srcdir)/database/Track.hpp \
|
||||
$(srcdir)/database/TrackBookmark.cpp \
|
||||
$(srcdir)/database/TrackBookmark.hpp \
|
||||
$(srcdir)/database/User.cpp \
|
||||
$(srcdir)/database/User.hpp \
|
||||
$(srcdir)/image/Image.cpp \
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "similarity/SimilaritySearcher.hpp"
|
||||
@@ -48,6 +49,8 @@ using namespace Database;
|
||||
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedCreatedBookmarkDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedChangedBookmarkDate {"2000-01-01T00:00:00"};
|
||||
|
||||
template<>
|
||||
std::optional<API::Subsonic::Id>
|
||||
@@ -412,6 +415,22 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
|
||||
return trackResponse;
|
||||
}
|
||||
|
||||
static
|
||||
Response::Node
|
||||
trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark)
|
||||
{
|
||||
Response::Node trackBookmarkNode;
|
||||
|
||||
trackBookmarkNode.setAttribute("position", std::to_string(trackBookmark->getOffset().count()));
|
||||
if (!trackBookmark->getComment().empty())
|
||||
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
|
||||
trackBookmarkNode.setAttribute("created", reportedCreatedBookmarkDate);
|
||||
trackBookmarkNode.setAttribute("changed", reportedChangedBookmarkDate);
|
||||
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
|
||||
|
||||
return trackBookmarkNode;
|
||||
}
|
||||
|
||||
static
|
||||
Response::Node
|
||||
releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3)
|
||||
@@ -1656,6 +1675,94 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
return Response::createOkResponse();
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetBookmarks(RequestContext& context)
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& bookmarksNode {response.createNode("bookmarks")};
|
||||
|
||||
for (const TrackBookmark::pointer& bookmark : bookmarks)
|
||||
{
|
||||
Response::Node bookmarkNode {trackBookmarkToResponseNode(bookmark)};
|
||||
bookmarkNode.addArrayChild("entry", trackToResponseNode(bookmark->getTrack(), context.dbSession, user));
|
||||
|
||||
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
|
||||
}
|
||||
|
||||
return response ;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleCreateBookmark(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
|
||||
if (id.type != Id::Type::Track)
|
||||
throw BadParameterGenericError {"id"};
|
||||
|
||||
unsigned long position {getMandatoryParameterAs<unsigned long>(context.parameters, "position")};
|
||||
const std::optional<std::string> comment {getParameterAs<std::string>(context.parameters, "comment")};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
const Track::pointer track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
// Replace any existing bookmark
|
||||
auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)};
|
||||
if (!bookmark)
|
||||
bookmark = TrackBookmark::create(context.dbSession, user, track);
|
||||
|
||||
bookmark.modify()->setOffset(std::chrono::milliseconds {position});
|
||||
if (comment)
|
||||
bookmark.modify()->setComment(*comment);
|
||||
|
||||
return Response::createOkResponse();
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleDeleteBookmark(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
|
||||
if (id.type != Id::Type::Track)
|
||||
throw BadParameterGenericError {"id"};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
const Track::pointer track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)};
|
||||
if (!bookmark)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
bookmark.remove();
|
||||
|
||||
return Response::createOkResponse();
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleNotImplemented(RequestContext&)
|
||||
@@ -1898,9 +2005,9 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||
{"changePassword", {handleChangePassword, false}},
|
||||
|
||||
// Bookmarks
|
||||
{"getBookmarks", {handleNotImplemented, false}},
|
||||
{"createBookmarks", {handleNotImplemented, false}},
|
||||
{"deleteBookmarks", {handleNotImplemented, false}},
|
||||
{"getBookmarks", {handleGetBookmarks, false}},
|
||||
{"createBookmark", {handleCreateBookmark, false}},
|
||||
{"deleteBookmark", {handleDeleteBookmark, false}},
|
||||
{"getPlayQueue", {handleNotImplemented, false}},
|
||||
{"savePlayQueue", {handleNotImplemented, false}},
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ Response::Node::setValue(const std::string& value)
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setAttribute(const std::string& key, const std::string& value)
|
||||
Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
_attributes[key] = value;
|
||||
_attributes[std::string {key}] = value;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -178,7 +179,7 @@ class Response
|
||||
class Node
|
||||
{
|
||||
public:
|
||||
void setAttribute(const std::string& key, const std::string& value);
|
||||
void setAttribute(std::string_view key, std::string_view value);
|
||||
|
||||
// A Node has either a value or some children
|
||||
void setValue(const std::string& value);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "Release.hpp"
|
||||
#include "ScanSettings.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "TrackBookmark.hpp"
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "TrackList.hpp"
|
||||
#include "TrackFeatures.hpp"
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 9
|
||||
#define LMS_DATABASE_VERSION 10
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -125,6 +126,20 @@ Session::doDatabaseMigrationIfNeeded()
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 9)
|
||||
{
|
||||
_session.execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"offset" integer,
|
||||
"comment" text not null,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
);)");
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
@@ -150,6 +165,7 @@ Session::Session(Db& db)
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
@@ -264,6 +280,8 @@ Session::prepareTables()
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||
}
|
||||
|
||||
// Initial settings tables
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackBookmark.hpp"
|
||||
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "User.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackBookmark::TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
: _user {user},
|
||||
_track {track}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackBookmark::pointer res {session.getDboSession().add(std::make_unique<TrackBookmark>(user, track))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<TrackBookmark::pointer>
|
||||
TrackBookmark::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackBookmark::pointer> res {session.getDboSession().find<TrackBookmark>()};
|
||||
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<TrackBookmark::pointer>
|
||||
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackBookmark::pointer> res
|
||||
{
|
||||
session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
};
|
||||
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user.id())
|
||||
.where("track_id = ?").bind(track.id());
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getById(Session& session, IdType id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("id = ?").bind(id);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class TrackBookmark : public Wt::Dbo::Dbo<TrackBookmark>
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<TrackBookmark>;
|
||||
|
||||
TrackBookmark () = default;
|
||||
TrackBookmark(Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
|
||||
// Find utility functions
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getByUser(Session& session, Wt::Dbo::ptr<User> user);
|
||||
static pointer getByUser(Session& session, Wt::Dbo::ptr<User> user, Wt::Dbo::ptr<Track> track);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
|
||||
// Setters
|
||||
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
|
||||
// Getters
|
||||
std::chrono::milliseconds getOffset() const { return _offset; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
Wt::Dbo::ptr<User> getUser() const { return _user; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _offset, "offset");
|
||||
Wt::Dbo::field(a, _comment, "comment");
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
private:
|
||||
|
||||
static const std::size_t _maxCommentLength = 128;
|
||||
|
||||
std::chrono::duration<int, std::milli> _offset;
|
||||
std::string _comment;
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
} // namespace database
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getStarredReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
// Stars
|
||||
void starTrack(Wt::Dbo::ptr<Track> track);
|
||||
void unstarTrack(Wt::Dbo::ptr<Track> track);
|
||||
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
|
||||
|
||||
Reference in New Issue
Block a user