SongFilter: new extensible filter syntax
Will allow more complex fitler expression, such as negation (#89).
This commit is contained in:
		
							
								
								
									
										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 }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Max Kellermann
					Max Kellermann