Stickers: added support for stickers on playlists and some tag types

This commit is contained in:
gd 2022-10-29 19:58:42 +03:00 committed by Max Kellermann
parent 70ac638d93
commit 432675d4c2
17 changed files with 955 additions and 99 deletions

1
NEWS
View File

@ -9,6 +9,7 @@ ver 0.24 (not yet released)
- operator "starts_with" - operator "starts_with"
- show PCRE support in "config" response - show PCRE support in "config" response
- apply Unicode normalization to case-insensitive filter expressions - apply Unicode normalization to case-insensitive filter expressions
- stickers on playlists and some tag types
* database * database
- proxy: require MPD 0.21 or later - proxy: require MPD 0.21 or later
- proxy: require libmpdclient 2.15 or later - proxy: require libmpdclient 2.15 or later

View File

@ -1401,6 +1401,41 @@ Objects which may have stickers are addressed by their object
type ("song" for song objects) and their URI (the path within type ("song" for song objects) and their URI (the path within
the database for songs). the database for songs).
.. note:: Since :program:`MPD` 0.24 stickers can also be attached to playlists,
some tag types, and :ref:`filter expressions <filter_syntax>`.
The following tag types are alloed: Title, Album, Artist, AlbumArtist, Genre,
Composer, Performer, Conductor, Work, Ensable, Location, and Label.
.. list-table:: Sticker addressing
:widths: 10 45 45
:header-rows: 2
* - Type
- URI
- URI
* -
- get, set, delete, list, find
- find only
* - "song"
- File path within the database
- Directory path within the database to find a sticker on
all songs under this path recursively
* - "playlist"
- The playlist name of a stored playlist
- An empty string to find a sticker in all playlists
* - Tag type e.g. "Album"
- The tag value
- An empty string to find a sticker in all instances of the tag type
* - "filter"
- A :ref:`filter expression <filter_syntax>`.
- An empty string to find a sticker in all instances of the filter
.. _command_sticker_get: .. _command_sticker_get:
:command:`sticker get {TYPE} {URI} {NAME}` :command:`sticker get {TYPE} {URI} {NAME}`
@ -1441,6 +1476,43 @@ the database for songs).
Other supported operators are: Other supported operators are:
"``<``", "``>``" "``<``", "``>``"
Examples:
.. code-block::
sitcker set song "path/to/song_1.mp3" "name_1" "value_1"
OK
sitcker set song "path/to/song_2.mp3" "name_1" "value_2"
OK
sticker get song "path/to/song_1.mp3" "name_1"
sticker: name_1=value_1
OK
sitcker find song "path" "name_1"
file: path/to/song_1.mp3
sticker: name_1=value_1
file: path/to/song_2.mp3
sticker: name_1=value_2
OK
.. code-block::
sticker set Album "Greatest Hits" "name_1" "value_1"
OK
sticker find Album "" name_1
Album: Greatest Hits
sticker: name_1=value_1
OK
stciker set filter "((album == 'Greatest Hits') AND (artist == 'Vera Lynn'))" name_1 value_1
OK
stciker set filter "((album == 'Greatest Hits') AND (artist == 'Johnny Chester'))" name_1 value_1
OK
sticker find filter "" name_1
filter: ((album == 'Greatest Hits') AND (artist == 'Johnny Chester'))
sticker: name_1=value_1
filter: ((album == 'Greatest Hits') AND (artist == 'Vera Lynn'))
sticker: name_1=value_1
OK
Connection settings Connection settings
=================== ===================

View File

@ -454,6 +454,9 @@ if sqlite_dep.found()
'src/sticker/Database.cxx', 'src/sticker/Database.cxx',
'src/sticker/Print.cxx', 'src/sticker/Print.cxx',
'src/sticker/SongSticker.cxx', 'src/sticker/SongSticker.cxx',
'src/sticker/TagSticker.cxx',
'src/sticker/AllowedTags.cxx',
'src/sticker/CleanupService.cxx',
] ]
endif endif

