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)
- "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>
<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>
</listitem>
<listitem>
<para>
<parameter>file</parameter> checks the full path
(relative to the music directory)
<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>
<parameter>base</parameter> restricts the search to
songs in the given directory (also relative to the
music directory)
"<code>(file == 'VALUE')</code>": match the full song URI
(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)
"<code>(base 'VALUE')</code>": restrict the search to
songs in the given directory (relative to the music
directory).
</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.
"<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 },
......
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