SongFilter: new extensible filter syntax
Will allow more complex fitler expression, such as negation (#89).
This commit is contained in:
parent
a174159496
commit
5271e81ebe
1
NEWS
1
NEWS
@ -7,6 +7,7 @@ ver 0.21 (not yet released)
|
||||
- "outputs" prints the plugin name
|
||||
- "outputset" sets runtime attributes
|
||||
- close connection when client sends HTTP request
|
||||
- new filter syntax for "find"/"search" etc.
|
||||
* database
|
||||
- simple: scan audio formats
|
||||
* player
|
||||
|
@ -208,63 +208,75 @@
|
||||
|
||||
<cmdsynopsis>
|
||||
<command>find</command>
|
||||
<arg choice="req" rep="repeat">
|
||||
<arg choice="req"><replaceable>TYPE</replaceable></arg>
|
||||
<arg choice="req"><replaceable>VALUE</replaceable></arg>
|
||||
</arg>
|
||||
<arg choice="req" rep="repeat">EXPRESSION</arg>
|
||||
</cmdsynopsis>
|
||||
|
||||
<para>
|
||||
<varname>TYPE</varname> can
|
||||
be any tag supported by <application>MPD</application>, or one of the special
|
||||
parameters:
|
||||
<varname>EXPRESSION</varname> is a string enclosed in
|
||||
parantheses which can be one of:
|
||||
</para>
|
||||
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
<parameter>any</parameter> checks all tag values
|
||||
"<code>(TAG == 'VALUE')</code>": match a tag value.
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
<parameter>file</parameter> checks the full path
|
||||
(relative to the music directory)
|
||||
The special tag "<parameter>any</parameter>" checks all
|
||||
tag values.
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
<parameter>base</parameter> restricts the search to
|
||||
songs in the given directory (also relative to the
|
||||
music directory)
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
<parameter>modified-since</parameter> compares the
|
||||
file's time stamp with the given value (ISO 8601 or
|
||||
UNIX time stamp)
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
<parameter>albumartist</parameter> looks for
|
||||
<varname>VALUE</varname> in AlbumArtist and falls back to
|
||||
Artist tags if AlbumArtist does not exist.
|
||||
<varname>VALUE</varname> in <varname>AlbumArtist</varname>
|
||||
and falls back to <varname>Artist</varname> tags if
|
||||
<varname>AlbumArtist</varname> does not exist.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
<varname>VALUE</varname> is what to find. The
|
||||
<command>find</command> commands specify an exact value
|
||||
and are case-sensitive; the <command>search</command>
|
||||
commands specify a sub string and ignore case.
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
"<code>(file == 'VALUE')</code>": match the full song URI
|
||||
(relative to the music directory).
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
"<code>(base 'VALUE')</code>": restrict the search to
|
||||
songs in the given directory (relative to the music
|
||||
directory).
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
"<code>(modified-since 'VALUE')</code>": compares the
|
||||
file's time stamp with the given value (ISO 8601 or UNIX
|
||||
time stamp).
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
||||
<para>
|
||||
<varname>VALUE</varname> is what to find. The
|
||||
<command>find</command> commands specify an exact value and
|
||||
are case-sensitive; the <command>search</command> commands
|
||||
specify a sub string and ignore case.
|
||||
Prior to MPD 0.21, the syntax looked like this:
|
||||
</para>
|
||||
|
||||
<cmdsynopsis>
|
||||
<command>find</command>
|
||||
<arg choice="req" rep="repeat">
|
||||
<arg choice="req"><replaceable>TYPE</replaceable></arg>
|
||||
<arg choice="req"><replaceable>VALUE</replaceable></arg>
|
||||
</arg>
|
||||
</cmdsynopsis>
|
||||
</section>
|
||||
|
||||
<section id="tags">
|
||||
|
@ -22,10 +22,13 @@
|
||||
#include "db/LightSong.hxx"
|
||||
#include "DetachedSong.hxx"
|
||||
#include "tag/ParseName.hxx"
|
||||
#include "util/CharUtil.hxx"
|
||||
#include "util/ChronoUtil.hxx"
|
||||
#include "util/ConstBuffer.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/StringAPI.hxx"
|
||||
#include "util/StringCompare.hxx"
|
||||
#include "util/StringStrip.hxx"
|
||||
#include "util/StringView.hxx"
|
||||
#include "util/ASCII.hxx"
|
||||
#include "util/TimeParser.hxx"
|
||||
@ -234,6 +237,99 @@ ParseTimeStamp(const char *s)
|
||||
return ParseTimePoint(s, "%FT%TZ");
|
||||
}
|
||||
|
||||
static constexpr bool
|
||||
IsTagNameChar(char ch) noexcept
|
||||
{
|
||||
return IsAlphaASCII(ch) || ch == '_';
|
||||
}
|
||||
|
||||
static const char *
|
||||
FirstNonTagNameChar(const char *s) noexcept
|
||||
{
|
||||
while (IsTagNameChar(*s))
|
||||
++s;
|
||||
return s;
|
||||
}
|
||||
|
||||
static auto
|
||||
ExpectFilterType(const char *&s)
|
||||
{
|
||||
const char *end = FirstNonTagNameChar(s);
|
||||
if (end == s)
|
||||
throw std::runtime_error("Tag name expected");
|
||||
|
||||
const std::string name(s, end);
|
||||
s = StripLeft(end);
|
||||
|
||||
const auto type = locate_parse_type(name.c_str());
|
||||
if (type == TAG_NUM_OF_ITEM_TYPES)
|
||||
throw FormatRuntimeError("Unknown filter type: %s",
|
||||
name.c_str());
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
static constexpr bool
|
||||
IsQuote(char ch) noexcept
|
||||
{
|
||||
return ch == '"' || ch == '\'';
|
||||
}
|
||||
|
||||
static std::string
|
||||
ExpectQuoted(const char *&s)
|
||||
{
|
||||
const char quote = *s++;
|
||||
if (!IsQuote(quote))
|
||||
throw std::runtime_error("Quoted string expected");
|
||||
|
||||
const char *begin = s;
|
||||
const char *end = strchr(s, quote);
|
||||
if (end == nullptr)
|
||||
throw std::runtime_error("Closing quote not found");
|
||||
|
||||
s = StripLeft(end + 1);
|
||||
return {begin, end};
|
||||
}
|
||||
|
||||
const char *
|
||||
SongFilter::ParseExpression(const char *s, bool fold_case)
|
||||
{
|
||||
assert(*s == '(');
|
||||
|
||||
s = StripLeft(s + 1);
|
||||
|
||||
if (*s == '(')
|
||||
throw std::runtime_error("Nested expressions not yet implemented");
|
||||
|
||||
const auto type = ExpectFilterType(s);
|
||||
|
||||
if (type == LOCATE_TAG_MODIFIED_SINCE) {
|
||||
const auto value_s = ExpectQuoted(s);
|
||||
if (*s != ')')
|
||||
throw std::runtime_error("')' expected");
|
||||
items.emplace_back(type, ParseTimeStamp(value_s.c_str()));
|
||||
return StripLeft(s + 1);
|
||||
} else if (type == LOCATE_TAG_BASE_TYPE) {
|
||||
auto value = ExpectQuoted(s);
|
||||
if (*s != ')')
|
||||
throw std::runtime_error("')' expected");
|
||||
|
||||
items.emplace_back(type, std::move(value), fold_case);
|
||||
return StripLeft(s + 1);
|
||||
} else {
|
||||
if (s[0] != '=' || s[1] != '=')
|
||||
throw std::runtime_error("'==' expected");
|
||||
|
||||
s = StripLeft(s + 2);
|
||||
auto value = ExpectQuoted(s);
|
||||
if (*s != ')')
|
||||
throw std::runtime_error("')' expected");
|
||||
|
||||
items.emplace_back(type, std::move(value), fold_case);
|
||||
return StripLeft(s + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
|
||||
{
|
||||
@ -262,6 +358,15 @@ SongFilter::Parse(ConstBuffer<const char *> args, bool fold_case)
|
||||
throw std::runtime_error("Incorrect number of filter arguments");
|
||||
|
||||
do {
|
||||
if (*args.front() == '(') {
|
||||
const char *s = args.shift();
|
||||
const char *end = ParseExpression(s, fold_case);
|
||||
if (*end != 0)
|
||||
throw std::runtime_error("Unparsed garbage after expression");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.size < 2)
|
||||
throw std::runtime_error("Incorrect number of filter arguments");
|
||||
|
||||
|
@ -142,6 +142,8 @@ public:
|
||||
std::string ToExpression() const noexcept;
|
||||
|
||||
private:
|
||||
const char *ParseExpression(const char *s, bool fold_case=false);
|
||||
|
||||
gcc_nonnull(2,3)
|
||||
void Parse(const char *tag, const char *value, bool fold_case=false);
|
||||
|
||||
|
@ -94,7 +94,7 @@ static constexpr struct command commands[] = {
|
||||
{ "config", PERMISSION_ADMIN, 0, 0, handle_config },
|
||||
{ "consume", PERMISSION_CONTROL, 1, 1, handle_consume },
|
||||
#ifdef ENABLE_DATABASE
|
||||
{ "count", PERMISSION_READ, 2, -1, handle_count },
|
||||
{ "count", PERMISSION_READ, 1, -1, handle_count },
|
||||
#endif
|
||||
{ "crossfade", PERMISSION_CONTROL, 1, 1, handle_crossfade },
|
||||
{ "currentsong", PERMISSION_READ, 0, 0, handle_currentsong },
|
||||
@ -104,8 +104,8 @@ static constexpr struct command commands[] = {
|
||||
{ "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput },
|
||||
{ "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput },
|
||||
#ifdef ENABLE_DATABASE
|
||||
{ "find", PERMISSION_READ, 2, -1, handle_find },
|
||||
{ "findadd", PERMISSION_ADD, 2, -1, handle_findadd},
|
||||
{ "find", PERMISSION_READ, 1, -1, handle_find },
|
||||
{ "findadd", PERMISSION_ADD, 1, -1, handle_findadd},
|
||||
#endif
|
||||
{ "idle", PERMISSION_READ, 0, -1, handle_idle },
|
||||
{ "kill", PERMISSION_ADMIN, -1, -1, handle_kill },
|
||||
@ -149,11 +149,11 @@ static constexpr struct command commands[] = {
|
||||
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
|
||||
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
||||
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
|
||||
{ "playlistfind", PERMISSION_READ, 2, -1, handle_playlistfind },
|
||||
{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
|
||||
{ "playlistid", PERMISSION_READ, 0, 1, handle_playlistid },
|
||||
{ "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
|
||||
{ "playlistmove", PERMISSION_CONTROL, 3, 3, handle_playlistmove },
|
||||
{ "playlistsearch", PERMISSION_READ, 2, -1, handle_playlistsearch },
|
||||
{ "playlistsearch", PERMISSION_READ, 1, -1, handle_playlistsearch },
|
||||
{ "plchanges", PERMISSION_READ, 1, 2, handle_plchanges },
|
||||
{ "plchangesposid", PERMISSION_READ, 1, 2, handle_plchangesposid },
|
||||
{ "previous", PERMISSION_CONTROL, 0, 0, handle_previous },
|
||||
@ -173,9 +173,9 @@ static constexpr struct command commands[] = {
|
||||
{ "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
|
||||
{ "save", PERMISSION_CONTROL, 1, 1, handle_save },
|
||||
#ifdef ENABLE_DATABASE
|
||||
{ "search", PERMISSION_READ, 2, -1, handle_search },
|
||||
{ "searchadd", PERMISSION_ADD, 2, -1, handle_searchadd },
|
||||
{ "searchaddpl", PERMISSION_CONTROL, 3, -1, handle_searchaddpl },
|
||||
{ "search", PERMISSION_READ, 1, -1, handle_search },
|
||||
{ "searchadd", PERMISSION_ADD, 1, -1, handle_searchadd },
|
||||
{ "searchaddpl", PERMISSION_CONTROL, 2, -1, handle_searchaddpl },
|
||||
#endif
|
||||
{ "seek", PERMISSION_CONTROL, 2, 2, handle_seek },
|
||||
{ "seekcur", PERMISSION_CONTROL, 1, 1, handle_seekcur },
|
||||
|
Loading…
Reference in New Issue
Block a user