View File

@ -32,13 +32,21 @@
#ifdef ENABLE_SQLITE #ifdef ENABLE_SQLITE
#include "sticker/Database.hxx" #include "sticker/Database.hxx"
#include "sticker/SongSticker.hxx" #include "sticker/SongSticker.hxx"
#include "sticker/TagSticker.hxx"
#include "sticker/CleanupService.hxx"
#endif #endif
#endif #endif
Instance::Instance() = default; Instance::Instance() = default;
Instance::~Instance() noexcept Instance::~Instance() noexcept
{ {
#ifdef ENABLE_SQLITE
if (sticker_cleanup)
sticker_cleanup.reset();
#endif
#ifdef ENABLE_DATABASE #ifdef ENABLE_DATABASE
delete update; delete update;
@ -105,6 +113,11 @@ Instance::OnDatabaseModified() noexcept
for (auto &partition : partitions) for (auto &partition : partitions)
partition.DatabaseModified(*database); partition.DatabaseModified(*database);
#ifdef ENABLE_SQLITE
if (sticker_database)
StartStickerCleanup();
#endif
} }
void void
@ -186,3 +199,56 @@ Instance::FlushCaches() noexcept
if (input_cache) if (input_cache)
input_cache->Flush(); input_cache->Flush();
} }
void
Instance::OnPlaylistDeleted(const char *name) const noexcept
{
#ifdef ENABLE_SQLITE
/* if the playlist has stickers, remove theme */
if (HasStickerDatabase()) {
try {
sticker_database->Delete("playlist", name);
} catch (...) {
}
}
#endif
}
#ifdef ENABLE_SQLITE
void
Instance::OnStickerCleanupDone(bool changed) noexcept
{
assert(event_loop.IsInside());
sticker_cleanup.reset();
if (changed)
EmitIdle(IDLE_STICKER);
if (need_sticker_cleanup)
StartStickerCleanup();
}
void
Instance::StartStickerCleanup()
{
assert(sticker_database != nullptr);
if (sticker_cleanup) {
/* still runnning, start a new one when that one
finishes*/
need_sticker_cleanup = true;
return;
}
need_sticker_cleanup = false;
sticker_cleanup =
std::make_unique<StickerCleanupService>(*this,
*sticker_database,
*database);
sticker_cleanup->Start();
}
#endif // ENABLE_SQLITE

View File

@ -25,6 +25,7 @@ class NeighborGlue;
#ifdef ENABLE_DATABASE #ifdef ENABLE_DATABASE
#include "db/DatabaseListener.hxx" #include "db/DatabaseListener.hxx"
#include "db/Ptr.hxx" #include "db/Ptr.hxx"
class Storage; class Storage;
class UpdateService; class UpdateService;
#ifdef ENABLE_INOTIFY #ifdef ENABLE_INOTIFY
@ -40,6 +41,7 @@ struct Partition;
class StateFile; class StateFile;
class RemoteTagCache; class RemoteTagCache;
class StickerDatabase; class StickerDatabase;
class StickerCleanupService;
class InputCacheManager; class InputCacheManager;
/** /**
@ -125,6 +127,10 @@ struct Instance final
#ifdef ENABLE_SQLITE #ifdef ENABLE_SQLITE
std::unique_ptr<StickerDatabase> sticker_database; std::unique_ptr<StickerDatabase> sticker_database;
std::unique_ptr<StickerCleanupService> sticker_cleanup;
bool need_sticker_cleanup = false;
#endif #endif
Instance(); Instance();
@ -187,6 +193,9 @@ struct Instance final
bool HasStickerDatabase() const noexcept { bool HasStickerDatabase() const noexcept {
return sticker_database != nullptr; return sticker_database != nullptr;
} }
void OnStickerCleanupDone(bool changed) noexcept;
void StartStickerCleanup();
#endif #endif
void BeginShutdownUpdate() noexcept; void BeginShutdownUpdate() noexcept;
@ -201,6 +210,8 @@ struct Instance final
void FlushCaches() noexcept; void FlushCaches() noexcept;
void OnPlaylistDeleted(const char *name) const noexcept;
private: private:
#ifdef ENABLE_DATABASE #ifdef ENABLE_DATABASE
/* virtual methods from class DatabaseListener */ /* virtual methods from class DatabaseListener */

