Compare commits

...

39 commits

Author SHA1 Message Date
52b270a8d8
chore: make Object constructor internal. 2024-11-19 23:24:44 +01:00
1193ca3005
docs: document SortProgram and give a simple example. 2024-11-19 21:57:51 +01:00
8371dc8536
feat(lisp): add string comparison methods. 2024-11-19 21:57:34 +01:00
1b0c5455dd
feat(lisp): add quicksort implementation. 2024-11-19 21:15:41 +01:00
f479c93c5c
feat: add SortProgram.
Works similar to Program, but receives the list of matched items
and should return the same list but sorted.
This can actually also be used for further filtering.
2024-11-19 17:33:33 +01:00
0844cebd88
chore: bump jellyfin version. 2024-11-19 16:57:34 +01:00
24b3d41df5
fix: scale progress report to percentage. 2024-11-19 16:57:04 +01:00
45844cafec
fix: ToString call to avoid duplicates. 2024-11-18 20:59:20 +01:00
bf286d4ece
fix: Use ItemId instead of Id for LinkedChild.
See #12892
2024-11-18 20:51:52 +01:00
bfcf854d38
chore: bump jellyfin ABI version & tag for release. 2024-11-18 13:10:51 +01:00
0ccefa3b58
ci: prepare release for v0.2.1.0. 2024-11-11 18:17:32 +01:00
1f961ccb0c
chore: fix more warnings. 2024-11-11 18:00:55 +01:00
12d98c46cb
chore: fix warnings. 2024-11-11 17:53:47 +01:00
3c0d8a3809
feat: format default program for readability. 2024-11-11 14:50:09 +01:00
8f832ed224
fix!: make configuration program monospaced. 2024-11-11 14:43:19 +01:00
67cffd98ff
fix!: default program configuration.
also add new name-contains definition.
2024-11-11 14:17:18 +01:00
df2e07e519
fix: change initial program config to textarea. 2024-11-11 14:09:57 +01:00
74486640d8
docs: add instructions for releasing, so that I don't forget a step. 2024-11-08 22:58:43 +01:00
af63a8a696
docs: itemkind behavior and filtering warning, also extend examples. 2024-11-08 22:50:38 +01:00
f39633d7c5
feat: add is-type convenience definition. 2024-11-08 22:41:24 +01:00
b23587d721
docs: Fix typos in release notes. 2024-11-08 22:40:38 +01:00
7bf2923ad1
fix: allow all types to call "haskeys". 2024-11-08 22:39:56 +01:00
5cfb35a239
docs: update readme to include a notice for the correct release. 2024-11-08 22:38:46 +01:00
61478095ea
ci: prepare release. 2024-11-08 20:38:28 +01:00
5439b1de4f
docs: update examples. 2024-11-08 20:22:46 +01:00
8e97ef7170
docs: update readme for new yaml file format. 2024-11-08 03:54:49 +01:00
f41485cecf
feat: allow to initialize empty playlist file. 2024-11-08 03:41:24 +01:00
4537a3aee3
feat: configuration page with default lisp forms. 2024-11-08 03:38:46 +01:00
396384fd71
feat(lisp): more builtins instead of derived. 2024-11-08 03:09:06 +01:00
6ee9bd7f67
feat: use yaml files by default. 2024-11-07 22:36:12 +01:00
4e5cb8e64e
Merge branch 'yaml' 2024-11-07 22:33:56 +01:00
3c3ddc9e83
fix: report progress. 2024-11-07 22:32:11 +01:00
889df318db
test: method invocation. 2024-11-07 22:26:53 +01:00
8257acbfbb
fix! method invocation if no arguments are supplied. 2024-11-07 22:26:33 +01:00
0a5aed38eb
fix: evaluation of object method invocation. 2024-11-07 22:20:19 +01:00
8ec393f494
fix!: simplify parser and executor, streamline tests. 2024-11-07 00:48:56 +01:00
5835351401
refactor(lisp): refactor calling order of evaluation. 2024-10-30 21:15:16 +01:00
4993bdfa4c
refactor: use arbitrary strings as playlist ids. 2024-10-30 19:33:01 +01:00
906bfb9eeb
feat: Use yaml file instead of json. 2024-10-28 00:26:42 +01:00
19 changed files with 1102 additions and 831 deletions

View file

@ -1,269 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using Jellyfin.Plugin.SmartPlaylist.Lisp;
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
interface IAddable<T> where T : IAddable<T> {
static abstract T operator +(T left, T right);
}
interface ISubtractable<T> where T : ISubtractable<T> {
static abstract T operator -(T left, T right);
}
interface IMultiplicatable<T> where T : IMultiplicatable<T> {
static abstract T operator *(T left, T right);
}
interface IDivisible<T> where T : IDivisible<T> {
static abstract T operator /(T left, T right);
static abstract T operator %(T left, T right);
}
interface ISortable<T, E> where T : ISortable<T, E> {
static abstract E operator >(T left, T right);
static abstract E operator <(T left, T right);
static abstract E operator >=(T left, T right);
static abstract E operator <=(T left, T right);
}
interface IComparable<T, E> where T : IComparable<T, E> {
static abstract E operator ==(T left, T right);
static abstract E operator !=(T left, T right);
E Equals(T other);
}
public abstract class Expression: IComparable<Expression, bool> {
public override abstract string ToString();
public abstract override int GetHashCode();
public abstract bool Equals(Expression other);
public override bool Equals(object? other) {
if (other is Expression other_e) {
return Equals(other_e);
}
return false;
}
public static bool operator ==(Expression left, Expression right) {
return left.Equals(right);
}
public static bool operator !=(Expression left, Expression right) {
return !left.Equals(right);
}
public abstract object Inner();
}
public abstract class Atom : Expression {}
public class Symbol : Atom {
private readonly string _name;
public Symbol(string name) {
_name = name;
}
public string name { get => _name; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _name.GetHashCode();
return hash;
}
public override bool Equals(Expression? other) {
if (other is Symbol other_s) {
return _name == other_s._name;
}
return false;
}
public override string ToString() {
return _name;
}
public override object Inner() {
return _name;
}
}
public class Boolean : Atom {
private readonly bool _value;
public Boolean(bool value) {
_value = value;
}
public bool value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Boolean other_b) {
return _value == other_b.value;
}
return false;
}
public override string ToString() {
return _value? "t" : "nil";
}
public override object Inner() {
return _value;
}
}
public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean> {
private readonly int _value;
public Integer(int value) {
_value = value;
}
public int value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Integer other_i) {
return _value == other_i._value;
}
return false;
}
public override string ToString() {
return _value.ToString();
}
public static Integer operator +(Integer a, Integer b) {
return new Integer(a.value + b.value);
}
public static Integer operator -(Integer a, Integer b) {
return new Integer(a.value - b.value);
}
public static Integer operator *(Integer a, Integer b) {
return new Integer(a.value * b.value);
}
public static Integer operator /(Integer a, Integer b) {
return new Integer(a.value / b.value);
}
public static Integer operator %(Integer a, Integer b) {
return new Integer(a.value % b.value);
}
public static Boolean operator >(Integer a, Integer b) {
return new Boolean(a.value > b.value);
}
public static Boolean operator <(Integer a, Integer b) {
return new Boolean(a.value < b.value);
}
public static Boolean operator >=(Integer a, Integer b) {
return new Boolean(a.value >= b.value);
}
public static Boolean operator <=(Integer a, Integer b) {
return new Boolean(a.value <= b.value);
}
public static Boolean operator ==(Integer a, Integer b) {
return new Boolean(a.value == b.value);
}
public static Boolean operator !=(Integer a, Integer b) {
return new Boolean(a.value != b.value);
}
public override object Inner() {
return _value;
}
}
public class String : Atom, IAddable<String> {
private readonly string _value;
public String(string value) {
_value = value;
}
public string value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is String other_s) {
return _value == other_s._value;
}
return false;
}
public override string ToString() {
return "\"" + _value + "\"";
}
public static String operator +(String a, String b) {
return new String (a.value + b.value);
}
public override object Inner() {
return _value;
}
}
public class Object : Atom {
private readonly object _value;
public Object(object value) {
_value = value;
}
public object value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Object other_o) {
return _value == other_o._value;
}
return false;
}
public override string ToString() {
return _value.ToString();
}
public static Expression FromBase(object o) {
switch (o) {
case bool b:
return new Boolean(b);
case int i:
return new Integer(i);
case string s:
return new String(s);
case IEnumerable<object> e:
return new List(e.Select(x => Object.FromBase(x)).ToList());
default:
return new Object(o);
}
}
public override object Inner() {
return _value;
}
}
public class List : Expression {
private IList<Expression> _expressions;
public List(IList<Expression> expressions) {
_expressions = expressions;
}
public IList<Expression> expressions { get => _expressions; }
public override int GetHashCode() {
int hash = 17;
foreach (Expression i in _expressions) {
hash *= 23;
hash += i.GetHashCode();
}
return hash;
}
public override bool Equals(Expression other) {
if (other is List other_l) {
return _expressions.SequenceEqual(other_l._expressions);
}
return false;
}
public override string ToString() {
return "(" + string.Join(" ", _expressions.Select(x => x.ToString())) + ")";
}
public static List operator +(List a, List b) {
List<Expression> r = new List<Expression>();
r.AddRange(a.expressions);
r.AddRange(b.expressions);
return new List(r);
}
public override object Inner() {
return _expressions.Select(x => x.Inner()).ToArray();
}
}
public class Parser { public class Parser {
private StringTokenStream _sts; private StringTokenStream _sts;
public Parser(StringTokenStream tokens) { public Parser(StringTokenStream tokens) {
@ -280,17 +18,16 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return parse_grouping(gt, gt.closing_value); return parse_grouping(gt, gt.closing_value);
case AtomToken at: case AtomToken at:
return parse_atom(at); return parse_atom(at);
case OperatorToken ot:
return parse_operator(ot);
case SpaceToken sp: case SpaceToken sp:
return parse(); return parse();
} }
return parse(); return parse();
} }
Expression parse_string(GroupingToken start, GroupingToken end) { Expression parse_string(GroupingToken start, GroupingToken? end) {
Debug.Assert(end != null);
Debug.Assert(start.value == end.value); Debug.Assert(start.value == end.value);
Debug.Assert("'\"".Contains(start.value)); Debug.Assert("\"".Contains(start.value));
string r = ""; string r = "";
while (_sts.Available() > 0) { while (_sts.Available() > 0) {
Token<string> t = _sts.Get(); Token<string> t = _sts.Get();
@ -303,8 +40,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new String(r); return new String(r);
} }
Expression parse_grouping(GroupingToken start, GroupingToken end) { Expression parse_grouping(GroupingToken start, GroupingToken? end) {
if ("'\"".Contains(start.value)) { Debug.Assert(end != null);
if ("\"".Contains(start.value)) {
return parse_string(start, end); return parse_string(start, end);
} }
IList<Expression> expressions = new List<Expression>(); IList<Expression> expressions = new List<Expression>();
@ -317,7 +55,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
_sts.Rewind(1); _sts.Rewind(1);
expressions.Add(parse()); expressions.Add(parse());
} }
return new List(expressions); return Cons.FromList(expressions);
} }
Expression parse_atom(AtomToken at) { Expression parse_atom(AtomToken at) {
@ -327,27 +65,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new Integer(parsed_value); return new Integer(parsed_value);
} }
if (at.value.Equals("t")) { if (at.value.Equals("t")) {
return new Boolean(true); return Boolean.TRUE;
} }
if (at.value.Equals("nil")) { if (at.value.Equals("nil")) {
return new Boolean(false); return Boolean.FALSE;
} }
_sts.Commit(); _sts.Commit();
return new Symbol(at.value); return new Symbol(at.value);
} }
Expression parse_operator(OperatorToken ot) {
string v = ot.value;
while (_sts.Available() > 0) {
Token<string> t = _sts.Get();
if (t is OperatorToken ot_) {
v += ot_.value;
continue;
}
_sts.Rewind(1);
break;
}
return new Symbol(v);
}
} }
} }

