Stickers: added support for stickers on playlists and some tag types
This commit is contained in:
parent
70ac638d93
commit
432675d4c2
1
NEWS
1
NEWS
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
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,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
struct sticker_song_find_data {
|
||||||
Response &r;
|
Response &r;
|
||||||
const char *name;
|
const char *name;
|
||||||
};
|
};
|
||||||
} // namespace
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sticker_song_find_print_cb(const LightSong &song, const char *value,
|
sticker_song_find_print_cb(const LightSong &song, const char *value,
|
||||||
void *user_data)
|
void *user_data)
|
||||||
{
|
{
|
||||||
auto *data =
|
auto *data = (struct sticker_song_find_data *)user_data;
|
||||||
(struct sticker_song_find_data *)user_data;
|
|
||||||
|
|
||||||
song_print_uri(data->r, song);
|
song_print_uri(data->r, song);
|
||||||
sticker_print_value(data->r, data->name, value);
|
sticker_print_value(data->r, data->name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static CommandResult
|
const LightSong* song = nullptr;
|
||||||
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()) {
|
|
||||||
r.Error(ACK_ERROR_NO_EXIST, "no such sticker");
|
|
||||||
return CommandResult::ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
sticker_print_value(r, args[3], 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);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
if (!ret) {
|
|
||||||
r.Error(ACK_ERROR_NO_EXIST, "no such sticker");
|
|
||||||
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],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sticker_song_find(sticker_database, db, base_uri, data.name,
|
/**
|
||||||
op, value,
|
* Base for Tag and Filter handlers
|
||||||
sticker_song_find_print_cb, &data);
|
*/
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return CommandResult::OK;
|
/**
|
||||||
} else {
|
* Tag type stickers handler
|
||||||
r.Error(ACK_ERROR_ARG, "bad request");
|
*/
|
||||||
return CommandResult::ERROR;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 CommandResult::ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return handler->Find(uri, sticker_name, op, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Error(ACK_ERROR_ARG, "bad request");
|
||||||
|
return CommandResult::ERROR;
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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 */
|
||||||
|
|
|
@ -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();
|
|
@ -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;
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
Loading…
Reference in New Issue