View File

@ -155,6 +155,9 @@ handle_rm([[maybe_unused]] Client &client, Request args, [[maybe_unused]] Respon
const char *const name = args.front(); const char *const name = args.front();
spl_delete(name); spl_delete(name);
client.GetInstance().OnPlaylistDeleted(name);
return CommandResult::OK; return CommandResult::OK;
} }

View File

@ -6,7 +6,10 @@
#include "SongPrint.hxx" #include "SongPrint.hxx"
#include "db/Interface.hxx" #include "db/Interface.hxx"
#include "sticker/Sticker.hxx" #include "sticker/Sticker.hxx"
#include "sticker/TagSticker.hxx"
#include "sticker/Database.hxx"
#include "sticker/SongSticker.hxx" #include "sticker/SongSticker.hxx"
#include "sticker/AllowedTags.hxx"
#include "sticker/Print.hxx" #include "sticker/Print.hxx"
#include "client/Client.hxx" #include "client/Client.hxx"
#include "client/Response.hxx" #include "client/Response.hxx"
@ -14,133 +17,285 @@
#include "Instance.hxx" #include "Instance.hxx"
#include "util/StringAPI.hxx" #include "util/StringAPI.hxx"
#include "util/ScopeExit.hxx" #include "util/ScopeExit.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 "song/Filter.hxx"
namespace { namespace {
struct sticker_song_find_data {
Response &r;
const char *name;
};
} // namespace
static void class DomainHandler {
sticker_song_find_print_cb(const LightSong &song, const char *value, public:
void *user_data) virtual ~DomainHandler() = default;
{
auto *data =
(struct sticker_song_find_data *)user_data;
song_print_uri(data->r, song); virtual CommandResult Get(const char *uri, const char *name) {
sticker_print_value(data->r, data->name, value); const auto value = sticker_database.LoadValue(sticker_type,
} ValidateUri(uri).c_str(),
name);
static CommandResult
handle_sticker_song(Response &r, Partition &partition,
StickerDatabase &sticker_database,
Request args)
{
const Database &db = partition.GetDatabaseOrThrow();
const char *const cmd = args.front();
/* get song song_id key */
if (args.size() == 4 && StringIsEqual(cmd, "get")) {
const LightSong *song = db.GetSong(args[2]);
assert(song != nullptr);
AtScopeExit(&db, song) { db.ReturnSong(song); };
const auto value = sticker_song_get_value(sticker_database,
*song, args[3]);
if (value.empty()) { if (value.empty()) {
r.Error(ACK_ERROR_NO_EXIST, "no such sticker"); response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: \"{}\"", name);
return CommandResult::ERROR; return CommandResult::ERROR;
} }
sticker_print_value(r, args[3], value.c_str()); sticker_print_value(response, name, value.c_str());
return CommandResult::OK; return CommandResult::OK;
/* list song song_id */ }
} else if (args.size() == 3 && StringIsEqual(cmd, "list")) {
const LightSong *song = db.GetSong(args[2]);
assert(song != nullptr);
AtScopeExit(&db, song) { db.ReturnSong(song); };
const auto sticker = sticker_song_get(sticker_database, *song); virtual CommandResult Set(const char *uri, const char *name, const char *value) {
sticker_print(r, sticker); sticker_database.StoreValue(sticker_type,
ValidateUri(uri).c_str(),
name,
value);
return CommandResult::OK; return CommandResult::OK;
/* set song song_id id key */ }
} else if (args.size() == 5 && StringIsEqual(cmd, "set")) {
const LightSong *song = db.GetSong(args[2]);
assert(song != nullptr);
AtScopeExit(&db, song) { db.ReturnSong(song); };
sticker_song_set_value(sticker_database, *song, virtual CommandResult Delete(const char *uri, const char *name) {
args[3], args[4]); std::string validated_uri = ValidateUri(uri);
return CommandResult::OK; uri = validated_uri.c_str();
/* delete song song_id [key] */ bool ret = name == nullptr
} else if ((args.size() == 3 || args.size() == 4) && ? sticker_database.Delete(sticker_type, uri)
StringIsEqual(cmd, "delete")) { : sticker_database.DeleteValue(sticker_type, uri, name);
const LightSong *song = db.GetSong(args[2]);
assert(song != nullptr);
AtScopeExit(&db, song) { db.ReturnSong(song); };
bool ret = args.size() == 3
? sticker_song_delete(sticker_database, *song)
: sticker_song_delete_value(sticker_database, *song,
args[3]);
if (!ret) { if (!ret) {
r.Error(ACK_ERROR_NO_EXIST, "no such sticker"); response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: \"{}\"", name);
return CommandResult::ERROR; return CommandResult::ERROR;
} }
return CommandResult::OK; return CommandResult::OK;
/* find song dir key */ }
} else if ((args.size() == 4 || args.size() == 6) &&
StringIsEqual(cmd, "find")) {
/* "sticker find song a/directory name" */
const char *const base_uri = args[2]; virtual CommandResult List(const char *uri) {
const auto sticker = sticker_database.Load(sticker_type,
ValidateUri(uri).c_str());
sticker_print(response, sticker);
StickerOperator op = StickerOperator::EXISTS; return CommandResult::OK;
const char *value = nullptr; }
if (args.size() == 6) { virtual CommandResult Find(const char *uri, const char *name, StickerOperator op, const char *value) {
/* match the value */ auto data = CallbackContext{
.name = name,
const char *op_s = args[4]; .sticker_type = sticker_type,
value = args[5]; .response = response,
.is_song = StringIsEqual("song", sticker_type)
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 {
r.Error(ACK_ERROR_ARG, "bad operator");
return CommandResult::ERROR;
}
}
struct sticker_song_find_data data = {
r,
args[3],
}; };
sticker_song_find(sticker_database, db, base_uri, data.name, 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,
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) override {
struct sticker_song_find_data data = {
response,
name,
};
sticker_song_find(sticker_database, database, uri, data.name,
op, value, op, value,
sticker_song_find_print_cb, &data); sticker_song_find_print_cb, &data);
return CommandResult::OK; return CommandResult::OK;
} else {
r.Error(ACK_ERROR_ARG, "bad request");
return CommandResult::ERROR;
} }
}
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 CommandResult
handle_sticker(Client &client, Request args, Response &r) handle_sticker(Client &client, Request args, Response &r)
{ {
// must be enforced by the caller
assert(args.size() >= 3); assert(args.size() >= 3);
auto &instance = client.GetInstance(); auto &instance = client.GetInstance();
@ -149,14 +304,73 @@ handle_sticker(Client &client, Request args, Response &r)
return CommandResult::ERROR; return CommandResult::ERROR;
} }
auto &db = client.GetPartition().GetDatabaseOrThrow();
auto &sticker_database = *instance.sticker_database; auto &sticker_database = *instance.sticker_database;
if (StringIsEqual(args[1], "song")) auto cmd = args.front();
return handle_sticker_song(r, client.GetPartition(), auto sticker_type = args[1];
sticker_database, auto uri = args[2];
args); 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 { else {
r.Error(ACK_ERROR_ARG, "unknown sticker domain"); r.FmtError(ACK_ERROR_ARG, "unknown sticker domain \"{}\"", sticker_type);
return CommandResult::ERROR; 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"))
return handler->Set(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 || args.size() == 6) && StringIsEqual(cmd, "find")) {
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 {
r.FmtError(ACK_ERROR_ARG, "bad operator \"{}\"", op_s);
return CommandResult::ERROR;
}
}
return handler->Find(uri, sticker_name, op, value);
}
r.Error(ACK_ERROR_ARG, "bad request");
return CommandResult::ERROR;
} }

