Compare commits

..

No commits in common. "main" and "yaml" have entirely different histories.
main ... yaml

22 changed files with 839 additions and 1838 deletions

3
.gitignore vendored
View file

@ -1,5 +1,2 @@
**/obj/
**/bin/
artifacts/
smart-playlist/
manifest.json

View file

@ -1,7 +1,269 @@
using System.Diagnostics;
using Jellyfin.Plugin.SmartPlaylist.Lisp;
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 {
private StringTokenStream _sts;
public Parser(StringTokenStream tokens) {
@ -18,16 +280,17 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return parse_grouping(gt, gt.closing_value);
case AtomToken at:
return parse_atom(at);
case OperatorToken ot:
return parse_operator(ot);
case SpaceToken sp:
return parse();
}
return parse();
}
Expression parse_string(GroupingToken start, GroupingToken? end) {
Debug.Assert(end != null);
Expression parse_string(GroupingToken start, GroupingToken end) {
Debug.Assert(start.value == end.value);
Debug.Assert("\"".Contains(start.value));
Debug.Assert("'\"".Contains(start.value));
string r = "";
while (_sts.Available() > 0) {
Token<string> t = _sts.Get();
@ -40,36 +303,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new String(r);
}
Expression parse_comment(GroupingToken start, GroupingToken? end) {
Debug.Assert(end != null);
Debug.Assert(";".Contains(start.value));
while (_sts.Available() > 0) {
Token<string> t = _sts.Get();
if (t.value == end.value) {
break;
}
}
_sts.Commit();
return parse();
}
Expression parse_quote(GroupingToken start, GroupingToken? end) {
Debug.Assert(end == null);
Debug.Assert("'".Contains(start.value));
return Cons.FromList(new Expression[]{ new Symbol("quote"), parse()});
}
Expression parse_grouping(GroupingToken start, GroupingToken? end) {
if ("\"".Contains(start.value)) {
Expression parse_grouping(GroupingToken start, GroupingToken end) {
if ("'\"".Contains(start.value)) {
return parse_string(start, end);
}
if ("'".Contains(start.value)) {
return parse_quote(start, end);
}
if (";".Contains(start.value)) {
return parse_comment(start, end);
}
Debug.Assert(end != null);
IList<Expression> expressions = new List<Expression>();
while (_sts.Available() > 0) {
Token<string> t = _sts.Get();
@ -77,17 +314,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
_sts.Commit();
break;
}
if (t is SpaceToken) {
// need this here because those tokens can never
// return an expression and trying to parse the last
// expression will not work if its only spaces and a
// closing parentheses.
continue;
}
_sts.Rewind(1);
expressions.Add(parse());
}
return Cons.FromList(expressions);
return new List(expressions);
}
Expression parse_atom(AtomToken at) {
@ -97,13 +327,27 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new Integer(parsed_value);
}
if (at.value.Equals("t")) {
return Boolean.TRUE;
return new Boolean(true);
}
if (at.value.Equals("nil")) {
return Boolean.FALSE;
return new Boolean(false);
}
_sts.Commit();
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,13 +24,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
class SpaceToken : Token<string> {
private SpaceToken(string value) : base(value) {}
private static IToken<string>? take(CharStream program) {
string spaces = " \n";
if (program.Available() == 0) {
return null;
}
var t = program.Get();
if (spaces.Contains(t)) {
return new SpaceToken(t.ToString());
if (program.Get() == ' ') {
return new SpaceToken(" ");
}
return null;
}
@ -43,7 +41,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return null;
}
char t = program.Get();
if ("()\"';".Contains(t)) {
if ("()\"'".Contains(t)) {
return new GroupingToken(t.ToString());
}
return null;
@ -53,10 +51,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new GroupingToken(")");
} else if (_value == ")") {
return null;
} else if (_value == "'") {
return null;
} else if (_value == ";") {
return new GroupingToken("\n");
}
return new GroupingToken(_value);
}
@ -69,7 +63,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
string value = "";
while (program.Available() > 0) {
char t = program.Get();
if (" \n()\"".Contains(t)) {
if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Contains(t)) {
if (value.Equals("")) {
return null;
}
@ -78,10 +72,22 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
}
value += t;
}
if (value.Equals("")) {
return null;
}
}
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 AtomToken(value);
return new OperatorToken(program.Get().ToString());
//char t = program.get();
//if ("+-*/%".Contains(t)) {
// return new OperatorToken(t.ToString());
//}
//return null;
}
}
@ -95,6 +101,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
typeof(SpaceToken),
typeof(GroupingToken),
typeof(AtomToken),
typeof(OperatorToken),
};
protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {}
private static StringTokenStream generate(CharStream program) {
@ -104,7 +111,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
if (prev_avail == program.Available() && prev_avail == 0) {
break;
} else if (prev_avail == program.Available()) {
throw new ApplicationException($"Program is invalid, still available: {program.Available()}");
throw new ApplicationException("Program is invalid");
}
prev_avail = program.Available();
foreach (Type c in _classes) {

View file

@ -1,344 +0,0 @@
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() {
if (Item1 is Symbol SymbolItem1
&& SymbolItem1.Name() == "quote"
&& Item2 is Cons ConsItem2
&& ConsItem2.Item2.Equals(Boolean.FALSE)
) {
return $"'{ConsItem2.Item1}";
}
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,19 +3,63 @@ using System.Reflection;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
using Function = Func<IEnumerable<Expression>, Expression>;
using FunctionLater = Func<Executor, IEnumerable<Expression>, Expression>;
using Function = Func<IList<Expression>, Expression>;
using FunctionLater = Func<Executor, IList<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 V? Get(K k);
public V Get(K k);
public void Set(K k, V v);
public IEnvironment<K, V>? Find(K k);
public IEnvironment<K, V> Parent(bool recursive);
}
public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
public Expression? Get(string k) {
if (TryGetValue(k, out Expression? v)) {
if (TryGetValue(k, out Expression v)) {
return v;
}
return null;
@ -30,71 +74,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
return null;
}
public IEnvironment<string, Expression> Parent(bool recursive) {
return this;
}
}
public class DefaultEnvironment: Environment {
public DefaultEnvironment() {
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)))
"""
);
this["rand"] = e.eval(
"""
(lambda
(. a)
(cond
((null a) (random))
((null (cdr a)) (% (random) (car a)))
(t (+
(car a)
(%
(random)
(-
(car (cdr a))
(car a)))))))
"""
);
this["shuf"] = new Symbol("shuffle");
this["find"] = new Parser("(lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))").parse();
}
}
@ -104,7 +88,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
_super = super;
}
public Expression? Get(string k) {
if (TryGetValue(k, out Expression? v)) {
if (TryGetValue(k, out Expression v)) {
return v;
}
return null;
@ -119,349 +103,272 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
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> {
private static Dictionary<string, Type?> ResolvedTypes = new Dictionary<string, Type?>();
private Random Random;
public Builtins() : base() {
Random = new Random();
this["atom"] = _atom;
this["eq"] = _eq;
this["+"] = _add;
this["-"] = _sub;
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["cdr"] = _cdr;
this["cons"] = _cons;
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["not"] = _not;
this["length"] = _length;
this["haskeys"] = _haskeys;
this["getitems"] = _getitems;
this["invoke"] = _invoke;
this["invoke-generic"] = _invoke_generic;
this["random"] = (x) => new Lisp.Integer(Random.Next());
this["shuffle"] = (x) => {
var newx = ((Lisp.Cons) x.First()).ToList().ToArray();
Random.Shuffle<Expression>(newx);
return Lisp.Cons.FromList(newx);
};
}
private static T _agg<T>(Func<T, T, T> op, IEnumerable<Expression> args) where T : Expression {
T agg = (T) args.First();
private static T _agg<T>(Func<T, T, T> op, IList<T> args) {
T agg = args[0];
foreach (var arg in args.Skip(1)) {
var arg_ = (T) arg;
agg = op(agg, arg_);
agg = op(agg, arg);
}
return agg;
}
private static E _cmp<T, E>(Func<T, T, E> op, IEnumerable<Expression> args) where T : Expression where E : Expression {
return op((T) args.First(), (T) args.Skip(1).First());
private static Expression _add(IList<Expression> args) {
Expression first = args[0];
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) {
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 _eq(IEnumerable<Expression> args) {
return args.First().Equals(args.Skip(1).First()) ? Boolean.TRUE : Boolean.FALSE;
private static Expression _mul(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 Expression _car(IEnumerable<Expression> args) {
return ((Cons)args.First()).Item1;
private static Expression _div(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 Expression _cdr(IEnumerable<Expression> args) {
return ((Cons)args.First()).Item2;
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 Expression _cons(IEnumerable<Expression> args) {
return new Cons(args.First(), args.Skip(1).First());
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 _begin(IEnumerable<Expression> args) {
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();
}
private static Expression _haskeys(IEnumerable<Expression> args) {
Object o = new Object(((IInner) args.First()).Inner());
private static Expression _car(IList<Expression> args) {
return ((List) args.First()).expressions.First();
}
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)) {
String s = (String) e;
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
Compiler.String s = (Compiler.String) e;
PropertyInfo? pi = o.value.GetType().GetProperty(s.value);
if (pi != null) {
continue;
}
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value());
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
if (mi != null) {
continue;
}
FieldInfo? fi = o.Value().GetType().GetField(s.Value());
FieldInfo? fi = o.value.GetType().GetField(s.value);
if (fi != null) {
continue;
}
return Boolean.FALSE;
return new Compiler.Boolean(false);
}
return Boolean.TRUE;
return new Compiler.Boolean(true);
}
private static Expression _getitems(IEnumerable<Expression> args) {
Object o = new Object(((IInner) args.First()).Inner());
private static Expression _getitems(IList<Expression> args) {
Compiler.Object o = (Compiler.Object) args[0];
IList<Expression> r = new List<Expression>();
foreach (var e in args.Skip(1)) {
String s = (String) e;
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
Compiler.String s = (Compiler.String) e;
PropertyInfo? pi = o.value.GetType().GetProperty(s.value);
if (pi != null) {
r.Add(Object.FromBase(pi.GetValue(o.Value())));
r.Add(Compiler.Object.FromBase(pi.GetValue(o.value)));
continue;
}
FieldInfo? fi = o.Value().GetType().GetField(s.Value());
FieldInfo? fi = o.value.GetType().GetField(s.value);
if (fi != null) {
r.Add(Object.FromBase(fi.GetValue(o.Value())));
r.Add(Compiler.Object.FromBase(fi.GetValue(o.value)));
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 Cons.FromList(r);
return new Compiler.List(r);
}
private static Expression _invoke(IEnumerable<Expression> args) {
Object o = new Object(((IInner) args.First()).Inner());
String s = (String) args.Skip(1).First();
IEnumerable<Expression> l;
if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) {
l = new List<Expression>();
} else if (args.Skip(2).First() is Cons lc) {
l = lc.ToList();
} else {
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
}
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();
private static Expression _invoke(IList<Expression> args) {
Compiler.Object o = (Compiler.Object) args[0];
Compiler.String s = (Compiler.String) args[1];
Compiler.List l = (Compiler.List) args[2];
IList<Expression> r = new List<Expression>();
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
if (mi == null) {
throw new ApplicationException($"{o.Value()} has no method {s.Value()}");
throw new ApplicationException($"{o.value} has not method {s.value}");
}
return Object.FromBase(mi.Invoke(o.Value(), l_));
}
private static Type? GetType(string name) {
if (ResolvedTypes.ContainsKey(name)) {
return ResolvedTypes[name];
}
var t = Type.GetType(
name,
(name) => { return AppDomain.CurrentDomain.GetAssemblies().Where(z => (z.FullName != null) && z.FullName.StartsWith(name.FullName)).FirstOrDefault(); },
null,
true
);
ResolvedTypes[name] = t;
return t;
}
private static Expression _invoke_generic(IEnumerable<Expression> args) {
Object o = new Object(((IInner) args.First()).Inner());
String s = (String) args.Skip(1).First();
IEnumerable<Expression> l;
if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) {
l = new List<Expression>();
} else if (args.Skip(2).First() is Cons lc) {
l = lc.ToList();
} else {
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
}
IEnumerable<Type> types;
if (args.Skip(3).First() is Boolean lb_ && lb_ == Boolean.FALSE) {
types = new List<Type>();
} else if (args.Skip(3).First() is Cons lc) {
types = lc.ToList().Select(x => GetType(((String) x).Value())).ToList();
} else {
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(3).First()}");
}
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 no method {s.Value()}");
}
mi = mi.MakeGenericMethod(types.ToArray());
return Object.FromBase(mi.Invoke(o.Value(), l_));
return Compiler.Object.FromBase(mi.Invoke(o.value, (object?[]?) l.Inner()));
}
}
public class BuiltinsLater : Dictionary<string, FunctionLater> {
public BuiltinsLater() : base() {
this["quote"] = _quote;
this["eval"] = _eval;
this["cond"] = _cond;
this["if"] = _if;
this["define"] = _define;
this["let"] = _let;
this["let*"] = _let_star;
this["lambda"] = _lambda;
this["lambda*"] = _lambda_star;
this["apply"] = _apply;
this["and"] = (e, x) => {
Expression? r = null;
foreach (var xi in x) {
r = e.eval(xi);
if (r == Boolean.FALSE) {
return r;
}
}
if (r is null) {
return Boolean.FALSE;
}
return r;
};
this["or"] = (e, x) => {
foreach (var xi in x) {
var r = e.eval(xi);
if (r != Boolean.FALSE) {
return r;
}
}
return Boolean.FALSE;
};
this["and"] = _and;
this["or"] = _or;
}
private static Expression _quote(Executor e, IEnumerable<Expression> args) {
return args.First();
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 _eval(Executor e, IEnumerable<Expression> args) {
return e.eval(e.eval(args.First()));
private static Expression _define(Executor e, IList<Expression> args) {
var refname = ((Symbol) args[0]).name;
e.environment.Set(refname, e.eval(args[1]));
return new Compiler.Boolean(false); // NOOP
}
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());
}
} else {
throw new ApplicationException($"Incorrect arguments to cond, expected list: {args}");
}
private static Expression _lambda(Executor e, IList<Expression> args) {
return new Procedure((Compiler.List) args[0], args[1]);
}
private static Expression _apply(Executor e, IList<Expression> args) {
if (args[0].GetType() != typeof(Symbol)) {
throw new ApplicationException();
}
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());
if (args[1].GetType() != typeof(List)) {
throw new ApplicationException();
}
return e.eval(args.Skip(1).First());
Symbol arg0 = (Compiler.Symbol) args[0];
Compiler.List other_args = (Compiler.List) args[1];
return e.EvalFunction(arg0, other_args.expressions);
}
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));
private static Expression _and(Executor e, IList<Expression> args) {
Expression result = new Compiler.Boolean(false);
foreach (var exp in args) {
result = e.eval(exp);
if (result == new Compiler.Boolean(false)) { return result; }
}
return new_e.eval(args.Last());
return result;
}
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_)));
private static Expression _or(Executor e, IList<Expression> args) {
Expression result = new Compiler.Boolean(false);
foreach (var exp in args) {
result = e.eval(exp);
if (result != new Compiler.Boolean(false)) { return result; }
}
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())));
return result;
}
}
@ -489,55 +396,54 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
public Builtins builtins { get => _builtins; }
public BuiltinsLater builtinsLater { get => _builtinsLater; }
public Expression? EvalFunction(Symbol fcname, IEnumerable<Expression> args) {
if (builtins.ContainsKey(fcname.Name())) {
return builtins[fcname.Name()](args.Select(x => eval(x)).ToList()); // call ToList for sideeffect
public Expression EvalFunction(Symbol fcname, IList<Expression> args) {
if (_environment.Find(fcname.name) is IEnvironment<string, Expression> _e) {
Expression? first = _e.Get(fcname.name);
return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList());
}
if (builtinsLater.ContainsKey(fcname.Name())) {
return builtinsLater[fcname.Name()](this, args);
if (_builtins.ContainsKey(fcname.name)) {
Function fc = _builtins[fcname.name];
return fc(args.Select(x => eval(x)).ToList());
}
return null;
if (_builtinsLater.ContainsKey(fcname.name)) {
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) {
switch (expression) {
case Symbol s:
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
if (builtins.ContainsKey(s.Name()) || builtinsLater.ContainsKey(s.Name())) {
return s;
}
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 _environment.Find(s.name).Get(s.name);
case Compiler.Boolean b:
return b;
case Integer i:
return i;
case String s:
case Compiler.String s:
return s;
case Object o:
case Compiler.Object o:
return o;
case Procedure p:
return p;
case Cons cons:
var l = cons.ToList();
if (cons.Item1 is Symbol cons_item1_symbol) {
Expression? r = EvalFunction(cons_item1_symbol, l.Skip(1));
if (r is not null) { return r; }
case List list:
if (list.expressions.Count == 0) {
return list;
}
var eval_Item1 = eval(cons.Item1);
if (eval_Item1 is Symbol eval_item1_symbol1) {
Expression? r = EvalFunction(eval_item1_symbol1, l.Skip(1));
if (r is not null) { return r; }
// do we really want to allow shadowing of builtins?
if (list.expressions[0].GetType() == typeof(Symbol)) {
return eval(EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList()));
}
if (eval_Item1 is Procedure eval_item1_procedure) {
return eval_item1_procedure.Call(this, l.Skip(1).Select(x => x).ToList());
if (list.expressions[0].GetType() == typeof(Procedure)) {
Procedure procedure = (Procedure) list.expressions[0];
return eval(procedure.Call(this, list.expressions.Skip(1).ToList()));
}
throw new ApplicationException($"Not handled case (type = {eval_Item1.GetType()}) '{cons}'");
var l = new List(list.expressions.Select(x => eval(x)).ToList());
if (l.expressions[0].GetType() == typeof(Procedure)) {
return eval(l);
}
return l;
}
throw new ApplicationException($"Not handled case '{expression}'");
}

View file

@ -1,24 +1,18 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
namespace Jellyfin.Plugin.SmartPlaylist {
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
public IServerApplicationPaths ServerApplicationPaths;
public IUserManager UserManager;
public Plugin(
IApplicationPaths applicationPaths,
IXmlSerializer xmlSerializer,
IServerApplicationPaths serverApplicationPaths,
IUserManager userManager
IXmlSerializer xmlSerializer
) : base (applicationPaths, xmlSerializer) {
Instance = this;
ServerApplicationPaths = serverApplicationPaths;
UserManager = userManager;
}
public static Plugin? Instance {get; private set; }
public override string Name => "Smart Playlist";

View file

@ -1,72 +1,14 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Controller;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.SmartPlaylist {
public class PluginConfiguration : BasePluginConfiguration {
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)
(define all-genres (lambda (want have) (all (lambda (x) (is-genre x have)) want)))
(define any-genres (lambda (want have) (any (lambda (x) (is-genre x have)) want)))
(define get-name (lambda (x) (car (getitems x "Name"))))
(define find-parent (lambda (typename) (invoke-generic *item* "FindParent" nil (list typename))))
(define find-artist (lambda nil (find-parent "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))))
""";
store = new Store(new SmartPlaylistFileSystem(Plugin.Instance.ServerApplicationPaths));
}
private Store store { get; set; }
public string InitialProgram { get; set; }
public SmartPlaylistDto[] Playlists {
get {
return store.GetAllSmartPlaylistsAsync().GetAwaiter().GetResult();
}
set {
var existing = store.GetAllSmartPlaylistsAsync().GetAwaiter().GetResult().Select(x => x.Id).ToList();
foreach (var p in value) {
existing.Remove(p.Id);
store.SaveSmartPlaylistAsync(p).GetAwaiter().GetResult();
}
foreach (var p in existing) {
store.DeleteSmartPlaylistById(p);
}
}
}
public object[][] Users {
get {
return Plugin.Instance.UserManager.Users.Select(x => new object[]{x.Id, x.Username}).ToArray();
}
set { }
public PluginConfiguration(
) {
}
}
}

