572 lines
15 KiB
C++
572 lines
15 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright The Music Player Daemon Project
|
|
|
|
#include "StickerCommands.hxx"
|
|
#include "Request.hxx"
|
|
#include "SongPrint.hxx"
|
|
#include "db/Interface.hxx"
|
|
#include "sticker/Sticker.hxx"
|
|
#include "sticker/TagSticker.hxx"
|
|
#include "sticker/Database.hxx"
|
|
#include "sticker/SongSticker.hxx"
|
|
#include "sticker/AllowedTags.hxx"
|
|
#include "sticker/Print.hxx"
|
|
#include "client/Client.hxx"
|
|
#include "client/Response.hxx"
|
|
#include "Partition.hxx"
|
|
#include "Instance.hxx"
|
|
#include "util/StringAPI.hxx"
|
|
#include "util/ScopeExit.hxx"
|
|
#include "util/StringCompare.hxx"
|
|
#include "tag/Settings.hxx"
|
|
#include "tag/ParseName.hxx"
|
|
#include "tag/Names.hxx"
|
|
#include "sticker/TagSticker.hxx"
|
|
#include "song/LightSong.hxx"
|
|
#include "PlaylistFile.hxx"
|
|
#include "db/PlaylistInfo.hxx"
|
|
#include "db/PlaylistVector.hxx"
|
|
#include "db/DatabaseLock.hxx"
|
|
#include <fmt/format.h>
|
|
#include "song/Filter.hxx"
|
|
|
|
namespace {
|
|
|
|
class DomainHandler {
|
|
public:
|
|
virtual ~DomainHandler() = default;
|
|
|
|
virtual CommandResult Get(const char *uri, const char *name) {
|
|
const auto value = sticker_database.LoadValue(sticker_type,
|
|
ValidateUri(uri).c_str(),
|
|
name);
|
|
if (value.empty()) {
|
|
response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: {:?}", name);
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
sticker_print_value(response, name, value.c_str());
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Set(const char *uri, const char *name, const char *value) {
|
|
sticker_database.StoreValue(sticker_type,
|
|
ValidateUri(uri).c_str(),
|
|
name,
|
|
value);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Inc(const char *uri, const char *name, const char *value) {
|
|
sticker_database.IncValue(sticker_type,
|
|
ValidateUri(uri).c_str(),
|
|
name,
|
|
value);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Dec(const char *uri, const char *name, const char *value) {
|
|
sticker_database.DecValue(sticker_type,
|
|
ValidateUri(uri).c_str(),
|
|
name,
|
|
value);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Delete(const char *uri, const char *name) {
|
|
std::string validated_uri = ValidateUri(uri);
|
|
uri = validated_uri.c_str();
|
|
bool ret = name == nullptr
|
|
? sticker_database.Delete(sticker_type, uri)
|
|
: sticker_database.DeleteValue(sticker_type, uri, name);
|
|
if (!ret) {
|
|
response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: {:?}", name);
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult List(const char *uri) {
|
|
const auto sticker = sticker_database.Load(sticker_type,
|
|
ValidateUri(uri).c_str());
|
|
sticker_print(response, sticker);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Find(const char *uri, const char *name, StickerOperator op, const char *value,
|
|
const char *sort, bool descending, RangeArg window) {
|
|
auto data = CallbackContext{
|
|
.name = name,
|
|
.sticker_type = sticker_type,
|
|
.response = response,
|
|
.is_song = StringIsEqual("song", sticker_type)
|
|
};
|
|
|
|
auto callback = [](const char *found_uri, const char *found_value, void *user_data) {
|
|
auto context = reinterpret_cast<CallbackContext *>(user_data);
|
|
context->response.Fmt("{}: {}\n",
|
|
context->is_song ? "file" : context->sticker_type, found_uri);
|
|
sticker_print_value(context->response, context->name, found_value);
|
|
};
|
|
|
|
sticker_database.Find(sticker_type,
|
|
uri,
|
|
name,
|
|
op, value,
|
|
sort, descending, window,
|
|
callback, &data);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
virtual CommandResult Names() {
|
|
auto data = CallbackContext{
|
|
.name = "",
|
|
.sticker_type = sticker_type,
|
|
.response = response,
|
|
.is_song = StringIsEqual("song", sticker_type)
|
|
};
|
|
|
|
auto callback = [](const char *found_value, void *user_data) {
|
|
auto context = reinterpret_cast<CallbackContext *>(user_data);
|
|
context->response.Fmt("name: {}\n", found_value);
|
|
};
|
|
|
|
sticker_database.Names(callback, &data);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
CommandResult NamesTypes(const char *type) {
|
|
auto data = CallbackContext{
|
|
.name = "",
|
|
.sticker_type = sticker_type,
|
|
.response = response,
|
|
.is_song = StringIsEqual("song", sticker_type)
|
|
};
|
|
|
|
auto callback = [](const char *found_value, const char *found_type, void *user_data) {
|
|
auto context = reinterpret_cast<CallbackContext *>(user_data);
|
|
context->response.Fmt("name: {}\n", found_value);
|
|
context->response.Fmt("type: {}\n", found_type);
|
|
};
|
|
|
|
sticker_database.NamesTypes(type, callback, &data);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
protected:
|
|
DomainHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database,
|
|
const char *_sticker_type) :
|
|
sticker_type(_sticker_type),
|
|
response(_response),
|
|
database(_database),
|
|
sticker_database(_sticker_database) {
|
|
}
|
|
|
|
/**
|
|
* Validate the command uri or throw std::runtime_error if not valid.
|
|
*
|
|
* @param uri the uri from the sticker command
|
|
*
|
|
* @return the uri to use in the sticker database query
|
|
*/
|
|
virtual std::string ValidateUri(const char *uri) {
|
|
return {uri};
|
|
}
|
|
|
|
const char *const sticker_type;
|
|
Response &response;
|
|
const Database &database;
|
|
StickerDatabase &sticker_database;
|
|
|
|
private:
|
|
struct CallbackContext {
|
|
const char *const name;
|
|
const char *const sticker_type;
|
|
Response &response;
|
|
const bool is_song;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 'song' stickers handler
|
|
*/
|
|
class SongHandler final : public DomainHandler {
|
|
public:
|
|
SongHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database) :
|
|
DomainHandler(_response, _database, _sticker_database, "song") {
|
|
}
|
|
|
|
~SongHandler() override {
|
|
if (song != nullptr)
|
|
database.ReturnSong(song);
|
|
}
|
|
|
|
CommandResult Find(const char *uri, const char *name, StickerOperator op, const char *value,
|
|
const char *sort, bool descending, RangeArg window) override {
|
|
struct sticker_song_find_data data = {
|
|
response,
|
|
name,
|
|
};
|
|
|
|
sticker_song_find(sticker_database, database, uri, data.name,
|
|
op, value,
|
|
sort, descending, window,
|
|
sticker_song_find_print_cb, &data);
|
|
|
|
return CommandResult::OK;
|
|
}
|
|
|
|
protected:
|
|
std::string ValidateUri(const char *uri) override {
|
|
// will throw if song uri not found
|
|
song = database.GetSong(uri);
|
|
assert(song != nullptr);
|
|
return song->GetURI();
|
|
}
|
|
|
|
private:
|
|
struct sticker_song_find_data {
|
|
Response &r;
|
|
const char *name;
|
|
};
|
|
|
|
static void
|
|
sticker_song_find_print_cb(const LightSong &song, const char *value,
|
|
void *user_data)
|
|
{
|
|
auto *data = (struct sticker_song_find_data *)user_data;
|
|
|
|
song_print_uri(data->r, song);
|
|
sticker_print_value(data->r, data->name, value);
|
|
}
|
|
|
|
const LightSong* song = nullptr;
|
|
};
|
|
|
|
/**
|
|
* Base for Tag and Filter handlers
|
|
*/
|
|
class SelectionHandler : public DomainHandler {
|
|
protected:
|
|
SelectionHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database,
|
|
const char* _sticker_type) :
|
|
DomainHandler(_response, _database, _sticker_database, _sticker_type) {
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Tag type stickers handler
|
|
*/
|
|
class TagHandler : public SelectionHandler {
|
|
public:
|
|
TagHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database,
|
|
TagType _tag_type) :
|
|
SelectionHandler(_response, _database, _sticker_database, tag_item_names[_tag_type]),
|
|
tag_type(_tag_type) {
|
|
|
|
assert(tag_type != TAG_NUM_OF_ITEM_TYPES);
|
|
}
|
|
|
|
protected:
|
|
std::string ValidateUri(const char *uri) override {
|
|
if (tag_type == TAG_NUM_OF_ITEM_TYPES)
|
|
throw std::invalid_argument(fmt::format("no such tag: {:?}", sticker_type));
|
|
|
|
if (!sticker_allowed_tags.Test(tag_type))
|
|
throw std::invalid_argument(fmt::format("unsupported tag: {:?}", sticker_type));
|
|
|
|
if (!TagExists(database, tag_type, uri))
|
|
throw std::invalid_argument(fmt::format("no such {}: {:?}", sticker_type, uri));
|
|
|
|
return {uri};
|
|
}
|
|
|
|
private:
|
|
const TagType tag_type;
|
|
};
|
|
|
|
/**
|
|
* Filter stickers handler
|
|
*
|
|
* The URI is parsed as a SongFilter
|
|
*/
|
|
class FilterHandler : public SelectionHandler {
|
|
public:
|
|
FilterHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database) :
|
|
SelectionHandler(_response, _database, _sticker_database, "filter") {
|
|
}
|
|
|
|
protected:
|
|
std::string ValidateUri(const char *uri) override {
|
|
|
|
auto filter = MakeSongFilter(uri);
|
|
|
|
auto normalized = filter.ToExpression();
|
|
|
|
if (!FilterMatches(database, filter))
|
|
throw std::invalid_argument(fmt::format("no matches found: {:?}", normalized));
|
|
|
|
return normalized;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* playlist stickers handler
|
|
*/
|
|
class PlaylistHandler : public DomainHandler {
|
|
public:
|
|
PlaylistHandler(Response &_response,
|
|
const Database &_database,
|
|
StickerDatabase &_sticker_database) :
|
|
DomainHandler(_response, _database, _sticker_database, "playlist") {
|
|
}
|
|
|
|
private:
|
|
std::string ValidateUri(const char *uri) override {
|
|
PlaylistVector playlists = ListPlaylistFiles();
|
|
|
|
const ScopeDatabaseLock protect;
|
|
if (!playlists.exists(uri))
|
|
throw std::invalid_argument(fmt::format("no such playlist: {:?}", uri));
|
|
|
|
return {uri};
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
CommandResult
|
|
handle_sticker_names(Client &client, Request args, Response &r)
|
|
{
|
|
(void) args;
|
|
auto &instance = client.GetInstance();
|
|
if (!instance.HasStickerDatabase()) {
|
|
r.Error(ACK_ERROR_UNKNOWN, "sticker database is disabled");
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
auto &db = client.GetPartition().GetDatabaseOrThrow();
|
|
auto &sticker_database = *instance.sticker_database;
|
|
|
|
std::unique_ptr<DomainHandler> handler = std::make_unique<SongHandler>(r, db, sticker_database);
|
|
|
|
return handler->Names();
|
|
}
|
|
|
|
CommandResult
|
|
handle_sticker_names_types(Client &client, Request args, Response &r)
|
|
{
|
|
auto &instance = client.GetInstance();
|
|
if (!instance.HasStickerDatabase()) {
|
|
r.Error(ACK_ERROR_UNKNOWN, "sticker database is disabled");
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
auto &db = client.GetPartition().GetDatabaseOrThrow();
|
|
auto &sticker_database = *instance.sticker_database;
|
|
|
|
auto type = args.GetOptional(0);
|
|
std::unique_ptr<DomainHandler> handler = std::make_unique<SongHandler>(r, db, sticker_database);
|
|
|
|
if (type == nullptr ||
|
|
StringIsEqual(type, "song") ||
|
|
StringIsEqual(type, "playlist") ||
|
|
StringIsEqual(type, "filter")) {
|
|
return handler->NamesTypes(type);
|
|
}
|
|
auto tag_type = tag_name_parse(type);
|
|
if (tag_type == TAG_NUM_OF_ITEM_TYPES) {
|
|
r.FmtError(ACK_ERROR_ARG, "no such tag {:?}", type);
|
|
return CommandResult::ERROR;
|
|
}
|
|
else if (sticker_allowed_tags.Test(tag_type)) {
|
|
return handler->NamesTypes(type);
|
|
}
|
|
else {
|
|
r.FmtError(ACK_ERROR_ARG, "unsupported tag {:?}", type);
|
|
return CommandResult::ERROR;
|
|
}
|
|
}
|
|
|
|
CommandResult
|
|
handle_sticker(Client &client, Request args, Response &r)
|
|
{
|
|
// must be enforced by the caller
|
|
assert(args.size() >= 3);
|
|
|
|
auto &instance = client.GetInstance();
|
|
if (!instance.HasStickerDatabase()) {
|
|
r.Error(ACK_ERROR_UNKNOWN, "sticker database is disabled");
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
auto &db = client.GetPartition().GetDatabaseOrThrow();
|
|
auto &sticker_database = *instance.sticker_database;
|
|
|
|
auto cmd = args.front();
|
|
auto sticker_type = args[1];
|
|
auto uri = args[2];
|
|
auto sticker_name = args.GetOptional(3);
|
|
|
|
std::unique_ptr<DomainHandler> handler;
|
|
|
|
if (StringIsEqual(sticker_type, "song"))
|
|
handler = std::make_unique<SongHandler>(r, db, sticker_database);
|
|
|
|
else if (StringIsEqual(sticker_type, "playlist"))
|
|
handler = std::make_unique<PlaylistHandler>(r, db, sticker_database);
|
|
|
|
else if (StringIsEqual(sticker_type, "filter"))
|
|
handler = std::make_unique<FilterHandler>(r, db, sticker_database);
|
|
|
|
// allow tags in the command to be case insensitive
|
|
// the handler will normalize the tag name with tag_item_names()
|
|
else if (auto tag_type = tag_name_parse_i(sticker_type); tag_type != TAG_NUM_OF_ITEM_TYPES)
|
|
handler = std::make_unique<TagHandler>(r, db, sticker_database, tag_type);
|
|
|
|
else {
|
|
r.FmtError(ACK_ERROR_ARG, "unknown sticker domain {:?}", sticker_type);
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
/* get */
|
|
if (args.size() == 4 && StringIsEqual(cmd, "get"))
|
|
return handler->Get(uri, sticker_name);
|
|
|
|
/* list */
|
|
if (args.size() == 3 && StringIsEqual(cmd, "list"))
|
|
return handler->List(uri);
|
|
|
|
/* set */
|
|
if (args.size() == 5 && StringIsEqual(cmd, "set")) {
|
|
if (StringIsEmpty(sticker_name)) {
|
|
r.FmtError(ACK_ERROR_ARG, "empty sticker name");
|
|
return CommandResult::ERROR;
|
|
}
|
|
return handler->Set(uri, sticker_name, args[4]);
|
|
}
|
|
|
|
/* inc */
|
|
if (args.size() == 5 && StringIsEqual(cmd, "inc")) {
|
|
if (StringIsEmpty(sticker_name)) {
|
|
r.FmtError(ACK_ERROR_ARG, "empty sticker name");
|
|
return CommandResult::ERROR;
|
|
}
|
|
return handler->Inc(uri, sticker_name, args[4]);
|
|
}
|
|
|
|
/* dec */
|
|
if (args.size() == 5 && StringIsEqual(cmd, "dec")) {
|
|
if (StringIsEmpty(sticker_name)) {
|
|
r.FmtError(ACK_ERROR_ARG, "empty sticker name");
|
|
return CommandResult::ERROR;
|
|
}
|
|
return handler->Dec(uri, sticker_name, args[4]);
|
|
}
|
|
|
|
/* delete */
|
|
if ((args.size() == 3 || args.size() == 4) && StringIsEqual(cmd, "delete"))
|
|
return handler->Delete(uri, sticker_name);
|
|
|
|
/* find */
|
|
if (args.size() >= 4 && StringIsEqual(cmd, "find")) {
|
|
RangeArg window = RangeArg::All();
|
|
if (args.size() >= 6 && StringIsEqual(args[args.size() - 2], "window")) {
|
|
window = args.ParseRange(args.size() - 1);
|
|
args.pop_back();
|
|
args.pop_back();
|
|
}
|
|
|
|
auto sort = "";
|
|
bool descending = false;
|
|
if (args.size() >= 6 && StringIsEqual(args[args.size() - 2], "sort")) {
|
|
const char *s = args.back();
|
|
if (*s == '-') {
|
|
descending = true;
|
|
++s;
|
|
}
|
|
if (StringIsEqual(s, "uri") ||
|
|
StringIsEqual(s, "value") ||
|
|
StringIsEqual(s, "value_int")
|
|
) {
|
|
sort = s;
|
|
}
|
|
else {
|
|
r.FmtError(ACK_ERROR_ARG, "Unknown sort tag {:?}", s);
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
args.pop_back();
|
|
args.pop_back();
|
|
}
|
|
|
|
bool has_op = args.size() > 4;
|
|
auto value = has_op ? args[5] : nullptr;
|
|
StickerOperator op = StickerOperator::EXISTS;
|
|
if (has_op) {
|
|
/* match the value */
|
|
auto op_s = args[4];
|
|
if (StringIsEqual(op_s, "="))
|
|
op = StickerOperator::EQUALS;
|
|
else if (StringIsEqual(op_s, "<"))
|
|
op = StickerOperator::LESS_THAN;
|
|
else if (StringIsEqual(op_s, ">"))
|
|
op = StickerOperator::GREATER_THAN;
|
|
else if (StringIsEqual(op_s, "eq"))
|
|
op = StickerOperator::EQUALS_INT;
|
|
else if (StringIsEqual(op_s, "lt"))
|
|
op = StickerOperator::LESS_THAN_INT;
|
|
else if (StringIsEqual(op_s, "gt"))
|
|
op = StickerOperator::GREATER_THAN_INT;
|
|
else if (StringIsEqual(op_s, "contains"))
|
|
op = StickerOperator::CONTAINS;
|
|
else if (StringIsEqual(op_s, "starts_with"))
|
|
op = StickerOperator::STARTS_WITH;
|
|
else {
|
|
r.FmtError(ACK_ERROR_ARG, "bad operator {:?}", op_s);
|
|
return CommandResult::ERROR;
|
|
}
|
|
}
|
|
return handler->Find(uri, sticker_name, op, value, sort, descending, window);
|
|
}
|
|
|
|
r.Error(ACK_ERROR_ARG, "bad request");
|
|
return CommandResult::ERROR;
|
|
}
|
|
|
|
CommandResult
|
|
handle_sticker_types(Client &client, Request args, Response &r)
|
|
{
|
|
(void) client;
|
|
(void) args;
|
|
const auto tag_mask = global_tag_mask & r.GetTagMask();
|
|
|
|
r.Fmt("stickertype: filter\n");
|
|
r.Fmt("stickertype: playlist\n");
|
|
r.Fmt("stickertype: song\n");
|
|
for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; i++)
|
|
if (sticker_allowed_tags.Test(TagType(i)) &&
|
|
tag_mask.Test(TagType(i)))
|
|
r.Fmt("stickertype: {}\n", tag_item_names[i]);
|
|
return CommandResult::OK;
|
|
}
|