View File

@ -48,3 +48,12 @@ PlaylistVector::erase(std::string_view name) noexcept
erase(i); erase(i);
return true; return true;
} }
bool
PlaylistVector::exists(std::string_view name) const noexcept
{
assert(holding_db_lock());
return std::find_if(begin(), end(),
PlaylistInfo::CompareName(name)) != end();
}

View File

@ -35,6 +35,9 @@ public:
* Caller must lock the #db_mutex. * Caller must lock the #db_mutex.
*/ */
bool erase(std::string_view name) noexcept; bool erase(std::string_view name) noexcept;
[[nodiscard]]
bool exists(std::string_view name) const noexcept;
}; };
#endif /* SONGVEC_H */ #endif /* SONGVEC_H */

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "AllowedTags.hxx"
#include "tag/Mask.hxx"
#include <cassert>
static constexpr TagType sticker_allowed_tags_init[] = {
TAG_ARTIST,
TAG_ALBUM,
TAG_ALBUM_ARTIST,
TAG_TITLE,
TAG_GENRE,
TAG_COMPOSER,
TAG_PERFORMER,
TAG_CONDUCTOR,
TAG_WORK,
TAG_ENSEMBLE,
TAG_LOCATION,
TAG_LABEL,
TAG_MUSICBRAINZ_ARTISTID,
TAG_MUSICBRAINZ_ALBUMID,
TAG_MUSICBRAINZ_ALBUMARTISTID,
TAG_MUSICBRAINZ_RELEASETRACKID,
TAG_MUSICBRAINZ_WORKID,
};
static constexpr auto
TagArrayToMask() noexcept
{
auto result = TagMask::None();
for (const auto i : sticker_allowed_tags_init) {
/* no duplicates allowed */
assert(!result.Test(i));
result |= i;
}
return result;
}
constinit const TagMask sticker_allowed_tags = TagArrayToMask();

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
class TagMask;
/**
* These are the tags that are allowed to have stickers.
*/
extern const TagMask sticker_allowed_tags;

