Compare commits
76 commits
Author | SHA1 | Date | |
---|---|---|---|
a5bc558486 | |||
8426d14500 | |||
31f36b6f38 | |||
d593bf8597 | |||
225dc7559f | |||
5cca31cf16 | |||
a2abe724b7 | |||
dc5435fa68 | |||
c5c2b86557 | |||
e236031920 | |||
6776d9461a | |||
b441a09831 | |||
b73f65829d | |||
097d267d24 | |||
28f3cc682e | |||
4d2d22e486 | |||
c9d7f1ee60 | |||
805d0efb4f | |||
d5e8b69b70 | |||
87dd4358d8 | |||
6d7cd33d04 | |||
8453705bc4 | |||
3bba58d4ac | |||
f7cbebdd9c | |||
db5a659dfc | |||
f0bfecad71 | |||
f73f501642 | |||
81184c23a7 | |||
0059fc43e1 | |||
3f1a1e1a78 | |||
2fc2959110 | |||
e180b397c6 | |||
18136e0fa6 | |||
6d62f6eeb0 | |||
4eb6ec1a04 | |||
96ebc366b6 | |||
05742dd17c | |||
52b270a8d8 | |||
1193ca3005 | |||
8371dc8536 | |||
1b0c5455dd | |||
f479c93c5c | |||
0844cebd88 | |||
24b3d41df5 | |||
45844cafec | |||
bf286d4ece | |||
bfcf854d38 | |||
0ccefa3b58 | |||
1f961ccb0c | |||
12d98c46cb | |||
3c0d8a3809 | |||
8f832ed224 | |||
67cffd98ff | |||
df2e07e519 | |||
74486640d8 | |||
af63a8a696 | |||
f39633d7c5 | |||
b23587d721 | |||
7bf2923ad1 | |||
5cfb35a239 | |||
61478095ea | |||
5439b1de4f | |||
8e97ef7170 | |||
f41485cecf | |||
4537a3aee3 | |||
396384fd71 | |||
6ee9bd7f67 | |||
4e5cb8e64e | |||
3c3ddc9e83 | |||
889df318db | |||
8257acbfbb | |||
0a5aed38eb | |||
8ec393f494 | |||
5835351401 | |||
4993bdfa4c | |||
906bfb9eeb |
21 changed files with 1692 additions and 864 deletions
|
@ -1,269 +1,7 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
interface IAddable<T> where T : IAddable<T> {
|
|
||||||
static abstract T operator +(T left, T right);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ISubtractable<T> where T : ISubtractable<T> {
|
|
||||||
static abstract T operator -(T left, T right);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMultiplicatable<T> where T : IMultiplicatable<T> {
|
|
||||||
static abstract T operator *(T left, T right);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDivisible<T> where T : IDivisible<T> {
|
|
||||||
static abstract T operator /(T left, T right);
|
|
||||||
static abstract T operator %(T left, T right);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ISortable<T, E> where T : ISortable<T, E> {
|
|
||||||
static abstract E operator >(T left, T right);
|
|
||||||
static abstract E operator <(T left, T right);
|
|
||||||
static abstract E operator >=(T left, T right);
|
|
||||||
static abstract E operator <=(T left, T right);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IComparable<T, E> where T : IComparable<T, E> {
|
|
||||||
static abstract E operator ==(T left, T right);
|
|
||||||
static abstract E operator !=(T left, T right);
|
|
||||||
E Equals(T other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class Expression: IComparable<Expression, bool> {
|
|
||||||
public override abstract string ToString();
|
|
||||||
public abstract override int GetHashCode();
|
|
||||||
public abstract bool Equals(Expression other);
|
|
||||||
public override bool Equals(object? other) {
|
|
||||||
if (other is Expression other_e) {
|
|
||||||
return Equals(other_e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public static bool operator ==(Expression left, Expression right) {
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
public static bool operator !=(Expression left, Expression right) {
|
|
||||||
return !left.Equals(right);
|
|
||||||
}
|
|
||||||
public abstract object Inner();
|
|
||||||
}
|
|
||||||
public abstract class Atom : Expression {}
|
|
||||||
public class Symbol : Atom {
|
|
||||||
private readonly string _name;
|
|
||||||
public Symbol(string name) {
|
|
||||||
_name = name;
|
|
||||||
}
|
|
||||||
public string name { get => _name; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _name.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression? other) {
|
|
||||||
if (other is Symbol other_s) {
|
|
||||||
return _name == other_s._name;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return _name;
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Boolean : Atom {
|
|
||||||
private readonly bool _value;
|
|
||||||
public Boolean(bool value) {
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
public bool value { get => _value; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _value.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression other) {
|
|
||||||
if (other is Boolean other_b) {
|
|
||||||
return _value == other_b.value;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return _value? "t" : "nil";
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean> {
|
|
||||||
private readonly int _value;
|
|
||||||
public Integer(int value) {
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
public int value { get => _value; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _value.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression other) {
|
|
||||||
if (other is Integer other_i) {
|
|
||||||
return _value == other_i._value;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return _value.ToString();
|
|
||||||
}
|
|
||||||
public static Integer operator +(Integer a, Integer b) {
|
|
||||||
return new Integer(a.value + b.value);
|
|
||||||
}
|
|
||||||
public static Integer operator -(Integer a, Integer b) {
|
|
||||||
return new Integer(a.value - b.value);
|
|
||||||
}
|
|
||||||
public static Integer operator *(Integer a, Integer b) {
|
|
||||||
return new Integer(a.value * b.value);
|
|
||||||
}
|
|
||||||
public static Integer operator /(Integer a, Integer b) {
|
|
||||||
return new Integer(a.value / b.value);
|
|
||||||
}
|
|
||||||
public static Integer operator %(Integer a, Integer b) {
|
|
||||||
return new Integer(a.value % b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator >(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value > b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator <(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value < b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator >=(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value >= b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator <=(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value <= b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator ==(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value == b.value);
|
|
||||||
}
|
|
||||||
public static Boolean operator !=(Integer a, Integer b) {
|
|
||||||
return new Boolean(a.value != b.value);
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class String : Atom, IAddable<String> {
|
|
||||||
private readonly string _value;
|
|
||||||
public String(string value) {
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
public string value { get => _value; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _value.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression other) {
|
|
||||||
if (other is String other_s) {
|
|
||||||
return _value == other_s._value;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return "\"" + _value + "\"";
|
|
||||||
}
|
|
||||||
public static String operator +(String a, String b) {
|
|
||||||
return new String (a.value + b.value);
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Object : Atom {
|
|
||||||
private readonly object _value;
|
|
||||||
public Object(object value) {
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
public object value { get => _value; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _value.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression other) {
|
|
||||||
if (other is Object other_o) {
|
|
||||||
return _value == other_o._value;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return _value.ToString();
|
|
||||||
}
|
|
||||||
public static Expression FromBase(object o) {
|
|
||||||
switch (o) {
|
|
||||||
case bool b:
|
|
||||||
return new Boolean(b);
|
|
||||||
case int i:
|
|
||||||
return new Integer(i);
|
|
||||||
case string s:
|
|
||||||
return new String(s);
|
|
||||||
case IEnumerable<object> e:
|
|
||||||
return new List(e.Select(x => Object.FromBase(x)).ToList());
|
|
||||||
default:
|
|
||||||
return new Object(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class List : Expression {
|
|
||||||
private IList<Expression> _expressions;
|
|
||||||
public List(IList<Expression> expressions) {
|
|
||||||
_expressions = expressions;
|
|
||||||
}
|
|
||||||
public IList<Expression> expressions { get => _expressions; }
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
foreach (Expression i in _expressions) {
|
|
||||||
hash *= 23;
|
|
||||||
hash += i.GetHashCode();
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression other) {
|
|
||||||
if (other is List other_l) {
|
|
||||||
return _expressions.SequenceEqual(other_l._expressions);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return "(" + string.Join(" ", _expressions.Select(x => x.ToString())) + ")";
|
|
||||||
}
|
|
||||||
public static List operator +(List a, List b) {
|
|
||||||
List<Expression> r = new List<Expression>();
|
|
||||||
r.AddRange(a.expressions);
|
|
||||||
r.AddRange(b.expressions);
|
|
||||||
return new List(r);
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
return _expressions.Select(x => x.Inner()).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Parser {
|
public class Parser {
|
||||||
private StringTokenStream _sts;
|
private StringTokenStream _sts;
|
||||||
public Parser(StringTokenStream tokens) {
|
public Parser(StringTokenStream tokens) {
|
||||||
|
@ -280,17 +18,16 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return parse_grouping(gt, gt.closing_value);
|
return parse_grouping(gt, gt.closing_value);
|
||||||
case AtomToken at:
|
case AtomToken at:
|
||||||
return parse_atom(at);
|
return parse_atom(at);
|
||||||
case OperatorToken ot:
|
|
||||||
return parse_operator(ot);
|
|
||||||
case SpaceToken sp:
|
case SpaceToken sp:
|
||||||
return parse();
|
return parse();
|
||||||
}
|
}
|
||||||
return parse();
|
return parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parse_string(GroupingToken start, GroupingToken end) {
|
Expression parse_string(GroupingToken start, GroupingToken? end) {
|
||||||
|
Debug.Assert(end != null);
|
||||||
Debug.Assert(start.value == end.value);
|
Debug.Assert(start.value == end.value);
|
||||||
Debug.Assert("'\"".Contains(start.value));
|
Debug.Assert("\"".Contains(start.value));
|
||||||
string r = "";
|
string r = "";
|
||||||
while (_sts.Available() > 0) {
|
while (_sts.Available() > 0) {
|
||||||
Token<string> t = _sts.Get();
|
Token<string> t = _sts.Get();
|
||||||
|
@ -303,10 +40,36 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new String(r);
|
return new String(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parse_grouping(GroupingToken start, GroupingToken end) {
|
Expression parse_comment(GroupingToken start, GroupingToken? end) {
|
||||||
if ("'\"".Contains(start.value)) {
|
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)) {
|
||||||
return parse_string(start, end);
|
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>();
|
IList<Expression> expressions = new List<Expression>();
|
||||||
while (_sts.Available() > 0) {
|
while (_sts.Available() > 0) {
|
||||||
Token<string> t = _sts.Get();
|
Token<string> t = _sts.Get();
|
||||||
|
@ -314,10 +77,17 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
_sts.Commit();
|
_sts.Commit();
|
||||||
break;
|
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);
|
_sts.Rewind(1);
|
||||||
expressions.Add(parse());
|
expressions.Add(parse());
|
||||||
}
|
}
|
||||||
return new List(expressions);
|
return Cons.FromList(expressions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parse_atom(AtomToken at) {
|
Expression parse_atom(AtomToken at) {
|
||||||
|
@ -327,27 +97,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new Integer(parsed_value);
|
return new Integer(parsed_value);
|
||||||
}
|
}
|
||||||
if (at.value.Equals("t")) {
|
if (at.value.Equals("t")) {
|
||||||
return new Boolean(true);
|
return Boolean.TRUE;
|
||||||
}
|
}
|
||||||
if (at.value.Equals("nil")) {
|
if (at.value.Equals("nil")) {
|
||||||
return new Boolean(false);
|
return Boolean.FALSE;
|
||||||
}
|
}
|
||||||
_sts.Commit();
|
_sts.Commit();
|
||||||
return new Symbol(at.value);
|
return new Symbol(at.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parse_operator(OperatorToken ot) {
|
|
||||||
string v = ot.value;
|
|
||||||
while (_sts.Available() > 0) {
|
|
||||||
Token<string> t = _sts.Get();
|
|
||||||
if (t is OperatorToken ot_) {
|
|
||||||
v += ot_.value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_sts.Rewind(1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return new Symbol(v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
class SpaceToken : Token<string> {
|
class SpaceToken : Token<string> {
|
||||||
private SpaceToken(string value) : base(value) {}
|
private SpaceToken(string value) : base(value) {}
|
||||||
private static IToken<string>? take(CharStream program) {
|
private static IToken<string>? take(CharStream program) {
|
||||||
|
string spaces = " \n";
|
||||||
if (program.Available() == 0) {
|
if (program.Available() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (program.Get() == ' ') {
|
var t = program.Get();
|
||||||
return new SpaceToken(" ");
|
if (spaces.Contains(t)) {
|
||||||
|
return new SpaceToken(t.ToString());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +43,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
char t = program.Get();
|
char t = program.Get();
|
||||||
if ("()\"'".Contains(t)) {
|
if ("()\"';".Contains(t)) {
|
||||||
return new GroupingToken(t.ToString());
|
return new GroupingToken(t.ToString());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,6 +53,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new GroupingToken(")");
|
return new GroupingToken(")");
|
||||||
} else if (_value == ")") {
|
} else if (_value == ")") {
|
||||||
return null;
|
return null;
|
||||||
|
} else if (_value == "'") {
|
||||||
|
return null;
|
||||||
|
} else if (_value == ";") {
|
||||||
|
return new GroupingToken("\n");
|
||||||
}
|
}
|
||||||
return new GroupingToken(_value);
|
return new GroupingToken(_value);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +69,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
string value = "";
|
string value = "";
|
||||||
while (program.Available() > 0) {
|
while (program.Available() > 0) {
|
||||||
char t = program.Get();
|
char t = program.Get();
|
||||||
if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Contains(t)) {
|
if (" \n()\"".Contains(t)) {
|
||||||
if (value.Equals("")) {
|
if (value.Equals("")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -72,22 +78,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
value += t;
|
value += t;
|
||||||
}
|
}
|
||||||
|
if (value.Equals("")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
return new AtomToken(value);
|
||||||
|
|
||||||
class OperatorToken : Token<string> {
|
|
||||||
private OperatorToken(string value) : base(value) {}
|
|
||||||
private static IToken<string>? take(CharStream program) {
|
|
||||||
if (program.Available() == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new OperatorToken(program.Get().ToString());
|
|
||||||
//char t = program.get();
|
|
||||||
//if ("+-*/%".Contains(t)) {
|
|
||||||
// return new OperatorToken(t.ToString());
|
|
||||||
//}
|
|
||||||
//return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +95,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
typeof(SpaceToken),
|
typeof(SpaceToken),
|
||||||
typeof(GroupingToken),
|
typeof(GroupingToken),
|
||||||
typeof(AtomToken),
|
typeof(AtomToken),
|
||||||
typeof(OperatorToken),
|
|
||||||
};
|
};
|
||||||
protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {}
|
protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {}
|
||||||
private static StringTokenStream generate(CharStream program) {
|
private static StringTokenStream generate(CharStream program) {
|
||||||
|
@ -111,7 +104,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
if (prev_avail == program.Available() && prev_avail == 0) {
|
if (prev_avail == program.Available() && prev_avail == 0) {
|
||||||
break;
|
break;
|
||||||
} else if (prev_avail == program.Available()) {
|
} else if (prev_avail == program.Available()) {
|
||||||
throw new ApplicationException("Program is invalid");
|
throw new ApplicationException($"Program is invalid, still available: {program.Available()}");
|
||||||
}
|
}
|
||||||
prev_avail = program.Available();
|
prev_avail = program.Available();
|
||||||
foreach (Type c in _classes) {
|
foreach (Type c in _classes) {
|
||||||
|
|
344
Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs
Normal file
344
Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,63 +3,19 @@ using System.Reflection;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
using Function = Func<IList<Expression>, Expression>;
|
using Function = Func<IEnumerable<Expression>, Expression>;
|
||||||
using FunctionLater = Func<Executor, IList<Expression>, Expression>;
|
using FunctionLater = Func<Executor, IEnumerable<Expression>, Expression>;
|
||||||
|
|
||||||
public class Procedure : Expression {
|
|
||||||
private Compiler.List _parameters;
|
|
||||||
private Expression _body;
|
|
||||||
public Procedure(Compiler.List parameters, Expression body) {
|
|
||||||
_parameters = parameters;
|
|
||||||
_body = body;
|
|
||||||
}
|
|
||||||
private static IEnumerable<(T1, T2)> Zip<T1, T2>(IEnumerable<T1> a, IEnumerable<T2> b) {
|
|
||||||
using (var e1 = a.GetEnumerator()) using (var e2 = b.GetEnumerator()) {
|
|
||||||
while (e1.MoveNext() && e2.MoveNext()) {
|
|
||||||
yield return (e1.Current, e2.Current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public override int GetHashCode() {
|
|
||||||
int hash = 17;
|
|
||||||
hash *= 23;
|
|
||||||
hash += _parameters.GetHashCode();
|
|
||||||
hash *= 23;
|
|
||||||
hash += _body.GetHashCode();
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
public override bool Equals(Expression? other) {
|
|
||||||
if (other is Procedure other_p) {
|
|
||||||
return _parameters == other_p._parameters && _body == other_p._body;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public override object Inner() {
|
|
||||||
throw new ApplicationException("This is not sensible");
|
|
||||||
}
|
|
||||||
public override string ToString() {
|
|
||||||
return $"(lambda {_parameters} {_body})";
|
|
||||||
}
|
|
||||||
|
|
||||||
public Expression Call(Executor e, IList<Expression> args) {
|
|
||||||
var p = _parameters.expressions.Select(x => x.Inner().ToString()).ToList();
|
|
||||||
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
|
||||||
foreach (var tuple in Zip<string, Expression>(p, args)) {
|
|
||||||
new_e.environment.Set(tuple.Item1, tuple.Item2);
|
|
||||||
}
|
|
||||||
return new_e.eval(_body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IEnvironment<K, V> {
|
public interface IEnvironment<K, V> {
|
||||||
public V Get(K k);
|
public V? Get(K k);
|
||||||
public void Set(K k, V v);
|
public void Set(K k, V v);
|
||||||
public IEnvironment<K, V>? Find(K k);
|
public IEnvironment<K, V>? Find(K k);
|
||||||
|
public IEnvironment<K, V> Parent(bool recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
|
public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
|
||||||
public Expression? Get(string k) {
|
public Expression? Get(string k) {
|
||||||
if (TryGetValue(k, out Expression v)) {
|
if (TryGetValue(k, out Expression? v)) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -74,11 +30,71 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnvironment<string, Expression> Parent(bool recursive) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DefaultEnvironment: Environment {
|
public class DefaultEnvironment: Environment {
|
||||||
public DefaultEnvironment() {
|
public DefaultEnvironment() {
|
||||||
this["find"] = new Parser("(lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))").parse();
|
var e = new Executor();
|
||||||
|
this["null"] = new Symbol("not");
|
||||||
|
this["list"] = e.eval("(lambda (. args) args)");
|
||||||
|
this["find"] = e.eval("(lambda (item list_) (if (null list_) nil (if (= item (car list_)) (car list_) (find item (cdr list_)))))");
|
||||||
|
this["map"] = e.eval("(lambda (fc l) (if (null l) nil (cons (fc (car l)) (map fc (cdr l)))))");
|
||||||
|
this["fold"] = e.eval("(lambda (fc i l) (if (null l) i (fold fc (fc i (car l)) (cdr l))))");
|
||||||
|
this["any"] = e.eval("(lambda (fc l) (apply or (map fc l)))");
|
||||||
|
this["all"] = e.eval("(lambda (fc l) (apply and (map fc l)))");
|
||||||
|
this["append"] = e.eval("(lambda (l i) (if (null l) i (cons (car l) (append (cdr l) i))))");
|
||||||
|
this["qsort"] = e.eval(
|
||||||
|
"""
|
||||||
|
(lambda
|
||||||
|
(fc list00)
|
||||||
|
(let
|
||||||
|
(getpivot
|
||||||
|
(lambda
|
||||||
|
(list0)
|
||||||
|
(car list0)))
|
||||||
|
(split
|
||||||
|
(lambda
|
||||||
|
(list0 pivot fc h0 h1)
|
||||||
|
(cond
|
||||||
|
((null list0) (list list0 pivot fc h0 h1))
|
||||||
|
((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
|
||||||
|
(t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
|
||||||
|
(sort
|
||||||
|
(lambda
|
||||||
|
(fc list0)
|
||||||
|
(cond
|
||||||
|
((null list0) nil)
|
||||||
|
((null (cdr list0)) list0)
|
||||||
|
(t
|
||||||
|
(let*
|
||||||
|
(halves (split list0 (getpivot list0) fc nil nil))
|
||||||
|
(h0 (car (cdr (cdr (cdr halves)))))
|
||||||
|
(h1 (car (cdr (cdr (cdr (cdr halves))))))
|
||||||
|
(append (sort fc h0) (sort fc h1)))))))
|
||||||
|
(sort fc list00)))
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +104,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
_super = super;
|
_super = super;
|
||||||
}
|
}
|
||||||
public Expression? Get(string k) {
|
public Expression? Get(string k) {
|
||||||
if (TryGetValue(k, out Expression v)) {
|
if (TryGetValue(k, out Expression? v)) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -103,272 +119,349 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
return _super.Find(k);
|
return _super.Find(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnvironment<string, Expression> Parent(bool recursive) {
|
||||||
|
if (recursive) {
|
||||||
|
return this._super.Parent(recursive);
|
||||||
|
}
|
||||||
|
return this._super;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Builtins : Dictionary<string, Function> {
|
public class Builtins : Dictionary<string, Function> {
|
||||||
|
|
||||||
|
private static Dictionary<string, Type?> ResolvedTypes = new Dictionary<string, Type?>();
|
||||||
|
private Random Random;
|
||||||
|
|
||||||
public Builtins() : base() {
|
public Builtins() : base() {
|
||||||
this["+"] = _add;
|
Random = new Random();
|
||||||
this["-"] = _sub;
|
this["atom"] = _atom;
|
||||||
this["*"] = _mul;
|
this["eq"] = _eq;
|
||||||
this["/"] = _div;
|
|
||||||
this["%"] = _mod;
|
|
||||||
this[">"] = _gt;
|
|
||||||
this["<"] = _lt;
|
|
||||||
this[">="] = _ge;
|
|
||||||
this["<="] = _le;
|
|
||||||
this["eq?"] = _eq;
|
|
||||||
this["="] = _eq;
|
|
||||||
this["!="] = _ne;
|
|
||||||
this["abs"] = _abs;
|
|
||||||
this["append"] = _append;
|
|
||||||
this["begin"] = _begin;
|
|
||||||
this["car"] = _car;
|
this["car"] = _car;
|
||||||
this["cdr"] = _cdr;
|
this["cdr"] = _cdr;
|
||||||
this["cons"] = _cons;
|
this["cons"] = _cons;
|
||||||
this["not"] = _not;
|
|
||||||
this["length"] = _length;
|
this["begin"] = _begin;
|
||||||
|
|
||||||
|
this["+"] = (x) => _agg((Integer a, Integer b) => a + b, x);
|
||||||
|
this["-"] = (x) => _agg((Integer a, Integer b) => a - b, x);
|
||||||
|
this["*"] = (x) => _agg((Integer a, Integer b) => a * b, x);
|
||||||
|
this["/"] = (x) => _agg((Integer a, Integer b) => a / b, x);
|
||||||
|
this["%"] = (x) => _agg((Integer a, Integer b) => a % b, x);
|
||||||
|
|
||||||
|
this["="] = (x) => _cmp((Atom a, Atom b) => (a == b)? Boolean.TRUE : Boolean.FALSE, x);
|
||||||
|
this["!="] = (x) => _cmp((Atom a, Atom b) => (a != b)? Boolean.TRUE : Boolean.FALSE, x);
|
||||||
|
this["<"] = (x) => _cmp((Integer a, Integer b) => a < b, x);
|
||||||
|
this["<="] = (x) => _cmp((Integer a, Integer b) => a <= b, x);
|
||||||
|
this[">"] = (x) => _cmp((Integer a, Integer b) => a > b, x);
|
||||||
|
this[">="] = (x) => _cmp((Integer a, Integer b) => a >= b, x);
|
||||||
|
this["not"] = (x) => {
|
||||||
|
return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
|
};
|
||||||
|
this["string="] = (x) => _cmp((String a, String b) => a == b, x);
|
||||||
|
this["string!="] = (x) => _cmp((String a, String b) => a != b, x);
|
||||||
|
this["string>"] = (x) => _cmp((String a, String b) => a > b, x);
|
||||||
|
this["string>="] = (x) => _cmp((String a, String b) => a >= b, x);
|
||||||
|
this["string<"] = (x) => _cmp((String a, String b) => a < b, x);
|
||||||
|
this["string<="] = (x) => _cmp((String a, String b) => a <= b, x);
|
||||||
|
|
||||||
this["haskeys"] = _haskeys;
|
this["haskeys"] = _haskeys;
|
||||||
this["getitems"] = _getitems;
|
this["getitems"] = _getitems;
|
||||||
this["invoke"] = _invoke;
|
this["invoke"] = _invoke;
|
||||||
}
|
this["invoke-generic"] = _invoke_generic;
|
||||||
|
|
||||||
private static T _agg<T>(Func<T, T, T> op, IList<T> args) {
|
this["random"] = (x) => new Lisp.Integer(Random.Next());
|
||||||
T agg = args[0];
|
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();
|
||||||
foreach (var arg in args.Skip(1)) {
|
foreach (var arg in args.Skip(1)) {
|
||||||
agg = op(agg, arg);
|
var arg_ = (T) arg;
|
||||||
|
agg = op(agg, arg_);
|
||||||
}
|
}
|
||||||
return agg;
|
return agg;
|
||||||
}
|
}
|
||||||
private static Expression _add(IList<Expression> args) {
|
private static E _cmp<T, E>(Func<T, T, E> op, IEnumerable<Expression> args) where T : Expression where E : Expression {
|
||||||
Expression first = args[0];
|
return op((T) args.First(), (T) args.Skip(1).First());
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _agg((a, b) => a + b, args.Select(x => (Integer) x).ToList());
|
|
||||||
case Compiler.String s:
|
|
||||||
return _agg((a, b) => a + b, args.Select(x => (Compiler.String) x).ToList());
|
|
||||||
}
|
}
|
||||||
throw new ApplicationException();
|
private static Expression _atom(IEnumerable<Expression> args) {
|
||||||
|
return (args.First() is Atom) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
}
|
}
|
||||||
private static Expression _sub(IList<Expression> args) {
|
private static Expression _eq(IEnumerable<Expression> args) {
|
||||||
Expression first = args[0];
|
return args.First().Equals(args.Skip(1).First()) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _agg((a, b) => a - b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
}
|
||||||
throw new ApplicationException();
|
private static Expression _car(IEnumerable<Expression> args) {
|
||||||
|
return ((Cons)args.First()).Item1;
|
||||||
}
|
}
|
||||||
private static Expression _mul(IList<Expression> args) {
|
private static Expression _cdr(IEnumerable<Expression> args) {
|
||||||
Expression first = args[0];
|
return ((Cons)args.First()).Item2;
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _agg((a, b) => a * b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
}
|
||||||
throw new ApplicationException();
|
private static Expression _cons(IEnumerable<Expression> args) {
|
||||||
|
return new Cons(args.First(), args.Skip(1).First());
|
||||||
}
|
}
|
||||||
private static Expression _div(IList<Expression> args) {
|
private static Expression _begin(IEnumerable<Expression> args) {
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _agg((a, b) => a / b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _mod(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _agg((a, b) => a % b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static E _cmp<T, E>(Func<T, T, E> op, IList<T> args) {
|
|
||||||
T first = args[0];
|
|
||||||
T second = args[1];
|
|
||||||
return op(first, second);
|
|
||||||
}
|
|
||||||
private static Expression _gt(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _cmp((a, b) => a > b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _lt(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _cmp((a, b) => a < b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _ge(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _cmp((a, b) => a >= b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _le(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return _cmp((a, b) => a <= b, args.Select(x => (Integer) x).ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _eq(IList<Expression> args) {
|
|
||||||
bool r = _cmp((a, b) => a == b, args);
|
|
||||||
return new Compiler.Boolean(r);
|
|
||||||
}
|
|
||||||
private static Expression _ne(IList<Expression> args) {
|
|
||||||
bool r = _cmp((a, b) => a != b, args);
|
|
||||||
return new Compiler.Boolean(r);
|
|
||||||
}
|
|
||||||
private static Expression _abs(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case Integer i:
|
|
||||||
return i.value >= 0 ? i : new Integer(-i.value);
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _append(IList<Expression> args) {
|
|
||||||
Expression first = args[0];
|
|
||||||
switch (first) {
|
|
||||||
case List l:
|
|
||||||
return l + new List(args);
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _begin(IList<Expression> args) {
|
|
||||||
return args.Last();
|
return args.Last();
|
||||||
}
|
}
|
||||||
private static Expression _car(IList<Expression> args) {
|
private static Expression _haskeys(IEnumerable<Expression> args) {
|
||||||
return ((List) args.First()).expressions.First();
|
Object o = new Object(((IInner) args.First()).Inner());
|
||||||
}
|
|
||||||
private static Expression _cdr(IList<Expression> args) {
|
|
||||||
return new List(((List) args.First()).expressions.Skip(1).ToList());
|
|
||||||
}
|
|
||||||
private static Expression _cons(IList<Expression> args) {
|
|
||||||
switch (args[1]) {
|
|
||||||
case Compiler.List other_list:
|
|
||||||
return (new Compiler.List(new []{args[0]}.ToList()) + new Compiler.List(other_list.expressions));
|
|
||||||
case Atom other_atom:
|
|
||||||
return new Compiler.List(new[]{args[0], args[1]}.ToList());
|
|
||||||
}
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
|
||||||
private static Expression _not(IList<Expression> args) {
|
|
||||||
if (args[0] == new Compiler.Boolean(false)) {
|
|
||||||
return new Compiler.Boolean(true);
|
|
||||||
}
|
|
||||||
return new Compiler.Boolean(false);
|
|
||||||
}
|
|
||||||
private static Expression _length(IList<Expression> args) {
|
|
||||||
return new Integer(((Compiler.List)args[0]).expressions.Count());
|
|
||||||
}
|
|
||||||
private static Expression _haskeys(IList<Expression> args) {
|
|
||||||
Compiler.Object o = (Compiler.Object) args[0];
|
|
||||||
foreach (var e in args.Skip(1)) {
|
foreach (var e in args.Skip(1)) {
|
||||||
Compiler.String s = (Compiler.String) e;
|
String s = (String) e;
|
||||||
PropertyInfo? pi = o.value.GetType().GetProperty(s.value);
|
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
|
||||||
if (pi != null) {
|
if (pi != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
|
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value());
|
||||||
if (mi != null) {
|
if (mi != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
FieldInfo? fi = o.value.GetType().GetField(s.value);
|
FieldInfo? fi = o.Value().GetType().GetField(s.Value());
|
||||||
if (fi != null) {
|
if (fi != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return new Compiler.Boolean(false);
|
return Boolean.FALSE;
|
||||||
}
|
}
|
||||||
return new Compiler.Boolean(true);
|
return Boolean.TRUE;
|
||||||
}
|
}
|
||||||
private static Expression _getitems(IList<Expression> args) {
|
private static Expression _getitems(IEnumerable<Expression> args) {
|
||||||
Compiler.Object o = (Compiler.Object) args[0];
|
Object o = new Object(((IInner) args.First()).Inner());
|
||||||
IList<Expression> r = new List<Expression>();
|
IList<Expression> r = new List<Expression>();
|
||||||
foreach (var e in args.Skip(1)) {
|
foreach (var e in args.Skip(1)) {
|
||||||
Compiler.String s = (Compiler.String) e;
|
String s = (String) e;
|
||||||
PropertyInfo? pi = o.value.GetType().GetProperty(s.value);
|
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
|
||||||
if (pi != null) {
|
if (pi != null) {
|
||||||
r.Add(Compiler.Object.FromBase(pi.GetValue(o.value)));
|
r.Add(Object.FromBase(pi.GetValue(o.Value())));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
FieldInfo? fi = o.value.GetType().GetField(s.value);
|
FieldInfo? fi = o.Value().GetType().GetField(s.Value());
|
||||||
if (fi != null) {
|
if (fi != null) {
|
||||||
r.Add(Compiler.Object.FromBase(fi.GetValue(o.value)));
|
r.Add(Object.FromBase(fi.GetValue(o.Value())));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new ApplicationException($"{o.value} has no property or field {s.value}");
|
throw new ApplicationException($"{o.Value()} has no property or field {s.Value()}");
|
||||||
}
|
}
|
||||||
return new Compiler.List(r);
|
return Cons.FromList(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression _invoke(IList<Expression> args) {
|
private static Expression _invoke(IEnumerable<Expression> args) {
|
||||||
Compiler.Object o = (Compiler.Object) args[0];
|
Object o = new Object(((IInner) args.First()).Inner());
|
||||||
Compiler.String s = (Compiler.String) args[1];
|
String s = (String) args.Skip(1).First();
|
||||||
Compiler.List l = (Compiler.List) args[2];
|
IEnumerable<Expression> l;
|
||||||
IList<Expression> r = new List<Expression>();
|
if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) {
|
||||||
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
|
l = new List<Expression>();
|
||||||
if (mi == null) {
|
} else if (args.Skip(2).First() is Cons lc) {
|
||||||
throw new ApplicationException($"{o.value} has not method {s.value}");
|
l = lc.ToList();
|
||||||
|
} else {
|
||||||
|
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
|
||||||
}
|
}
|
||||||
return Compiler.Object.FromBase(mi.Invoke(o.value, (object?[]?) l.Inner()));
|
object[]? l_ = l.Select<Expression, object>(x => {
|
||||||
|
switch (x) {
|
||||||
|
case Integer s:
|
||||||
|
return s.Value();
|
||||||
|
case Boolean b:
|
||||||
|
return b.Value();
|
||||||
|
case String s:
|
||||||
|
return s.Value();
|
||||||
|
case Object o:
|
||||||
|
return o.Value();
|
||||||
|
case Cons c:
|
||||||
|
return c.ToList().ToList();
|
||||||
|
}
|
||||||
|
throw new ApplicationException($"Unhandled value {x} (type {x.GetType()})");
|
||||||
|
}).ToArray();
|
||||||
|
Type[] l_types = l_.Select( x => {
|
||||||
|
return x.GetType();
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
IList<Expression> r = new List<Expression>();
|
||||||
|
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
|
||||||
|
if (mi == null) {
|
||||||
|
throw new ApplicationException($"{o.Value()} has no 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_));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
||||||
public BuiltinsLater() : base() {
|
public BuiltinsLater() : base() {
|
||||||
|
this["quote"] = _quote;
|
||||||
|
this["eval"] = _eval;
|
||||||
|
this["cond"] = _cond;
|
||||||
this["if"] = _if;
|
this["if"] = _if;
|
||||||
this["define"] = _define;
|
this["define"] = _define;
|
||||||
|
this["let"] = _let;
|
||||||
|
this["let*"] = _let_star;
|
||||||
this["lambda"] = _lambda;
|
this["lambda"] = _lambda;
|
||||||
|
this["lambda*"] = _lambda_star;
|
||||||
this["apply"] = _apply;
|
this["apply"] = _apply;
|
||||||
this["and"] = _and;
|
|
||||||
this["or"] = _or;
|
this["and"] = (e, x) => {
|
||||||
|
Expression? r = null;
|
||||||
|
foreach (var xi in x) {
|
||||||
|
r = e.eval(xi);
|
||||||
|
if (r == Boolean.FALSE) {
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
private static Expression _if(Executor e, IList<Expression> args) {
|
|
||||||
bool test = e.eval(args[0]) != (new Compiler.Boolean(false));
|
|
||||||
return e.eval(args[1 + (test ? 0 : 1)]);
|
|
||||||
}
|
}
|
||||||
private static Expression _define(Executor e, IList<Expression> args) {
|
if (r is null) {
|
||||||
var refname = ((Symbol) args[0]).name;
|
return Boolean.FALSE;
|
||||||
e.environment.Set(refname, e.eval(args[1]));
|
|
||||||
return new Compiler.Boolean(false); // NOOP
|
|
||||||
}
|
}
|
||||||
private static Expression _lambda(Executor e, IList<Expression> args) {
|
return r;
|
||||||
return new Procedure((Compiler.List) args[0], args[1]);
|
};
|
||||||
|
this["or"] = (e, x) => {
|
||||||
|
foreach (var xi in x) {
|
||||||
|
var r = e.eval(xi);
|
||||||
|
if (r != Boolean.FALSE) {
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
private static Expression _apply(Executor e, IList<Expression> args) {
|
|
||||||
if (args[0].GetType() != typeof(Symbol)) {
|
|
||||||
throw new ApplicationException();
|
|
||||||
}
|
}
|
||||||
if (args[1].GetType() != typeof(List)) {
|
return Boolean.FALSE;
|
||||||
throw new ApplicationException();
|
};
|
||||||
}
|
}
|
||||||
Symbol arg0 = (Compiler.Symbol) args[0];
|
private static Expression _quote(Executor e, IEnumerable<Expression> args) {
|
||||||
Compiler.List other_args = (Compiler.List) args[1];
|
return args.First();
|
||||||
return e.EvalFunction(arg0, other_args.expressions);
|
|
||||||
}
|
}
|
||||||
private static Expression _and(Executor e, IList<Expression> args) {
|
private static Expression _eval(Executor e, IEnumerable<Expression> args) {
|
||||||
Expression result = new Compiler.Boolean(false);
|
return e.eval(e.eval(args.First()));
|
||||||
foreach (var exp in args) {
|
|
||||||
result = e.eval(exp);
|
|
||||||
if (result == new Compiler.Boolean(false)) { return result; }
|
|
||||||
}
|
}
|
||||||
return result;
|
private static Expression _cond(Executor e, IEnumerable<Expression> args) {
|
||||||
|
foreach (var a in args) {
|
||||||
|
if (a is Cons a_cons) {
|
||||||
|
var a_ = a_cons.ToList();
|
||||||
|
if (!e.eval(a_.First()).Equals(Boolean.FALSE)) {
|
||||||
|
return e.eval(a_.Skip(1).First());
|
||||||
}
|
}
|
||||||
private static Expression _or(Executor e, IList<Expression> args) {
|
} else {
|
||||||
Expression result = new Compiler.Boolean(false);
|
throw new ApplicationException($"Incorrect arguments to cond, expected list: {args}");
|
||||||
foreach (var exp in args) {
|
|
||||||
result = e.eval(exp);
|
|
||||||
if (result != new Compiler.Boolean(false)) { return result; }
|
|
||||||
}
|
}
|
||||||
return result;
|
}
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
private static Expression _if(Executor e, IEnumerable<Expression> args) {
|
||||||
|
if (e.eval(args.First()).Equals(Boolean.FALSE)) {
|
||||||
|
return e.eval(args.Skip(2).First());
|
||||||
|
}
|
||||||
|
return e.eval(args.Skip(1).First());
|
||||||
|
}
|
||||||
|
private static Expression _define(Executor e, IEnumerable<Expression> args) {
|
||||||
|
Symbol refname = (Symbol) args.First();
|
||||||
|
e.environment.Parent(true).Set(refname.Name(), args.Skip(1).Select(x => e.eval(x)).First());
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
private static Expression _let_star(Executor e, IEnumerable<Expression> args) {
|
||||||
|
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
||||||
|
foreach (var pair in args.SkipLast(1)) {
|
||||||
|
if (pair is not Cons pair_cons) {
|
||||||
|
throw new ApplicationException("No expression for let*");
|
||||||
|
}
|
||||||
|
Symbol refname = (Symbol) pair_cons.Item1;
|
||||||
|
Expression exp = ((Cons) pair_cons.Item2).Item1;
|
||||||
|
new_e.environment.Set(refname.Name(), new_e.eval(exp));
|
||||||
|
}
|
||||||
|
return new_e.eval(args.Last());
|
||||||
|
}
|
||||||
|
private static Expression _let(Executor e, IEnumerable<Expression> args) {
|
||||||
|
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
||||||
|
List<(Symbol, Expression)> vars = new List<(Symbol, Expression)>();
|
||||||
|
foreach (var pair in args.SkipLast(1)) {
|
||||||
|
if (pair is not Cons pair_cons) {
|
||||||
|
throw new ApplicationException("");
|
||||||
|
}
|
||||||
|
Symbol refname = (Symbol) pair_cons.Item1;
|
||||||
|
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
|
||||||
|
vars.Add((refname, e.eval(exp_)));
|
||||||
|
}
|
||||||
|
foreach (var pair in vars) {
|
||||||
|
new_e.environment.Set(pair.Item1.Name(), pair.Item2);
|
||||||
|
}
|
||||||
|
return new_e.eval(args.Last());
|
||||||
|
}
|
||||||
|
private static Expression _lambda(Executor e, IEnumerable<Expression> args) {
|
||||||
|
IEnumerable<Symbol> proc_args;
|
||||||
|
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
||||||
|
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||||
|
else {
|
||||||
|
throw new ApplicationException("");
|
||||||
|
}
|
||||||
|
return new Procedure(proc_args, args.Skip(1).First(), true);
|
||||||
|
}
|
||||||
|
private static Expression _lambda_star(Executor e, IEnumerable<Expression> args) {
|
||||||
|
IEnumerable<Symbol> proc_args;
|
||||||
|
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
||||||
|
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||||
|
else {
|
||||||
|
throw new ApplicationException("");
|
||||||
|
}
|
||||||
|
return new Procedure(proc_args, args.Skip(1).First(), false);
|
||||||
|
}
|
||||||
|
private static Expression _apply(Executor e, IEnumerable<Expression> args) {
|
||||||
|
return e.eval(new Cons(args.First(), e.eval(args.Skip(1).First())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,54 +489,55 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
public Builtins builtins { get => _builtins; }
|
public Builtins builtins { get => _builtins; }
|
||||||
public BuiltinsLater builtinsLater { get => _builtinsLater; }
|
public BuiltinsLater builtinsLater { get => _builtinsLater; }
|
||||||
|
|
||||||
public Expression EvalFunction(Symbol fcname, IList<Expression> args) {
|
public Expression? EvalFunction(Symbol fcname, IEnumerable<Expression> args) {
|
||||||
if (_environment.Find(fcname.name) is IEnvironment<string, Expression> _e) {
|
if (builtins.ContainsKey(fcname.Name())) {
|
||||||
Expression? first = _e.Get(fcname.name);
|
return builtins[fcname.Name()](args.Select(x => eval(x)).ToList()); // call ToList for sideeffect
|
||||||
return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList());
|
|
||||||
}
|
}
|
||||||
if (_builtins.ContainsKey(fcname.name)) {
|
if (builtinsLater.ContainsKey(fcname.Name())) {
|
||||||
Function fc = _builtins[fcname.name];
|
return builtinsLater[fcname.Name()](this, args);
|
||||||
return fc(args.Select(x => eval(x)).ToList());
|
|
||||||
}
|
}
|
||||||
if (_builtinsLater.ContainsKey(fcname.name)) {
|
return null;
|
||||||
FunctionLater fc = _builtinsLater[fcname.name];
|
|
||||||
return fc(this, args);
|
|
||||||
}
|
|
||||||
throw new ApplicationException($"Key '{fcname.name}' not found in environment or builtins");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Expression eval(Expression expression) {
|
public Expression eval(Expression expression) {
|
||||||
switch (expression) {
|
switch (expression) {
|
||||||
case Symbol s:
|
case Symbol s:
|
||||||
return _environment.Find(s.name).Get(s.name);
|
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
|
||||||
case Compiler.Boolean b:
|
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 b;
|
return b;
|
||||||
case Integer i:
|
case Integer i:
|
||||||
return i;
|
return i;
|
||||||
case Compiler.String s:
|
case String s:
|
||||||
return s;
|
return s;
|
||||||
case Compiler.Object o:
|
case Object o:
|
||||||
return o;
|
return o;
|
||||||
case Procedure p:
|
case Procedure p:
|
||||||
return p;
|
return p;
|
||||||
case List list:
|
case Cons cons:
|
||||||
if (list.expressions.Count == 0) {
|
var l = cons.ToList();
|
||||||
return list;
|
if (cons.Item1 is Symbol cons_item1_symbol) {
|
||||||
|
Expression? r = EvalFunction(cons_item1_symbol, l.Skip(1));
|
||||||
|
if (r is not null) { return r; }
|
||||||
}
|
}
|
||||||
// do we really want to allow shadowing of builtins?
|
var eval_Item1 = eval(cons.Item1);
|
||||||
if (list.expressions[0].GetType() == typeof(Symbol)) {
|
if (eval_Item1 is Symbol eval_item1_symbol1) {
|
||||||
return eval(EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList()));
|
Expression? r = EvalFunction(eval_item1_symbol1, l.Skip(1));
|
||||||
|
if (r is not null) { return r; }
|
||||||
}
|
}
|
||||||
if (list.expressions[0].GetType() == typeof(Procedure)) {
|
if (eval_Item1 is Procedure eval_item1_procedure) {
|
||||||
Procedure procedure = (Procedure) list.expressions[0];
|
return eval_item1_procedure.Call(this, l.Skip(1).Select(x => x).ToList());
|
||||||
return eval(procedure.Call(this, list.expressions.Skip(1).ToList()));
|
|
||||||
}
|
}
|
||||||
var l = new List(list.expressions.Select(x => eval(x)).ToList());
|
throw new ApplicationException($"Not handled case (type = {eval_Item1.GetType()}) '{cons}'");
|
||||||
if (l.expressions[0].GetType() == typeof(Procedure)) {
|
|
||||||
return eval(l);
|
|
||||||
}
|
|
||||||
return l;
|
|
||||||
}
|
}
|
||||||
throw new ApplicationException($"Not handled case '{expression}'");
|
throw new ApplicationException($"Not handled case '{expression}'");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
|
||||||
|
public IServerApplicationPaths ServerApplicationPaths;
|
||||||
|
public IUserManager UserManager;
|
||||||
public Plugin(
|
public Plugin(
|
||||||
IApplicationPaths applicationPaths,
|
IApplicationPaths applicationPaths,
|
||||||
IXmlSerializer xmlSerializer
|
IXmlSerializer xmlSerializer,
|
||||||
|
IServerApplicationPaths serverApplicationPaths,
|
||||||
|
IUserManager userManager
|
||||||
) : base (applicationPaths, xmlSerializer) {
|
) : base (applicationPaths, xmlSerializer) {
|
||||||
Instance = this;
|
Instance = this;
|
||||||
|
ServerApplicationPaths = serverApplicationPaths;
|
||||||
|
UserManager = userManager;
|
||||||
}
|
}
|
||||||
public static Plugin? Instance {get; private set; }
|
public static Plugin? Instance {get; private set; }
|
||||||
public override string Name => "Smart Playlist";
|
public override string Name => "Smart Playlist";
|
||||||
|
|
|
@ -1,14 +1,72 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Plugins;
|
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Controller;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public class PluginConfiguration : BasePluginConfiguration {
|
public class PluginConfiguration : BasePluginConfiguration {
|
||||||
public PluginConfiguration(
|
public PluginConfiguration() {
|
||||||
) {
|
InitialProgram = """
|
||||||
|
(begin
|
||||||
|
(define lower
|
||||||
|
(lambda (s)
|
||||||
|
(invoke s "ToLower" nil)))
|
||||||
|
(define is-genre
|
||||||
|
(lambda (g g-list)
|
||||||
|
(any
|
||||||
|
(lambda (x)
|
||||||
|
(invoke (lower x) "Contains" (list (lower g))))
|
||||||
|
g-list)))
|
||||||
|
(define is-genre-exact
|
||||||
|
(lambda (g g-list)
|
||||||
|
(find g g-list)))
|
||||||
|
(define genre-list
|
||||||
|
(lambda nil
|
||||||
|
(let
|
||||||
|
(_g (getitems *item* "Genres"))
|
||||||
|
(if (null _g)
|
||||||
|
nil
|
||||||
|
(car _g)))))
|
||||||
|
(define is-favorite
|
||||||
|
(lambda nil
|
||||||
|
(invoke *item* "IsFavoriteOrLiked" (list *user*))))
|
||||||
|
(define is-type
|
||||||
|
(lambda (x)
|
||||||
|
(and
|
||||||
|
(haskeys *item* "GetClientTypeName")
|
||||||
|
(invoke (invoke *item* "GetClientTypeName" nil) "Equals" (list x)))))
|
||||||
|
(define name-contains
|
||||||
|
(lambda (x)
|
||||||
|
(invoke (lower (car (getitems *item* "Name"))) "Contains" (list (lower x)))))
|
||||||
|
(define is-favourite is-favorite)
|
||||||
|
(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 { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -5,28 +6,16 @@ using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Playlists;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Playlists;
|
using MediaBrowser.Model.Playlists;
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object;
|
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
|
||||||
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
|
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
|
||||||
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
|
@ -93,7 +82,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SmartPlaylistId CreateNewPlaylist(string name, UserId userId) {
|
private PlaylistId CreateNewPlaylist(string name, UserId userId) {
|
||||||
_logger.LogDebug("Creating playlist '{0}'", name);
|
_logger.LogDebug("Creating playlist '{0}'", name);
|
||||||
var req = new PlaylistCreationRequest {
|
var req = new PlaylistCreationRequest {
|
||||||
Name = name,
|
Name = name,
|
||||||
|
@ -105,21 +94,66 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
return playlistGuid;
|
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) {
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
||||||
List<Guid> results = new List<Guid>();
|
List<BaseItem> results = new List<BaseItem>();
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
||||||
Executor executor = new Executor(new DefaultEnvironment());
|
Executor executor = SetupExecutor();
|
||||||
executor.environment.Set("user", new Lisp_Object(user));
|
|
||||||
|
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
||||||
foreach (var i in items) {
|
foreach (var i in items) {
|
||||||
executor.environment.Set("item", new Lisp_Object(i));
|
executor.environment.Set("*item*", Lisp_Object.FromBase(i));
|
||||||
var r = executor.eval(expression);
|
var r = executor.eval(expression);
|
||||||
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
||||||
if ((r is not Lisp_Boolean r_bool) || (r_bool.value)) {
|
if ((r is not Lisp_Boolean r_bool) || (r_bool.Value())) {
|
||||||
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
||||||
results.Add(i.Id);
|
results.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
executor = 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
|
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
|
||||||
|
@ -132,7 +166,17 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
|
|
||||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
||||||
_logger.LogInformation("Started regenerate Smart Playlists");
|
_logger.LogInformation("Started regenerate Smart Playlists");
|
||||||
foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) {
|
_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;
|
||||||
|
}
|
||||||
var changedDto = false;
|
var changedDto = false;
|
||||||
if (dto.Playlists.Length == 0) {
|
if (dto.Playlists.Length == 0) {
|
||||||
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
||||||
|
@ -167,6 +211,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
await ClearPlaylist(playlist);
|
await ClearPlaylist(playlist);
|
||||||
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
||||||
}
|
}
|
||||||
|
i += 1;
|
||||||
|
progress.Report(100 * ((double)i)/all_playlists.Count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +222,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
throw new ArgumentException("");
|
throw new ArgumentException("");
|
||||||
}
|
}
|
||||||
var existingItems = playlist_new.GetManageableItems().ToList();
|
var existingItems = playlist_new.GetManageableItems().ToList();
|
||||||
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id));
|
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,19 +41,22 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class SmartPlaylistDto : ISerializable {
|
public class SmartPlaylistDto : ISerializable {
|
||||||
private static string DEFAULT_PROGRAM = "(begin (invoke item 'IsFavoriteOrLiked' (user)))";
|
private static string DEFAULT_PROGRAM = "(begin (invoke item \"IsFavoriteOrLiked\" (list *user*)))";
|
||||||
|
private static string DEFAULT_SORT_PROGRAM = "(begin *items*)";
|
||||||
public SmartPlaylistId Id { get; set; }
|
public SmartPlaylistId Id { get; set; }
|
||||||
public SmartPlaylistLinkDto[] Playlists { get; set; }
|
public SmartPlaylistLinkDto[] Playlists { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Program { get; set; }
|
public string Program { get; set; }
|
||||||
|
public string SortProgram { get; set; }
|
||||||
public string? Filename { get; set; }
|
public string? Filename { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
public SmartPlaylistDto() {
|
public SmartPlaylistDto() {
|
||||||
Id = Guid.NewGuid();
|
Id = "";
|
||||||
Playlists = [];
|
Playlists = [];
|
||||||
Name = Id.ToString();
|
Name = Id.ToString();
|
||||||
Program = DEFAULT_PROGRAM;
|
Program = DEFAULT_PROGRAM;
|
||||||
|
SortProgram = DEFAULT_SORT_PROGRAM;
|
||||||
Filename = null;
|
Filename = null;
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
if (info.GetValue("Id", typeof(SmartPlaylistId)) is SmartPlaylistId _Id) {
|
if (info.GetValue("Id", typeof(SmartPlaylistId)) is SmartPlaylistId _Id) {
|
||||||
Id = _Id;
|
Id = _Id;
|
||||||
} else {
|
} else {
|
||||||
Id = Guid.NewGuid();
|
Id = "";
|
||||||
}
|
}
|
||||||
if (info.GetValue("Playlists", typeof(SmartPlaylistLinkDto[])) is SmartPlaylistLinkDto[] _Playlists) {
|
if (info.GetValue("Playlists", typeof(SmartPlaylistLinkDto[])) is SmartPlaylistLinkDto[] _Playlists) {
|
||||||
Playlists = _Playlists;
|
Playlists = _Playlists;
|
||||||
|
@ -79,6 +82,11 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
} else {
|
} else {
|
||||||
Program = DEFAULT_PROGRAM;
|
Program = DEFAULT_PROGRAM;
|
||||||
}
|
}
|
||||||
|
if (info.GetValue("SortProgram", typeof(string)) is string _SortProgram) {
|
||||||
|
SortProgram = _SortProgram;
|
||||||
|
} else {
|
||||||
|
SortProgram = DEFAULT_SORT_PROGRAM;
|
||||||
|
}
|
||||||
if (info.GetValue("Filename", typeof(string)) is string _Filename) {
|
if (info.GetValue("Filename", typeof(string)) is string _Filename) {
|
||||||
Filename = _Filename;
|
Filename = _Filename;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,13 +16,21 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
}
|
}
|
||||||
public string StoragePath { get; }
|
public string StoragePath { get; }
|
||||||
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
||||||
return Path.Combine(StoragePath, $"{smartPlaylistId}.json");
|
return Path.Combine(StoragePath, $"{smartPlaylistId}.yaml");
|
||||||
}
|
}
|
||||||
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
||||||
return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories).First();
|
return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yaml", SearchOption.AllDirectories).Concat(
|
||||||
|
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yml", SearchOption.AllDirectories)
|
||||||
|
).Concat(
|
||||||
|
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories)
|
||||||
|
).First();
|
||||||
}
|
}
|
||||||
public string[] FindAllSmartPlaylistFilePaths() {
|
public string[] FindAllSmartPlaylistFilePaths() {
|
||||||
return Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories);
|
return Directory.GetFiles(StoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
|
||||||
|
Directory.GetFiles(StoragePath, "*.yml", SearchOption.AllDirectories)
|
||||||
|
).Concat(
|
||||||
|
Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories)
|
||||||
|
).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
using System.Text.Json;
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public interface IStore {
|
public interface IStore {
|
||||||
Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
||||||
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
||||||
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
||||||
|
void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId);
|
||||||
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,12 +15,24 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
}
|
}
|
||||||
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
|
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
|
||||||
await using var r = File.OpenRead(filename);
|
var r = await File.ReadAllTextAsync(filename);
|
||||||
var dto = (await JsonSerializer.DeserializeAsync<SmartPlaylistDto>(r).ConfigureAwait(false));
|
if (r.Equals("")) {
|
||||||
if (dto == null) {
|
r = "{}";
|
||||||
|
}
|
||||||
|
var dto = new DeserializerBuilder().Build().Deserialize<SmartPlaylistDto>(r);
|
||||||
|
if (dto == null)
|
||||||
|
{
|
||||||
throw new ApplicationException("");
|
throw new ApplicationException("");
|
||||||
}
|
}
|
||||||
|
if (dto.Id != Path.GetFileNameWithoutExtension(filename)) {
|
||||||
|
dto.Id = Path.GetFileNameWithoutExtension(filename);
|
||||||
|
}
|
||||||
|
if (dto.Name != Path.GetFileNameWithoutExtension(filename)) {
|
||||||
|
dto.Name = Path.GetFileNameWithoutExtension(filename);
|
||||||
|
}
|
||||||
|
if (dto.Filename != filename) {
|
||||||
dto.Filename = filename;
|
dto.Filename = filename;
|
||||||
|
}
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
public async Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
|
public async Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
|
||||||
|
@ -33,10 +46,10 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
}
|
}
|
||||||
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
|
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
|
||||||
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
|
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
|
||||||
await using var w = File.Create(filename);
|
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
|
||||||
await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false);
|
await File.WriteAllTextAsync(filename, text);
|
||||||
}
|
}
|
||||||
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
public void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
||||||
try {
|
try {
|
||||||
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
||||||
if (File.Exists(filename)) { File.Delete(filename); }
|
if (File.Exists(filename)) { File.Delete(filename); }
|
||||||
|
|
|
@ -2,4 +2,4 @@ global using System;
|
||||||
|
|
||||||
global using UserId = System.Guid;
|
global using UserId = System.Guid;
|
||||||
global using PlaylistId = System.Guid;
|
global using PlaylistId = System.Guid;
|
||||||
global using SmartPlaylistId = System.Guid;
|
global using SmartPlaylistId = string;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Smart Playlist
|
name: Smart Playlist
|
||||||
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
||||||
version: 0.1.1.0
|
version: 0.4.0.0
|
||||||
targetAbi: 10.10.0.0
|
targetAbi: 10.10.3.0
|
||||||
framework: net8.0
|
framework: net8.0
|
||||||
owner: redxef
|
owner: redxef
|
||||||
overview: Smart playlists with Lisp filter engine.
|
overview: Smart playlists with Lisp filter engine.
|
||||||
|
@ -12,5 +12,68 @@ description: |
|
||||||
category: "General"
|
category: "General"
|
||||||
artifacts:
|
artifacts:
|
||||||
- jellyfin-smart-playlist.dll
|
- jellyfin-smart-playlist.dll
|
||||||
|
- YamlDotNet.dll
|
||||||
changelog: |
|
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.
|
- Initial Alpha release.
|
||||||
|
|
|
@ -2,35 +2,47 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Template</title>
|
<title>SmartPlaylist</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||||
<div data-role="content">
|
<div data-role="content">
|
||||||
<div class="content-primary">
|
<div class="content-primary">
|
||||||
<form id="TemplateConfigForm">
|
<form id="SmartPlaylistConfigForm">
|
||||||
<div class="selectContainer">
|
<div class="inputContainer">
|
||||||
<label class="selectLabel" for="Options">Several Options</label>
|
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
|
||||||
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
|
<div class="fieldDescription">A program which can set up the environment</div>
|
||||||
<option id="optOneOption" value="OneOption">One Option</option>
|
<textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" cols="120"></textarea>
|
||||||
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
|
</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">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
|
<label class="inputLabel inputLabelUnfocused" for="SmartPlaylistEditEnabled">Enabled</label>
|
||||||
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
|
<div class="fieldDescription">Is the playlist enabled.</div>
|
||||||
<div class="fieldDescription">A Description</div>
|
<input id="SmartplaylistEditEnabled" type="checkbox" class="emby-input"/>
|
||||||
</div>
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label class="emby-checkbox-label">
|
|
||||||
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
|
|
||||||
<span>A Checkbox</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="AString">A String</label>
|
|
||||||
<input id="AString" name="AString" type="text" is="emby-input" />
|
|
||||||
<div class="fieldDescription">Another Description</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
@ -40,36 +52,146 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartplaylist-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var TemplateConfig = {
|
var SmartPlaylistConfig = {
|
||||||
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelector('#TemplateConfigPage')
|
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')
|
||||||
.addEventListener('pageshow', function() {
|
.addEventListener('pageshow', function() {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
||||||
document.querySelector('#Options').value = config.Options;
|
document.querySelector('#InitialProgram').value = config.InitialProgram;
|
||||||
document.querySelector('#AnInteger').value = config.AnInteger;
|
fillPlaylistSelect(config);
|
||||||
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
|
changeEditBox(config, (config.Playlists.length > 0) ? config.Playlists[0].Id : null);
|
||||||
document.querySelector('#AString').value = config.AString;
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#TemplateConfigForm')
|
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')
|
||||||
.addEventListener('submit', function(e) {
|
.addEventListener('submit', function(e) {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
||||||
config.Options = document.querySelector('#Options').value;
|
config.InitialProgram = document.querySelector('#InitialProgram').value;
|
||||||
config.AnInteger = document.querySelector('#AnInteger').value;
|
const selection = document.querySelector('#SmartplaylistSelection');
|
||||||
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
|
const editName = document.querySelector('#SmartplaylistEditName');
|
||||||
config.AString = document.querySelector('#AString').value;
|
const editProgram = document.querySelector('#SmartplaylistEditProgram');
|
||||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
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) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(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();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,15 +2,21 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>jellyfin_smart_playlist</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.1.1.0</Version>
|
<Version>0.4.0.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.0" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="configPage.html"/>
|
||||||
|
<EmbeddedResource Include="configPage.html"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
115
README.md
115
README.md
|
@ -2,78 +2,67 @@
|
||||||
|
|
||||||
Smart playlists with Lisp filter engine.
|
Smart playlists with Lisp filter engine.
|
||||||
|
|
||||||
|
This readme contains instructions for the most recent changes in
|
||||||
|
the development branch (`main`). To view the file appropriate
|
||||||
|
for your version select the tag corresponding to your version.
|
||||||
|
The latest version is [v0.4.0.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.4.0.0).
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
After [installing](#installation) the plugin and restarting Jellyfin
|
After [installing](#installation) the plugin and restarting Jellyfin
|
||||||
create a file in `config/data/smartplaylists` like this:
|
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.
|
||||||
|
|
||||||
```
|
[Go here](examples.md) to see some example configurations.
|
||||||
$ echo '{}' > config/data/smartplaylists/a.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterwards run the Task `(re)generate Smart Playlists`, this will rename
|
Below are all the configuration values for a smart playlist.
|
||||||
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
|
### Name
|
||||||
|
|
||||||
The name of the generated playlists, this is just a default value.
|
The name of the generated playlists, this is just a default value.
|
||||||
If the user changes the name of their playlist the plugin will
|
If the user changes the name of their playlist the plugin will
|
||||||
work as usual
|
still work and remember the correct playlist.
|
||||||
|
|
||||||
### Program
|
### Program
|
||||||
|
|
||||||
A lisp program to decide on a per item basis if it should be included in
|
A lisp program to decide on a per item basis if it should be included in
|
||||||
the playlist, return `nil` to not include items, return `t` to include
|
the playlist, return `nil` to not include items, return any other value
|
||||||
them.
|
to include them. Global variables `*user*` and `*item*` are predefined
|
||||||
|
and contain a [User](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Entities/User.cs) and
|
||||||
|
[BaseItem](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Controller/Entities/BaseItem.cs)
|
||||||
|
respectively.
|
||||||
|
|
||||||
### Filename
|
**!!! 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 !!!**
|
||||||
|
|
||||||
The path to this file.
|
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*)
|
||||||
|
```
|
||||||
|
|
||||||
### Enabled
|
### Enabled
|
||||||
|
|
||||||
|
@ -89,3 +78,17 @@ to Jellyfin:
|
||||||
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
|
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
|
||||||
the field labeled `Repository URL`, give the plugin a descriptive name
|
the field labeled `Repository URL`, give the plugin a descriptive name
|
||||||
too.
|
too.
|
||||||
|
|
||||||
|
## Releasing a new version
|
||||||
|
|
||||||
|
1. Write the changelog: `git log --oneline $prev_version..`
|
||||||
|
2. Update the following files to include up-to-date version numbers
|
||||||
|
and changelogs, if applicable:
|
||||||
|
- `README.md`
|
||||||
|
- `Jellyfin.Plugin.SmartPlaylist/build.yaml`
|
||||||
|
- `Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj`
|
||||||
|
Don't forget to also bump the ABI version of Jellyfin.
|
||||||
|
3. Push the changes
|
||||||
|
4. Create a new release with the changelog, mark as pre-release if
|
||||||
|
applicable.
|
||||||
|
5. Done! The build pipeline will do the rest.
|
||||||
|
|
315
Tests/Tests.cs
315
Tests/Tests.cs
|
@ -1,7 +1,6 @@
|
||||||
using Xunit;
|
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
|
||||||
using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment;
|
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
|
||||||
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
|
using Lisp_Integer = Jellyfin.Plugin.SmartPlaylist.Lisp.Integer;
|
||||||
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object;
|
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
|
|
||||||
|
@ -16,45 +15,45 @@ namespace Tests
|
||||||
}
|
}
|
||||||
public int i { get => _i; }
|
public int i { get => _i; }
|
||||||
public bool b { get => _b; }
|
public bool b { get => _b; }
|
||||||
|
public int I() {
|
||||||
|
return _i;
|
||||||
|
}
|
||||||
|
public string G<E>() {
|
||||||
|
return typeof(E).FullName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Test {
|
public class Test {
|
||||||
[Fact]
|
[Fact]
|
||||||
public static void TestTokenizer() {
|
public static void TestTokenizer() {
|
||||||
StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1 >= ++ !=)");
|
StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1 >= ++ !=)");
|
||||||
Assert.Equal(sts.Get().value, "(");
|
Assert.Equal("(", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "\"");
|
Assert.Equal("\"", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "some");
|
Assert.Equal("some", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "literal");
|
Assert.Equal("literal", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "string");
|
Assert.Equal("string", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "\"");
|
Assert.Equal("\"", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "def");
|
Assert.Equal("def", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "ghj");
|
Assert.Equal("ghj", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "+");
|
Assert.Equal("+100", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "100");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal("-+300", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "-");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "+");
|
Assert.Equal("1", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "300");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(">=", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "1");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal("++", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, ">");
|
Assert.Equal(" ", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "=");
|
Assert.Equal("!=", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, " ");
|
Assert.Equal(")", sts.Get().value);
|
||||||
Assert.Equal(sts.Get().value, "+");
|
|
||||||
Assert.Equal(sts.Get().value, "+");
|
|
||||||
Assert.Equal(sts.Get().value, " ");
|
|
||||||
Assert.Equal(sts.Get().value, "!");
|
|
||||||
Assert.Equal(sts.Get().value, "=");
|
|
||||||
Assert.Equal(sts.Get().value, ")");
|
|
||||||
sts.Commit();
|
sts.Commit();
|
||||||
Assert.Equal(sts.Available(), 0);
|
Assert.Equal(0, sts.Available());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -69,115 +68,205 @@ namespace Tests
|
||||||
p = new Parser(sts);
|
p = new Parser(sts);
|
||||||
Assert.Equal(program, string.Format("{0}", p.parse()));
|
Assert.Equal(program, string.Format("{0}", p.parse()));
|
||||||
|
|
||||||
//program = "(* 2.4 2)";
|
program = "(abc '(1 2 3))";
|
||||||
//sts = StringTokenStream.generate(program);
|
sts = StringTokenStream.generate(program);
|
||||||
//p = new Parser(sts);
|
p = new Parser(sts);
|
||||||
//Assert.Equal(program, p.parse().ToString());
|
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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public static void TestFunctions() {
|
public static void TestFunctions() {
|
||||||
IList<Tuple<string, Expression>> cases = new List<Tuple<string, Expression>>();
|
Executor e = new Executor();
|
||||||
Expression e = new Executor().eval("(+ 10 20)");
|
Assert.Equal("(1 2 3)", e.eval("(quote (1 2 3))").ToString());
|
||||||
Assert.Equal(((Integer) e).value, 30);
|
Assert.Equal("abc", e.eval("(quote abc)").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(> 1 2)");
|
Assert.Equal("t", e.eval("(atom 1)").ToString());
|
||||||
Assert.Equal(((Lisp_Boolean) e).value, false);
|
Assert.Equal("nil", e.eval("(atom (quote (1 2 3)))").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(if (> 1 2) 3 4)");
|
Assert.Equal("t", e.eval("(eq 2 2)").ToString());
|
||||||
Assert.Equal(((Integer) e).value, 4);
|
Assert.Equal("nil", e.eval("(eq 2 3)").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(begin (define x 1) x)");
|
Assert.Equal("1", e.eval("(car (quote (1 2 3)))").ToString());
|
||||||
Assert.Equal(((Integer) e).value, 1);
|
Assert.Equal("(2 3)", e.eval("(cdr (quote (1 2 3)))").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(apply + (1 2))");
|
Assert.Equal("(1 . 2)", e.eval("(cons 1 2)").ToString());
|
||||||
Assert.Equal(((Integer) e).value, 3);
|
Assert.Equal("(1 2)", e.eval("(cons 1 (cons 2 nil))").ToString());
|
||||||
|
Assert.Equal("(1)", e.eval("(cons 1 nil)").ToString());
|
||||||
|
Assert.Equal("(1)", e.eval("(cons 1 ())").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(car (10 20 30))");
|
Assert.Equal("\"Case 2\"", e.eval("""
|
||||||
Assert.Equal(((Integer) e).value, 10);
|
(cond
|
||||||
|
((eq 1 2) "Case 1")
|
||||||
|
((eq 2 2) "Case 2"))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("\"Case 1\"", e.eval("""
|
||||||
|
(cond
|
||||||
|
((eq 2 2) "Case 1")
|
||||||
|
((eq 2 2) "Case 2"))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("""
|
||||||
|
(cond
|
||||||
|
((eq 1 2) "Case 1")
|
||||||
|
((eq 3 2) "Case 2"))
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(cdr (10 20 30))");
|
Assert.Equal("t", e.eval("((lambda (a) (eq a a)) 2)").ToString());
|
||||||
Assert.Equal(string.Format("{0}", e), "(20 30)");
|
|
||||||
|
|
||||||
e = new Executor().eval("(cons 1 3)");
|
Assert.Equal("t", e.eval("(begin (car (quote (nil 1))) t)").ToString());
|
||||||
Assert.Equal(string.Format("{0}", e), "(1 3)");
|
Assert.Equal("(1)", e.eval("(begin t (cdr (quote (nil 1))))").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(cons 1 (2 3))");
|
Assert.Equal("t", e.eval("""
|
||||||
Assert.Equal(string.Format("{0}", e), "(1 2 3)");
|
(begin
|
||||||
|
(define abc 10)
|
||||||
|
(eq abc abc))
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(length (cons 1 (2 3)))");
|
Assert.Equal("1", e.eval("""
|
||||||
Assert.Equal(string.Format("{0}", e), "3");
|
(begin
|
||||||
|
(define if (lambda (condition a b) (
|
||||||
|
cond (condition a) (t b))))
|
||||||
|
(if (> 2 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("(3 4)", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define if (lambda (condition a b) (
|
||||||
|
cond (condition a) (t b))))
|
||||||
|
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(>= 2 2)");
|
Assert.Equal("a", e.eval("'a").ToString());
|
||||||
Assert.Equal(string.Format("{0}", e), "t");
|
}
|
||||||
|
|
||||||
e = new Executor().eval("(> 2 2))");
|
[Fact]
|
||||||
Assert.Equal(string.Format("{0}", e), "nil");
|
public static void TestFunctionsAdvanced() {
|
||||||
|
Executor e = new Executor();
|
||||||
|
Assert.Equal("2", e.eval("""
|
||||||
|
((lambda (b) b) (car (quote (2 3))))
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(and 2 3 4)");
|
Assert.Equal("(3 4 5)", e.eval("""
|
||||||
Assert.Equal("4", e.ToString());
|
((lambda (x y . z) z) 1 2 3 4 5)
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(and 2 nil 4)");
|
Assert.Equal("3", e.eval("""
|
||||||
Assert.Equal("nil", e.ToString());
|
(begin
|
||||||
|
(define if (lambda (condition a b) (cond (condition a) (t b))))
|
||||||
|
(if (< 1 2) 3 2))
|
||||||
|
""").ToString());
|
||||||
|
|
||||||
e = new Executor().eval("(or 2 nil 4)");
|
Assert.Equal("2", e.eval("""
|
||||||
Assert.Equal("2", e.ToString());
|
(begin
|
||||||
|
(define if (lambda (condition a b) (cond (condition a) (t b))))
|
||||||
e = new Executor().eval("(or nil 4)");
|
(if (> 1 2) 3 2))
|
||||||
Assert.Equal("4", e.ToString());
|
""").ToString());
|
||||||
|
Assert.Equal("1", e.eval("""
|
||||||
e = new Executor().eval("(= (1 2) (1 2))");
|
(begin
|
||||||
Assert.Equal(e.ToString(), "t");
|
(define if (lambda* (condition a b) (
|
||||||
|
cond ((eval condition) (eval a)) (t (eval b)))))
|
||||||
e = new Executor().eval("(= (1 2 3) (1 2))");
|
(if (> 2 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
||||||
Assert.Equal(e.ToString(), "nil");
|
""").ToString());
|
||||||
|
Assert.Equal("(3 4)", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define if (lambda* (condition a b) (
|
||||||
|
cond ((eval condition) (eval a)) (t (eval b)))))
|
||||||
|
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("120", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define f (lambda (n) (cond ((<= n 1) 1) (t (* n (f (- n 1)))))))
|
||||||
|
(f 5))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("120", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define if (lambda* (condition a b) (
|
||||||
|
cond ((eval condition) (eval a)) (t (eval b)))))
|
||||||
|
(define f (lambda (n) (if (<= n 1) 1 (* n (f (- n 1))))))
|
||||||
|
(f 5))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("(1 2 3 4 5)", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define if (lambda* (condition a b) (
|
||||||
|
cond ((eval condition) (eval a)) (t (eval b)))))
|
||||||
|
((lambda (. args) args) 1 2 3 4 5))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("t", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define null (lambda* (x) (
|
||||||
|
cond ((eval x) nil) (t t))))
|
||||||
|
(null nil))
|
||||||
|
""").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("""
|
||||||
|
(begin
|
||||||
|
(define null (lambda* (x) (cond ((eval x) nil) (t t))))
|
||||||
|
(null (quote (1 2))))
|
||||||
|
""").ToString());
|
||||||
}
|
}
|
||||||
[Fact]
|
[Fact]
|
||||||
public static void ObjectTest() {
|
public static void ObjectTest() {
|
||||||
Executor e = new Executor();
|
Executor e = new Executor();
|
||||||
Expression r;
|
Expression r;
|
||||||
e.environment.Set("o", new Lisp_Object(new O(5, false)));
|
e.environment.Set("o", Lisp_Object.FromBase(new O(5, false)));
|
||||||
r = e.eval("(haskeys o 'i' 'b')");
|
r = e.eval("""(haskeys o "i" "b")""");
|
||||||
Assert.Equal(((Lisp_Boolean)r).value, true);
|
Assert.True(((Lisp_Boolean)r).Value());
|
||||||
r = e.eval("(getitems o 'i' 'b')");
|
r = e.eval("""(getitems o "i" "b")""");
|
||||||
Assert.Equal(string.Format("{0}", r), "(5 nil)");
|
Assert.Equal("(5 nil)", string.Format("{0}", r));
|
||||||
|
r = e.eval("""(invoke o "I" nil)""");
|
||||||
|
Assert.Equal("5", string.Format("{0}", r));
|
||||||
|
r = e.eval("""(invoke-generic o "G" nil ((lambda (. args) args) "System.String"))""");
|
||||||
|
Assert.Equal("\"System.String\"", string.Format("{0}", r));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public static void ScalarTest() {
|
public static void GlobalVariableTest() {
|
||||||
Executor e = new Executor();
|
Executor e = new Executor();
|
||||||
Expression r;
|
e.environment.Set("*o*", new Lisp_Integer(5));
|
||||||
|
Assert.Equal("10", e.eval("(* *o* 2)").ToString());
|
||||||
r = e.eval("(* 2 2)");
|
|
||||||
Assert.Equal("4", r.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public static void ProcedureTest() {
|
|
||||||
Executor e = new Executor();
|
|
||||||
Expression r;
|
|
||||||
r = e.eval("((lambda (a) (* a a)) 2)");
|
|
||||||
Assert.Equal(string.Format("{0}", r), "4");
|
|
||||||
|
|
||||||
r = e.eval("(begin (define mull (lambda (a) (* a a))) (mull 3))");
|
|
||||||
Assert.Equal(string.Format("{0}", r), "9");
|
|
||||||
|
|
||||||
r = e.eval("(begin (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) (fact 10))");
|
|
||||||
Assert.Equal(string.Format("{0}", r), "3628800");
|
|
||||||
|
|
||||||
r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 3 (1 2 3 4)))");
|
|
||||||
Assert.Equal(string.Format("{0}", r), "3");
|
|
||||||
|
|
||||||
e = new Executor();
|
|
||||||
r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 0 (1 2 3 4)))");
|
|
||||||
Assert.Equal(string.Format("{0}", r), "nil");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public static void DefaultEnvironmentTest() {
|
public static void DefaultEnvironmentTest() {
|
||||||
Executor e = new Executor(new DefaultEnvironment());
|
Executor e = new Executor(new DefaultEnvironment());
|
||||||
Assert.Equal("nil", e.eval("(find 0 (1 2 3 4))").ToString());
|
Assert.Equal("1", e.eval("(if nil 0 1)").ToString());
|
||||||
Assert.Equal("3", e.eval("(find 3 (1 2 3 4))").ToString());
|
Assert.Equal("0", e.eval("(if t 0 1)").ToString());
|
||||||
|
Assert.Equal("5", e.eval("(if t (if t 5 nil) nil)").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(if t (if nil 5 nil) nil)").ToString());
|
||||||
|
Assert.Equal("(1 2 3)", e.eval("(list 1 2 3)").ToString());
|
||||||
|
Assert.Equal("3", e.eval("(find 3 (list 1 2 3 4))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(find 0 (list 1 2 3 4))").ToString());
|
||||||
|
Assert.Equal("(2 4 6)", e.eval("(map (lambda (x) (* x 2)) (quote (1 2 3)))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(and 1 2 3 nil)").ToString());
|
||||||
|
Assert.Equal("t", e.eval("(and t t t t)").ToString());
|
||||||
|
Assert.Equal("t", e.eval("(or nil nil t nil)").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(or nil nil nil nil)").ToString());
|
||||||
|
Assert.Equal("t", e.eval("(any (lambda (x) (= x 2)) (list 1 2 3 4 5 6))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) (list 1 3 4 5 6))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) nil)").ToString());
|
||||||
|
Assert.Equal("t", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 5))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 4 5))").ToString());
|
||||||
|
Assert.Equal("nil", e.eval("(all (lambda (x) (= x 2)) nil)").ToString());
|
||||||
|
Assert.Equal("10", e.eval("(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))").ToString());
|
||||||
|
Assert.Equal("(2 3 4 5 6 7)", e.eval("(append (list 2 3 4) (list 5 6 7))").ToString());
|
||||||
|
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort (lambda (a b) (> a b)) (list 5 4 7 3 2 6 1))").ToString());
|
||||||
|
|
||||||
|
//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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,20 +10,20 @@ resources:
|
||||||
- name: source
|
- name: source
|
||||||
type: git
|
type: git
|
||||||
source:
|
source:
|
||||||
uri: https://gitea.redxef.at/redxef/jellyfin-smart-playlist
|
uri: https://git.redxef.at/redxef/jellyfin-smart-playlist
|
||||||
branch: main
|
branch: main
|
||||||
fetch_tags: true
|
fetch_tags: true
|
||||||
tag_regex: 'v.*'
|
tag_regex: 'v.*'
|
||||||
- name: manifest
|
- name: manifest
|
||||||
type: git
|
type: git
|
||||||
source:
|
source:
|
||||||
uri: ssh://git@gitea.redxef.at:8022/redxef/jellyfin-smart-playlist.git
|
uri: ssh://git@git.redxef.at:8022/redxef/jellyfin-smart-playlist.git
|
||||||
branch: manifest
|
branch: manifest
|
||||||
private_key: ((gitea.id_ed25519))
|
private_key: ((gitea.id_ed25519))
|
||||||
- name: releases
|
- name: releases
|
||||||
type: http-resource
|
type: http-resource
|
||||||
source:
|
source:
|
||||||
url: https://gitea.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases
|
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases
|
||||||
method: get
|
method: get
|
||||||
auth:
|
auth:
|
||||||
basic:
|
basic:
|
||||||
|
@ -32,7 +32,7 @@ resources:
|
||||||
- name: artifact
|
- name: artifact
|
||||||
type: http-resource
|
type: http-resource
|
||||||
source:
|
source:
|
||||||
url: https://gitea.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases/((.:release_id))/assets
|
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases/((.:release_id))/assets
|
||||||
method: get
|
method: get
|
||||||
auth:
|
auth:
|
||||||
basic:
|
basic:
|
||||||
|
|
99
examples.md
Normal file
99
examples.md
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
# 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
|
||||||
|
```
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Id": "a1d02dee-f1da-4472-bee3-f568c15c8360",
|
|
||||||
"Name": "Favourite Pop",
|
|
||||||
"Program": "(and (invoke item 'IsFavoriteOrLiked' (user)) (find 'Pop' (car (getitems item 'Genres'))))"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
# Examples
|
|
||||||
|
|
||||||
* [Favourite Pop](a1d02dee-f1da-4472-bee3-f568c15c8360.json): A Playlist
|
|
||||||
containing all favourite items of the genre pop.
|
|
129
lisp.md
Normal file
129
lisp.md
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# 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.
|
Loading…
Reference in a new issue