View file

@ -1,4 +1,3 @@
using System.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using MediaBrowser.Controller;
@ -8,14 +7,13 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Playlists;
using Jellyfin.Plugin.SmartPlaylist.Lisp;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object;
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
@ -82,78 +80,30 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
}
}
private PlaylistId CreateNewPlaylist(string name, UserId userId) {
_logger.LogDebug("Creating playlist '{0}'", name);
private SmartPlaylistId CreateNewPlaylist(string name, UserId userId) {
var req = new PlaylistCreationRequest {
Name = name,
UserId = userId,
Users = [new PlaylistUserPermissions(userId)],
Public = false,
};
var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
return playlistGuid;
}
private Executor SetupExecutor() {
var env = new DefaultEnvironment();
var executor = new Executor(env);
executor.builtins["logd"] = (x) => {
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
return Lisp_Boolean.TRUE;
};
executor.builtins["logi"] = (x) => {
_logger.LogInformation(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
return Lisp_Boolean.TRUE;
};
executor.builtins["logw"] = (x) => {
_logger.LogWarning(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
return Lisp_Boolean.TRUE;
};
executor.builtins["loge"] = (x) => {
_logger.LogError(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
return Lisp_Boolean.TRUE;
};
if (Plugin.Instance is not null) {
executor.eval(Plugin.Instance.Configuration.InitialProgram);
} else {
throw new ApplicationException("Plugin Instance is not yet initialized");
}
return executor;
}
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
List<BaseItem> results = new List<BaseItem>();
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
Executor executor = SetupExecutor();
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
List<Guid> results = new List<Guid>();
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
Executor executor = new Executor(new DefaultEnvironment());
executor.environment.Set("user", new Lisp_Object(user));
foreach (var i in items) {
executor.environment.Set("*item*", Lisp_Object.FromBase(i));
executor.environment.Set("item", new Lisp_Object(i));
var r = executor.eval(expression);
_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);
results.Add(i);
results.Add(i.Id);
}
}
executor = SetupExecutor();
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);
return results;
}
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
@ -166,17 +116,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
_logger.LogInformation("Started regenerate Smart Playlists");
_logger.LogDebug("Loaded Assemblies:");
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
_logger.LogDebug("- {0}", asm);
}
var i = 0;
var all_playlists = await _store.GetAllSmartPlaylistsAsync();
foreach (SmartPlaylistDto dto in all_playlists) {
if (!dto.Enabled) {
i += 1;
continue;
}
foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) {
var changedDto = false;
if (dto.Playlists.Length == 0) {
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
@ -211,8 +151,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
await ClearPlaylist(playlist);
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
}
i += 1;
progress.Report(100 * ((double)i)/all_playlists.Count());
}
}
@ -222,7 +160,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
throw new ArgumentException("");
}
var existingItems = playlist_new.GetManageableItems().ToList();
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id));
}
}
}