View File

@ -0,0 +1,112 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "CleanupService.hxx"
#include "Database.hxx"
#include "Sticker.hxx"
#include "TagSticker.hxx"
#include "lib/fmt/ExceptionFormatter.hxx"
#include "song/Filter.hxx"
#include "thread/Name.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
#include "Instance.hxx"
static constexpr Domain sticker_domain{"sticker"};
StickerCleanupService::StickerCleanupService(Instance &_instance,
StickerDatabase &_sticker_db,
Database &_db) noexcept
:instance(_instance),
sticker_db(_sticker_db.Reopen()),
music_db(_db),
defer(_instance.event_loop, BIND_THIS_METHOD(RunDeferred))
{
}
StickerCleanupService::~StickerCleanupService() noexcept
{
// call only by the owning instance
assert(GetEventLoop().IsInside());
CancelAndJoin();
}
void
StickerCleanupService::Start()
{
// call only by the owning instance
assert(GetEventLoop().IsInside());
thread.Start();
FmtDebug(sticker_domain,
"spawned thread for cleanup job");
}
void
StickerCleanupService::RunDeferred() noexcept
{
instance.OnStickerCleanupDone(deleted_count != 0);
}
static std::size_t
DeleteStickers(StickerDatabase &sticker_db,
std::list<StickerDatabase::StickerTypeUriPair> &stickers)
{
if (stickers.empty())
return 0;
sticker_db.BatchDeleteNoIdle(stickers);
auto count = stickers.size();
stickers.clear();
return count;
}
void
StickerCleanupService::Task() noexcept
{
SetThreadName("sticker");
FmtDebug(sticker_domain, "begin cleanup");
try {
auto stickers = sticker_db.GetUniqueStickers();
auto iter = stickers.cbegin();
std::list<StickerDatabase::StickerTypeUriPair> batch;
while (!cancel_flag && !stickers.empty()) {
const auto &[sticker_type, sticker_uri] = *iter;
const auto filter = MakeSongFilterNoThrow(sticker_type.c_str(), sticker_uri.c_str());
if (filter.IsEmpty() || FilterMatches(music_db, filter))
// skip if found a match or if not a valid sticker filter
iter = stickers.erase(iter);
else {
batch.splice(batch.end(), stickers, iter++);
if (batch.size() == DeleteBatchSize)
deleted_count += DeleteStickers(sticker_db, batch);
}
}
if (!cancel_flag)
deleted_count += DeleteStickers(sticker_db, batch);
} catch (...) {
FmtError(sticker_domain, "cleanup failed: {}",
std::current_exception());
}
defer.Schedule();
FmtDebug(sticker_domain, "end cleanup: {} stickers deleted",
deleted_count);
}
void
StickerCleanupService::CancelAndJoin() noexcept
{
if (thread.IsDefined()) {
cancel_flag = true;
thread.Join();
}
}

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
#include "Database.hxx"
#include "event/InjectEvent.hxx"
#include "thread/Thread.hxx"
#include <atomic>
class Database;
struct Instance;
/**
* Delete stickers that no longer match items in the music database.
*
* When done calls Instance::OnSickerCleanupDone() in the instance event loop.
*/
class StickerCleanupService {
/**
* number of stickers to delete in one transaction
*/
static constexpr std::size_t DeleteBatchSize = 50;
Instance &instance;
StickerDatabase sticker_db;
Database &music_db;
Thread thread{BIND_THIS_METHOD(Task)};
InjectEvent defer;
std::size_t deleted_count{0};
std::atomic_bool cancel_flag{false};
public:
StickerCleanupService(Instance &_instance,
StickerDatabase &_sticker_db,
Database &_db) noexcept;
~StickerCleanupService() noexcept;
auto &GetEventLoop() const noexcept {
return defer.GetEventLoop();
}
void Start();
private:
void Task() noexcept;
void RunDeferred() noexcept;
void CancelAndJoin() noexcept;
};