View file

@ -24,11 +24,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
class SpaceToken : Token<string> { class SpaceToken : Token<string> {
private SpaceToken(string value) : base(value) {} private SpaceToken(string value) : base(value) {}
private static IToken<string>? take(CharStream program) { private static IToken<string>? take(CharStream program) {
string spaces = " \n";
if (program.Available() == 0) { if (program.Available() == 0) {
return null; return null;
} }
if (program.Get() == ' ') { var t = program.Get();
return new SpaceToken(" "); if (spaces.Contains(t)) {
return new SpaceToken(t.ToString());
} }
return null; return null;
} }
@ -41,7 +43,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return null; return null;
} }
char t = program.Get(); char t = program.Get();
if ("()\"'".Contains(t)) { if ("()\"".Contains(t)) {
return new GroupingToken(t.ToString()); return new GroupingToken(t.ToString());
} }
return null; return null;
@ -63,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
string value = ""; string value = "";
while (program.Available() > 0) { while (program.Available() > 0) {
char t = program.Get(); char t = program.Get();
if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Contains(t)) { if (" \n()\"".Contains(t)) {
if (value.Equals("")) { if (value.Equals("")) {
return null; return null;
} }
@ -76,21 +78,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
} }
} }
class OperatorToken : Token<string> {
private OperatorToken(string value) : base(value) {}
private static IToken<string>? take(CharStream program) {
if (program.Available() == 0) {
return null;
}
return new OperatorToken(program.Get().ToString());
//char t = program.get();
//if ("+-*/%".Contains(t)) {
// return new OperatorToken(t.ToString());
//}
//return null;
}
}
class CharStream: Stream<char> { class CharStream: Stream<char> {
public CharStream(IList<char> items) : base(items) {} public CharStream(IList<char> items) : base(items) {}
public CharStream(string items) : base(items.ToCharArray().Cast<char>().ToList()) {} public CharStream(string items) : base(items.ToCharArray().Cast<char>().ToList()) {}
@ -101,7 +88,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
typeof(SpaceToken), typeof(SpaceToken),
typeof(GroupingToken), typeof(GroupingToken),
typeof(AtomToken), typeof(AtomToken),
typeof(OperatorToken),
}; };
protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {} protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {}
private static StringTokenStream generate(CharStream program) { private static StringTokenStream generate(CharStream program) {

View file

@ -0,0 +1,337 @@
namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
interface IAddable<T> where T : IAddable<T> {
static abstract T operator +(T left, T right);
}
interface ISubtractable<T> where T : ISubtractable<T> {
static abstract T operator -(T left, T right);
}
interface IMultiplicatable<T> where T : IMultiplicatable<T> {
static abstract T operator *(T left, T right);
}
interface IDivisible<T> where T : IDivisible<T> {
static abstract T operator /(T left, T right);
static abstract T operator %(T left, T right);
}
interface ISortable<T, E> where T : ISortable<T, E> {
static abstract E operator >(T left, T right);
static abstract E operator <(T left, T right);
static abstract E operator >=(T left, T right);
static abstract E operator <=(T left, T right);
}
interface IComparable<T, E> where T : IComparable<T, E> {
static abstract E operator ==(T left, T right);
static abstract E operator !=(T left, T right);
E Equals(T other);
}
interface IInner {
public object Inner();
}
public abstract class Expression: IComparable<Expression, bool> {
public override abstract string? ToString();
public abstract override int GetHashCode();
public abstract bool Equals(Expression other);
public override bool Equals(object? other) {
if (other is Expression other_e) {
return Equals(other_e);
}
return false;
}
public static bool operator ==(Expression left, Expression right) {
return left.Equals(right);
}
public static bool operator !=(Expression left, Expression right) {
return !left.Equals(right);
}
}
public abstract class Atom : Expression {}
public class Scalar<V> : Atom, IInner where V : notnull {
protected V _value;
public Scalar(V value) {
_value = value;
}
public override int GetHashCode() {
return 17 * 23 + _value.GetHashCode();
}
public override bool Equals(Expression other) {
if (other is Scalar<V> other_scalar) {
return _value.Equals(other_scalar._value);
}
return false;
}
public override string? ToString() {
return _value.ToString();
}
public V Value() {
return _value;
}
public object Inner() {
return _value;
}
}
public class Symbol : Atom {
private string _name;
public Symbol(string name) {
_name = name;
}
public override int GetHashCode() {
return 17 * 23 + _name.GetHashCode();
}
public override bool Equals(Expression other) {
if (other is Symbol other_symbol) {
return _name.Equals(other_symbol._name);
}
return false;
}
public override string? ToString() {
return _name.ToString();
}
public string Name() {
return _name;
}
}
public class Integer : Scalar<int>, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean> {
public Integer(int value) : base(value) {}
public static Integer operator +(Integer a, Integer b) {
return new Integer(a._value + b._value);
}
public static Integer operator -(Integer a, Integer b) {
return new Integer(a._value - b._value);
}
public static Integer operator *(Integer a, Integer b) {
return new Integer(a._value * b._value);
}
public static Integer operator /(Integer a, Integer b) {
return new Integer(a._value / b._value);
}
public static Integer operator %(Integer a, Integer b) {
return new Integer(a._value % b._value);
}
public static Boolean operator >(Integer a, Integer b) {
return (a._value > b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator <(Integer a, Integer b) {
return (a._value < b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator >=(Integer a, Integer b) {
return (a._value >= b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator <=(Integer a, Integer b) {
return (a._value <= b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public override int GetHashCode() {
return base.GetHashCode();
}
public override bool Equals(object? other) {
return base.Equals(other);
}
public static Boolean operator ==(Integer a, Integer b) {
return (a._value == b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator !=(Integer a, Integer b) {
return (a._value != b._value) ? Boolean.TRUE : Boolean.FALSE;
}
}
public class Boolean: Scalar<bool> {
public static Boolean TRUE = new Boolean(true);
public static Boolean FALSE = new Boolean(false);
private Boolean(bool value) : base(value) {}
public override string? ToString() {
if (_value) {
return "t";
}
return "nil";
}
public IList<Expression> ToList() {
if (_value) {
throw new ApplicationException("Cannot use t as list");
}
return new List<Expression>();
}
}
public class String: Scalar<string>, ISortable<String, Boolean> {
public String(string value) : base(value) {}
public override string? ToString() {
return $"\"{base.ToString()}\"";
}
public static Boolean operator <(String a, String b) {
return (a.Value().CompareTo(b.Value()) < 0) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator >(String a, String b) {
return b < a;
}
public static Boolean operator <=(String a, String b) {
return (a.Value().CompareTo(b.Value()) <= 0) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator >=(String a, String b) {
return b <= a;
}
public override int GetHashCode() {
return base.GetHashCode();
}
public override bool Equals(object? other) {
return base.Equals(other);
}
public static Boolean operator ==(String a, String b) {
return (a._value == b._value) ? Boolean.TRUE : Boolean.FALSE;
}
public static Boolean operator !=(String a, String b) {
return (a._value != b._value) ? Boolean.TRUE : Boolean.FALSE;
}
}
public class Cons: Expression {
public Expression Item1;
public Expression Item2;
public Cons(Expression item1, Expression item2) {
Item1 = item1;
Item2 = item2;
}
public static Expression FromList(IEnumerable<Expression> expressions) {
var e = expressions.ToList();
if (e.Count == 0) {
return Boolean.FALSE;
}
var item1 = expressions.First();
if (e.Count == 1) {
return new Cons(item1, Boolean.FALSE);
}
var item2 = expressions.Skip(1).ToList();
return new Cons(item1, FromList(item2));
}
public IEnumerable<Expression> ToList() {
var l = new List<Expression>();
l.Add(Item1);
if (Item2 == Boolean.FALSE) {
return l;
}
if (Item2 is Cons item2_cons) {
l.AddRange(item2_cons.ToList());
return l;
}
l.Add(Item2);
return l;
}
public override int GetHashCode() {
var hash = 17;
hash *= 23;
hash += Item1.GetHashCode();
hash *= 23;
hash += Item2.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Cons other_list) {
return Item1.Equals(other_list.Item1) && Item2.Equals(other_list.Item2);
}
return false;
}
private string? ToStringSimple() {
if (Item2.Equals(Boolean.FALSE)) {
return Item1.ToString();
}
if (Item2 is Cons item2_cons) {
return $"{Item1} {item2_cons.ToStringSimple()}";
}
return $"{Item1} . {Item2}";
}
public override string? ToString() {
return $"({ToStringSimple()})";
}
}
public class Object : Scalar<object> {
internal Object(object value) : base(value) {}
public static Expression FromBase(object? o) {
if (o == null) {
return Boolean.FALSE;
}
switch (o) {
case bool b:
return b ? Boolean.TRUE : Boolean.FALSE;
case int i:
return new Integer(i);
case string s:
return new String(s);
case Expression e:
return e;
case IEnumerable<object> e:
return Cons.FromList(e.Select(x => FromBase(x)));
default:
return new Object(o);
}
}
}
public class Procedure : Expression {
private IEnumerable<Symbol> _parameters;
private Expression _body;
private bool _eval_args;
public Procedure(IEnumerable<Symbol> parameters, Expression body, bool eval_args) {
_parameters = parameters;
_body = body;
_eval_args = eval_args;
}
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _parameters.GetHashCode();
hash *= 23;
hash += _body.GetHashCode();
return hash;
}
public override bool Equals(Expression? other) {
if (other is Procedure other_p) {
return _parameters == other_p._parameters && _body == other_p._body;
}
return false;
}
public override string ToString() {
var star = _eval_args ? "" : "*";
return $"(lambda{star} {Cons.FromList(_parameters)} {_body})";
}
private Expression __eval(Executor e, Expression exp) {
if (!_eval_args) return exp;
return e.eval(exp);
}
private Expression _eval(Executor e, Expression exp) {
var r = __eval(e, exp);
//Console.WriteLine($"{exp} = {r}");
return r;
}
public Expression Call(Executor e, IList<Expression> args) {
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
var _params = _parameters.Select(x => x.Name()).ToArray();
var idx_rest = -1;
IList<(string, Expression)> name_args = new List<(string, Expression)>();
for (var i = 0; i < _parameters.Count(); i++) {
var name = _params[i];
if (name.Equals(".")) {
idx_rest = i + 1;
break;
}
name_args.Add((name, _eval(e, args[i])));
}
if (idx_rest > 0) {
name_args.Add((_params[idx_rest], Cons.FromList(args.Skip(idx_rest - 1).Select(x => _eval(e, x)))));
}
foreach (var na in name_args) {
new_e.environment.Set(na.Item1, na.Item2);
}
var r = new_e.eval(_body);
return r;
}
}
}

View file

@ -3,63 +3,19 @@ using System.Reflection;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
namespace Jellyfin.Plugin.SmartPlaylist.Lisp { namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
using Function = Func<IList<Expression>, Expression>; using Function = Func<IEnumerable<Expression>, Expression>;
using FunctionLater = Func<Executor, IList<Expression>, Expression>; using FunctionLater = Func<Executor, IEnumerable<Expression>, Expression>;
public class Procedure : Expression {
private Compiler.List _parameters;
private Expression _body;
public Procedure(Compiler.List parameters, Expression body) {
_parameters = parameters;
_body = body;
}
private static IEnumerable<(T1, T2)> Zip<T1, T2>(IEnumerable<T1> a, IEnumerable<T2> b) {
using (var e1 = a.GetEnumerator()) using (var e2 = b.GetEnumerator()) {
while (e1.MoveNext() && e2.MoveNext()) {
yield return (e1.Current, e2.Current);
}
}
}
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _parameters.GetHashCode();
hash *= 23;
hash += _body.GetHashCode();
return hash;
}
public override bool Equals(Expression? other) {
if (other is Procedure other_p) {
return _parameters == other_p._parameters && _body == other_p._body;
}
return false;
}
public override object Inner() {
throw new ApplicationException("This is not sensible");
}
public override string ToString() {
return $"(lambda {_parameters} {_body})";
}
public Expression Call(Executor e, IList<Expression> args) {
var p = _parameters.expressions.Select(x => x.Inner().ToString()).ToList();
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
foreach (var tuple in Zip<string, Expression>(p, args)) {
new_e.environment.Set(tuple.Item1, tuple.Item2);
}
return new_e.eval(_body);
}
}
public interface IEnvironment<K, V> { public interface IEnvironment<K, V> {
public V Get(K k); public V? Get(K k);
public void Set(K k, V v); public void Set(K k, V v);
public IEnvironment<K, V>? Find(K k); public IEnvironment<K, V>? Find(K k);
public IEnvironment<K, V> Parent(bool recursive);
} }
public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> { public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
public Expression? Get(string k) { public Expression? Get(string k) {
if (TryGetValue(k, out Expression v)) { if (TryGetValue(k, out Expression? v)) {
return v; return v;
} }
return null; return null;
@ -74,11 +30,54 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
} }
return null; return null;
} }
public IEnvironment<string, Expression> Parent(bool recursive) {
return this;
}
} }
public class DefaultEnvironment: Environment { public class DefaultEnvironment: Environment {
public DefaultEnvironment() { public DefaultEnvironment() {
this["find"] = new Parser("(lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))").parse(); var e = new Executor();
this["null"] = new Symbol("not");
this["list"] = e.eval("(lambda (. args) args)");
this["find"] = e.eval("(lambda (item list_) (if (null list_) nil (if (= item (car list_)) (car list_) (find item (cdr list_)))))");
this["map"] = e.eval("(lambda (fc l) (if (null l) nil (cons (fc (car l)) (map fc (cdr l)))))");
this["fold"] = e.eval("(lambda (fc i l) (if (null l) i (fold fc (fc i (car l)) (cdr l))))");
this["any"] = e.eval("(lambda (fc l) (apply or (map fc l)))");
this["all"] = e.eval("(lambda (fc l) (apply and (map fc l)))");
this["append"] = e.eval("(lambda (l i) (if (null l) i (cons (car l) (append (cdr l) i))))");
this["qsort"] = e.eval(
"""
(lambda
(fc list00)
(let
(getpivot
(lambda
(list0)
(car list0)))
(split
(lambda
(list0 pivot fc h0 h1)
(cond
((null list0) (list list0 pivot fc h0 h1))
((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
(t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
(sort
(lambda
(fc list0)
(cond
((null list0) nil)
((null (cdr list0)) list0)
(t
(let*
(halves (split list0 (getpivot list0) fc nil nil))
(h0 (car (cdr (cdr (cdr halves)))))
(h1 (car (cdr (cdr (cdr (cdr halves))))))
(append (sort fc h0) (sort fc h1)))))))
(sort fc list00)))
"""
);
} }
} }
@ -88,7 +87,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
_super = super; _super = super;
} }
public Expression? Get(string k) { public Expression? Get(string k) {
if (TryGetValue(k, out Expression v)) { if (TryGetValue(k, out Expression? v)) {
return v; return v;
} }
return null; return null;
@ -103,272 +102,275 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
} }
return _super.Find(k); return _super.Find(k);
} }
public IEnvironment<string, Expression> Parent(bool recursive) {
if (recursive) {
return this._super.Parent(recursive);
}
return this._super;
}
} }
public class Builtins : Dictionary<string, Function> { public class Builtins : Dictionary<string, Function> {
public Builtins() : base() { public Builtins() : base() {
this["+"] = _add; this["atom"] = _atom;
this["-"] = _sub; this["eq"] = _eq;
this["*"] = _mul;
this["/"] = _div;
this["%"] = _mod;
this[">"] = _gt;
this["<"] = _lt;
this[">="] = _ge;
this["<="] = _le;
this["eq?"] = _eq;
this["="] = _eq;
this["!="] = _ne;
this["abs"] = _abs;
this["append"] = _append;
this["begin"] = _begin;
this["car"] = _car; this["car"] = _car;
this["cdr"] = _cdr; this["cdr"] = _cdr;
this["cons"] = _cons; this["cons"] = _cons;
this["not"] = _not;
this["length"] = _length; this["begin"] = _begin;
this["+"] = (x) => _agg((Integer a, Integer b) => a + b, x);
this["-"] = (x) => _agg((Integer a, Integer b) => a - b, x);
this["*"] = (x) => _agg((Integer a, Integer b) => a * b, x);
this["/"] = (x) => _agg((Integer a, Integer b) => a / b, x);
this["%"] = (x) => _agg((Integer a, Integer b) => a % b, x);
this["="] = (x) => _cmp((Atom a, Atom b) => (a == b)? Boolean.TRUE : Boolean.FALSE, x);
this["!="] = (x) => _cmp((Atom a, Atom b) => (a != b)? Boolean.TRUE : Boolean.FALSE, x);
this["<"] = (x) => _cmp((Integer a, Integer b) => a < b, x);
this["<="] = (x) => _cmp((Integer a, Integer b) => a <= b, x);
this[">"] = (x) => _cmp((Integer a, Integer b) => a > b, x);
this[">="] = (x) => _cmp((Integer a, Integer b) => a >= b, x);
this["not"] = (x) => {
return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
};
this["string="] = (x) => _cmp((String a, String b) => a == b, x);
this["string!="] = (x) => _cmp((String a, String b) => a != b, x);
this["string>"] = (x) => _cmp((String a, String b) => a > b, x);
this["string>="] = (x) => _cmp((String a, String b) => a >= b, x);
this["string<"] = (x) => _cmp((String a, String b) => a < b, x);
this["string<="] = (x) => _cmp((String a, String b) => a <= b, x);
this["haskeys"] = _haskeys; this["haskeys"] = _haskeys;
this["getitems"] = _getitems; this["getitems"] = _getitems;
this["invoke"] = _invoke; this["invoke"] = _invoke;
} }
private static T _agg<T>(Func<T, T, T> op, IEnumerable<Expression> args) where T : Expression {
private static T _agg<T>(Func<T, T, T> op, IList<T> args) { T agg = (T) args.First();
T agg = args[0];
foreach (var arg in args.Skip(1)) { foreach (var arg in args.Skip(1)) {
agg = op(agg, arg); var arg_ = (T) arg;
agg = op(agg, arg_);
} }
return agg; return agg;
} }
private static Expression _add(IList<Expression> args) { private static E _cmp<T, E>(Func<T, T, E> op, IEnumerable<Expression> args) where T : Expression where E : Expression {
Expression first = args[0]; return op((T) args.First(), (T) args.Skip(1).First());
switch (first) {
case Integer i:
return _agg((a, b) => a + b, args.Select(x => (Integer) x).ToList());
case Compiler.String s:
return _agg((a, b) => a + b, args.Select(x => (Compiler.String) x).ToList());
} }
throw new ApplicationException(); private static Expression _atom(IEnumerable<Expression> args) {
return (args.First() is Atom) ? Boolean.TRUE : Boolean.FALSE;
} }
private static Expression _sub(IList<Expression> args) { private static Expression _eq(IEnumerable<Expression> args) {
Expression first = args[0]; return args.First().Equals(args.Skip(1).First()) ? Boolean.TRUE : Boolean.FALSE;
switch (first) {
case Integer i:
return _agg((a, b) => a - b, args.Select(x => (Integer) x).ToList());
} }
throw new ApplicationException(); private static Expression _car(IEnumerable<Expression> args) {
return ((Cons)args.First()).Item1;
} }
private static Expression _mul(IList<Expression> args) { private static Expression _cdr(IEnumerable<Expression> args) {
Expression first = args[0]; return ((Cons)args.First()).Item2;
switch (first) {
case Integer i:
return _agg((a, b) => a * b, args.Select(x => (Integer) x).ToList());
} }
throw new ApplicationException(); private static Expression _cons(IEnumerable<Expression> args) {
return new Cons(args.First(), args.Skip(1).First());
} }
private static Expression _div(IList<Expression> args) { private static Expression _begin(IEnumerable<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _agg((a, b) => a / b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static Expression _mod(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _agg((a, b) => a % b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static E _cmp<T, E>(Func<T, T, E> op, IList<T> args) {
T first = args[0];
T second = args[1];
return op(first, second);
}
private static Expression _gt(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _cmp((a, b) => a > b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static Expression _lt(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _cmp((a, b) => a < b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static Expression _ge(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _cmp((a, b) => a >= b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static Expression _le(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return _cmp((a, b) => a <= b, args.Select(x => (Integer) x).ToList());
}
throw new ApplicationException();
}
private static Expression _eq(IList<Expression> args) {
bool r = _cmp((a, b) => a == b, args);
return new Compiler.Boolean(r);
}
private static Expression _ne(IList<Expression> args) {
bool r = _cmp((a, b) => a != b, args);
return new Compiler.Boolean(r);
}
private static Expression _abs(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case Integer i:
return i.value >= 0 ? i : new Integer(-i.value);
}
throw new ApplicationException();
}
private static Expression _append(IList<Expression> args) {
Expression first = args[0];
switch (first) {
case List l:
return l + new List(args);
}
throw new ApplicationException();
}
private static Expression _begin(IList<Expression> args) {
return args.Last(); return args.Last();
} }
private static Expression _car(IList<Expression> args) { private static Expression _haskeys(IEnumerable<Expression> args) {
return ((List) args.First()).expressions.First(); Object o = new Object(((IInner) args.First()).Inner());
}
private static Expression _cdr(IList<Expression> args) {
return new List(((List) args.First()).expressions.Skip(1).ToList());
}
private static Expression _cons(IList<Expression> args) {
switch (args[1]) {
case Compiler.List other_list:
return (new Compiler.List(new []{args[0]}.ToList()) + new Compiler.List(other_list.expressions));
case Atom other_atom:
return new Compiler.List(new[]{args[0], args[1]}.ToList());
}
throw new ApplicationException();
}
private static Expression _not(IList<Expression> args) {
if (args[0] == new Compiler.Boolean(false)) {
return new Compiler.Boolean(true);
}
return new Compiler.Boolean(false);
}
private static Expression _length(IList<Expression> args) {
return new Integer(((Compiler.List)args[0]).expressions.Count());
}
private static Expression _haskeys(IList<Expression> args) {
Compiler.Object o = (Compiler.Object) args[0];
foreach (var e in args.Skip(1)) { foreach (var e in args.Skip(1)) {
Compiler.String s = (Compiler.String) e; String s = (String) e;
PropertyInfo? pi = o.value.GetType().GetProperty(s.value); PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
if (pi != null) { if (pi != null) {
continue; continue;
} }
MethodInfo? mi = o.value.GetType().GetMethod(s.value); MethodInfo? mi = o.Value().GetType().GetMethod(s.Value());
if (mi != null) { if (mi != null) {
continue; continue;
} }
FieldInfo? fi = o.value.GetType().GetField(s.value); FieldInfo? fi = o.Value().GetType().GetField(s.Value());
if (fi != null) { if (fi != null) {
continue; continue;
} }
return new Compiler.Boolean(false); return Boolean.FALSE;
} }
return new Compiler.Boolean(true); return Boolean.TRUE;
} }
private static Expression _getitems(IList<Expression> args) { private static Expression _getitems(IEnumerable<Expression> args) {
Compiler.Object o = (Compiler.Object) args[0]; Object o = new Object(((IInner) args.First()).Inner());
IList<Expression> r = new List<Expression>(); IList<Expression> r = new List<Expression>();
foreach (var e in args.Skip(1)) { foreach (var e in args.Skip(1)) {
Compiler.String s = (Compiler.String) e; String s = (String) e;
PropertyInfo? pi = o.value.GetType().GetProperty(s.value); PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
if (pi != null) { if (pi != null) {
r.Add(Compiler.Object.FromBase(pi.GetValue(o.value))); r.Add(Object.FromBase(pi.GetValue(o.Value())));
continue; continue;
} }
FieldInfo? fi = o.value.GetType().GetField(s.value); FieldInfo? fi = o.Value().GetType().GetField(s.Value());
if (fi != null) { if (fi != null) {
r.Add(Compiler.Object.FromBase(fi.GetValue(o.value))); r.Add(Object.FromBase(fi.GetValue(o.Value())));
continue; continue;
} }
throw new ApplicationException($"{o.value} has no property or field {s.value}"); throw new ApplicationException($"{o.Value()} has no property or field {s.Value()}");
} }
return new Compiler.List(r); return Cons.FromList(r);
} }
private static Expression _invoke(IList<Expression> args) { private static Expression _invoke(IEnumerable<Expression> args) {
Compiler.Object o = (Compiler.Object) args[0]; Object o = new Object(((IInner) args.First()).Inner());
Compiler.String s = (Compiler.String) args[1]; String s = (String) args.Skip(1).First();
Compiler.List l = (Compiler.List) args[2]; IEnumerable<Expression> l;
IList<Expression> r = new List<Expression>(); if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) {
MethodInfo? mi = o.value.GetType().GetMethod(s.value); l = new List<Expression>();
if (mi == null) { } else if (args.Skip(2).First() is Cons lc) {
throw new ApplicationException($"{o.value} has not method {s.value}"); l = lc.ToList();
} else {
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
} }
return Compiler.Object.FromBase(mi.Invoke(o.value, (object?[]?) l.Inner())); object[]? l_ = l.Select<Expression, object>(x => {
switch (x) {
case Integer s:
return s.Value();
case Boolean b:
return b.Value();
case String s:
return s.Value();
case Object o:
return o.Value();
case Cons c:
return c.ToList().ToList();
}
throw new ApplicationException($"Unhandled value {x} (type {x.GetType()})");
}).ToArray();
Type[] l_types = l_.Select( x => {
return x.GetType();
}).ToArray();
IList<Expression> r = new List<Expression>();
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
if (mi == null) {
throw new ApplicationException($"{o.Value()} has not method {s.Value()}");
}
return Object.FromBase(mi.Invoke(o.Value(), l_));
} }
} }
public class BuiltinsLater : Dictionary<string, FunctionLater> { public class BuiltinsLater : Dictionary<string, FunctionLater> {
public BuiltinsLater() : base() { public BuiltinsLater() : base() {
this["quote"] = _quote;
this["eval"] = _eval;
this["cond"] = _cond;
this["if"] = _if; this["if"] = _if;
this["define"] = _define; this["define"] = _define;
this["let"] = _let;
this["let*"] = _let_star;
this["lambda"] = _lambda; this["lambda"] = _lambda;
this["lambda*"] = _lambda_star;
this["apply"] = _apply; this["apply"] = _apply;
this["and"] = _and;
this["or"] = _or; this["and"] = (e, x) => {
Expression? r = null;
foreach (var xi in x) {
r = e.eval(xi);
if (r == Boolean.FALSE) {
return r;
} }
private static Expression _if(Executor e, IList<Expression> args) {
bool test = e.eval(args[0]) != (new Compiler.Boolean(false));
return e.eval(args[1 + (test ? 0 : 1)]);
} }
private static Expression _define(Executor e, IList<Expression> args) { if (r is null) {
var refname = ((Symbol) args[0]).name; return Boolean.FALSE;
e.environment.Set(refname, e.eval(args[1]));
return new Compiler.Boolean(false); // NOOP
} }
private static Expression _lambda(Executor e, IList<Expression> args) { return r;
return new Procedure((Compiler.List) args[0], args[1]); };
this["or"] = (e, x) => {
foreach (var xi in x) {
var r = e.eval(xi);
if (r != Boolean.FALSE) {
return r;
} }
private static Expression _apply(Executor e, IList<Expression> args) {
if (args[0].GetType() != typeof(Symbol)) {
throw new ApplicationException();
} }
if (args[1].GetType() != typeof(List)) { return Boolean.FALSE;
throw new ApplicationException(); };
} }
Symbol arg0 = (Compiler.Symbol) args[0]; private static Expression _quote(Executor e, IEnumerable<Expression> args) {
Compiler.List other_args = (Compiler.List) args[1]; return args.First();
return e.EvalFunction(arg0, other_args.expressions);
} }
private static Expression _and(Executor e, IList<Expression> args) { private static Expression _eval(Executor e, IEnumerable<Expression> args) {
Expression result = new Compiler.Boolean(false); return e.eval(e.eval(args.First()));
foreach (var exp in args) {
result = e.eval(exp);
if (result == new Compiler.Boolean(false)) { return result; }
} }
return result; private static Expression _cond(Executor e, IEnumerable<Expression> args) {
foreach (var a in args) {
if (a is Cons a_cons) {
var a_ = a_cons.ToList();
if (!e.eval(a_.First()).Equals(Boolean.FALSE)) {
return e.eval(a_.Skip(1).First());
} }
private static Expression _or(Executor e, IList<Expression> args) { } else {
Expression result = new Compiler.Boolean(false); throw new ApplicationException($"Incorrect arguments to cond, expected list: {args}");
foreach (var exp in args) {
result = e.eval(exp);
if (result != new Compiler.Boolean(false)) { return result; }
} }
return result; }
return Boolean.FALSE;
}
private static Expression _if(Executor e, IEnumerable<Expression> args) {
if (e.eval(args.First()).Equals(Boolean.FALSE)) {
return e.eval(args.Skip(2).First());
}
return e.eval(args.Skip(1).First());
}
private static Expression _define(Executor e, IEnumerable<Expression> args) {
Symbol refname = (Symbol) args.First();
e.environment.Parent(true).Set(refname.Name(), args.Skip(1).Select(x => e.eval(x)).First());
return Boolean.TRUE;
}
private static Expression _let_star(Executor e, IEnumerable<Expression> args) {
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
foreach (var pair in args.SkipLast(1)) {
if (pair is not Cons pair_cons) {
throw new ApplicationException("No expression for let*");
}
Symbol refname = (Symbol) pair_cons.Item1;
Expression exp = ((Cons) pair_cons.Item2).Item1;
new_e.environment.Set(refname.Name(), new_e.eval(exp));
}
return new_e.eval(args.Last());
}
private static Expression _let(Executor e, IEnumerable<Expression> args) {
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
List<(Symbol, Expression)> vars = new List<(Symbol, Expression)>();
foreach (var pair in args.SkipLast(1)) {
if (pair is not Cons pair_cons) {
throw new ApplicationException("");
}
Symbol refname = (Symbol) pair_cons.Item1;
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
vars.Add((refname, e.eval(exp_)));
}
foreach (var pair in vars) {
new_e.environment.Set(pair.Item1.Name(), pair.Item2);
}
return new_e.eval(args.Last());
}
private static Expression _lambda(Executor e, IEnumerable<Expression> args) {
IEnumerable<Symbol> proc_args;
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
else {
throw new ApplicationException("");
}
return new Procedure(proc_args, args.Skip(1).First(), true);
}
private static Expression _lambda_star(Executor e, IEnumerable<Expression> args) {
IEnumerable<Symbol> proc_args;
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
else {
throw new ApplicationException("");
}
return new Procedure(proc_args, args.Skip(1).First(), false);
}
private static Expression _apply(Executor e, IEnumerable<Expression> args) {
return e.eval(new Cons(args.First(), e.eval(args.Skip(1).First())));
} }
} }
@ -396,54 +398,52 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
public Builtins builtins { get => _builtins; } public Builtins builtins { get => _builtins; }
public BuiltinsLater builtinsLater { get => _builtinsLater; } public BuiltinsLater builtinsLater { get => _builtinsLater; }
public Expression EvalFunction(Symbol fcname, IList<Expression> args) { public Expression? EvalFunction(Symbol fcname, IEnumerable<Expression> args) {
if (_environment.Find(fcname.name) is IEnvironment<string, Expression> _e) { if (builtins.ContainsKey(fcname.Name())) {
Expression? first = _e.Get(fcname.name); return builtins[fcname.Name()](args.Select(x => eval(x)).ToList()); // call ToList for sideeffect
return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList());
} }
if (_builtins.ContainsKey(fcname.name)) { if (builtinsLater.ContainsKey(fcname.Name())) {
Function fc = _builtins[fcname.name]; return builtinsLater[fcname.Name()](this, args);
return fc(args.Select(x => eval(x)).ToList());
} }
if (_builtinsLater.ContainsKey(fcname.name)) { return null;
FunctionLater fc = _builtinsLater[fcname.name];
return fc(this, args);
}
throw new ApplicationException($"Key '{fcname.name}' not found in environment or builtins");
} }
public Expression eval(Expression expression) { public Expression eval(Expression expression) {
switch (expression) { switch (expression) {
case Symbol s: case Symbol s:
return _environment.Find(s.name).Get(s.name); if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
case Compiler.Boolean b: throw new ApplicationException($"Could not find '{s.Name()}'");
}
var r_ = env.Get(s.Name());
if (r_ is null) {
throw new ApplicationException($"Could not find '{s.Name()}'");
}
return r_;
case Boolean b:
return b; return b;
case Integer i: case Integer i:
return i; return i;
case Compiler.String s: case String s:
return s; return s;
case Compiler.Object o: case Object o:
return o; return o;
case Procedure p: case Procedure p:
return p; return p;
case List list: case Cons cons:
if (list.expressions.Count == 0) { var l = cons.ToList();
return list; if (cons.Item1 is Symbol cons_item1_symbol) {
Expression? r = EvalFunction(cons_item1_symbol, l.Skip(1));
if (r is not null) { return r; }
} }
// do we really want to allow shadowing of builtins? var eval_Item1 = eval(cons.Item1);
if (list.expressions[0].GetType() == typeof(Symbol)) { if (eval_Item1 is Symbol eval_item1_symbol1) {
return eval(EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList())); Expression? r = EvalFunction(eval_item1_symbol1, l.Skip(1));
if (r is not null) { return r; }
} }
if (list.expressions[0].GetType() == typeof(Procedure)) { if (eval_Item1 is Procedure eval_item1_procedure) {
Procedure procedure = (Procedure) list.expressions[0]; return eval_item1_procedure.Call(this, l.Skip(1).Select(x => x).ToList());
return eval(procedure.Call(this, list.expressions.Skip(1).ToList()));
} }
var l = new List(list.expressions.Select(x => eval(x)).ToList()); throw new ApplicationException($"Not handled case (type = {eval_Item1.GetType()}) '{cons}'");
if (l.expressions[0].GetType() == typeof(Procedure)) {
return eval(l);
}
return l;
} }
throw new ApplicationException($"Not handled case '{expression}'"); throw new ApplicationException($"Not handled case '{expression}'");
} }

View file

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;

View file

@ -1,14 +1,43 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.SmartPlaylist { namespace Jellyfin.Plugin.SmartPlaylist {
public class PluginConfiguration : BasePluginConfiguration { public class PluginConfiguration : BasePluginConfiguration {
public PluginConfiguration( public PluginConfiguration() {
) { InitialProgram = """
(begin
(define lower
(lambda (s)
(invoke s "ToLower" nil)))
(define is-genre
(lambda (g g-list)
(any
(lambda (x)
(invoke (lower x) "Contains" (list (lower g))))
g-list)))
(define is-genre-exact
(lambda (g g-list)
(find g g-list)))
(define genre-list
(lambda nil
(let
(_g (getitems item "Genres"))
(if (null _g)
nil
(car _g)))))
(define is-favorite
(lambda nil
(invoke item "IsFavoriteOrLiked" (list user))))
(define is-type
(lambda (x)
(and
(haskeys item "GetClientTypeName")
(invoke (invoke item "GetClientTypeName" nil) "Equals" (list x)))))
(define name-contains
(lambda (x)
(invoke (lower (car (getitems item "Name"))) "Contains" (list (lower x)))))
(define is-favourite is-favorite))
""";
} }
public string InitialProgram { get; set; }
} }
} }

View file

@ -1,3 +1,4 @@
using System.Globalization;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -5,28 +6,16 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object; using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean; using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
@ -93,7 +82,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
} }
} }
private SmartPlaylistId CreateNewPlaylist(string name, UserId userId) { private PlaylistId CreateNewPlaylist(string name, UserId userId) {
_logger.LogDebug("Creating playlist '{0}'", name); _logger.LogDebug("Creating playlist '{0}'", name);
var req = new PlaylistCreationRequest { var req = new PlaylistCreationRequest {
Name = name, Name = name,
@ -106,20 +95,42 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
} }
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) { private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
List<Guid> results = new List<Guid>(); List<BaseItem> results = new List<BaseItem>();
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
Executor executor = new Executor(new DefaultEnvironment()); Executor executor = new Executor(new DefaultEnvironment());
executor.environment.Set("user", new Lisp_Object(user)); executor.environment.Set("user", Lisp_Object.FromBase(user));
if (Plugin.Instance is not null) {
executor.eval(Plugin.Instance.Configuration.InitialProgram);
} else {
throw new ApplicationException("Plugin Instance is not yet initialized");
}
foreach (var i in items) { foreach (var i in items) {
executor.environment.Set("item", new Lisp_Object(i)); executor.environment.Set("item", Lisp_Object.FromBase(i));
var r = executor.eval(expression); var r = executor.eval(expression);
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString()); _logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
if ((r is not Lisp_Boolean r_bool) || (r_bool.value)) { if ((r is not Lisp_Boolean r_bool) || (r_bool.Value())) {
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name); _logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
results.Add(i.Id); results.Add(i);
} }
} }
return results; executor = new Executor(new DefaultEnvironment());
executor.environment.Set("user", Lisp_Object.FromBase(user));
executor.environment.Set("items", Lisp_Object.FromBase(results));
results = new List<BaseItem>();
var sort_result = executor.eval(smartPlaylist.SortProgram);
if (sort_result is Cons sorted_items) {
foreach (var i in sorted_items.ToList()) {
if (i is Lisp_Object iObject && iObject.Value() is BaseItem iBaseItem) {
results.Add(iBaseItem);
continue;
}
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
}
} else if (sort_result == Lisp_Boolean.FALSE) {
} else {
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
}
return results.Select(x => x.Id);
} }
private IEnumerable<BaseItem> GetAllUserMedia(User user) { private IEnumerable<BaseItem> GetAllUserMedia(User user) {
@ -157,6 +168,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
_store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one. _store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one.
await _store.SaveSmartPlaylistAsync(dto); await _store.SaveSmartPlaylistAsync(dto);
} }
var i = 0;
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) { foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
User? user = _userManager.GetUserById(playlistLink.UserId); User? user = _userManager.GetUserById(playlistLink.UserId);
if (user == null) { if (user == null) {
@ -166,6 +178,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First(); var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First();
await ClearPlaylist(playlist); await ClearPlaylist(playlist);
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId); await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
i += 1;
progress.Report(100 * ((double)i)/dto.Playlists.Count());
} }
} }
} }
@ -176,7 +190,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
throw new ArgumentException(""); throw new ArgumentException("");
} }
var existingItems = playlist_new.GetManageableItems().ToList(); var existingItems = playlist_new.GetManageableItems().ToList();
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id)); await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
} }
} }
} }