View file

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

View file

@ -5,7 +5,6 @@ namespace Jellyfin.Plugin.SmartPlaylist {
Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId);
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
}
@ -15,24 +14,13 @@ namespace Jellyfin.Plugin.SmartPlaylist {
_fileSystem = fileSystem;
}
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
var r = await File.ReadAllTextAsync(filename);
if (r.Equals("")) {
r = "{}";
}
var r = File.ReadAllText(filename);
var dto = new DeserializerBuilder().Build().Deserialize<SmartPlaylistDto>(r);
if (dto == null)
{
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;
}
public async Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
@ -47,9 +35,9 @@ namespace Jellyfin.Plugin.SmartPlaylist {
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
await File.WriteAllTextAsync(filename, text);
File.WriteAllText(filename, text);
}
public void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
try {
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
if (File.Exists(filename)) { File.Delete(filename); }

View file

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

View file

@ -1,79 +0,0 @@
name: Smart Playlist
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
version: 0.4.0.0
targetAbi: 10.10.3.0
framework: net8.0
owner: redxef
overview: Smart playlists with Lisp filter engine.
description: |
Create smart playlists with a Lisp filter engine.
The same playlist definition can be used for multiple users.
category: "General"
artifacts:
- jellyfin-smart-playlist.dll
- YamlDotNet.dll
changelog: |
## v0.4.0.0
- Add a basic UI to configure the playlists.
- It's now possible to print log messages to the jellyfin log by calling `logd`, `logi`, `logw` or `loge`
for the respective levels `debug`, `info`, `warning` or `error`.
- Allow calling generic methods via `(invoke-generic object methodname args list-of-types)`.
- Add quoting via single quote: `'`.
- Add special case for `(quote <form>)` to be rendered as `'<form>`.
- It is now possible to include comments in the source via a semicolon (`;`).
- Respect the `Enabled` flag and only process the playlists that are enabled.
- New methods have been added: `rand`, `shuf`.
- Add `find-artist`, `get-name` and `find-parent` default definitions.
- Update YamlDotNet to v16.2.1.
**Breaking changes**:
- Rename global environment variables to be enclosed by `*`.
**Fixes**:
- The initialization of the executor now contains the same default definitions for the SortProgram and the normal Program.
- The progress report now considers the SmartPlaylists and not the individual playlists per user.
- It is now possible to pass builtins as arguments. Previously to get `(qsort > (list 1 2 3))` one had to write
something like this: `(qsort (lambda (a b) (> a b)) (list 1 2 3))`.
- A program no longer has to be a list, `t` is a valid program.
- Fix list parsing in cases where a space was before the closing parenthesis.
## v0.3.0.0
- Add a second program (`SortProgram`) which is run after the filtering, this
program should return the list of items, but in the order in which they should appear in
the playlist. The default is `(begin items)` which returns the list as is.
- Extend builtin lisp definitions: add `qsort` and string comparison methods
- Extend default program definitions: add `all-genres` and `any-genres` to quickly specify a list of genres which to include (or excluding when negating)
- Update Jellyfin to v 10.10.3
**Fixes**:
- The progress report now correctly gives a percentage in the range [0, 100].
## 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.

View file

@ -2,47 +2,35 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>SmartPlaylist</title>
<title>Template</title>
</head>
<body>
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="SmartPlaylistConfigForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
<div class="fieldDescription">A program which can set up the environment</div>
<textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" cols="120"></textarea>
</div>
<div>
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistSelection">Choose a playlist to edit</label>
<select id="SmartplaylistSelection" class="emby-select">
</select>
</div>
<div>
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditName">Name</label>
<input id="SmartplaylistEditName" type="text" class="emby-input"/>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditProgram">Program</label>
<div class="fieldDescription">A program which should return <code>t</code> or <code>nil</code> to include or exclude the provided <code>item</code>.</div>
<textarea id="SmartplaylistEditProgram" class="emby-input smartplaylist-monospace" name="Program" rows="16" cols="120"></textarea>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditSortProgram">Sort Program</label>
<div class="fieldDescription">A program which should return a list of items to include in the playlist, sorted however you like.</div>
<textarea id="SmartplaylistEditSortProgram" class="emby-input smartplaylist-monospace" name="SortProgram" rows="16" cols="120"></textarea>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditUsers">Users</label>
<div class="fieldDescription">Which users should get access to the playlist.</div>
<select multiple id="SmartplaylistEditUsers" class="emby-select">
<form id="TemplateConfigForm">
<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">
<label class="inputLabel inputLabelUnfocused" for="SmartPlaylistEditEnabled">Enabled</label>
<div class="fieldDescription">Is the playlist enabled.</div>
<input id="SmartplaylistEditEnabled" type="checkbox" class="emby-input"/>
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
<div class="fieldDescription">A Description</div>
</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>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
@ -52,144 +40,34 @@
</form>
</div>
</div>
<style>
.smartplaylist-monospace {
font-family: monospace;
}
</style>
<script type="text/javascript">
var SmartPlaylistConfig = {
var TemplateConfig = {
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
};
function changeEditBox(config, id) {
const selection = document.querySelector('#SmartplaylistSelection');
const editName = document.querySelector('#SmartplaylistEditName');
const editProgram = document.querySelector('#SmartplaylistEditProgram');
const editSortProgram = document.querySelector('#SmartplaylistEditSortProgram');
const editUsers = document.querySelector('#SmartplaylistEditUsers');
const editEnabled = document.querySelector('#SmartplaylistEditEnabled');
if (id === null) {
selection.selectedIndex = 0;
editName.value = 'My New Smartplaylist';
editProgram.value = '(is-favourite)';
editSortProgram.value = '(begin *items*)';
editUsers.innerHTML = '';
for (const u of config.Users) {
var o = document.createElement('option');
o.value = u[0];
o.innerHTML = u[1];
o.setAttribute('selected', 'selected');
editUsers.appendChild(o);
}
editEnabled.checked = true;
return;
}
function matchId(p) {
return p.Id == id;
}
const index = config.Playlists.map(function (x) { return x.Id }).indexOf(id);
selection.selectedIndex = index + 1;
const p = config.Playlists[index];
editName.value = p.Name;
editProgram.value = p.Program;
editSortProgram.value = p.SortProgram;
editUsers.innerHTML = '';
for (const u of config.Users) {
var o = document.createElement('option');
o.value = u[0];
o.innerHTML = u[1];
if (p.Playlists.map((x) => x.UserId).includes(u[0])) {
o.setAttribute('selected', 'selected');
}
editUsers.appendChild(o);
}
editEnabled.checked = p.Enabled;
}
function fillPlaylistSelect(config) {
const selection = document.querySelector('#SmartplaylistSelection');
selection.innerHTML = '';
var o = document.createElement('option');
o.value = null;
o.innerHTML = 'Create new playlist ...';
selection.appendChild(o);
for (const i of config.Playlists) {
var o = document.createElement('option');
o.value = i.Id;
o.innerHTML = i.Name;
selection.appendChild(o);
}
}
document.querySelector('#SmartPlaylistConfigPage')
document.querySelector('#TemplateConfigPage')
.addEventListener('pageshow', function() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
document.querySelector('#InitialProgram').value = config.InitialProgram;
fillPlaylistSelect(config);
changeEditBox(config, (config.Playlists.length > 0) ? config.Playlists[0].Id : null);
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
document.querySelector('#Options').value = config.Options;
document.querySelector('#AnInteger').value = config.AnInteger;
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
document.querySelector('#AString').value = config.AString;
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#SmartplaylistSelection')
.addEventListener('change', function() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
const selection = document.querySelector('#SmartplaylistSelection');
changeEditBox(config, (selection.selectedIndex > 0) ? config.Playlists[selection.selectedIndex - 1].Id : null);
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#SmartPlaylistConfigForm')
document.querySelector('#TemplateConfigForm')
.addEventListener('submit', function(e) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
config.InitialProgram = document.querySelector('#InitialProgram').value;
const selection = document.querySelector('#SmartplaylistSelection');
const editName = document.querySelector('#SmartplaylistEditName');
const editProgram = document.querySelector('#SmartplaylistEditProgram');
const editSortProgram = document.querySelector('#SmartplaylistEditSortProgram');
const editUsers = document.querySelector('#SmartplaylistEditUsers');
const editEnabled = document.querySelector('#SmartplaylistEditEnabled');
var index = selection.selectedIndex;
if (index === 0) {
const o = {
Id: editName.value,
Name: editName.value,
Program: editProgram.value,
SortProgram: editSortProgram.value,
Playlists: Array.from(editUsers.options).filter((x) => x.selected).map((x) => {
const m = {UserId: x.value, PlaylistId: "00000000-0000-0000-0000-000000000000"};
return m;
}),
Enabled: editEnabled.checked,
};
config.Playlists.push(o);
} else {
config.Playlists[index-1].Id = editName.value;
config.Playlists[index-1].Name = editName.value;
config.Playlists[index-1].Program = editProgram.value;
config.Playlists[index-1].SortProgram = editSortProgram.value;
config.Playlists[index-1].Playlists = Array.from(editUsers.options).filter((x) => x.selected).map((x) => {
const existing = config.Playlists[index-1].Playlists.filter((x_) => x_.UserId === x.value).map((x_) => x_.PlaylistId);
const m = {UserId: x.value, PlaylistId: ((existing.length > 0) ? existing[0] : "00000000-0000-0000-0000-000000000000")};
return m;
}),
config.Playlists[index-1].Enabled = editEnabled.checked;
}
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
config.Options = document.querySelector('#Options').value;
config.AnInteger = document.querySelector('#AnInteger').value;
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
config.AString = document.querySelector('#AString').value;
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
document.querySelector('#InitialProgram').value = config.InitialProgram;
fillPlaylistSelect(config);
changeEditBox(config, (config.Playlists.length > 0) ? config.Playlists[0].Id : null);
Dashboard.hideLoadingMsg();
});
});
});
e.preventDefault();

View file

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

125
README.md
View file

@ -1,68 +1,79 @@
# Jellyfin SmartPlaylist Plugin
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.4.0.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.4.0.0).
Smart playlists with lisp filter engine.
## How to use
After [installing](#installation) the plugin and restarting Jellyfin
go to the plugin settings and below the `Initial Program` configuration
choose the smart playlist you want to edit, or `Create new playlist ...`
to create a new one.
create a file in `config/data/smartplaylists` like this:
[Go here](examples.md) to see some example configurations.
```
$ echo '{}' > config/data/smartplaylists/a.json
```
Below are all the configuration values for a smart playlist.
Afterwards run the Task `(re)generate Smart Playlists`, this will rename
the `json` 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
examples.
Example file
```json
{
"Id": "a1d02dee-f1da-4472-bee3-f568c15c8360",
"Playlists": [
{
"PlaylistId": "24f12e1e-3278-d6d6-0ca4-066e93296c95",
"UserId": "6eec632a-ff0d-4d09-aad0-bf9e90b14bc6"
}
],
"Name": "a1d02dee-f1da-4472-bee3-f568c15c8360",
"Program": "(begin (invoke item 'IsFavoriteOrLiked' (user)))",
"Filename": "/config/data/smartplaylists/a1d02dee-f1da-4472-bee3-f568c15c8360.json",
"Enabled": true
}
```
### Id
Arbitrary Id assigned to this playlist, can usually be left alone.
The filename is derived from this.
### Playlists
A list of Playlist/User mappings. By default all users get an entry.
The ids must have the dashes in them as of now. To convert a id
from without dashes to the canonical form run this command:
`echo '<your id here>' | python3 -c 'import uuid; import sys; print(uuid.UUID(sys.stdin.read().strip()))'`
To get your user id navigate to your user profile and copy the part
after `userId` in the address bar.
#### PlaylistId
The id of the playlist that should be managed, must be owned by the
corresponding user.
#### UserId
The user associated with this playlist.
### Name
The name of the generated playlists, this is just a default value.
If the user changes the name of their playlist the plugin will
still work and remember the correct playlist.
work as usual
### Program
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 any other value
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 playlist, return `nil` to not include items, return `t` to include
them.
**!!! 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 !!!**
### Filename
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)`.
*Go [here](lisp.md) to get an overview of the built-in functions.*
### 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*)
```
The path to this file.
### Enabled
@ -71,24 +82,4 @@ Enable this playlist, currently ignored.
## Installation
Add the [plugin repository](https://jellyfin.org/docs/general/server/plugins/#catalog)
to Jellyfin:
`https://gitea.redxef.at/redxef/jellyfin-smart-playlist/raw/branch/manifest/manifest.json`
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
the field labeled `Repository URL`, give the plugin a descriptive name
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.
Download the `.dll` in the [releases](/redxef/jellyfin-smart-playlist/releases) section and place it in the `plugins/jellyfin-smart-playlist/` directory (create it).

View file

@ -1,6 +1,7 @@
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
using Lisp_Integer = Jellyfin.Plugin.SmartPlaylist.Lisp.Integer;
using Xunit;
using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment;
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.Compiler;
@ -15,45 +16,45 @@ namespace Tests
}
public int i { get => _i; }
public bool b { get => _b; }
public int I() {
return _i;
}
public string G<E>() {
return typeof(E).FullName;
}
}
public class Test {
[Fact]
public static void TestTokenizer() {
StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1 >= ++ !=)");
Assert.Equal("(", sts.Get().value);
Assert.Equal("\"", sts.Get().value);
Assert.Equal("some", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("literal", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("string", sts.Get().value);
Assert.Equal("\"", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("def", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("ghj", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("+100", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("-+300", sts.Get().value);
Assert.Equal(" ", sts.Get().value);
Assert.Equal("1", 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, "some");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "literal");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "string");
Assert.Equal(sts.Get().value, "\"");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "def");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "ghj");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "+");
Assert.Equal(sts.Get().value, "100");
Assert.Equal(sts.Get().value, " ");
Assert.Equal(sts.Get().value, "-");
Assert.Equal(sts.Get().value, "+");
Assert.Equal(sts.Get().value, "300");
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, ")");
sts.Commit();
Assert.Equal(0, sts.Available());
Assert.Equal(sts.Available(), 0);
}
[Fact]
@ -68,205 +69,115 @@ namespace Tests
p = new Parser(sts);
Assert.Equal(program, string.Format("{0}", p.parse()));
program = "(abc '(1 2 3))";
sts = StringTokenStream.generate(program);
p = new Parser(sts);
Assert.Equal(program, string.Format("{0}", p.parse()));
program = "(abc \"'(1 2 3)\")";
sts = StringTokenStream.generate(program);
p = new Parser(sts);
Assert.Equal(program, string.Format("{0}", p.parse()));
program = """
(begin ;this too
;;; this is a comment
t
)
""";
sts = StringTokenStream.generate(program);
p = new Parser(sts);
Assert.Equal("(begin t)", string.Format("{0}", p.parse()));
//program = "(* 2.4 2)";
//sts = StringTokenStream.generate(program);
//p = new Parser(sts);
//Assert.Equal(program, p.parse().ToString());
}
[Fact]
public static void TestFunctions() {
Executor e = new Executor();
Assert.Equal("(1 2 3)", e.eval("(quote (1 2 3))").ToString());
Assert.Equal("abc", e.eval("(quote abc)").ToString());
IList<Tuple<string, Expression>> cases = new List<Tuple<string, Expression>>();
Expression e = new Executor().eval("(+ 10 20)");
Assert.Equal(((Integer) e).value, 30);
Assert.Equal("t", e.eval("(atom 1)").ToString());
Assert.Equal("nil", e.eval("(atom (quote (1 2 3)))").ToString());
e = new Executor().eval("(> 1 2)");
Assert.Equal(((Lisp_Boolean) e).value, false);
Assert.Equal("t", e.eval("(eq 2 2)").ToString());
Assert.Equal("nil", e.eval("(eq 2 3)").ToString());
e = new Executor().eval("(if (> 1 2) 3 4)");
Assert.Equal(((Integer) e).value, 4);
Assert.Equal("1", e.eval("(car (quote (1 2 3)))").ToString());
Assert.Equal("(2 3)", e.eval("(cdr (quote (1 2 3)))").ToString());
e = new Executor().eval("(begin (define x 1) x)");
Assert.Equal(((Integer) e).value, 1);
Assert.Equal("(1 . 2)", e.eval("(cons 1 2)").ToString());
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("(apply + (1 2))");
Assert.Equal(((Integer) e).value, 3);
Assert.Equal("\"Case 2\"", e.eval("""
(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("(car (10 20 30))");
Assert.Equal(((Integer) e).value, 10);
Assert.Equal("t", e.eval("((lambda (a) (eq a a)) 2)").ToString());
e = new Executor().eval("(cdr (10 20 30))");
Assert.Equal(string.Format("{0}", e), "(20 30)");
Assert.Equal("t", e.eval("(begin (car (quote (nil 1))) t)").ToString());
Assert.Equal("(1)", e.eval("(begin t (cdr (quote (nil 1))))").ToString());
e = new Executor().eval("(cons 1 3)");
Assert.Equal(string.Format("{0}", e), "(1 3)");
Assert.Equal("t", e.eval("""
(begin
(define abc 10)
(eq abc abc))
""").ToString());
e = new Executor().eval("(cons 1 (2 3))");
Assert.Equal(string.Format("{0}", e), "(1 2 3)");
Assert.Equal("1", e.eval("""
(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("(length (cons 1 (2 3)))");
Assert.Equal(string.Format("{0}", e), "3");
Assert.Equal("a", e.eval("'a").ToString());
}
e = new Executor().eval("(>= 2 2)");
Assert.Equal(string.Format("{0}", e), "t");
[Fact]
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("(> 2 2))");
Assert.Equal(string.Format("{0}", e), "nil");
Assert.Equal("(3 4 5)", e.eval("""
((lambda (x y . z) z) 1 2 3 4 5)
""").ToString());
e = new Executor().eval("(and 2 3 4)");
Assert.Equal("4", e.ToString());
Assert.Equal("3", e.eval("""
(begin
(define if (lambda (condition a b) (cond (condition a) (t b))))
(if (< 1 2) 3 2))
""").ToString());
e = new Executor().eval("(and 2 nil 4)");
Assert.Equal("nil", e.ToString());
Assert.Equal("2", e.eval("""
(begin
(define if (lambda (condition a b) (cond (condition a) (t b))))
(if (> 1 2) 3 2))
""").ToString());
Assert.Equal("1", e.eval("""
(begin
(define if (lambda* (condition a b) (
cond ((eval condition) (eval a)) (t (eval 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 ((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());
e = new Executor().eval("(or 2 nil 4)");
Assert.Equal("2", e.ToString());
e = new Executor().eval("(or nil 4)");
Assert.Equal("4", e.ToString());
e = new Executor().eval("(= (1 2) (1 2))");
Assert.Equal(e.ToString(), "t");
e = new Executor().eval("(= (1 2 3) (1 2))");
Assert.Equal(e.ToString(), "nil");
}
[Fact]
public static void ObjectTest() {
Executor e = new Executor();
Expression r;
e.environment.Set("o", Lisp_Object.FromBase(new O(5, false)));
r = e.eval("""(haskeys o "i" "b")""");
Assert.True(((Lisp_Boolean)r).Value());
r = e.eval("""(getitems o "i" "b")""");
Assert.Equal("(5 nil)", string.Format("{0}", r));
r = e.eval("""(invoke o "I" nil)""");
Assert.Equal("5", string.Format("{0}", r));
r = e.eval("""(invoke-generic o "G" nil ((lambda (. args) args) "System.String"))""");
Assert.Equal("\"System.String\"", string.Format("{0}", r));
e.environment.Set("o", new Lisp_Object(new O(5, false)));
r = e.eval("(haskeys o 'i' 'b')");
Assert.Equal(((Lisp_Boolean)r).value, true);
r = e.eval("(getitems o 'i' 'b')");
Assert.Equal(string.Format("{0}", r), "(5 nil)");
}
[Fact]
public static void GlobalVariableTest() {
public static void ScalarTest() {
Executor e = new Executor();
e.environment.Set("*o*", new Lisp_Integer(5));
Assert.Equal("10", e.eval("(* *o* 2)").ToString());
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]
public static void DefaultEnvironmentTest() {
Executor e = new Executor(new DefaultEnvironment());
Assert.Equal("1", e.eval("(if nil 0 1)").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());
//Assert.Equal("", e.eval("(rand)").ToString());
//Assert.Equal("", e.eval("(shuf (list 0 1 2 3 4 5 6))").ToString());
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort (lambda (a b) (> a b)) '(5 4 7 3 2 6 1))").ToString());
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort > (list 5 4 7 3 2 6 1))").ToString());
Assert.Equal("nil", e.eval("(find 0 (1 2 3 4))").ToString());
Assert.Equal("3", e.eval("(find 3 (1 2 3 4))").ToString());
}
}
}

View file

@ -1,117 +0,0 @@
---
resource_types:
- name: http-resource
type: registry-image
source:
repository: redxef/concourse-http-resource
tag: edge
resources:
- name: source
type: git
source:
uri: https://git.redxef.at/redxef/jellyfin-smart-playlist
branch: main
fetch_tags: true
tag_regex: 'v.*'
- name: manifest
type: git
source:
uri: ssh://git@git.redxef.at:8022/redxef/jellyfin-smart-playlist.git
branch: manifest
private_key: ((gitea.id_ed25519))
- name: releases
type: http-resource
source:
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases
method: get
auth:
basic:
username: ((gitea.username))
password: ((gitea.token))
- name: artifact
type: http-resource
source:
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases/((.:release_id))/assets
method: get
auth:
basic:
username: ((gitea.username))
password: ((gitea.token))
headers:
- - Accept
- application/json
query:
- - name
- smart-playlist_((.:version)).zip
body:
multipart:
files:
attachment: artifacts/smart-playlist_((.:version)).zip
version_check_method: none
jobs:
- name: release
plan:
- get: source
trigger: true
- get: manifest
- get: releases
- task: run-build
config:
platform: linux
image_resource:
type: registry-image
source:
repository: alpine
inputs:
- name: source
- name: manifest
- name: releases
outputs:
- name: artifacts
- name: manifest
run:
path: sh
args:
- -c
- |
#!/bin/sh
set -eu
apk add --no-cache jq git dotnet8-sdk python3 py3-pip
pip3 install --break-system-packages jprm
cp manifest/manifest.json source/
(
cd source
./release.sh build
)
cp source/artifacts/*.zip artifacts/
cd source
VERSION="$(./release.sh version)"
cd ..
printf '%s' "$VERSION" > artifacts/version
cp source/manifest.json manifest/
(
cd manifest
git add .
git config user.name 'Concourse CI/CD'
git config user.email 'concourse@redxef.at'
git commit -m "ci: update manifest for version ${VERSION}."
)
jq --arg tag "v$VERSION" -r '.[] | select(.tag_name == $tag) | .id' < releases/body > artifacts/release_id
ls artifacts
- load_var: release_id
file: artifacts/release_id
reveal: true
- load_var: version
file: artifacts/version
reveal: true
- put: artifact
inputs:
- artifacts
params:
method: post
- put: manifest
params:
repository: manifest

View file

@ -1,99 +0,0 @@
# 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)))
```
- `All of Seeed`: A playlist containing all items by artist `Seeed`,
useless, since you also can just navigate to the artist to play all
their songs, but a good example.
```
Id: Seeed
Name: Seeed
Program: |
(let
(parent
(invoke-generic *item* "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller")))
(cond
((null parent) nil)
(t (string= (car (getitems parent "Name")) "Haller"))))
```
or simplified with definitions contained in the preamble:
```
Id: Seeed
Name: Seeed
Program: |
(let
(parent
(find-parent "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))
(cond
((null parent) nil)
(t (string= (car (getitems parent "Name")) "Seeed"))))
```
- `Artists`: A playlist including everything from a list of artists.
```
Id: Artists
Name: Artists
Program: >-
(begin
(define *include-artists* '(
"seeed"
"juli"
"falco"
"peter fox"
"alligatoah"
"kraftklub"
"wanda"
"annennaykantereit"
"feine sahne fischfilet"
"madsen"
"wiener blond"
"die toten hosen"
"die ärzte"
"wir sind helden"
))
(define my-prefilter-only-tracks
(lambda nil
(and
(is-type "Audio")
(not (name-contains "live"))
(not (name-contains "acoustic"))
(not (name-contains "instrumental"))
(not (name-contains "remix")))))
(and
(my-prefilter-only-tracks)
(is-favourite)
(any
(lambda
(i)
;; the `(and (find-artist)` is here to prevent null violations.
(and (find-artist) (string= i (lower (get-name (find-artist))))))
*include-artists*)))
SortProgram: (begin (shuf *items*))
Filename: /config/data/smartplaylists/German.yaml
Enabled: true
```

View file

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

4
examples/index.md Normal file
View file

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

129
lisp.md
View file

@ -1,129 +0,0 @@
# The lisp interpreter
The interpreter is a lisp-like language used to build the filter
expressions.
## Builtins
**atom**: check if a receive value is a atom. `(atom 1)`
**eq**: check if two values are equal. `(eq (quote a) (quote a))`
**car**: get the first item of the cons.
**cdr**: get the remainder of the list.
**cons**: create a new cons. `(cons 1 2)`
**begin**: evaluate a series of statements, returning the result of the
last one. `(begin t nil)`
**+-\*/%**: arithmetic operations for integers. `(+ 1 2)`
**=**, **!=**, **<**, **<=**, **>**, **>=**: compare two integers. `(> 1 2)`
**not**: negate the given value. `nil -> t`, everything else will be `nil`. `(not (quote a))`
**string=**, **string!=**, **string<**, **string<=**, **string>**, **string>=**: compare two strings. `(> "1" "2")`
**haskeys**: takes any object and a variadic number of arguments (strings) and
returns a list with either `t` or `nil` describing if a corresponding property/field/method
with that name exists on the object. `(haskeys mystring "Length")`
**getitems**: takes any object and a variadic number of arguments
(strings) and returns the values of the fields/properties. `(getitems mystring "Length" "Chars")`
**invoke**: takes 3 arguments and invokes the method defined on the
object.
The first argument is the object on which to invoke the method, the
second one is the name of the method and the third one is a list of
arguments to pass to the method. `(invoke mystring "Lower" nil)`
**invoke-generic**: the same as **invoke**, but takes a fourth
argument, a list of string describing the types for the generic method.
`(invoke-generic mybaseitem "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))`
**random**: gives a random integer. `(random)`
**shuffle**: shuffles a list. `(shuffle (list 1 2 3 4))`
**quote**: quotes a value. `(quote a)`
**eval**: evaluates a expression. `(eval (quote a))`
**cond**: checks conditions and evaluates the corresponding expression.
```
(cond
((> 1 2) t)
(t f))
```
**if**: a conditional. `(if t 1 2)`
**define**: defines a new symbol. `(define foo 1)`, `(define add (lambda (a b) (+ a b)))`
**let**: define variables in the let context and evaluate the last
expression. `(let (a 1) (b 2) (+ a b))`
**let\***: the same as **let**, but allows to reference variables
defined earlier in the let statement. `(let* (a 1) (b (+ 2 a)) (+ a b))`
**apply**: call a function with the specified arguments. `(apply + (list 1 2))`
**and**: evaluate the given expressions in order, if any one of them
evaluates to `nil` return early with that value, otherwise return the
last value. `(and 1 2 nil 3)`
**or**: return `nil` if all arguments evaluate to `nil` otherwise the
first non-nil value.
## Derived builtins
**null**: the same as **not**. Can be useful to indicate semantics of a
program.
**list**: create a list from the given arguments. `(list 1 2 3)`
**find**: find if an item is in the given list and return it, otherwise
return `nil`. `(find 4 (list 1 2 3 4 5 6 7))`
**map**: apply a function to every item in the list. `(map (lambda (x) (* 2 x)) (list 1 2 3))`
**fold**: also known as reduce. Apply the function to a sequence of
values, reducing the sequence to a single item. It takes a initial value
which is returned for empty lists. `(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))`
**any**: equivalent to `(apply or (map function list))`.
`(any (lambda (a) (% a 2)) (list 2 4 6 7 8))`
**all**: equivalent to `(apply and (map function list))`.
`(all (lambda (a) (% a 2)) (list 2 4 6 7 8))`
**append**: append an item to the given list. `(append (list 1 2 3) 4)`
**qsort**: quicksort, takes a comparison function and the list.
`(qsort (lambda (a b) (> a b)) (list 1 2 6 4 9 1 19 0))`
**rand**: get a random integer. Takes either zero, one or two arguments.
If zero arguments are given it gives a random integer from all possibly
representable integers. If one argument is given it gives a integer
between `0` (inclusive) and `n` (exclusive). If two arguments are given
it gives a integer between `a` (inclusive) and `b` (exclusive).
**shuf**: same as **shuffle**.
## Pre-defined convenience functions
- **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")`)
- **logd**, **logi**, **logw**, **loge**: write to the logger with the respective levels (`debug`, `information`, `warning`, `error`).
Takes the same arguments as `Logger.LogInformation(...)`.
And many more.

View file

@ -1,23 +0,0 @@
#!/bin/sh
JPRM="jprm"
PLUGIN=Jellyfin.Plugin.SmartPlaylist
VERSION=$(sed -n '/^version:/ s/^version: *//p' "${PLUGIN}/build.yaml")
ARTIFACT_DIR=artifacts
REPO_URL="https://gitea.redxef.at/redxef/jellyfin-smart-playlist/releases/download/v${VERSION}/smart-playlist_${VERSION}.zip"
build() {
mkdir -p "${ARTIFACT_DIR}"
find "${PLUGIN}" -name project.assets.json -exec rm -v '{}' ';'
zipfile=$($JPRM --verbosity=debug plugin build "${PLUGIN}" --output="${ARTIFACT_DIR}" --version="${VERSION}") && {
$JPRM --verbosity=debug repo add --plugin-url="${REPO_URL}" . "${zipfile}"
}
}
version() {
echo "$VERSION"
}
"$@"
exit $?