diff --git a/src/SongFilter.cxx b/src/SongFilter.cxx index 886a7b71a..17dece666 100644 --- a/src/SongFilter.cxx +++ b/src/SongFilter.cxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2017 The Music Player Daemon Project + * Copyright 2003-2018 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -78,74 +78,76 @@ StringFilter::Match(const char *s) const noexcept } } -SongFilter::Item::Item(unsigned _tag, std::string &&_value, bool _fold_case) - :tag(_tag), string_filter(std::move(_value), _fold_case) +std::string +UriSongFilter::ToExpression() const noexcept { + return std::string("(" LOCATE_TAG_FILE_KEY " ") + (negated ? "!=" : "==") + " \"" + filter.GetValue() + "\")"; } -SongFilter::Item::Item(unsigned _tag, - std::chrono::system_clock::time_point _time) - :tag(_tag), time(_time) +bool +UriSongFilter::Match(const LightSong &song) const noexcept { + return filter.Match(song.GetURI().c_str()) != negated; } std::string -SongFilter::Item::ToExpression() const noexcept +BaseSongFilter::ToExpression() const noexcept { - switch (tag) { - case LOCATE_TAG_FILE_TYPE: - return std::string("(" LOCATE_TAG_FILE_KEY " ") + (IsNegated() ? "!=" : "==") + " \"" + string_filter.GetValue() + "\")"; - - case LOCATE_TAG_BASE_TYPE: - return "(base \"" + string_filter.GetValue() + "\")"; - - case LOCATE_TAG_MODIFIED_SINCE: - return "(modified-since \"" + string_filter.GetValue() + "\")"; - - case LOCATE_TAG_ANY_TYPE: - return std::string("(" LOCATE_TAG_ANY_KEY " ") + (IsNegated() ? "!=" : "==") + " \"" + string_filter.GetValue() + "\")"; - - default: - return std::string("(") + tag_item_names[tag] + " " + (IsNegated() ? "!=" : "==") + " \"" + string_filter.GetValue() + "\")"; - } + return "(base \"" + value + "\")"; } bool -SongFilter::Item::MatchNN(const TagItem &item) const noexcept +BaseSongFilter::Match(const LightSong &song) const noexcept { - return (tag == LOCATE_TAG_ANY_TYPE || (unsigned)item.type == tag) && - string_filter.Match(item.value); + return uri_is_child_or_same(value.c_str(), song.GetURI().c_str()); +} + +std::string +TagSongFilter::ToExpression() const noexcept +{ + const char *name = type == TAG_NUM_OF_ITEM_TYPES + ? LOCATE_TAG_ANY_KEY + : tag_item_names[type]; + + return std::string("(") + name + " " + (negated ? "!=" : "==") + " \"" + filter.GetValue() + "\")"; } bool -SongFilter::Item::MatchNN(const Tag &_tag) const noexcept +TagSongFilter::MatchNN(const TagItem &item) const noexcept +{ + return (type == TAG_NUM_OF_ITEM_TYPES || item.type == type) && + filter.Match(item.value); +} + +bool +TagSongFilter::MatchNN(const Tag &tag) const noexcept { bool visited_types[TAG_NUM_OF_ITEM_TYPES]; std::fill_n(visited_types, size_t(TAG_NUM_OF_ITEM_TYPES), false); - for (const auto &i : _tag) { + for (const auto &i : tag) { visited_types[i.type] = true; if (MatchNN(i)) return true; } - if (tag < TAG_NUM_OF_ITEM_TYPES && !visited_types[tag]) { + if (type < TAG_NUM_OF_ITEM_TYPES && !visited_types[type]) { /* If the search critieron was not visited during the sweep through the song's tag, it means this field is absent from the tag or empty. Thus, if the searched string is also empty then it's a match as well and we should return true. */ - if (string_filter.empty()) + if (filter.empty()) return true; - if (tag == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) { + if (type == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) { /* if we're looking for "album artist", but only "artist" exists, use that */ - for (const auto &item : _tag) + for (const auto &item : tag) if (item.type == TAG_ARTIST && - string_filter.Match(item.value)) + filter.Match(item.value)) return true; } } @@ -154,28 +156,27 @@ SongFilter::Item::MatchNN(const Tag &_tag) const noexcept } bool -SongFilter::Item::MatchNN(const LightSong &song) const noexcept +TagSongFilter::Match(const LightSong &song) const noexcept { - if (tag == LOCATE_TAG_BASE_TYPE) { - const auto uri = song.GetURI(); - return uri_is_child_or_same(string_filter.GetValue().c_str(), - uri.c_str()); - } - - if (tag == LOCATE_TAG_MODIFIED_SINCE) - return song.mtime >= time; - - if (tag == LOCATE_TAG_FILE_TYPE) { - const auto uri = song.GetURI(); - return string_filter.Match(uri.c_str()); - } - - return MatchNN(song.tag); + return MatchNN(song.tag) != negated; } -SongFilter::SongFilter(unsigned tag, const char *value, bool fold_case) +std::string +ModifiedSinceSongFilter::ToExpression() const noexcept { - items.emplace_back(tag, value, fold_case); + return std::string("(modified-since \"") + FormatISO8601(value).c_str() + "\")"; +} + +bool +ModifiedSinceSongFilter::Match(const LightSong &song) const noexcept +{ + return song.mtime >= value; +} + +SongFilter::SongFilter(TagType tag, const char *value, bool fold_case) +{ + items.emplace_back(std::make_unique(tag, value, + fold_case, false)); } SongFilter::~SongFilter() @@ -190,14 +191,14 @@ SongFilter::ToExpression() const noexcept const auto end = items.end(); if (std::next(i) == end) - return i->ToExpression(); + return (*i)->ToExpression(); std::string e("("); - e += i->ToExpression(); + e += (*i)->ToExpression(); for (++i; i != end; ++i) { e += " AND "; - e += i->ToExpression(); + e += (*i)->ToExpression(); } e.push_back(')'); @@ -273,8 +274,8 @@ ExpectQuoted(const char *&s) return {begin, end}; } -const char * -SongFilter::ParseExpression(const char *s, bool fold_case) +ISongFilterPtr +SongFilter::ParseExpression(const char *&s, bool fold_case) { assert(*s == '('); @@ -283,21 +284,21 @@ SongFilter::ParseExpression(const char *s, bool fold_case) if (*s == '(') throw std::runtime_error("Nested expressions not yet implemented"); - const auto type = ExpectFilterType(s); + 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); + s = StripLeft(s + 1); + return std::make_unique(ParseTimeStamp(value_s.c_str())); } else if (type == LOCATE_TAG_BASE_TYPE) { auto value = ExpectQuoted(s); if (*s != ')') throw std::runtime_error("')' expected"); + s = StripLeft(s + 1); - items.emplace_back(type, std::move(value), fold_case); - return StripLeft(s + 1); + return std::make_unique(std::move(value)); } else { bool negated = false; if (s[0] == '!' && s[1] == '=') @@ -310,9 +311,19 @@ SongFilter::ParseExpression(const char *s, bool fold_case) if (*s != ')') throw std::runtime_error("')' expected"); - items.emplace_back(type, std::move(value), fold_case); - items.back().SetNegated(negated); - return StripLeft(s + 1); + s = StripLeft(s + 1); + + if (type == LOCATE_TAG_ANY_TYPE) + type = TAG_NUM_OF_ITEM_TYPES; + + if (type == LOCATE_TAG_FILE_TYPE) + return std::make_unique(std::move(value), + fold_case, + negated); + + return std::make_unique(TagType(type), + std::move(value), + fold_case, negated); } } @@ -320,21 +331,38 @@ void SongFilter::Parse(const char *tag_string, const char *value, bool fold_case) { unsigned tag = locate_parse_type(tag_string); - if (tag == TAG_NUM_OF_ITEM_TYPES) + + switch (tag) { + case TAG_NUM_OF_ITEM_TYPES: throw std::runtime_error("Unknown filter type"); - if (tag == LOCATE_TAG_BASE_TYPE) { + case LOCATE_TAG_BASE_TYPE: if (!uri_safe_local(value)) throw std::runtime_error("Bad URI"); - /* case folding doesn't work with "base" */ - fold_case = false; - } + items.emplace_back(std::make_unique(value)); + break; - if (tag == LOCATE_TAG_MODIFIED_SINCE) - items.emplace_back(tag, ParseTimeStamp(value)); - else - items.emplace_back(tag, value, fold_case); + case LOCATE_TAG_MODIFIED_SINCE: + items.emplace_back(std::make_unique(ParseTimeStamp(value))); + break; + + case LOCATE_TAG_FILE_TYPE: + items.emplace_back(std::make_unique(value, + fold_case, + false)); + break; + + default: + if (tag == LOCATE_TAG_ANY_TYPE) + tag = TAG_NUM_OF_ITEM_TYPES; + + items.emplace_back(std::make_unique(TagType(tag), + value, + fold_case, + false)); + break; + } } void @@ -346,10 +374,12 @@ SongFilter::Parse(ConstBuffer args, bool fold_case) do { if (*args.front() == '(') { const char *s = args.shift(); - const char *end = ParseExpression(s, fold_case); + const char *end = s; + auto f = ParseExpression(end, fold_case); if (*end != 0) throw std::runtime_error("Unparsed garbage after expression"); + items.emplace_back(std::move(f)); continue; } @@ -366,18 +396,36 @@ bool SongFilter::Match(const LightSong &song) const noexcept { for (const auto &i : items) - if (!i.Match(song)) + if (!i->Match(song)) return false; return true; } +bool +SongFilter::HasFoldCase() const noexcept +{ + for (const auto &i : items) { + if (auto t = dynamic_cast(i.get())) { + if (t->GetFoldCase()) + return true; + } else if (auto u = dynamic_cast(i.get())) { + if (u->GetFoldCase()) + return true; + } + } + + return false; +} + bool SongFilter::HasOtherThanBase() const noexcept { - for (const auto &i : items) - if (i.GetTag() != LOCATE_TAG_BASE_TYPE) + for (const auto &i : items) { + const auto *f = dynamic_cast(i.get()); + if (f == nullptr) return true; + } return false; } @@ -385,9 +433,11 @@ SongFilter::HasOtherThanBase() const noexcept const char * SongFilter::GetBase() const noexcept { - for (const auto &i : items) - if (i.GetTag() == LOCATE_TAG_BASE_TYPE) - return i.GetValue(); + for (const auto &i : items) { + const auto *f = dynamic_cast(i.get()); + if (f != nullptr) + return f->GetValue(); + } return nullptr; } @@ -399,8 +449,9 @@ SongFilter::WithoutBasePrefix(const char *_prefix) const noexcept SongFilter result; for (const auto &i : items) { - if (i.GetTag() == LOCATE_TAG_BASE_TYPE) { - const char *s = StringAfterPrefix(i.GetValue(), prefix); + const auto *f = dynamic_cast(i.get()); + if (f != nullptr) { + const char *s = StringAfterPrefix(f->GetValue(), prefix); if (s != nullptr) { if (*s == 0) continue; @@ -409,14 +460,14 @@ SongFilter::WithoutBasePrefix(const char *_prefix) const noexcept ++s; if (*s != 0) - result.items.emplace_back(LOCATE_TAG_BASE_TYPE, s); + result.items.emplace_back(std::make_unique(s)); continue; } } } - result.items.emplace_back(i); + result.items.emplace_back(i->Clone()); } return result; diff --git a/src/SongFilter.hxx b/src/SongFilter.hxx index 90b579007..53200ed95 100644 --- a/src/SongFilter.hxx +++ b/src/SongFilter.hxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2017 The Music Player Daemon Project + * Copyright 2003-2018 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -23,10 +23,13 @@ #include "lib/icu/Compare.hxx" #include "Compiler.h" +#include #include #include #include +#include + /** * Limit the search to files within the given directory. */ @@ -42,9 +45,28 @@ #define LOCATE_TAG_ANY_TYPE TAG_NUM_OF_ITEM_TYPES+20 template struct ConstBuffer; +enum TagType : uint8_t; struct Tag; struct TagItem; struct LightSong; +class ISongFilter; +using ISongFilterPtr = std::unique_ptr; + +class ISongFilter { +public: + virtual ~ISongFilter() noexcept {} + + virtual ISongFilterPtr Clone() const noexcept = 0; + + /** + * Convert this object into an "expression". This is + * only useful for debugging. + */ + virtual std::string ToExpression() const noexcept = 0; + + gcc_pure + virtual bool Match(const LightSong &song) const noexcept = 0; +}; class StringFilter { std::string value; @@ -55,8 +77,6 @@ class StringFilter { IcuCompare fold_case; public: - StringFilter() = default; - template StringFilter(V &&_value, bool _fold_case) :value(std::forward(_value)), @@ -80,82 +100,129 @@ public: bool Match(const char *s) const noexcept; }; -class SongFilter { +class UriSongFilter final : public ISongFilter { + StringFilter filter; + + bool negated; + public: - class Item { - unsigned tag; + template + UriSongFilter(V &&_value, bool fold_case, bool _negated) + :filter(std::forward(_value), fold_case), + negated(_negated) {} - bool negated = false; + const auto &GetValue() const noexcept { + return filter.GetValue(); + } - StringFilter string_filter; + bool GetFoldCase() const { + return filter.GetFoldCase(); + } - /** - * For #LOCATE_TAG_MODIFIED_SINCE - */ - std::chrono::system_clock::time_point time; + bool IsNegated() const noexcept { + return negated; + } - public: - Item(unsigned tag, std::string &&_value, bool fold_case=false); - Item(unsigned tag, std::chrono::system_clock::time_point time); + ISongFilterPtr Clone() const noexcept override { + return std::make_unique(*this); + } - /** - * Convert this object into an "expression". This is - * only useful for debugging. - */ - std::string ToExpression() const noexcept; + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; - unsigned GetTag() const { - return tag; - } +class BaseSongFilter final : public ISongFilter { + std::string value; - bool IsNegated() const noexcept { - return negated; - } +public: + BaseSongFilter(const BaseSongFilter &) = default; - void SetNegated(bool _negated=true) noexcept { - negated = _negated; - } + template + explicit BaseSongFilter(V &&_value) + :value(std::forward(_value)) {} - bool GetFoldCase() const { - return string_filter.GetFoldCase(); - } + const char *GetValue() const noexcept { + return value.c_str(); + } - const char *GetValue() const { - return string_filter.GetValue().c_str(); - } + ISongFilterPtr Clone() const noexcept override { + return std::make_unique(*this); + } - private: - /* note: the "NN" suffix means "no negation", i.e. the - method pretends negation is unset, and the caller - is responsibly for considering it */ + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; - gcc_pure - bool MatchNN(const TagItem &tag_item) const noexcept; +class TagSongFilter final : public ISongFilter { + TagType type; - gcc_pure - bool MatchNN(const Tag &tag) const noexcept; + bool negated; - gcc_pure - bool MatchNN(const LightSong &song) const noexcept; + StringFilter filter; - public: - gcc_pure - bool Match(const LightSong &song) const noexcept { - return MatchNN(song) != IsNegated(); - } - }; +public: + template + TagSongFilter(TagType _type, V &&_value, bool fold_case, bool _negated) + :type(_type), negated(_negated), + filter(std::forward(_value), fold_case) {} + + TagType GetTagType() const { + return type; + } + + const auto &GetValue() const noexcept { + return filter.GetValue(); + } + + bool GetFoldCase() const { + return filter.GetFoldCase(); + } + + bool IsNegated() const noexcept { + return negated; + } + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; private: - std::list items; + bool MatchNN(const Tag &tag) const noexcept; + bool MatchNN(const TagItem &tag) const noexcept; +}; + +class ModifiedSinceSongFilter final : public ISongFilter { + std::chrono::system_clock::time_point value; + +public: + explicit ModifiedSinceSongFilter(std::chrono::system_clock::time_point _value) noexcept + :value(_value) {} + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +class SongFilter { + std::list items; public: SongFilter() = default; gcc_nonnull(3) - SongFilter(unsigned tag, const char *value, bool fold_case=false); + SongFilter(TagType tag, const char *value, bool fold_case=false); ~SongFilter(); + SongFilter(SongFilter &&) = default; + SongFilter &operator=(SongFilter &&) = default; + /** * Convert this object into an "expression". This is * only useful for debugging. @@ -163,7 +230,7 @@ public: std::string ToExpression() const noexcept; private: - const char *ParseExpression(const char *s, bool fold_case=false); + ISongFilterPtr ParseExpression(const char *&s, bool fold_case=false); gcc_nonnull(2,3) void Parse(const char *tag, const char *value, bool fold_case=false); @@ -177,7 +244,7 @@ public: gcc_pure bool Match(const LightSong &song) const noexcept; - const std::list &GetItems() const noexcept { + const auto &GetItems() const noexcept { return items; } @@ -190,13 +257,7 @@ public: * Is there at least one item with "fold case" enabled? */ gcc_pure - bool HasFoldCase() const noexcept { - for (const auto &i : items) - if (i.GetFoldCase()) - return true; - - return false; - } + bool HasFoldCase() const noexcept; /** * Does this filter contain constraints other than "base"? diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx index 1b6e59769..746dfcb50 100644 --- a/src/command/DatabaseCommands.cxx +++ b/src/command/DatabaseCommands.cxx @@ -248,7 +248,7 @@ handle_list(Client &client, Request args, Response &r) return CommandResult::ERROR; } - filter.reset(new SongFilter((unsigned)TAG_ARTIST, + filter.reset(new SongFilter(TAG_ARTIST, args.shift())); } diff --git a/src/db/plugins/ProxyDatabasePlugin.cxx b/src/db/plugins/ProxyDatabasePlugin.cxx index af7c2c336..9e8fc6802 100644 --- a/src/db/plugins/ProxyDatabasePlugin.cxx +++ b/src/db/plugins/ProxyDatabasePlugin.cxx @@ -268,49 +268,53 @@ CheckError(struct mpd_connection *connection) } static bool -SendConstraints(mpd_connection *connection, const SongFilter::Item &item) +SendConstraints(mpd_connection *connection, const ISongFilter &f) { - switch (item.GetTag()) { - mpd_tag_type tag; - -#if LIBMPDCLIENT_CHECK_VERSION(2,9,0) - case LOCATE_TAG_BASE_TYPE: - if (mpd_connection_cmp_server_version(connection, 0, 18, 0) < 0) - /* requires MPD 0.18 */ + if (auto t = dynamic_cast(&f)) { + if (t->IsNegated()) + // TODO implement return true; - return mpd_search_add_base_constraint(connection, - MPD_OPERATOR_DEFAULT, - item.GetValue()); -#endif + if (t->GetTagType() == TAG_NUM_OF_ITEM_TYPES) + return mpd_search_add_any_tag_constraint(connection, + MPD_OPERATOR_DEFAULT, + t->GetValue().c_str()); - case LOCATE_TAG_FILE_TYPE: - return mpd_search_add_uri_constraint(connection, - MPD_OPERATOR_DEFAULT, - item.GetValue()); - - case LOCATE_TAG_ANY_TYPE: - return mpd_search_add_any_tag_constraint(connection, - MPD_OPERATOR_DEFAULT, - item.GetValue()); - - default: - tag = Convert(TagType(item.GetTag())); + const auto tag = Convert(t->GetTagType()); if (tag == MPD_TAG_COUNT) return true; return mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT, tag, - item.GetValue()); - } + t->GetValue().c_str()); + } else if (auto u = dynamic_cast(&f)) { + if (u->IsNegated()) + // TODO implement + return true; + + return mpd_search_add_uri_constraint(connection, + MPD_OPERATOR_DEFAULT, + u->GetValue().c_str()); +#if LIBMPDCLIENT_CHECK_VERSION(2,9,0) + } else if (auto b = dynamic_cast(&f)) { + if (mpd_connection_cmp_server_version(connection, 0, 18, 0) < 0) + /* requires MPD 0.18 */ + return true; + + return mpd_search_add_base_constraint(connection, + MPD_OPERATOR_DEFAULT, + b->GetValue()); +#endif + } else + return true; } static bool SendConstraints(mpd_connection *connection, const SongFilter &filter) { for (const auto &i : filter.GetItems()) - if (!SendConstraints(connection, i)) + if (!SendConstraints(connection, *i)) return false; return true; diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx index 4679e70db..a343d06a4 100644 --- a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx +++ b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx @@ -254,9 +254,9 @@ UpnpDatabase::SearchSongs(const ContentDirectoryService &server, std::string cond; for (const auto &item : filter->GetItems()) { - switch (auto tag = item.GetTag()) { - case LOCATE_TAG_ANY_TYPE: - { + if (auto t = dynamic_cast(item.get())) { + auto tag = t->GetTagType(); + if (tag == TAG_NUM_OF_ITEM_TYPES) { if (!cond.empty()) { cond += " and "; } @@ -268,29 +268,21 @@ UpnpDatabase::SearchSongs(const ContentDirectoryService &server, else cond += " or "; cond += cap; - if (item.GetFoldCase()) { + if (t->GetFoldCase()) { cond += " contains "; } else { cond += " = "; } - dquote(cond, item.GetValue()); + dquote(cond, t->GetValue().c_str()); } cond += ')'; + continue; } - break; - default: - /* Unhandled conditions like - LOCATE_TAG_BASE_TYPE or - LOCATE_TAG_FILE_TYPE won't have a - corresponding upnp prop, so they will be - skipped */ if (tag == TAG_ALBUM_ARTIST) tag = TAG_ARTIST; - // TODO: support LOCATE_TAG_ANY_TYPE etc. - const char *name = tag_table_lookup(upnp_tags, - TagType(tag)); + const char *name = tag_table_lookup(upnp_tags, tag); if (name == nullptr) continue; @@ -304,13 +296,15 @@ UpnpDatabase::SearchSongs(const ContentDirectoryService &server, case-insensitive, but at least some servers have the same convention as mpd (e.g.: minidlna) */ - if (item.GetFoldCase()) { + if (t->GetFoldCase()) { cond += " contains "; } else { cond += " = "; } - dquote(cond, item.GetValue()); + dquote(cond, t->GetValue().c_str()); } + + // TODO: support other ISongFilter implementations } return server.search(handle, objid, cond.c_str());