View file

@ -41,19 +41,22 @@ namespace Jellyfin.Plugin.SmartPlaylist {
[Serializable] [Serializable]
public class SmartPlaylistDto : ISerializable { public class SmartPlaylistDto : ISerializable {
private static string DEFAULT_PROGRAM = "(begin (invoke item 'IsFavoriteOrLiked' (user)))"; private static string DEFAULT_PROGRAM = "(begin (invoke item \"IsFavoriteOrLiked\" (list user)))";
private static string DEFAULT_SORT_PROGRAM = "(begin items)";
public SmartPlaylistId Id { get; set; } public SmartPlaylistId Id { get; set; }
public SmartPlaylistLinkDto[] Playlists { get; set; } public SmartPlaylistLinkDto[] Playlists { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Program { get; set; } public string Program { get; set; }
public string SortProgram { get; set; }
public string? Filename { get; set; } public string? Filename { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public SmartPlaylistDto() { public SmartPlaylistDto() {
Id = Guid.NewGuid(); Id = "";
Playlists = []; Playlists = [];
Name = Id.ToString(); Name = Id.ToString();
Program = DEFAULT_PROGRAM; Program = DEFAULT_PROGRAM;
SortProgram = DEFAULT_SORT_PROGRAM;
Filename = null; Filename = null;
Enabled = true; Enabled = true;
} }
@ -62,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
if (info.GetValue("Id", typeof(SmartPlaylistId)) is SmartPlaylistId _Id) { if (info.GetValue("Id", typeof(SmartPlaylistId)) is SmartPlaylistId _Id) {
Id = _Id; Id = _Id;
} else { } else {
Id = Guid.NewGuid(); Id = "";
} }
if (info.GetValue("Playlists", typeof(SmartPlaylistLinkDto[])) is SmartPlaylistLinkDto[] _Playlists) { if (info.GetValue("Playlists", typeof(SmartPlaylistLinkDto[])) is SmartPlaylistLinkDto[] _Playlists) {
Playlists = _Playlists; Playlists = _Playlists;
@ -79,6 +82,11 @@ namespace Jellyfin.Plugin.SmartPlaylist {
} else { } else {
Program = DEFAULT_PROGRAM; Program = DEFAULT_PROGRAM;
} }
if (info.GetValue("SortProgram", typeof(string)) is string _SortProgram) {
SortProgram = _SortProgram;
} else {
SortProgram = DEFAULT_SORT_PROGRAM;
}
if (info.GetValue("Filename", typeof(string)) is string _Filename) { if (info.GetValue("Filename", typeof(string)) is string _Filename) {
Filename = _Filename; Filename = _Filename;
} else { } else {

View file

@ -16,13 +16,21 @@ namespace Jellyfin.Plugin.SmartPlaylist {
} }
public string StoragePath { get; } public string StoragePath { get; }
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) { public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
return Path.Combine(StoragePath, $"{smartPlaylistId}.json"); return Path.Combine(StoragePath, $"{smartPlaylistId}.yaml");
} }
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) { public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories).First(); return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yaml", SearchOption.AllDirectories).Concat(
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yml", SearchOption.AllDirectories)
).Concat(
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories)
).First();
} }
public string[] FindAllSmartPlaylistFilePaths() { public string[] FindAllSmartPlaylistFilePaths() {
return Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories); return Directory.GetFiles(StoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
Directory.GetFiles(StoragePath, "*.yml", SearchOption.AllDirectories)
).Concat(
Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories)
).ToArray();
} }
} }
} }

