diff --git a/NEWS b/NEWS index 7a87dd67f..19df4304b 100644 --- a/NEWS +++ b/NEWS @@ -10,7 +10,7 @@ ver 0.24 (not yet released) - show PCRE support in "config" response - apply Unicode normalization to case-insensitive filter expressions - stickers on playlists and some tag types - - new commands "stickernames" and "playlistlength" + - new commands "stickernames", "playlistlength" and "searchplaylist" - new "search"/"find" filter "added-since" - allow range in listplaylist and listplaylistinfo - "sticker find" supports sort and window parameter and new sticker compare operators "eq", "lt" and "gt" diff --git a/doc/protocol.rst b/doc/protocol.rst index d46c4d9a2..82affbd3f 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -994,6 +994,14 @@ remote playlists (absolute URI with a supported scheme). plugins are supported. A range may be specified to list only a part of the playlist. [#since_0_24]_ +.. _command_searchplaylist: + +:command:`searchplaylist {NAME} {FILTER} [{START:END}]` + Search the playlist for songs matching + ``FILTER`` (see :ref:`Filters `). Playlist + plugins are supported. A range may be specified to list + only a part of the playlist. + .. _command_listplaylists: :command:`listplaylists` diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx index 1e7b1c4fb..e94ada02d 100644 --- a/src/command/AllCommands.cxx +++ b/src/command/AllCommands.cxx @@ -176,6 +176,7 @@ static constexpr struct command commands[] = { { "searchaddpl", PERMISSION_CONTROL, 2, -1, handle_searchaddpl }, { "searchcount", PERMISSION_READ, 1, -1, handle_searchcount }, #endif + { "searchplaylist", PERMISSION_READ, 2, 3, handle_searchplaylist }, { "seek", PERMISSION_PLAYER, 2, 2, handle_seek }, { "seekcur", PERMISSION_PLAYER, 1, 1, handle_seekcur }, { "seekid", PERMISSION_PLAYER, 2, 2, handle_seekid }, diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx index ea69548cd..b847d5d49 100644 --- a/src/command/PlaylistCommands.cxx +++ b/src/command/PlaylistCommands.cxx @@ -15,6 +15,7 @@ #include "PlaylistError.hxx" #include "db/PlaylistVector.hxx" #include "SongLoader.hxx" +#include "song/Filter.hxx" #include "song/DetachedSong.hxx" #include "BulkEdit.hxx" #include "playlist/Length.hxx" @@ -26,6 +27,7 @@ #include "Mapper.hxx" #include "fs/AllocatedPath.hxx" #include "time/ChronoUtil.hxx" +#include "util/Exception.hxx" #include "util/UriExtract.hxx" #include "LocateUri.hxx" @@ -129,7 +131,7 @@ handle_listplaylist(Client &client, Request args, Response &r) RangeArg range = args.ParseOptional(1, RangeArg::All()); playlist_file_print(r, client.GetPartition(), SongLoader(client), - name, range.start, range.end, false); + name, range.start, range.end, false, nullptr); return CommandResult::OK; } @@ -146,7 +148,39 @@ handle_listplaylistinfo(Client &client, Request args, Response &r) RangeArg range = args.ParseOptional(1, RangeArg::All()); playlist_file_print(r, client.GetPartition(), SongLoader(client), - name, range.start, range.end, true); + name, range.start, range.end, true, nullptr); + return CommandResult::OK; +} + +CommandResult +handle_searchplaylist(Client &client, Request args, Response &r) +{ + const auto name = LocateUri(UriPluginKind::PLAYLIST, args.front(), + &client +#ifdef ENABLE_DATABASE + , nullptr +#endif + ); + args.shift(); + + RangeArg window = RangeArg::All(); + if (args.size() == 2) { + window = args.ParseRange(args.size() - 1); + args.pop_back(); + } + + SongFilter filter; + try { + filter.Parse(args, true); + } catch (...) { + r.Error(ACK_ERROR_ARG, + GetFullMessage(std::current_exception()).c_str()); + return CommandResult::ERROR; + } + filter.Optimize(); + + playlist_file_print(r, client.GetPartition(), SongLoader(client), + name, window.start, window.end, true, &filter); return CommandResult::OK; } diff --git a/src/command/PlaylistCommands.hxx b/src/command/PlaylistCommands.hxx index 1c4ce7ce4..2f98cabe0 100644 --- a/src/command/PlaylistCommands.hxx +++ b/src/command/PlaylistCommands.hxx @@ -26,6 +26,9 @@ handle_listplaylist(Client &client, Request request, Response &response); CommandResult handle_listplaylistinfo(Client &client, Request request, Response &response); +CommandResult +handle_searchplaylist(Client &client, Request request, Response &response); + CommandResult handle_playlistlength(Client &client, Request request, Response &response); diff --git a/src/playlist/Print.cxx b/src/playlist/Print.cxx index fe42a10f3..10fecfa9d 100644 --- a/src/playlist/Print.cxx +++ b/src/playlist/Print.cxx @@ -8,7 +8,9 @@ #include "PlaylistSong.hxx" #include "SongEnumerator.hxx" #include "SongPrint.hxx" +#include "song/Filter.hxx" #include "song/DetachedSong.hxx" +#include "song/LightSong.hxx" #include "input/Error.hxx" #include "fs/Traits.hxx" #include "thread/Mutex.hxx" @@ -50,13 +52,55 @@ playlist_provider_print(Response &r, } } +static void +playlist_provider_search_print(Response &r, + const SongLoader &loader, + const char *uri, + SongEnumerator &e, + unsigned start_index, + unsigned end_index, + SongFilter *filter) noexcept +{ + const auto base_uri = uri != nullptr + ? PathTraitsUTF8::GetParent(uri) + : "."; + + std::unique_ptr song; + + unsigned skip = start_index; + unsigned n = end_index - start_index; + + while ((song = e.NextSong()) != nullptr) { + const bool detail = playlist_check_translate_song(*song, base_uri, + loader); + if (!filter->Match(static_cast(*song))) + continue; + + if (skip > 0) { + --skip; + continue; + } + + if (detail) + song_print_info(r, *song); + else + /* fallback if no detail was requested or no + detail was available */ + song_print_uri(r, *song); + + if (--n == 0) + break; + } +} + void playlist_file_print(Response &r, Partition &partition, const SongLoader &loader, const LocatedUri &uri, unsigned start_index, unsigned end_index, - bool detail) + bool detail, + SongFilter *filter) try { Mutex mutex; @@ -72,8 +116,12 @@ try { if (playlist == nullptr) throw PlaylistError::NoSuchList(); - playlist_provider_print(r, loader, uri.canonical_uri, *playlist, - start_index, end_index, detail); + if (filter == nullptr) + playlist_provider_print(r, loader, uri.canonical_uri, *playlist, + start_index, end_index, detail); + else + playlist_provider_search_print(r, loader, uri.canonical_uri, *playlist, + start_index, end_index, filter); } catch (...) { if (IsFileNotFound(std::current_exception())) throw PlaylistError::NoSuchList(); diff --git a/src/playlist/Print.hxx b/src/playlist/Print.hxx index 487c9af36..e3021892b 100644 --- a/src/playlist/Print.hxx +++ b/src/playlist/Print.hxx @@ -5,6 +5,7 @@ class Response; class SongLoader; +class SongFilter; struct Partition; /** @@ -20,4 +21,6 @@ playlist_file_print(Response &r, Partition &partition, const SongLoader &loader, const LocatedUri &uri, unsigned start_index, unsigned end_index, - bool detail); + bool detail, + SongFilter *filter); +