diff --git a/doc/protocol.rst b/doc/protocol.rst
index f0b477bd1..e2425e250 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -243,6 +243,43 @@ applies `Unicode normalization `__
and converts all punctuation to ASCII equivalents
if MPD was compiled with `ICU `__ support.
+Explicit case-sensitivity [#since_0_24]_
+----------------------------------------
+
+.. note:: The following variants of filter operators override the default case sensitivity
+ that is command dependant with explicit case sensitivity.
+
+.. list-table:: Explicitly case-sensitive operators
+ :widths: 33 33 33
+
+ * - Explicitly case-sensitive
+ - Explicitly case-insensitive
+ - Equivalent command dependant
+
+ * - ``eq_cs``
+ - ``eq_ci``
+ - ``==``
+
+ * - ``!eq_cs``
+ - ``!eq_ci``
+ - ``!=``
+
+ * - ``contains_cs``
+ - ``contains_ci``
+ - ``contains``
+
+ * - ``!contains_cs``
+ - ``!contains_ci``
+ - ``!contains``
+
+ * - ``starts_with_cs``
+ - ``starts_with_ci``
+ - ``starts_with``
+
+ * - ``!starts_with_cs``
+ - ``!starts_with_ci``
+ - ``!starts_with``
+
Prior to MPD 0.21, the syntax looked like this::
find TYPE VALUE
diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx
index 047389da9..8f7dbb2c6 100644
--- a/src/song/Filter.cxx
+++ b/src/song/Filter.cxx
@@ -190,6 +190,39 @@ ExpectQuoted(const char *&s)
return {buffer, length};
}
+/**
+ * Operator definition used to parse the operator
+ * from the command and create the StringFilter
+ * if it matched the operator prefix.
+ */
+struct OperatorDef {
+ const char *prefix;
+ bool fold_case;
+ bool negated;
+ StringFilter::Position position;
+};
+
+/**
+ * Pre-defined operators with explicit case-sensitivity.
+ */
+static constexpr std::array operators = {
+ // operator prefix fold case negated position
+ OperatorDef { "contains_cs ", false, false, StringFilter::Position::ANYWHERE },
+ OperatorDef { "!contains_cs ", false, true, StringFilter::Position::ANYWHERE },
+ OperatorDef { "contains_ci ", true, false, StringFilter::Position::ANYWHERE },
+ OperatorDef { "!contains_ci ", true, true, StringFilter::Position::ANYWHERE },
+
+ OperatorDef { "starts_with_cs ", false, false, StringFilter::Position::PREFIX },
+ OperatorDef { "!starts_with_cs ", false, true, StringFilter::Position::PREFIX },
+ OperatorDef { "starts_with_ci ", true, false, StringFilter::Position::PREFIX },
+ OperatorDef { "!starts_with_ci ", true, true, StringFilter::Position::PREFIX },
+
+ OperatorDef { "eq_cs ", false, false, StringFilter::Position::FULL },
+ OperatorDef { "!eq_cs ", false, true, StringFilter::Position::FULL },
+ OperatorDef { "eq_ci ", true, false, StringFilter::Position::FULL },
+ OperatorDef { "!eq_ci ", true, true, StringFilter::Position::FULL },
+};
+
/**
* Parse a string operator and its second operand and convert it to a
* #StringFilter.
@@ -199,6 +232,17 @@ ExpectQuoted(const char *&s)
static StringFilter
ParseStringFilter(const char *&s, bool fold_case)
{
+ for (auto& op: operators) {
+ if (auto after_prefix = StringAfterPrefixIgnoreCase(s, op.prefix)) {
+ s = StripLeft(after_prefix);
+ return StringFilter(
+ ExpectQuoted(s),
+ op.fold_case,
+ op.position,
+ op.negated);
+ }
+ }
+
if (auto after_contains = StringAfterPrefixIgnoreCase(s, "contains ")) {
s = StripLeft(after_contains);
auto value = ExpectQuoted(s);