Files
mpd/src/command/StickerCommands.cxx

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;
}