diff --git a/Sources/MpcNET/Commands/Database/FindAndSearchCommands.cs b/Sources/MpcNET/Commands/Database/FindAndSearchCommands.cs index 23a06e3..1f407ed 100644 --- a/Sources/MpcNET/Commands/Database/FindAndSearchCommands.cs +++ b/Sources/MpcNET/Commands/Database/FindAndSearchCommands.cs @@ -7,248 +7,15 @@ namespace MpcNET.Commands.Database { using System.Collections.Generic; - using System.ComponentModel; using System.Linq; using MpcNET.Tags; using MpcNET.Types; - - /// - /// Operators available for filters in the protocol - /// - public enum FilterOperator - { - /// - /// Equal (==) - /// - [Description("==")] - Equal, - /// - /// Different (!=) - /// - [Description("!=")] - Different, - /// - /// Contains (contains) - /// - [Description("contains")] - Contains, - /// - /// Mask (=~) - /// - [Description("=~")] - Mask, - /// - /// None ("") - /// - [Description("")] - None, - } - - /// - /// - public static class EnumHelper - { - /// - /// of the enum - /// - /// - public static string GetDescription(this T enumValue) - where T : struct, System.IConvertible - { - if (!typeof(T).IsEnum) - return null; - - string description = enumValue.ToString(); - System.Reflection.FieldInfo fieldInfo = enumValue.GetType().GetField(enumValue.ToString()); - - if (fieldInfo != null) - { - object[] attributes = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), true); - if (attributes != null && attributes.Length > 0) - description = ((DescriptionAttribute)attributes[0]).Description; - } - - return description; - } - } - - /// - /// Filter interface specialized by filters in the protocol - /// - public abstract class IFilter - { - /// - /// Name of the filter - /// - public string Name; - /// - /// Operator of the filter - /// - public FilterOperator Operator; - /// - /// Value to test - /// - public string Value; - /// - /// If the expression is negated - /// - public bool Negation; - - /// - /// Initializes a new instance of the class. - /// - /// Name of the filter - /// Operator of the filter - /// Value to test - /// If the expression is negated - public IFilter(string Name, FilterOperator Operator, string Value, bool Negation = false) - { - this.Name = Name; - this.Operator = Operator; - this.Value = Value; - this.Negation = Negation; - } - - /// - /// Gets the formatted command - /// - public string GetFormattedCommand() - { - string make = ""; - - if (Negation) - make += "(!"; - - make += "(" + Name + " "; - make += Operator.GetDescription(); - make += " " + "\\\"" + Value + "\\\"" + ")"; - - if (Negation) - make += ")"; - - return make; - } - } - - /// - /// Filter "file" - /// - public class FilterFile : IFilter - { - /// - /// Initializes a new instance of the class. - /// - /// Operator of the filter - /// Value to test - /// If the expression is negated - public FilterFile(string Value, FilterOperator Operator, bool Negation = false) : base("file", Operator, Value, Negation) - { - if (Operator != FilterOperator.Equal) - throw new System.ArgumentException("Operator is not compatible: for \"File\" use FilterOperator.Equal."); - - this.Name = "file"; - this.Value = Value; - this.Operator = Operator; - this.Negation = Negation; - } - } - - /// - /// Filter "TAG" - /// - public class FilterTag : IFilter - { - /// - /// Initializes a new instance of the class. - /// - /// The tag. - /// Operator of the filter - /// Value to test - /// If the expression is negated - public FilterTag(ITag Tag, string Value, FilterOperator Operator, bool Negation = false) : base(Tag.Value, Operator, Value, Negation) - { - if (Operator != FilterOperator.Equal && Operator != FilterOperator.Different && Operator != FilterOperator.Contains) - throw new System.ArgumentException("Operator is not compatible: for \"TAG\" use FilterOperator.Equal, FilterOperator.Different or FilterOperator.Contains."); - - this.Value = Value; - this.Operator = Operator; - this.Negation = Negation; - } - } - - /// - /// Filter "base" (not the interface base class, the base command: restrict search songs to a directory) - /// - public class FilterBase : IFilter - { - /// - /// Initializes a new instance of the class. - /// - /// Operator of the filter - /// Value to test - /// If the expression is negated - public FilterBase(string Value, FilterOperator Operator, bool Negation = false) : base("base", Operator, Value, Negation) - { - if (Operator != FilterOperator.None) - throw new System.ArgumentException("Operator is not compatible: for \"base\" use FilterOperator.None."); - - this.Name = "base"; - this.Value = Value; - this.Operator = Operator; - this.Negation = Negation; - } - } - - /// - /// Filter "modified-since" - /// - public class FilterModifiedSince : IFilter - { - /// - /// Initializes a new instance of the class. - /// - /// Operator of the filter - /// Value to test - /// If the expression is negated - public FilterModifiedSince(string Value, FilterOperator Operator, bool Negation = false) : base("modified-since", Operator, Value, Negation) - { - if (Operator != FilterOperator.None) - throw new System.ArgumentException("Operator is not compatible: for \"ModifiedSince\" use FilterOperator.None."); - - this.Name = "modified-since"; - this.Value = Value; - this.Operator = Operator; - this.Negation = Negation; - } - } - - /// - /// Filter "AudioFormat" - /// - public class FilterAudioFormat : IFilter - { - /// - /// Initializes a new instance of the class. - /// - /// Operator of the filter - /// Value to test - /// If the expression is negated - public FilterAudioFormat(string Value, FilterOperator Operator, bool Negation = false) : base("AudioFormat", Operator, Value, Negation) - { - if (Operator != FilterOperator.Equal && Operator != FilterOperator.Mask) - throw new System.ArgumentException("Operator is not compatible: for \"AudioFormat\" use FilterOperator.Equal or FilterOperator.Mask."); - - this.Name = "AudioFormat"; - this.Value = Value; - this.Operator = Operator; - this.Negation = Negation; - } - } + using MpcNET.Types.Filters; /// /// Finds songs in the database that contain "searchText". /// Since MPD 0.21, search syntax is now (TAG == 'VALUE'). - /// https://www.musicpd.org/doc/html/protocol.html#filters + /// https://mpd.readthedocs.io/en/stable/protocol.html#filters /// public class SearchCommand : BaseFilterCommand { @@ -256,16 +23,12 @@ namespace MpcNET.Commands.Database /// /// public override string CommandName => "search"; - /// - /// - /// - public override string Operand => "contains"; /// /// Initializes a new instance of the class. /// /// The tag. - /// The search text. + /// The search text. /// Start of the portion of the results desired /// End of the portion of the results desired public SearchCommand(ITag tag, string searchText, int windowStart = -1, int windowEnd = -1) : base(tag, searchText, windowStart, windowEnd) { } @@ -299,7 +62,7 @@ namespace MpcNET.Commands.Database /// /// Finds songs in the database that contain "searchText" and adds them to the queue. /// Since MPD 0.21, search syntax is now (TAG == 'VALUE'). - /// https://www.musicpd.org/doc/html/protocol.html#filters + /// https://mpd.readthedocs.io/en/stable/protocol.html#filters /// public class SearchAddCommand : BaseFilterCommand { @@ -307,13 +70,9 @@ namespace MpcNET.Commands.Database /// /// public override string CommandName => "searchadd"; - /// - /// - /// - public override string Operand => "contains"; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The tag. /// The search text. @@ -322,7 +81,7 @@ namespace MpcNET.Commands.Database public SearchAddCommand(ITag tag, string searchText, int windowStart = -1, int windowEnd = -1) : base(tag, searchText, windowStart, windowEnd) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// List of key/value filters /// Start of the portion of the results desired @@ -330,7 +89,7 @@ namespace MpcNET.Commands.Database public SearchAddCommand(List> filters, int windowStart = -1, int windowEnd = -1) : base(filters, windowStart, windowEnd) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Filter /// Start of the portion of the results desired @@ -338,7 +97,7 @@ namespace MpcNET.Commands.Database public SearchAddCommand(IFilter filter, int windowStart = -1, int windowEnd = -1) : base(filter, windowStart, windowEnd) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// List of filters /// Start of the portion of the results desired @@ -349,7 +108,7 @@ namespace MpcNET.Commands.Database /// /// Finds songs in the database that is exactly "searchText". /// Since MPD 0.21, search syntax is now (TAG == 'VALUE'). - /// https://www.musicpd.org/doc/html/protocol.html#filters + /// https://mpd.readthedocs.io/en/stable/protocol.html#filters /// public class FindCommand : BaseFilterCommand { @@ -357,10 +116,6 @@ namespace MpcNET.Commands.Database /// /// public override string CommandName => "find"; - /// - /// - /// - public override string Operand => "=="; /// /// Initializes a new instance of the class. @@ -402,8 +157,7 @@ namespace MpcNET.Commands.Database /// public abstract class BaseFilterCommand : IMpcCommand> { - private readonly List> tagFilters; - private readonly List completeFilters; + private readonly List _filters; private readonly int _start; private readonly int _end; @@ -411,11 +165,6 @@ namespace MpcNET.Commands.Database /// Name of the command to use when deserializing /// public abstract string CommandName { get; } - /// - /// Operand to use between tags and search text. Can be ==, !=, contains... - /// - public abstract string Operand { get; } - /// /// Initializes a new instance of the class. @@ -424,10 +173,12 @@ namespace MpcNET.Commands.Database /// The search text. /// Start of the portion of the results desired /// End of the portion of the results desired - public BaseFilterCommand(ITag tag, string searchText, int windowStart = -1, int windowEnd = -1) + /// Operator of the filter + public BaseFilterCommand(ITag tag, string searchText, int windowStart = -1, int windowEnd = -1, FilterOperator operand = FilterOperator.Equal) { - tagFilters = new List>(); - tagFilters.Add(new KeyValuePair(tag, searchText)); + _filters = new List(); + FilterTag Tag = new FilterTag(tag, searchText, operand); + _filters.Add(Tag); _start = windowStart; _end = windowEnd; @@ -439,9 +190,11 @@ namespace MpcNET.Commands.Database /// List of key/value filters /// Start of the portion of the results desired /// End of the portion of the results desired - public BaseFilterCommand(List> filters, int windowStart = -1, int windowEnd = -1) + /// Operator of the filter + public BaseFilterCommand(List> filters, int windowStart = -1, int windowEnd = -1, FilterOperator operand = FilterOperator.Equal) { - tagFilters = filters; + _filters = new List(); + _filters.AddRange(filters.Select(filter => new FilterTag(filter.Key, filter.Value, operand)).ToList()); _start = windowStart; _end = windowEnd; @@ -455,8 +208,8 @@ namespace MpcNET.Commands.Database /// End of the portion of the results desired public BaseFilterCommand(IFilter filters, int windowStart = -1, int windowEnd = -1) { - completeFilters = new List(); - completeFilters.Add(filters); + _filters = new List(); + _filters.Add(filters); _start = windowStart; _end = windowEnd; @@ -470,7 +223,7 @@ namespace MpcNET.Commands.Database /// End of the portion of the results desired public BaseFilterCommand(List filters, int windowStart = -1, int windowEnd = -1) { - completeFilters = filters; + _filters = filters; _start = windowStart; _end = windowEnd; @@ -486,18 +239,10 @@ namespace MpcNET.Commands.Database { string cmd = ""; - if (tagFilters != null) + if (_filters != null) { var serializedFilters = string.Join(" AND ", - tagFilters.Select(x => $"({x.Key.Value} {Operand} {escape(x.Value)})") - ); - cmd = $@"{CommandName} ""({serializedFilters})"""; - } - - if (completeFilters != null) - { - var serializedFilters = string.Join(" AND ", - completeFilters.Select(x => $"{x.GetFormattedCommand()}") + _filters.Select(x => $"{x.GetFormattedCommand()}") ); cmd = $@"{CommandName} ""({serializedFilters})"""; } @@ -521,31 +266,6 @@ namespace MpcNET.Commands.Database { return MpdFile.CreateList(response.ResponseValues); } - - /// - /// String values are quoted with single or double quotes, - /// and special characters within those values must be escaped with the backslash (\). - /// Keep in mind that the backslash is also the escape character on the protocol level, - /// which means you may need to use double backslash. - /// - /// Example expression which matches an artist named foo'bar": - /// (Artist == "foo\'bar\"") - /// - /// At the protocol level, the command must look like this: - /// find "(Artist == \"foo\\'bar\\\"\")" - /// - /// (https://mpd.readthedocs.io/en/stable/protocol.html#filter-syntax) - /// - /// Value to escape - /// - private string escape(string value) - { - var escapedValue = value.Replace(@"\", @"\\\\") - .Replace("'", @"\\'") - .Replace(@"""", @"\\\"""); - - return $@"\""{escapedValue}\"""; - } } // TODO: rescan } diff --git a/Sources/MpcNET/Types/Filters/FilterAudioFormat.cs b/Sources/MpcNET/Types/Filters/FilterAudioFormat.cs new file mode 100644 index 0000000..3fba471 --- /dev/null +++ b/Sources/MpcNET/Types/Filters/FilterAudioFormat.cs @@ -0,0 +1,25 @@ +namespace MpcNET.Types.Filters +{ + /// + /// Filter "AudioFormat" + /// + public class FilterAudioFormat : IFilter + { + /// + /// Initializes a new instance of the class. + /// + /// Operator of the filter + /// Value to test + /// If the expression is negated + public FilterAudioFormat(string Value, FilterOperator Operator, bool Negation = false) : base("AudioFormat", Operator, Value, Negation) + { + if (Operator != FilterOperator.Equal && Operator != FilterOperator.Mask) + throw new System.ArgumentException("Operator is not compatible: for \"AudioFormat\" use FilterOperator.Equal or FilterOperator.Mask."); + + this.Name = "AudioFormat"; + this.Value = Value; + this.Operator = Operator; + this.Negation = Negation; + } + } +} diff --git a/Sources/MpcNET/Types/Filters/FilterBase.cs b/Sources/MpcNET/Types/Filters/FilterBase.cs new file mode 100644 index 0000000..fd3f55e --- /dev/null +++ b/Sources/MpcNET/Types/Filters/FilterBase.cs @@ -0,0 +1,25 @@ +namespace MpcNET.Types.Filters +{ + /// + /// Filter "base" (not the interface base class, the base command: restrict search songs to a directory) + /// + public class FilterBase : IFilter + { + /// + /// Initializes a new instance of the class. + /// + /// Operator of the filter + /// Value to test + /// If the expression is negated + public FilterBase(string Value, FilterOperator Operator, bool Negation = false) : base("base", Operator, Value, Negation) + { + if (Operator != FilterOperator.None) + throw new System.ArgumentException("Operator is not compatible: for \"base\" use FilterOperator.None."); + + this.Name = "base"; + this.Value = Value; + this.Operator = Operator; + this.Negation = Negation; + } + } +} diff --git a/Sources/MpcNET/Types/Filters/FilterFile.cs b/Sources/MpcNET/Types/Filters/FilterFile.cs new file mode 100644 index 0000000..5be0aef --- /dev/null +++ b/Sources/MpcNET/Types/Filters/FilterFile.cs @@ -0,0 +1,25 @@ +namespace MpcNET.Types.Filters +{ + /// + /// Filter "file" + /// + public class FilterFile : IFilter + { + /// + /// Initializes a new instance of the class. + /// + /// Operator of the filter + /// Value to test + /// If the expression is negated + public FilterFile(string Value, FilterOperator Operator, bool Negation = false) : base("file", Operator, Value, Negation) + { + if (Operator != FilterOperator.Equal) + throw new System.ArgumentException("Operator is not compatible: for \"File\" use FilterOperator.Equal."); + + this.Name = "file"; + this.Value = Value; + this.Operator = Operator; + this.Negation = Negation; + } + } +} diff --git a/Sources/MpcNET/Types/Filters/FilterModifiedSince.cs b/Sources/MpcNET/Types/Filters/FilterModifiedSince.cs new file mode 100644 index 0000000..f7a4f68 --- /dev/null +++ b/Sources/MpcNET/Types/Filters/FilterModifiedSince.cs @@ -0,0 +1,25 @@ +namespace MpcNET.Types.Filters +{ + /// + /// Filter "modified-since" + /// + public class FilterModifiedSince : IFilter + { + /// + /// Initializes a new instance of the class. + /// + /// Operator of the filter + /// Value to test + /// If the expression is negated + public FilterModifiedSince(string Value, FilterOperator Operator, bool Negation = false) : base("modified-since", Operator, Value, Negation) + { + if (Operator != FilterOperator.None) + throw new System.ArgumentException("Operator is not compatible: for \"ModifiedSince\" use FilterOperator.None."); + + this.Name = "modified-since"; + this.Value = Value; + this.Operator = Operator; + this.Negation = Negation; + } + } +} diff --git a/Sources/MpcNET/Types/Filters/FilterTag.cs b/Sources/MpcNET/Types/Filters/FilterTag.cs new file mode 100644 index 0000000..7e6f7a1 --- /dev/null +++ b/Sources/MpcNET/Types/Filters/FilterTag.cs @@ -0,0 +1,27 @@ +using MpcNET.Tags; + +namespace MpcNET.Types.Filters +{ + /// + /// Filter "TAG" + /// + public class FilterTag : IFilter + { + /// + /// Initializes a new instance of the class. + /// + /// The tag. + /// Operator of the filter + /// Value to test + /// If the expression is negated + public FilterTag(ITag Tag, string Value, FilterOperator Operator, bool Negation = false) : base(Tag.Value, Operator, Value, Negation) + { + if (Operator != FilterOperator.Equal && Operator != FilterOperator.Different && Operator != FilterOperator.Contains) + throw new System.ArgumentException("Operator is not compatible: for \"TAG\" use FilterOperator.Equal, FilterOperator.Different or FilterOperator.Contains."); + + this.Value = Value; + this.Operator = Operator; + this.Negation = Negation; + } + } +} diff --git a/Sources/MpcNET/Types/IFilter.cs b/Sources/MpcNET/Types/IFilter.cs new file mode 100644 index 0000000..78dfdb3 --- /dev/null +++ b/Sources/MpcNET/Types/IFilter.cs @@ -0,0 +1,135 @@ +using System.ComponentModel; + +namespace MpcNET.Types +{ + /// + /// Operators available for filters in the protocol + /// + public enum FilterOperator + { + /// + /// Equal (==) + /// + [Description("==")] + Equal, + /// + /// Different (!=) + /// + [Description("!=")] + Different, + /// + /// Contains (contains) + /// + [Description("contains")] + Contains, + /// + /// Mask (=~) + /// + [Description("=~")] + Mask, + /// + /// None ("") + /// + [Description("")] + None, + } + + /// + /// + public static class EnumHelper + { + /// + /// of the enum + /// + /// + public static string GetDescription(this T enumValue) + where T : struct, System.IConvertible + { + if (!typeof(T).IsEnum) + return null; + + string description = enumValue.ToString(); + System.Reflection.FieldInfo fieldInfo = enumValue.GetType().GetField(enumValue.ToString()); + + if (fieldInfo != null) + { + object[] attributes = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), true); + if (attributes != null && attributes.Length > 0) + description = ((DescriptionAttribute)attributes[0]).Description; + } + + return description; + } + } + + /// + /// Filter interface specialized by filters in the protocol + /// + public abstract class IFilter + { + /// + /// Name of the filter + /// + public string Name; + /// + /// Operator of the filter + /// + public FilterOperator Operator; + /// + /// Value to test + /// + public string Value; + /// + /// If the expression is negated + /// + public bool Negation; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the filter + /// Operator of the filter + /// Value to test + /// If the expression is negated + public IFilter(string Name, FilterOperator Operator, string Value, bool Negation = false) + { + this.Name = Name; + this.Operator = Operator; + this.Value = Value; + this.Negation = Negation; + } + + /// + /// Gets the formatted command + /// + /// String values are quoted with single or double quotes, + /// and special characters within those values must be escaped with the backslash (\). + /// Keep in mind that the backslash is also the escape character on the protocol level, + /// which means you may need to use double backslash. + /// + /// Example expression which matches an artist named foo'bar": + /// (Artist == "foo\'bar\"") + /// + /// At the protocol level, the command must look like this: + /// find "(Artist == \"foo\\'bar\\\"\")" + /// + /// (https://mpd.readthedocs.io/en/stable/protocol.html#filters) + /// + public string GetFormattedCommand() + { + string make = ""; + + if (Negation) + make += "(!"; + + make += "(" + Name + " "; + make += Operator.GetDescription(); + make += " " + "\\\"" + Value + "\\\"" + ")"; + + if (Negation) + make += ")"; + + return make; + } + } +}