Commit 5271e81e authored by Max Kellermann's avatar Max Kellermann

SongFilter: new extensible filter syntax

Will allow more complex fitler expression, such as negation (#89).
parent a1741594
...@@ -7,6 +7,7 @@ ver 0.21 (not yet released) ...@@ -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 @@ ...@@ -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>
The special tag "<parameter>any</parameter>" checks all
tag values.
</para>
<para>
<parameter>albumartist</parameter> looks for
<varname>VALUE</varname> in <varname>AlbumArtist</varname>
and falls back to <varname>Artist</varname> tags if
<varname>AlbumArtist</varname> does not exist.
</para> </para>
</listitem>
<listitem>
<para> <para>
<parameter>file</parameter> checks the full path <varname>VALUE</varname> is what to find. The
(relative to the music directory) <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>
</listitem> </listitem>
<listitem> <listitem>
<para> <para>
<parameter>base</parameter> restricts the search to "<code>(file == 'VALUE')</code>": match the full song URI
songs in the given directory (also relative to the (relative to the music directory).
music directory)
</para> </para>
</listitem> </listitem>
<listitem> <listitem>
<para> <para>
<parameter>modified-since</parameter> compares the "<code>(base 'VALUE')</code>": restrict the search to
file's time stamp with the given value (ISO 8601 or songs in the given directory (relative to the music
UNIX time stamp) directory).
</para> </para>
</listitem> </listitem>
<listitem> <listitem>
<para> <para>
<parameter>albumartist</parameter> looks for "<code>(modified-since 'VALUE')</code>": compares the
<varname>VALUE</varname> in AlbumArtist and falls back to file's time stamp with the given value (ISO 8601 or UNIX
Artist tags if AlbumArtist does not exist. 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 @@ ...@@ -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) ...@@ -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) ...@@ -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: ...@@ -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[] = { ...@@ -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[] = { ...@@ -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[] = { ...@@ -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[] = { ...@@ -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 },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment