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"
- show PCRE support in "config" response
- apply Unicode normalization to case-insensitive filter expressions
- stickers on playlists and some tag types
* database
- proxy: require MPD 0.21 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
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 {TYPE} {URI} {NAME}`
@ -1441,6 +1476,43 @@ the database for songs).
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
===================

View File

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

View File

@ -32,13 +32,21 @@
#ifdef ENABLE_SQLITE
#include "sticker/Database.hxx"
#include "sticker/SongSticker.hxx"
#include "sticker/TagSticker.hxx"
#include "sticker/CleanupService.hxx"
#endif
#endif
Instance::Instance() = default;
Instance::~Instance() noexcept
{
#ifdef ENABLE_SQLITE
if (sticker_cleanup)
sticker_cleanup.reset();
#endif
#ifdef ENABLE_DATABASE
delete update;
@ -105,6 +113,11 @@ Instance::OnDatabaseModified() noexcept
for (auto &partition : partitions)
partition.DatabaseModified(*database);
#ifdef ENABLE_SQLITE
if (sticker_database)
StartStickerCleanup();
#endif
}
void
@ -186,3 +199,56 @@ Instance::FlushCaches() noexcept
if (input_cache)
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
#include "db/DatabaseListener.hxx"
#include "db/Ptr.hxx"
class Storage;
class UpdateService;
#ifdef ENABLE_INOTIFY
@ -40,6 +41,7 @@ struct Partition;
class StateFile;
class RemoteTagCache;
class StickerDatabase;
class StickerCleanupService;
class InputCacheManager;
/**
@ -125,6 +127,10 @@ struct Instance final
#ifdef ENABLE_SQLITE
std::unique_ptr<StickerDatabase> sticker_database;
std::unique_ptr<StickerCleanupService> sticker_cleanup;
bool need_sticker_cleanup = false;
#endif
Instance();
@ -187,6 +193,9 @@ struct Instance final
bool HasStickerDatabase() const noexcept {
return sticker_database != nullptr;
}
void OnStickerCleanupDone(bool changed) noexcept;
void StartStickerCleanup();
#endif
void BeginShutdownUpdate() noexcept;
@ -201,6 +210,8 @@ struct Instance final
void FlushCaches() noexcept;
void OnPlaylistDeleted(const char *name) const noexcept;
private:
#ifdef ENABLE_DATABASE
/* 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();
spl_delete(name);
client.GetInstance().OnPlaylistDeleted(name);
return CommandResult::OK;
}

View File

@ -6,7 +6,10 @@
#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"
@ -14,133 +17,285 @@
#include "Instance.hxx"
#include "util/StringAPI.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 {
struct sticker_song_find_data {
Response &r;
const char *name;
};
} // namespace
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;
class DomainHandler {
public:
virtual ~DomainHandler() = default;
song_print_uri(data->r, song);
sticker_print_value(data->r, data->name, value);
}
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]);
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()) {
r.Error(ACK_ERROR_NO_EXIST, "no such sticker");
response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: \"{}\"", name);
return CommandResult::ERROR;
}
sticker_print_value(r, args[3], value.c_str());
sticker_print_value(response, name, value.c_str());
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);
sticker_print(r, sticker);
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;
/* 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,
args[3], args[4]);
return CommandResult::OK;
/* delete song song_id [key] */
} else if ((args.size() == 3 || args.size() == 4) &&
StringIsEqual(cmd, "delete")) {
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]);
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) {
r.Error(ACK_ERROR_NO_EXIST, "no such sticker");
response.FmtError(ACK_ERROR_NO_EXIST, "no such sticker: \"{}\"", name);
return CommandResult::ERROR;
}
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];
StickerOperator op = StickerOperator::EXISTS;
const char *value = nullptr;
if (args.size() == 6) {
/* match the value */
const char *op_s = args[4];
value = args[5];
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],
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) {
auto data = CallbackContext{
.name = name,
.sticker_type = sticker_type,
.response = response,
.is_song = StringIsEqual("song", sticker_type)
};
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,
sticker_song_find_print_cb, &data);
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
handle_sticker(Client &client, Request args, Response &r)
{
// must be enforced by the caller
assert(args.size() >= 3);
auto &instance = client.GetInstance();
@ -149,14 +304,73 @@ handle_sticker(Client &client, Request args, Response &r)
return CommandResult::ERROR;
}
auto &db = client.GetPartition().GetDatabaseOrThrow();
auto &sticker_database = *instance.sticker_database;
if (StringIsEqual(args[1], "song"))
return handle_sticker_song(r, client.GetPartition(),
sticker_database,
args);
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.Error(ACK_ERROR_ARG, "unknown sticker domain");
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"))
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);
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.
*/
bool erase(std::string_view name) noexcept;
[[nodiscard]]
bool exists(std::string_view name) const noexcept;
};
#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 <iterator>
#include <array>
#include <stdexcept>
using namespace Sqlite;
@ -27,6 +28,11 @@ enum sticker_sql {
STICKER_SQL_FIND_VALUE,
STICKER_SQL_FIND_LT,
STICKER_SQL_FIND_GT,
STICKER_SQL_DISTINCT_TYPE_URI,
STICKER_SQL_TRANSACTION_BEGIN,
STICKER_SQL_TRANSACTION_COMMIT,
STICKER_SQL_TRANSACTION_ROLLBACK,
STICKER_SQL_COUNT
};
@ -54,6 +60,18 @@ static constexpr auto sticker_sql = std::array {
//[STICKER_SQL_FIND_GT] =
"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[] =
@ -331,3 +349,51 @@ StickerDatabase::Find(const char *type, const char *base_uri, const char *name,
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 <string>
#include <list>
class Path;
struct Sticker;
@ -49,6 +50,10 @@ class StickerDatabase {
SQL_FIND_VALUE,
SQL_FIND_LT,
SQL_FIND_GT,
SQL_DISTINCT_TYPE_URI,
SQL_TRANSACTION_BEGIN,
SQL_TRANSACTION_COMMIT,
SQL_TRANSACTION_ROLLBACK,
SQL_COUNT
};
@ -141,6 +146,20 @@ public:
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:
void ListValues(std::map<std::string, std::string, std::less<>> &table,
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;