View File

@ -13,6 +13,7 @@
#include <cassert> #include <cassert>
#include <iterator> #include <iterator>
#include <array> #include <array>
#include <stdexcept>
using namespace Sqlite; using namespace Sqlite;
@ -27,6 +28,11 @@ enum sticker_sql {
STICKER_SQL_FIND_VALUE, STICKER_SQL_FIND_VALUE,
STICKER_SQL_FIND_LT, STICKER_SQL_FIND_LT,
STICKER_SQL_FIND_GT, STICKER_SQL_FIND_GT,
STICKER_SQL_DISTINCT_TYPE_URI,
STICKER_SQL_TRANSACTION_BEGIN,
STICKER_SQL_TRANSACTION_COMMIT,
STICKER_SQL_TRANSACTION_ROLLBACK,
STICKER_SQL_COUNT STICKER_SQL_COUNT
}; };
@ -54,6 +60,18 @@ static constexpr auto sticker_sql = std::array {
//[STICKER_SQL_FIND_GT] = //[STICKER_SQL_FIND_GT] =
"SELECT uri,value FROM sticker WHERE type=? AND uri LIKE (? || '%') AND name=? AND value>?", "SELECT uri,value FROM sticker WHERE type=? AND uri LIKE (? || '%') AND name=? AND value>?",
//[STICKER_SQL_DISTINCT_TYPE_URI] =
"SELECT DISTINCT type,uri FROM sticker",
//[STICKER_SQL_TRANSACTION_BEGIN]
"BEGIN",
//[STICKER_SQL_TRANSACTION_COMMIT]
"COMMIT",
//[STICKER_SQL_TRANSACTION_ROLLBACK]
"ROLLBACK",
}; };
static constexpr const char sticker_sql_create[] = static constexpr const char sticker_sql_create[] =
@ -331,3 +349,51 @@ StickerDatabase::Find(const char *type, const char *base_uri, const char *name,
user_data); user_data);
}); });
} }
std::list<StickerDatabase::StickerTypeUriPair>
StickerDatabase::GetUniqueStickers()
{
auto result = std::list<StickerTypeUriPair>{};
sqlite3_stmt *const s = stmt[STICKER_SQL_DISTINCT_TYPE_URI];
assert(s != nullptr);
AtScopeExit(s) {
sqlite3_reset(s);
};
ExecuteForEach(s, [&s, &result]() {
result.emplace_back((const char*)sqlite3_column_text(s, 0),
(const char*)sqlite3_column_text(s, 1));
});
return result;
}
void
StickerDatabase::BatchDeleteNoIdle(const std::list<StickerTypeUriPair> &stickers)
{
sqlite3_stmt *const s = stmt[STICKER_SQL_DELETE];
sqlite3_stmt *const begin = stmt[STICKER_SQL_TRANSACTION_BEGIN];
sqlite3_stmt *const rollback = stmt[STICKER_SQL_TRANSACTION_ROLLBACK];
sqlite3_stmt *const commit = stmt[STICKER_SQL_TRANSACTION_COMMIT];
try {
ExecuteBusy(begin);
for (auto &sticker: stickers) {
AtScopeExit(s) {
sqlite3_reset(s);
sqlite3_clear_bindings(s);
};
BindAll(s, sticker.first.c_str(), sticker.second.c_str());
ExecuteCommand(s);
}
ExecuteBusy(commit);
} catch (...) {
// "If the transaction has already been rolled back automatically by the error response,
// then the ROLLBACK command will fail with an error, but no harm is caused by this."
ExecuteBusy(rollback);
std::throw_with_nested(std::runtime_error{"failed to batch-delete stickers"});
}
}

