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
|
- "outputs" prints the plugin name
|
||||||
- "outputset" sets runtime attributes
|
- "outputset" sets runtime attributes
|
||||||
- close connection when client sends HTTP request
|
- close connection when client sends HTTP request
|
||||||
|
- new filter syntax for "find"/"search" etc.
|
||||||
* database
|
* database
|
||||||
- simple: scan audio formats
|
- simple: scan audio formats
|
||||||
* player
|
* player
|
||||||
|
@ -208,63 +208,75 @@
|
|||||||
|
|
||||||
<cmdsynopsis>
|
<cmdsynopsis>
|
||||||
<command>find</command>
|
<command>find</command>
|
||||||
<arg choice="req" rep="repeat">
|
<arg choice="req" rep="repeat">EXPRESSION</arg>
|
||||||
<arg choice="req"><replaceable>TYPE</replaceable></arg>
|
|
||||||
<arg choice="req"><replaceable>VALUE</replaceable></arg>
|
|
||||||
</arg>
|
|
||||||
</cmdsynopsis>
|
</cmdsynopsis>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
<varname>TYPE</varname> can
|
<varname>EXPRESSION</varname> is a string enclosed in
|
||||||
be any tag supported by <application>MPD</application>, or one of the special
|
parantheses which can be one of:
|
||||||
parameters:
|
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<itemizedlist>
|
<itemizedlist>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
<parameter>any</parameter> checks all tag values
|
"<code>(TAG == 'VALUE')</code>": match a tag value.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
|
||||||
|
|
||||||
<listitem>
|
|
||||||
<para>
|
<para>
|
||||||
<parameter>file</parameter> checks the full path
|
The special tag "<parameter>any</parameter>" checks all
|
||||||
(relative to the music directory)
|
tag values.
|
||||||
</para>
|
</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>
|
<para>
|
||||||
<parameter>albumartist</parameter> looks for
|
<parameter>albumartist</parameter> looks for
|
||||||
<varname>VALUE</varname> in AlbumArtist and falls back to
|
<varname>VALUE</varname> in <varname>AlbumArtist</varname>
|
||||||
Artist tags if AlbumArtist does not exist.
|
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>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
</itemizedlist>
|
</itemizedlist>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
<varname>VALUE</varname> is what to find. The
|
Prior to MPD 0.21, the syntax looked like this:
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
<section id="tags">
|
<section id="tags">
|
||||||
|
@ -22,10 +22,13 @@
|
|||||||
#include "db/LightSong.hxx"
|
#include "db/LightSong.hxx"
|
||||||
#include "DetachedSong.hxx"
|
#include "DetachedSong.hxx"
|
||||||
#include "tag/ParseName.hxx"
|
#include "tag/ParseName.hxx"
|
||||||
|
#include "util/CharUtil.hxx"
|
||||||
#include "util/ChronoUtil.hxx"
|
#include "util/ChronoUtil.hxx"
|
||||||
#include "util/ConstBuffer.hxx"
|
#include "util/ConstBuffer.hxx"
|
||||||
|
#include "util/RuntimeError.hxx"
|
||||||
#include "util/StringAPI.hxx"
|
#include "util/StringAPI.hxx"
|
||||||
#include "util/StringCompare.hxx"
|
#include "util/StringCompare.hxx"
|
||||||
|
#include "util/StringStrip.hxx"
|
||||||
#include "util/StringView.hxx"
|
#include "util/StringView.hxx"
|
||||||
#include "util/ASCII.hxx"
|
#include "util/ASCII.hxx"
|
||||||
#include "util/TimeParser.hxx"
|
#include "util/TimeParser.hxx"
|
||||||
@ -234,6 +237,99 @@ ParseTimeStamp(const char *s)
|
|||||||
return ParseTimePoint(s, "%FT%TZ");
|
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
|
void
|
||||||
SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
|
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");
|
throw std::runtime_error("Incorrect number of filter arguments");
|
||||||
|
|
||||||
do {
|
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)
|
if (args.size < 2)
|
||||||
throw std::runtime_error("Incorrect number of filter arguments");
|
throw std::runtime_error("Incorrect number of filter arguments");
|
||||||
|
|
||||||
|
@ -142,6 +142,8 @@ public:
|
|||||||
std::string ToExpression() const noexcept;
|
std::string ToExpression() const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
const char *ParseExpression(const char *s, bool fold_case=false);
|
||||||
|
|
||||||
gcc_nonnull(2,3)
|
gcc_nonnull(2,3)
|
||||||
void Parse(const char *tag, const char *value, bool fold_case=false);
|
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 },
|
{ "config", PERMISSION_ADMIN, 0, 0, handle_config },
|
||||||
{ "consume", PERMISSION_CONTROL, 1, 1, handle_consume },
|
{ "consume", PERMISSION_CONTROL, 1, 1, handle_consume },
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
{ "count", PERMISSION_READ, 2, -1, handle_count },
|
{ "count", PERMISSION_READ, 1, -1, handle_count },
|
||||||
#endif
|
#endif
|
||||||
{ "crossfade", PERMISSION_CONTROL, 1, 1, handle_crossfade },
|
{ "crossfade", PERMISSION_CONTROL, 1, 1, handle_crossfade },
|
||||||
{ "currentsong", PERMISSION_READ, 0, 0, handle_currentsong },
|
{ "currentsong", PERMISSION_READ, 0, 0, handle_currentsong },
|
||||||
@ -104,8 +104,8 @@ static constexpr struct command commands[] = {
|
|||||||
{ "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput },
|
{ "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput },
|
||||||
{ "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput },
|
{ "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput },
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
{ "find", PERMISSION_READ, 2, -1, handle_find },
|
{ "find", PERMISSION_READ, 1, -1, handle_find },
|
||||||
{ "findadd", PERMISSION_ADD, 2, -1, handle_findadd},
|
{ "findadd", PERMISSION_ADD, 1, -1, handle_findadd},
|
||||||
#endif
|
#endif
|
||||||
{ "idle", PERMISSION_READ, 0, -1, handle_idle },
|
{ "idle", PERMISSION_READ, 0, -1, handle_idle },
|
||||||
{ "kill", PERMISSION_ADMIN, -1, -1, handle_kill },
|
{ "kill", PERMISSION_ADMIN, -1, -1, handle_kill },
|
||||||
@ -149,11 +149,11 @@ static constexpr struct command commands[] = {
|
|||||||
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
|
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
|
||||||
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
||||||
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
|
{ "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 },
|
{ "playlistid", PERMISSION_READ, 0, 1, handle_playlistid },
|
||||||
{ "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
|
{ "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
|
||||||
{ "playlistmove", PERMISSION_CONTROL, 3, 3, handle_playlistmove },
|
{ "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 },
|
{ "plchanges", PERMISSION_READ, 1, 2, handle_plchanges },
|
||||||
{ "plchangesposid", PERMISSION_READ, 1, 2, handle_plchangesposid },
|
{ "plchangesposid", PERMISSION_READ, 1, 2, handle_plchangesposid },
|
||||||
{ "previous", PERMISSION_CONTROL, 0, 0, handle_previous },
|
{ "previous", PERMISSION_CONTROL, 0, 0, handle_previous },
|
||||||
@ -173,9 +173,9 @@ static constexpr struct command commands[] = {
|
|||||||
{ "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
|
{ "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
|
||||||
{ "save", PERMISSION_CONTROL, 1, 1, handle_save },
|
{ "save", PERMISSION_CONTROL, 1, 1, handle_save },
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
{ "search", PERMISSION_READ, 2, -1, handle_search },
|
{ "search", PERMISSION_READ, 1, -1, handle_search },
|
||||||
{ "searchadd", PERMISSION_ADD, 2, -1, handle_searchadd },
|
{ "searchadd", PERMISSION_ADD, 1, -1, handle_searchadd },
|
||||||
{ "searchaddpl", PERMISSION_CONTROL, 3, -1, handle_searchaddpl },
|
{ "searchaddpl", PERMISSION_CONTROL, 2, -1, handle_searchaddpl },
|
||||||
#endif
|
#endif
|
||||||
{ "seek", PERMISSION_CONTROL, 2, 2, handle_seek },
|
{ "seek", PERMISSION_CONTROL, 2, 2, handle_seek },
|
||||||
{ "seekcur", PERMISSION_CONTROL, 1, 1, handle_seekcur },
|
{ "seekcur", PERMISSION_CONTROL, 1, 1, handle_seekcur },
|
||||||
|
Loading…
Reference in New Issue
Block a user