This commit is contained in:
Max Kellermann 2022-09-27 20:05:14 +02:00
commit cd253e470a
8 changed files with 93 additions and 19 deletions

1
NEWS
View File

@ -3,6 +3,7 @@ ver 0.24 (not yet released)
- "playlistfind"/"playlistsearch" have "sort" and "window" parameters
- filter "prio" (for "playlistfind"/"playlistsearch")
- limit "player" idle events to the current partition
- operator "starts_with"
* archive
- add option to disable archive plugins in mpd.conf
* decoder

View File

@ -196,6 +196,8 @@ of:
- ``(TAG contains 'VALUE')`` checks if the given value is a substring
of the tag value.
- ``(TAG starts_with 'VALUE')`` checks if the tag value starts with the given value.
- ``(TAG =~ 'VALUE')`` and ``(TAG !~ 'VALUE')`` use a Perl-compatible
regular expression instead of doing a simple string comparison.
(This feature is only available if :program:`MPD` was compiled with

View File

@ -112,3 +112,32 @@ IcuCompare::IsIn(const char *haystack) const noexcept
return false;
#endif
}
bool
IcuCompare::StartsWith(const char *haystack) const noexcept
{
#ifdef HAVE_ICU_CASE_FOLD
return StringIsEqual(IcuCaseFold(haystack).c_str(),
needle.c_str(), strlen(needle.c_str()));
#elif defined(_WIN32)
if (needle == nullptr)
/* the MultiByteToWideChar() call in the constructor
has failed, so let's always fail the comparison */
return false;
try {
auto w_haystack = MultiByteToWideChar(CP_UTF8, haystack);
return FindNLSStringEx(LOCALE_NAME_INVARIANT,
FIND_STARTSWITH|NORM_IGNORECASE,
w_haystack.c_str(), -1,
needle.c_str(), -1,
nullptr,
nullptr, nullptr, 0) == 0;
} catch (...) {
/* MultiByteToWideChar() has failed */
return false;
}
#else
return strncmp(haystack, needle.c_str(), strlen(needle.c_str()));
#endif
}

View File

@ -72,6 +72,9 @@ public:
[[gnu::pure]]
bool IsIn(const char *haystack) const noexcept;
[[gnu::pure]]
bool StartsWith(const char *haystack) const noexcept;
};
#endif

View File

@ -91,7 +91,7 @@ SongFilter::SongFilter(TagType tag, const char *value, bool fold_case)
/* for compatibility with MPD 0.20 and older, "fold_case" also
switches on "substring" */
and_filter.AddItem(std::make_unique<TagSongFilter>(tag,
StringFilter(value, fold_case, fold_case, false)));
StringFilter(value, fold_case, fold_case, false, false)));
}
/* this destructor exists here just so it won't get inlined */
@ -209,13 +209,25 @@ ParseStringFilter(const char *&s, bool fold_case)
if (auto after_contains = StringAfterPrefixIgnoreCase(s, "contains ")) {
s = StripLeft(after_contains);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, true, false};
return {std::move(value), fold_case, true, false, false};
}
if (auto after_not_contains = StringAfterPrefixIgnoreCase(s, "!contains ")) {
s = StripLeft(after_not_contains);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, true, true};
return {std::move(value), fold_case, true, false, true};
}
if (auto after_starts_with = StringAfterPrefixIgnoreCase(s, "starts_with ")) {
s = StripLeft(after_starts_with);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, true, false};
}
if (auto after_not_starts_with = StringAfterPrefixIgnoreCase(s, "!starts_with ")) {
s = StripLeft(after_not_starts_with);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, true, true};
}
bool negated = false;
@ -225,7 +237,7 @@ ParseStringFilter(const char *&s, bool fold_case)
negated = s[0] == '!';
s = StripLeft(s + 2);
auto value = ExpectQuoted(s);
StringFilter f(std::move(value), fold_case, false, negated);
StringFilter f(std::move(value), fold_case, false, false, negated);
f.SetRegex(std::make_shared<UniqueRegex>(f.GetValue().c_str(),
false, false,
fold_case));
@ -241,7 +253,7 @@ ParseStringFilter(const char *&s, bool fold_case)
s = StripLeft(s + 2);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, negated};
return {std::move(value), fold_case, false, false, negated};
}
ISongFilterPtr
@ -390,6 +402,7 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
and_filter.AddItem(std::make_unique<UriSongFilter>(StringFilter(value,
fold_case,
fold_case,
false,
false)));
break;
@ -403,6 +416,7 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
StringFilter(value,
fold_case,
fold_case,
false,
false)));
break;
}