View File

@ -33,6 +33,7 @@
#include <map> #include <map>
#include <string> #include <string>
#include <list>
class Path; class Path;
struct Sticker; struct Sticker;
@ -49,6 +50,10 @@ class StickerDatabase {
SQL_FIND_VALUE, SQL_FIND_VALUE,
SQL_FIND_LT, SQL_FIND_LT,
SQL_FIND_GT, SQL_FIND_GT,
SQL_DISTINCT_TYPE_URI,
SQL_TRANSACTION_BEGIN,
SQL_TRANSACTION_COMMIT,
SQL_TRANSACTION_ROLLBACK,
SQL_COUNT SQL_COUNT
}; };
@ -141,6 +146,20 @@ public:
void *user_data), void *user_data),
void *user_data); void *user_data);
using StickerTypeUriPair = std::pair<std::string, std::string>;
/**
* @return A list of unique type-uri pairs of all the stickers
* in the database.
*/
std::list<StickerTypeUriPair> GetUniqueStickers();
/**
* Delete stickers by type and uri
* @param stickers A list of stickers to delete
*/
void BatchDeleteNoIdle(const std::list<StickerTypeUriPair> &stickers);
private: private:
void ListValues(std::map<std::string, std::string, std::less<>> &table, void ListValues(std::map<std::string, std::string, std::less<>> &table,
const char *type, const char *uri); const char *type, const char *uri);

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "TagSticker.hxx"
#include "Database.hxx"
#include "AllowedTags.hxx"
#include "db/Interface.hxx"
#include "db/Selection.hxx"
#include "tag/Mask.hxx"
#include "tag/Names.hxx"
#include "tag/ParseName.hxx"
#include "song/Filter.hxx"
#include "util/StringAPI.hxx"
SongFilter
MakeSongFilter(const char *filter_string)
{
const std::array args{filter_string};
auto filter = SongFilter();
filter.Parse(args, false);
filter.Optimize();
return filter;
}
SongFilter
MakeSongFilter(TagType tag_type, const char *tag_value)
{
if (!sticker_allowed_tags.Test(tag_type))
throw std::runtime_error("tag type not allowed for sticker");
const std::array args{tag_item_names[tag_type], tag_value};
SongFilter filter;
filter.Parse(args, false);
filter.Optimize();
return filter;
}
SongFilter
MakeSongFilter(const char *sticker_type, const char *sticker_uri)
{
if (StringIsEqual(sticker_type, "filter"))
return MakeSongFilter(sticker_uri);
if (auto tag_type = tag_name_parse_i(sticker_type); tag_type != TAG_NUM_OF_ITEM_TYPES)
return MakeSongFilter(tag_type, sticker_uri);
return {};
}
SongFilter
MakeSongFilterNoThrow(const char *sticker_type, const char *sticker_uri) noexcept
{
try {
return MakeSongFilter(sticker_type, sticker_uri);
} catch (...) {
return {};
}
}
bool
TagExists(const Database &database, TagType tag_type, const char *tag_value)
{
return FilterMatches(database, MakeSongFilter(tag_type, tag_value));
}
bool
FilterMatches(const Database &database, const SongFilter &filter) noexcept
{
if (filter.IsEmpty())
return false;
const DatabaseSelection selection{"", true, &filter};
// TODO: we just need to know if the tag selection has a match.
// a visitor callback that can stop the db visit by return value
// may be cleaner than throwing an exception.
struct MatchFoundException {};
try {
database.Visit(selection, [](const LightSong &) {
throw MatchFoundException{};
});
} catch (MatchFoundException) {
return true;
}
return false;
}