View file

@ -1,4 +1,4 @@
using System.Text.Json; using YamlDotNet.Serialization;
namespace Jellyfin.Plugin.SmartPlaylist { namespace Jellyfin.Plugin.SmartPlaylist {
public interface IStore { public interface IStore {
@ -14,12 +14,24 @@ namespace Jellyfin.Plugin.SmartPlaylist {
_fileSystem = fileSystem; _fileSystem = fileSystem;
} }
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) { private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
await using var r = File.OpenRead(filename); var r = await File.ReadAllTextAsync(filename);
var dto = (await JsonSerializer.DeserializeAsync<SmartPlaylistDto>(r).ConfigureAwait(false)); if (r.Equals("")) {
if (dto == null) { r = "{}";
}
var dto = new DeserializerBuilder().Build().Deserialize<SmartPlaylistDto>(r);
if (dto == null)
{
throw new ApplicationException(""); throw new ApplicationException("");
} }
if (dto.Id != Path.GetFileNameWithoutExtension(filename)) {
dto.Id = Path.GetFileNameWithoutExtension(filename);
}
if (dto.Name != Path.GetFileNameWithoutExtension(filename)) {
dto.Name = Path.GetFileNameWithoutExtension(filename);
}
if (dto.Filename != filename) {
dto.Filename = filename; dto.Filename = filename;
}
return dto; return dto;
} }
public async Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) { public async Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
@ -33,8 +45,8 @@ namespace Jellyfin.Plugin.SmartPlaylist {
} }
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) { public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id); string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
await using var w = File.Create(filename); var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false); await File.WriteAllTextAsync(filename, text);
} }
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) { private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
try { try {

View file

@ -2,4 +2,4 @@ global using System;
global using UserId = System.Guid; global using UserId = System.Guid;
global using PlaylistId = System.Guid; global using PlaylistId = System.Guid;
global using SmartPlaylistId = System.Guid; global using SmartPlaylistId = string;

View file

@ -1,7 +1,7 @@
name: Smart Playlist name: Smart Playlist
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
version: 0.1.1.0 version: 0.2.2.0
targetAbi: 10.10.0.0 targetAbi: 10.10.2.0
framework: net8.0 framework: net8.0
owner: redxef owner: redxef
overview: Smart playlists with Lisp filter engine. overview: Smart playlists with Lisp filter engine.
@ -12,5 +12,32 @@ description: |
category: "General" category: "General"
artifacts: artifacts:
- jellyfin-smart-playlist.dll - jellyfin-smart-playlist.dll
- YamlDotNet.dll
changelog: | changelog: |
## v0.2.2.0
- Update Jellyfin to v 10.10.2
## v0.2.1.0
- Make default program configuration a textarea in the settings page
- Add convinience definitions: `is-type`, `name-contains`
- Update YamlDotNet to v 16.2.0
**Fixes**:
- The default program was malformed, a closing bracket was at the wrong position
- The `haskeys` function could only be called on Objects
## v0.2.0.0
- Switch to yaml loading, old json files are still accepted
- Rework lisp interpreter to be more conventional
- Use arbitrary strings as ids for playlists
- Add configuration page with some default definitions for
the filter expressions.
**Breaking Changes:**
- The lisp interpreter will now only detect strings in double quotes (`"`).
- The interpreter will also not allow specifying lists without quoting them.
`(1 2 3)` ... used to work but will no longer, replace by either specifying
the list as `(list 1 2 3)` or `(quote (1 2 3))`.
## v0.1.1.0
- Initial Alpha release. - Initial Alpha release.

View file

@ -2,35 +2,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Template</title> <title>SmartPlaylist</title>
</head> </head>
<body> <body>
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox"> <div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<form id="TemplateConfigForm"> <form id="SmartPlaylistConfigForm">
<div class="selectContainer">
<label class="selectLabel" for="Options">Several Options</label>
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
<option id="optOneOption" value="OneOption">One Option</option>
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
</select>
</div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label> <label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" /> <div class="fieldDescription">A program which can set up the environment</div>
<div class="fieldDescription">A Description</div> <textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" cols="120"></textarea>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
<span>A Checkbox</span>
</label>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="AString">A String</label>
<input id="AString" name="AString" type="text" is="emby-input" />
<div class="fieldDescription">Another Description</div>
</div> </div>
<div> <div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button"> <button is="emby-button" type="submit" class="raised button-submit block emby-button">
@ -40,32 +22,31 @@
</form> </form>
</div> </div>
</div> </div>
<style>
.smartplaylist-monospace {
font-family: monospace;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
var TemplateConfig = { var SmartPlaylistConfig = {
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df' pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
}; };
document.querySelector('#TemplateConfigPage') document.querySelector('#SmartPlaylistConfigPage')
.addEventListener('pageshow', function() { .addEventListener('pageshow', function() {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
document.querySelector('#Options').value = config.Options; document.querySelector('#InitialProgram').value = config.InitialProgram;
document.querySelector('#AnInteger').value = config.AnInteger;
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
document.querySelector('#AString').value = config.AString;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
}); });
document.querySelector('#TemplateConfigForm') document.querySelector('#SmartPlaylistConfigForm')
.addEventListener('submit', function(e) { .addEventListener('submit', function(e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
config.Options = document.querySelector('#Options').value; config.InitialProgram = document.querySelector('#InitialProgram').value;
config.AnInteger = document.querySelector('#AnInteger').value; ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
config.AString = document.querySelector('#AString').value;
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result); Dashboard.processPluginConfigurationUpdateResult(result);
}); });
}); });