View File

@ -35,11 +35,15 @@ StringFilter::MatchWithoutNegation(const char *s) const noexcept
if (fold_case) {
return substring
? fold_case.IsIn(s)
: fold_case == s;
: (starts_with
? fold_case.StartsWith(s)
: fold_case == s);
} else {
return substring
? StringFind(s, value.c_str()) != nullptr
: value == s;
: (starts_with
? StringIsEqual(s, value.c_str(), value.length())
: value == s);
}
}

View File

@ -47,16 +47,21 @@ class StringFilter {
*/
bool substring;
/**
* Search for substrings instead of matching the whole string?
*/
bool starts_with;
bool negated;
public:
template<typename V>
StringFilter(V &&_value, bool _fold_case, bool _substring, bool _negated)
StringFilter(V &&_value, bool _fold_case, bool _substring, bool _starts_with, bool _negated)
:value(std::forward<V>(_value)),
fold_case(_fold_case
? IcuCompare(value)
: IcuCompare()),
substring(_substring), negated(_negated) {}
substring(_substring), starts_with(_starts_with), negated(_negated) {}
bool empty() const noexcept {
return value.empty();
@ -98,7 +103,9 @@ public:
? (negated ? "!~" : "=~")
: (substring
? (negated ? "!contains" : "contains")
: (negated ? "!=" : "=="));
: (starts_with
? (negated ? "!starts_with" : "starts_with")
: (negated ? "!=" : "==")));
}
[[gnu::pure]]

View File

@ -33,7 +33,7 @@ InvokeFilter(const TagSongFilter &f, const Tag &tag) noexcept
TEST(TagSongFilter, Basic)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, false));
StringFilter("needle", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "needle")));
@ -54,7 +54,7 @@ TEST(TagSongFilter, Basic)
TEST(TagSongFilter, Empty)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("", false, false, false));
StringFilter("", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag()));
@ -65,7 +65,7 @@ TEST(TagSongFilter, Empty)
TEST(TagSongFilter, Substring)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, true, false));
StringFilter("needle", false, true, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needleBAR")));
@ -76,10 +76,24 @@ TEST(TagSongFilter, Substring)
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "eedle")));
}
TEST(TagSongFilter, Startswith)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needleBAR")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "FOOneedle")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "FOOneedleBAR")));
EXPECT_FALSE(InvokeFilter(f, MakeTag()));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "eedle")));
}
TEST(TagSongFilter, Negated)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true));
StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag()));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
@ -92,7 +106,7 @@ TEST(TagSongFilter, Negated)
TEST(TagSongFilter, EmptyNegated)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("", false, false, true));
StringFilter("", false, false, false, true));
EXPECT_FALSE(InvokeFilter(f, MakeTag()));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo")));
@ -104,7 +118,7 @@ TEST(TagSongFilter, EmptyNegated)
TEST(TagSongFilter, MultiNegated)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true));
StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "bar")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle", TAG_TITLE, "bar")));
@ -118,7 +132,7 @@ TEST(TagSongFilter, MultiNegated)
TEST(TagSongFilter, Fallback)
{
const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("needle", false, false, false));
StringFilter("needle", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ARTIST, "needle")));
@ -138,7 +152,7 @@ TEST(TagSongFilter, Fallback)
TEST(TagSongFilter, EmptyFallback)
{
const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("", false, false, false));
StringFilter("", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag()));
@ -152,7 +166,7 @@ TEST(TagSongFilter, EmptyFallback)
TEST(TagSongFilter, NegatedFallback)
{
const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("needle", false, false, true));
StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag()));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "foo")));