View File

@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
#include <cstdint>
enum TagType : uint8_t;
class Database;
class StickerDatabase;
class SongFilter;
/**
* Parse a filter_string.
*
* @param filter_string a valid filter expression
*
* @return SongFilter
*
* @throws std::runtime_error if failed to parse filter string
*/
SongFilter
MakeSongFilter(const char *filter_string);
/**
* Make a song filter from tag and value e.g. album name
*
* @return SongFilter
*
* @throws std::runtime_error if failed to make song filter or tag type not allowd for sticker
*/
SongFilter
MakeSongFilter(TagType tag_type, const char *tag_value);
/**
* Make a song filter by sticker type and uri
*
* @param sticker_type either one of the allowed tag names or "filter"
* @param sticker_uri if the type is a tag name then this is the value,
* if the type if "filter" then this is a filter expression
*
* @return SongFilter
*
* @throws std::runtime_error if failed to make song filter or tag type not allowd for sticker
*/
SongFilter
MakeSongFilter(const char *sticker_type, const char *sticker_uri);
/**
* Like MakeSongFilter(const char *sticker_type, const char *sticker_uri)
* but return an empty filter instead of throwing
*
* @param sticker_type
* @param sticker_uri
*
* @return SongFilter
*/
SongFilter
MakeSongFilterNoThrow(const char *sticker_type, const char *sticker_uri) noexcept;
/**
* Try to make a selection on the database using the tag type and value
* from a sticker command.
*
* @return true if the selection returned at least one match or false otherwise
*
* @throws std::runtime_error if failed to make song filter
*/
bool
TagExists(const Database &database, TagType tag_type, const char *tag_value);
/**
* Try to make a selection on the database using a filter
* from a sticker command.
*
* @return true if the selection returned at least one match or false otherwise
*/
bool
FilterMatches(const Database &database, const SongFilter &filter) noexcept;