View file

@ -2,15 +2,21 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>jellyfin_smart_playlist</RootNamespace> <RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.1.1.0</Version> <Version>0.2.2.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" /> <PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
<PackageReference Include="Jellyfin.Model" Version="10.10.0" /> <PackageReference Include="Jellyfin.Model" Version="10.10.3" />
<PackageReference Include="YamlDotNet" Version="16.2.0" />
</ItemGroup>
<ItemGroup>
<None Remove="configPage.html"/>
<EmbeddedResource Include="configPage.html"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

121
README.md
View file

@ -2,41 +2,57 @@
Smart playlists with Lisp filter engine. Smart playlists with Lisp filter engine.
This readme contains instructions for the most recent changes in
the development branch (`main`). To view the file appropriate
for your version select the tag corresponding to your version.
The latest version is [v0.2.2.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.2.2.0).
## How to use ## How to use
After [installing](#installation) the plugin and restarting Jellyfin After [installing](#installation) the plugin and restarting Jellyfin
create a file in `config/data/smartplaylists` like this: create a empty file in `config/data/smartplaylists` like this, maybe
you want to generate a playlist of your favourite rock songs:
``` ```
$ echo '{}' > config/data/smartplaylists/a.json $ touch config/data/smartplaylists/Rock.yaml
``` ```
Afterwards run the Task `(re)generate Smart Playlists`, this will rename Afterwards run the Task `(re)generate Smart Playlists`, this will rename
the `json` file and populate it with some default values. You can now the `yaml` file and populate it with some default values. You can now
adjust the file to your liking. [Go here](examples/index.md) to see more adjust the file to your liking. [Go here](examples.md) to see more
examples. examples.
Example file ```yaml
```json Id: Rock
{ Playlists:
"Id": "a1d02dee-f1da-4472-bee3-f568c15c8360", - PlaylistId: 24f12e1e-3278-d6d6-0ca4-066e93296c95
"Playlists": [ UserId: 6eec632a-ff0d-4d09-aad0-bf9e90b14bc6
{ Name: Rock
"PlaylistId": "24f12e1e-3278-d6d6-0ca4-066e93296c95", Program: (begin (invoke item "IsFavoriteOrLiked" (user)))
"UserId": "6eec632a-ff0d-4d09-aad0-bf9e90b14bc6" SortProgram: (begin items)
} Filename: /config/data/smartplaylists/Rock.yaml
], Enabled: true
"Name": "a1d02dee-f1da-4472-bee3-f568c15c8360",
"Program": "(begin (invoke item 'IsFavoriteOrLiked' (user)))",
"Filename": "/config/data/smartplaylists/a1d02dee-f1da-4472-bee3-f568c15c8360.json",
"Enabled": true
}
``` ```
This is the default configuration and will always match all your
favorite songs (and songs which are in favourited albums).
To change the filter you can append a `|` (pipe) to the Program
line and write multiline filters like this:
```yaml
Porgram: |
(begin
(invoke item "IsFavoriteOrLiked" (list user)))
```
This is equivalent to the above example (not counting the other
fields obviously).
### Id ### Id
Arbitrary Id assigned to this playlist, can usually be left alone. Arbitrary Id assigned to this playlist, can usually be left alone.
The filename is derived from this.
### Playlists ### Playlists
@ -63,17 +79,60 @@ The user associated with this playlist.
The name of the generated playlists, this is just a default value. The name of the generated playlists, this is just a default value.
If the user changes the name of their playlist the plugin will If the user changes the name of their playlist the plugin will
work as usual still work and remember the correct playlist.
### Program ### Program
A lisp program to decide on a per item basis if it should be included in A lisp program to decide on a per item basis if it should be included in
the playlist, return `nil` to not include items, return `t` to include the playlist, return `nil` to not include items, return any other value
them. to include them. Global variables `user` and `item` are predefined
and contain a [User](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Entities/User.cs) and
[BaseItem](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Controller/Entities/BaseItem.cs)
respectively.
**!!! The filter expression will include all items matching, if you do
not specify the kind of item to include/exclude all of them will be
added. Should you allow a playlist to be included all of it's items
will be added to the generated playlist !!!**
It's best to be explicit and always specify the item kinds you want to
include: `(and (or (is-type "MusicAlbum") (is-type "Audio")) . rest of filter)`.
The configuration page defines some useful functions to make it easier
to create filters. The above filter for liked items could be simplified
to: `(is-favourite)`.
### SortProgram
This works exactly like [Program](#program), but the input is the
user and a list of items (`items`) matched by [Program](#program).
The default is `(begin items)`, which doesn't sort at all. To sort
the items by name you could use the following program:
```lisp
(qsort
(lambda
(a b)
(string>
(car (getitems a "Name"))
(car (getitems b "Name"))))
items)
```
#### Available definitions
- **lower**: lowercases a string (`(eq (lower "SomeString") "somestring")`)
- **is-genre**: check if the item is of this genre, partial matches
allowed, the example filter would match the genre "Nu-Metal" (`(is-genre "metal" (genre-list))`)
- **is-genre-exact**: the same as `is-genre`, but does not match paritally
- **is-favorite**: matches a favorite item (`(is-favorite)`)
- **is-type**: matches the type of item look at
[BaseItemKind.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/BaseItemKind.cs)
for a list of items. The plugin has enabled support for `Audio, MusicAlbum, Playlist` (`(is-type "Audio")`)
### Filename ### Filename
The path to this file. The path to this file, only used internally and updated by the program.
### Enabled ### Enabled
@ -89,3 +148,17 @@ to Jellyfin:
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
the field labeled `Repository URL`, give the plugin a descriptive name the field labeled `Repository URL`, give the plugin a descriptive name
too. too.
## Releasing a new version
1. Write the changelog: `git log --oneline $prev_version..`
2. Update the following files to include up-to-date version numbers
and changelogs, if applicable:
- `README.md`
- `Jellyfin.Plugin.SmartPlaylist/build.yaml`
- `Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj`
Don't forget to also bump the ABI version of Jellyfin.
3. Push the changes
4. Create a new release with the changelog, mark as pre-release if
applicable.
5. Done! The build pipeline will do the rest.

View file

@ -1,7 +1,5 @@
using Xunit; using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment; using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object;
using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
@ -16,45 +14,42 @@ namespace Tests
} }
public int i { get => _i; } public int i { get => _i; }
public bool b { get => _b; } public bool b { get => _b; }
public int I() {
return _i;
}
} }
public class Test { public class Test {
[Fact] [Fact]
public static void TestTokenizer() { public static void TestTokenizer() {
StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1 >= ++ !=)"); StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1 >= ++ !=)");
Assert.Equal(sts.Get().value, "("); Assert.Equal("(", sts.Get().value);
Assert.Equal(sts.Get().value, "\""); Assert.Equal("\"", sts.Get().value);
Assert.Equal(sts.Get().value, "some"); Assert.Equal("some", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "literal"); Assert.Equal("literal", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "string"); Assert.Equal("string", sts.Get().value);
Assert.Equal(sts.Get().value, "\""); Assert.Equal("\"", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "def"); Assert.Equal("def", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "ghj"); Assert.Equal("ghj", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "+"); Assert.Equal("+100", sts.Get().value);
Assert.Equal(sts.Get().value, "100"); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal("-+300", sts.Get().value);
Assert.Equal(sts.Get().value, "-"); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "+"); Assert.Equal("1", sts.Get().value);
Assert.Equal(sts.Get().value, "300"); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(">=", sts.Get().value);
Assert.Equal(sts.Get().value, "1"); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal("++", sts.Get().value);
Assert.Equal(sts.Get().value, ">"); Assert.Equal(" ", sts.Get().value);
Assert.Equal(sts.Get().value, "="); Assert.Equal("!=", sts.Get().value);
Assert.Equal(sts.Get().value, " "); Assert.Equal(")", sts.Get().value);
Assert.Equal(sts.Get().value, "+");
Assert.Equal(sts.Get().value, "+");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "!");
Assert.Equal(sts.Get().value, "=");
Assert.Equal(sts.Get().value, ")");
sts.Commit(); sts.Commit();
Assert.Equal(sts.Available(), 0); Assert.Equal(0, sts.Available());
} }
[Fact] [Fact]
@ -68,116 +63,171 @@ namespace Tests
sts = StringTokenStream.generate(program); sts = StringTokenStream.generate(program);
p = new Parser(sts); p = new Parser(sts);
Assert.Equal(program, string.Format("{0}", p.parse())); Assert.Equal(program, string.Format("{0}", p.parse()));
//program = "(* 2.4 2)";
//sts = StringTokenStream.generate(program);
//p = new Parser(sts);
//Assert.Equal(program, p.parse().ToString());
} }
[Fact] [Fact]
public static void TestFunctions() { public static void TestFunctions() {
IList<Tuple<string, Expression>> cases = new List<Tuple<string, Expression>>(); Executor e = new Executor();
Expression e = new Executor().eval("(+ 10 20)"); Assert.Equal("(1 2 3)", e.eval("(quote (1 2 3))").ToString());
Assert.Equal(((Integer) e).value, 30); Assert.Equal("abc", e.eval("(quote abc)").ToString());
e = new Executor().eval("(> 1 2)"); Assert.Equal("t", e.eval("(atom 1)").ToString());
Assert.Equal(((Lisp_Boolean) e).value, false); Assert.Equal("nil", e.eval("(atom (quote (1 2 3)))").ToString());
e = new Executor().eval("(if (> 1 2) 3 4)"); Assert.Equal("t", e.eval("(eq 2 2)").ToString());
Assert.Equal(((Integer) e).value, 4); Assert.Equal("nil", e.eval("(eq 2 3)").ToString());
e = new Executor().eval("(begin (define x 1) x)"); Assert.Equal("1", e.eval("(car (quote (1 2 3)))").ToString());
Assert.Equal(((Integer) e).value, 1); Assert.Equal("(2 3)", e.eval("(cdr (quote (1 2 3)))").ToString());
e = new Executor().eval("(apply + (1 2))"); Assert.Equal("(1 . 2)", e.eval("(cons 1 2)").ToString());
Assert.Equal(((Integer) e).value, 3); Assert.Equal("(1 2)", e.eval("(cons 1 (cons 2 nil))").ToString());
Assert.Equal("(1)", e.eval("(cons 1 nil)").ToString());
Assert.Equal("(1)", e.eval("(cons 1 ())").ToString());
e = new Executor().eval("(car (10 20 30))"); Assert.Equal("\"Case 2\"", e.eval("""
Assert.Equal(((Integer) e).value, 10); (cond
((eq 1 2) "Case 1")
((eq 2 2) "Case 2"))
""").ToString());
Assert.Equal("\"Case 1\"", e.eval("""
(cond
((eq 2 2) "Case 1")
((eq 2 2) "Case 2"))
""").ToString());
Assert.Equal("nil", e.eval("""
(cond
((eq 1 2) "Case 1")
((eq 3 2) "Case 2"))
""").ToString());
e = new Executor().eval("(cdr (10 20 30))"); Assert.Equal("t", e.eval("((lambda (a) (eq a a)) 2)").ToString());
Assert.Equal(string.Format("{0}", e), "(20 30)");
e = new Executor().eval("(cons 1 3)"); Assert.Equal("t", e.eval("(begin (car (quote (nil 1))) t)").ToString());
Assert.Equal(string.Format("{0}", e), "(1 3)"); Assert.Equal("(1)", e.eval("(begin t (cdr (quote (nil 1))))").ToString());
e = new Executor().eval("(cons 1 (2 3))"); Assert.Equal("t", e.eval("""
Assert.Equal(string.Format("{0}", e), "(1 2 3)"); (begin
(define abc 10)
(eq abc abc))
""").ToString());
e = new Executor().eval("(length (cons 1 (2 3)))"); Assert.Equal("1", e.eval("""
Assert.Equal(string.Format("{0}", e), "3"); (begin
(define if (lambda (condition a b) (
cond (condition a) (t b))))
(if (> 2 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
""").ToString());
Assert.Equal("(3 4)", e.eval("""
(begin
(define if (lambda (condition a b) (
cond (condition a) (t b))))
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
""").ToString());
e = new Executor().eval("(>= 2 2)"); }
Assert.Equal(string.Format("{0}", e), "t");
e = new Executor().eval("(> 2 2))"); [Fact]
Assert.Equal(string.Format("{0}", e), "nil"); public static void TestFunctionsAdvanced() {
Executor e = new Executor();
Assert.Equal("2", e.eval("""
((lambda (b) b) (car (quote (2 3))))
""").ToString());
e = new Executor().eval("(and 2 3 4)"); Assert.Equal("(3 4 5)", e.eval("""
Assert.Equal("4", e.ToString()); ((lambda (x y . z) z) 1 2 3 4 5)
""").ToString());
e = new Executor().eval("(and 2 nil 4)"); Assert.Equal("3", e.eval("""
Assert.Equal("nil", e.ToString()); (begin
(define if (lambda (condition a b) (cond (condition a) (t b))))
(if (< 1 2) 3 2))
""").ToString());
e = new Executor().eval("(or 2 nil 4)"); Assert.Equal("2", e.eval("""
Assert.Equal("2", e.ToString()); (begin
(define if (lambda (condition a b) (cond (condition a) (t b))))
e = new Executor().eval("(or nil 4)"); (if (> 1 2) 3 2))
Assert.Equal("4", e.ToString()); """).ToString());
Assert.Equal("1", e.eval("""
e = new Executor().eval("(= (1 2) (1 2))"); (begin
Assert.Equal(e.ToString(), "t"); (define if (lambda* (condition a b) (
cond ((eval condition) (eval a)) (t (eval b)))))
e = new Executor().eval("(= (1 2 3) (1 2))"); (if (> 2 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
Assert.Equal(e.ToString(), "nil"); """).ToString());
Assert.Equal("(3 4)", e.eval("""
(begin
(define if (lambda* (condition a b) (
cond ((eval condition) (eval a)) (t (eval b)))))
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
""").ToString());
Assert.Equal("120", e.eval("""
(begin
(define f (lambda (n) (cond ((<= n 1) 1) (t (* n (f (- n 1)))))))
(f 5))
""").ToString());
Assert.Equal("120", e.eval("""
(begin
(define if (lambda* (condition a b) (
cond ((eval condition) (eval a)) (t (eval b)))))
(define f (lambda (n) (if (<= n 1) 1 (* n (f (- n 1))))))
(f 5))
""").ToString());
Assert.Equal("(1 2 3 4 5)", e.eval("""
(begin
(define if (lambda* (condition a b) (
cond ((eval condition) (eval a)) (t (eval b)))))
((lambda (. args) args) 1 2 3 4 5))
""").ToString());
Assert.Equal("t", e.eval("""
(begin
(define null (lambda* (x) (
cond ((eval x) nil) (t t))))
(null nil))
""").ToString());
Assert.Equal("nil", e.eval("""
(begin
(define null (lambda* (x) (cond ((eval x) nil) (t t))))
(null (quote (1 2))))
""").ToString());
} }
[Fact] [Fact]
public static void ObjectTest() { public static void ObjectTest() {
Executor e = new Executor(); Executor e = new Executor();
Expression r; Expression r;
e.environment.Set("o", new Lisp_Object(new O(5, false))); e.environment.Set("o", Lisp_Object.FromBase(new O(5, false)));
r = e.eval("(haskeys o 'i' 'b')"); r = e.eval("""(haskeys o "i" "b")""");
Assert.Equal(((Lisp_Boolean)r).value, true); Assert.True(((Lisp_Boolean)r).Value());
r = e.eval("(getitems o 'i' 'b')"); r = e.eval("""(getitems o "i" "b")""");
Assert.Equal(string.Format("{0}", r), "(5 nil)"); Assert.Equal("(5 nil)", string.Format("{0}", r));
} r = e.eval("""(invoke o "I" nil)""");
Assert.Equal("5", string.Format("{0}", r));
[Fact]
public static void ScalarTest() {
Executor e = new Executor();
Expression r;
r = e.eval("(* 2 2)");
Assert.Equal("4", r.ToString());
}
[Fact]
public static void ProcedureTest() {
Executor e = new Executor();
Expression r;
r = e.eval("((lambda (a) (* a a)) 2)");
Assert.Equal(string.Format("{0}", r), "4");
r = e.eval("(begin (define mull (lambda (a) (* a a))) (mull 3))");
Assert.Equal(string.Format("{0}", r), "9");
r = e.eval("(begin (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) (fact 10))");
Assert.Equal(string.Format("{0}", r), "3628800");
r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 3 (1 2 3 4)))");
Assert.Equal(string.Format("{0}", r), "3");
e = new Executor();
r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 0 (1 2 3 4)))");
Assert.Equal(string.Format("{0}", r), "nil");
} }
[Fact] [Fact]
public static void DefaultEnvironmentTest() { public static void DefaultEnvironmentTest() {
Executor e = new Executor(new DefaultEnvironment()); Executor e = new Executor(new DefaultEnvironment());
Assert.Equal("nil", e.eval("(find 0 (1 2 3 4))").ToString()); Assert.Equal("1", e.eval("(if nil 0 1)").ToString());
Assert.Equal("3", e.eval("(find 3 (1 2 3 4))").ToString()); Assert.Equal("0", e.eval("(if t 0 1)").ToString());
Assert.Equal("5", e.eval("(if t (if t 5 nil) nil)").ToString());
Assert.Equal("nil", e.eval("(if t (if nil 5 nil) nil)").ToString());
Assert.Equal("(1 2 3)", e.eval("(list 1 2 3)").ToString());
Assert.Equal("3", e.eval("(find 3 (list 1 2 3 4))").ToString());
Assert.Equal("nil", e.eval("(find 0 (list 1 2 3 4))").ToString());
Assert.Equal("(2 4 6)", e.eval("(map (lambda (x) (* x 2)) (quote (1 2 3)))").ToString());
Assert.Equal("nil", e.eval("(and 1 2 3 nil)").ToString());
Assert.Equal("t", e.eval("(and t t t t)").ToString());
Assert.Equal("t", e.eval("(or nil nil t nil)").ToString());
Assert.Equal("nil", e.eval("(or nil nil nil nil)").ToString());
Assert.Equal("t", e.eval("(any (lambda (x) (= x 2)) (list 1 2 3 4 5 6))").ToString());
Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) (list 1 3 4 5 6))").ToString());
Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) nil)").ToString());
Assert.Equal("t", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 5))").ToString());
Assert.Equal("nil", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 4 5))").ToString());
Assert.Equal("nil", e.eval("(all (lambda (x) (= x 2)) nil)").ToString());
Assert.Equal("10", e.eval("(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))").ToString());
Assert.Equal("(2 3 4 5 6 7)", e.eval("(append (list 2 3 4) (list 5 6 7))").ToString());
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort (lambda (a b) (> a b)) (list 5 4 7 3 2 6 1))").ToString());
} }
} }
} }

27
examples.md Normal file
View file

@ -0,0 +1,27 @@
# Examples
- `Favourite Pop`: A playlist
containing all favourite items of the genre pop.
```
Id: Favourite Pop
Name: Favourite Pop
Program: |
(and (is-type "Audio") (is-favorite) (is-genre "pop" (genre-list)))
```
- `Electro Swing`: A playlist containing all items
which have a genre that contains "electro" and a
genre that contains "swing". It will only include
albums and single tracks.
```
Id: Electro Swing
Name: Electro Swing
Program: |
(let
(g (genre-list))
(and
(or
(is-type "Audio")
(is-type "MusicAlbum"))
(is-genre "electro" g)
(is-genre "swing" g)))
```

View file

@ -1,5 +0,0 @@
{
"Id": "a1d02dee-f1da-4472-bee3-f568c15c8360",
"Name": "Favourite Pop",
"Program": "(and (invoke item 'IsFavoriteOrLiked' (user)) (find 'Pop' (car (getitems item 'Genres'))))"
}

View file

@ -1,4 +0,0 @@
# Examples
* [Favourite Pop](a1d02dee-f1da-4472-bee3-f568c15c8360.json): A Playlist
containing all favourite items of the genre pop.