Initial commit.
This commit is contained in:
commit
3365521283
14 changed files with 918 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
**/obj/
|
||||
**/bin/
|
||||
cache/
|
||||
config/
|
198
Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs
Normal file
198
Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs
Normal file
|
@ -0,0 +1,198 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
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 IComparable<T, E> where T : IComparable<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);
|
||||
static abstract E operator ==(T left, T right);
|
||||
static abstract E operator !=(T left, T right);
|
||||
}
|
||||
|
||||
public abstract class Expression : IFormattable {
|
||||
public abstract string ToString(string? format, IFormatProvider? provider);
|
||||
}
|
||||
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 string ToString(string? format, IFormatProvider? provider) {
|
||||
return _name;
|
||||
}
|
||||
}
|
||||
public class Boolean : Atom {
|
||||
private readonly bool _value;
|
||||
public Boolean(bool value) {
|
||||
_value = value;
|
||||
}
|
||||
public bool value { get => _value; }
|
||||
public override string ToString(string? format, IFormatProvider? provider) {
|
||||
return _value? "t" : "nil";
|
||||
}
|
||||
}
|
||||
public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, IComparable<Integer, Boolean> {
|
||||
private readonly int _value;
|
||||
public Integer(int value) {
|
||||
_value = value;
|
||||
}
|
||||
public int value { get => _value; }
|
||||
public override string ToString(string? format, IFormatProvider? provider) {
|
||||
return _value.ToString("0", provider);
|
||||
}
|
||||
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 class String : Atom, IAddable<String> {
|
||||
private readonly string _value;
|
||||
public String(string value) {
|
||||
_value = value;
|
||||
}
|
||||
public string value { get => _value; }
|
||||
public override string ToString(string? format, IFormatProvider? provider) {
|
||||
return "\"" + _value + "\"";
|
||||
}
|
||||
public static String operator +(String a, String b) {
|
||||
return new String (a.value + b.value);
|
||||
}
|
||||
}
|
||||
public class List : Expression {
|
||||
private IList<Expression> _expressions;
|
||||
public List(IList<Expression> expressions) {
|
||||
_expressions = expressions;
|
||||
}
|
||||
public IList<Expression> expressions { get => _expressions; }
|
||||
public override string ToString(string? format, IFormatProvider? provider) {
|
||||
string r = "(";
|
||||
foreach (var e in _expressions) {
|
||||
r += " ";
|
||||
r += e.ToString("0", provider);
|
||||
}
|
||||
return r + ")";
|
||||
}
|
||||
public static List operator +(List a, List b) {
|
||||
IList<Expression> r = new List<Expression>();
|
||||
r.Concat(a.expressions);
|
||||
r.Concat(b.expressions);
|
||||
return new List (r);
|
||||
}
|
||||
}
|
||||
|
||||
public class Parser {
|
||||
private StringTokenStream _sts;
|
||||
public Parser(StringTokenStream tokens) {
|
||||
_sts = tokens;
|
||||
}
|
||||
|
||||
public Expression parse() {
|
||||
Token<string> token = _sts.get();
|
||||
switch (token) {
|
||||
case GroupingToken gt:
|
||||
return parse_grouping(gt, gt.closing_value);
|
||||
case AtomToken at:
|
||||
return parse_atom(at);
|
||||
case OperatorToken ot:
|
||||
return new Symbol(ot.value);
|
||||
case SpaceToken sp:
|
||||
return parse();
|
||||
}
|
||||
return parse();
|
||||
}
|
||||
|
||||
Expression parse_string(GroupingToken start, GroupingToken end) {
|
||||
Debug.Assert(start == end);
|
||||
Debug.Assert("'\"".Contains(start.value));
|
||||
string r = "";
|
||||
while (_sts.available() > 0) {
|
||||
Token<string> t = _sts.get();
|
||||
if (t == end) {
|
||||
break;
|
||||
}
|
||||
r += t.value;
|
||||
}
|
||||
_sts.commit();
|
||||
return new String(r);
|
||||
}
|
||||
|
||||
Expression parse_grouping(GroupingToken start, GroupingToken end) {
|
||||
IList<Expression> expressions = new List<Expression>();
|
||||
while (_sts.available() > 0) {
|
||||
Token<string> t = _sts.get();
|
||||
if (t.Equals(end)) {
|
||||
_sts.commit();
|
||||
break;
|
||||
}
|
||||
_sts.rewind(1);
|
||||
expressions.Add(parse());
|
||||
}
|
||||
return new List(expressions);
|
||||
}
|
||||
|
||||
Expression parse_atom(AtomToken at) {
|
||||
int parsed_value;
|
||||
if (int.TryParse(at.value, out parsed_value)) {
|
||||
_sts.commit();
|
||||
return new Integer(parsed_value);
|
||||
}
|
||||
if (at.value.Equals("t")) {
|
||||
return new Boolean(true);
|
||||
}
|
||||
if (at.value.Equals("nil")) {
|
||||
return new Boolean(false);
|
||||
}
|
||||
_sts.commit();
|
||||
return new Symbol(at.value);
|
||||
}
|
||||
}
|
||||
}
|
144
Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs
Normal file
144
Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs
Normal file
|
@ -0,0 +1,144 @@
|
|||
using System.Reflection;
|
||||
using Jellyfin.Plugin.SmartPlaylist.Util;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||
public interface IToken<T> {
|
||||
T value { get; }
|
||||
abstract static IToken<T>? take<E>(E program);
|
||||
}
|
||||
|
||||
public abstract class Token<T>: IToken<T> , IEquatable<Token<T>> {
|
||||
protected readonly T _value;
|
||||
protected Token(T value) {
|
||||
_value = value;
|
||||
}
|
||||
public T value { get => _value; }
|
||||
public static IToken<T>? take<E>(E program) {
|
||||
throw new NotImplementedException("Subclass this class");
|
||||
}
|
||||
public bool Equals(Token<T>? b) {
|
||||
return b != null && _value != null && _value.Equals(b._value);
|
||||
}
|
||||
}
|
||||
|
||||
class SpaceToken : Token<string> {
|
||||
private SpaceToken(string value) : base(value) {}
|
||||
private static IToken<string>? take(CharStream program) {
|
||||
if (program.available() == 0) {
|
||||
return null;
|
||||
}
|
||||
if (program.get() == ' ') {
|
||||
return new SpaceToken(" ");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class GroupingToken: Token<string> {
|
||||
private GroupingToken(string value) : base(value) {}
|
||||
private static IToken<string>? take(CharStream program) {
|
||||
if (program.available() == 0) {
|
||||
return null;
|
||||
}
|
||||
char t = program.get();
|
||||
if ("()\"'".Contains(t)) {
|
||||
return new GroupingToken(t.ToString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private GroupingToken? _closing_value() {
|
||||
if (_value == "(") {
|
||||
return new GroupingToken(")");
|
||||
} else if (_value == ")") {
|
||||
return null;
|
||||
}
|
||||
return new GroupingToken(_value);
|
||||
}
|
||||
public GroupingToken? closing_value { get => _closing_value(); }
|
||||
}
|
||||
|
||||
class AtomToken : Token<string> {
|
||||
private AtomToken(string value) : base(value) {}
|
||||
private static IToken<string>? take(CharStream program) {
|
||||
string value = "";
|
||||
while (program.available() > 0) {
|
||||
char t = program.get();
|
||||
if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Contains(t)) {
|
||||
if (value.Equals("")) {
|
||||
return null;
|
||||
}
|
||||
program.rewind(1);
|
||||
return new AtomToken(value);
|
||||
}
|
||||
value += t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorToken : Token<string> {
|
||||
private OperatorToken(string value) : base(value) {}
|
||||
private static IToken<string>? take(CharStream program) {
|
||||
if (program.available() == 0) {
|
||||
return null;
|
||||
}
|
||||
return new OperatorToken(program.get().ToString());
|
||||
//char t = program.get();
|
||||
//if ("+-*/%".Contains(t)) {
|
||||
// return new OperatorToken(t.ToString());
|
||||
//}
|
||||
//return null;
|
||||
}
|
||||
}
|
||||
|
||||
class CharStream: Stream<char> {
|
||||
public CharStream(IList<char> items) : base(items) {}
|
||||
public CharStream(string items) : base(items.ToCharArray().Cast<char>().ToList()) {}
|
||||
}
|
||||
|
||||
public class StringTokenStream : Stream<Token<string>> {
|
||||
private static readonly IList<Type> _classes = new List<Type> {
|
||||
typeof(SpaceToken),
|
||||
typeof(GroupingToken),
|
||||
typeof(AtomToken),
|
||||
typeof(OperatorToken),
|
||||
};
|
||||
protected StringTokenStream(IList<Token<string>> tokens) : base(tokens) {}
|
||||
private static StringTokenStream generate(CharStream program) {
|
||||
IList<Token<string>> result = new List<Token<string>>();
|
||||
int prev_avail = 0;
|
||||
while (true) {
|
||||
if (prev_avail == program.available() && prev_avail == 0) {
|
||||
break;
|
||||
} else if (prev_avail == program.available()) {
|
||||
throw new ApplicationException("Program is invalid");
|
||||
}
|
||||
prev_avail = program.available();
|
||||
foreach (Type c in _classes) {
|
||||
Token<string>? t = (Token<string>?) c.GetMethod(
|
||||
"take",
|
||||
BindingFlags.NonPublic | BindingFlags.Static,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
new Type[] { typeof(CharStream) },
|
||||
null
|
||||
)?.Invoke(
|
||||
null,
|
||||
new object[]{program}
|
||||
);
|
||||
if (t == null) {
|
||||
program.rewind();
|
||||
continue;
|
||||
}
|
||||
program.commit();
|
||||
result.Add(t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new StringTokenStream(result);
|
||||
}
|
||||
public static StringTokenStream generate(string program) {
|
||||
return StringTokenStream.generate(new CharStream(program));
|
||||
}
|
||||
}
|
||||
}
|
241
Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs
Normal file
241
Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs
Normal file
|
@ -0,0 +1,241 @@
|
|||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||
public class EnvironmentEntry {};
|
||||
public class Entry: EnvironmentEntry {
|
||||
private readonly Expression _expression;
|
||||
public Entry(Expression expression) {
|
||||
_expression = expression;
|
||||
}
|
||||
public Expression expression { get => _expression; }
|
||||
};
|
||||
public class NOOPEntry: Entry {
|
||||
public NOOPEntry() : base(new Compiler.Boolean(false)) {}
|
||||
}
|
||||
public class Function : EnvironmentEntry {
|
||||
private readonly Func<IList<Expression>, Expression> _func;
|
||||
public Function(Func<IList<Expression>, Expression> func) {
|
||||
_func = func;
|
||||
}
|
||||
public Func<IList<Expression>, Expression> func { get => _func; }
|
||||
}
|
||||
|
||||
public class Environment : Dictionary<string, EnvironmentEntry> {
|
||||
public static Environment create() {
|
||||
Environment e = new Environment();
|
||||
e.Add("+", new Function(op_add));
|
||||
e.Add("-", new Function(op_sub));
|
||||
e.Add("*", new Function(op_mul));
|
||||
e.Add("/", new Function(op_div));
|
||||
e.Add("%", new Function(op_rem));
|
||||
e.Add(">", new Function(op_gt));
|
||||
e.Add("<", new Function(op_lt));
|
||||
e.Add(">=", new Function(op_ge));
|
||||
e.Add("<=", new Function(op_le));
|
||||
e.Add("==", new Function(op_eq));
|
||||
e.Add("!=", new Function(op_ne));
|
||||
e.Add("abs", new Function(op_abs));
|
||||
e.Add("append", new Function(op_append));
|
||||
e.Add("apply", new Function(op_apply));
|
||||
e.Add("begin", new Function(op_begin));
|
||||
return e;
|
||||
}
|
||||
|
||||
private static T op_agg<T>(Func<T, T, T> op, IList<T> args) {
|
||||
T agg = args[0];
|
||||
foreach (var arg in args.Skip(1)) {
|
||||
agg = op(agg, arg);
|
||||
}
|
||||
return agg;
|
||||
}
|
||||
|
||||
private static Expression op_add(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_agg((a, b) => a + b, args.Select(x => (Integer) x).ToList());
|
||||
case Compiler.String s:
|
||||
return op_agg((a, b) => a + b, args.Select(x => (Compiler.String) x).ToList());
|
||||
//case Compiler.List:
|
||||
// return op_agg((a, b) => a + b, args.Select(x => (Compiler.List) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to add these types");
|
||||
}
|
||||
|
||||
private static Expression op_sub(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_agg((a, b) => a - b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_mul(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_agg((a, b) => a * b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_div(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_agg((a, b) => a / b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_rem(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_agg((a, b) => a % b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static E op_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 op_gt(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a > b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_lt(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a < b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_ge(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a >= b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_le(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a <= b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_eq(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a == b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_ne(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case Integer i:
|
||||
return op_cmp((a, b) => a != b, args.Select(x => (Integer) x).ToList());
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_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("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_append(IList<Expression> args) {
|
||||
Expression first = args[0];
|
||||
switch (first) {
|
||||
case List l:
|
||||
return l + new List(args);
|
||||
}
|
||||
throw new ApplicationException("Don't know how to subtract these types");
|
||||
}
|
||||
|
||||
private static Expression op_apply(IList<Expression> args) {
|
||||
IList<EnvironmentEntry> e_list = (IList<EnvironmentEntry>) args.Select(x => new Entry(x)).ToList();
|
||||
if (e_list.Count != 2) {
|
||||
throw new ApplicationException("Expected exactly two arguments");
|
||||
}
|
||||
if (e_list[0].GetType() != typeof(Function)) {
|
||||
throw new ApplicationException("Expected first argument to be a function to apply");
|
||||
}
|
||||
Function f = (Function) e_list[0];
|
||||
IList<Expression> new_args = e_list.Skip(1).Select(x => ((Entry) x).expression).ToList();
|
||||
return f.func(args);
|
||||
}
|
||||
|
||||
private static Expression op_begin(IList<Expression> args) {
|
||||
return args.Last();
|
||||
}
|
||||
|
||||
public EnvironmentEntry eval(Expression expression) {
|
||||
switch (expression) {
|
||||
case Symbol s:
|
||||
return this[s.name];
|
||||
case Integer i:
|
||||
return new Entry(i);
|
||||
case Compiler.String s_:
|
||||
return new Entry(s_);
|
||||
case List list:
|
||||
if (list.expressions[0].GetType() == typeof(Symbol)) {
|
||||
if (((Symbol) list.expressions[0]).name.Equals("if")) {
|
||||
Compiler.Boolean test = (Compiler.Boolean) ((Entry) eval(list.expressions[1])).expression;
|
||||
return eval(list.expressions[2 + (test.value? 0 : 1)]);
|
||||
}
|
||||
if (((Symbol) list.expressions[0]).name.Equals("define")) {
|
||||
Symbol test;
|
||||
if (list.expressions[1].GetType() == typeof(Symbol)) {
|
||||
test = (Symbol) list.expressions[1];
|
||||
} else {
|
||||
test = (Symbol) ((Entry) eval(list.expressions[1])).expression;
|
||||
}
|
||||
this[test.name] = eval(list.expressions[2]);
|
||||
return new NOOPEntry();
|
||||
}
|
||||
}
|
||||
IList<EnvironmentEntry> e_list = list.expressions.Select(x => eval(x)).ToList();
|
||||
if (e_list.Count > 0 && e_list[0].GetType() == typeof(Function)) {
|
||||
Function f = (Function) e_list[0];
|
||||
IList<Expression> args = e_list.Skip(1).Select(x => ((Entry) x).expression).ToList();
|
||||
return new Entry(f.func(args));
|
||||
}
|
||||
return new Entry(new List(list.expressions.Select(x => ((Entry) eval(x)).expression).ToList()));
|
||||
}
|
||||
throw new ApplicationException("Not handled case");
|
||||
}
|
||||
public EnvironmentEntry eval(Parser p) {
|
||||
return eval(p.parse());
|
||||
}
|
||||
public EnvironmentEntry eval(StringTokenStream sts) {
|
||||
return eval(new Parser(sts));
|
||||
}
|
||||
public EnvironmentEntry eval(string p) {
|
||||
return eval(StringTokenStream.generate(p));
|
||||
}
|
||||
}
|
||||
}
|
29
Jellyfin.Plugin.SmartPlaylist/Plugin.cs
Normal file
29
Jellyfin.Plugin.SmartPlaylist/Plugin.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
|
||||
public Plugin(
|
||||
IApplicationPaths applicationPaths,
|
||||
IXmlSerializer xmlSerializer
|
||||
) : base (applicationPaths, xmlSerializer) {
|
||||
Instance = this;
|
||||
}
|
||||
public static Plugin? Instance {get; private set; }
|
||||
public override string Name => "Smart Playlist";
|
||||
public override Guid Id => Guid.Parse("dd2326e3-4d3e-4bfc-80e6-28502c1131df");
|
||||
public IEnumerable<PluginPageInfo> GetPages() {
|
||||
return new[] {
|
||||
new PluginPageInfo {
|
||||
Name = this.Name,
|
||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.configPage.html", GetType().Namespace)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
14
Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs
Normal file
14
Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||
public class PluginConfiguration : BasePluginConfiguration {
|
||||
public PluginConfiguration(
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||
public class GeneratePlaylist : IScheduledTask {
|
||||
private readonly ILogger _logger;
|
||||
public GeneratePlaylist(
|
||||
ILogger<Plugin> logger
|
||||
) {
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Constructed Task");
|
||||
}
|
||||
public string Category => "Library";
|
||||
public string Name => "(re)generate Smart Playlists";
|
||||
public string Description => "Generate or regenerate all Smart Playlists";
|
||||
public string Key => nameof(GeneratePlaylist);
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
|
||||
return new[] {
|
||||
new TaskTriggerInfo {
|
||||
IntervalTicks = TimeSpan.FromMinutes(1).Ticks,
|
||||
Type = TaskTriggerInfo.TriggerInterval,
|
||||
}
|
||||
};
|
||||
}
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
||||
_logger.LogInformation("This is a test");
|
||||
}
|
||||
}
|
||||
}
|
47
Jellyfin.Plugin.SmartPlaylist/Util/Stream.cs
Normal file
47
Jellyfin.Plugin.SmartPlaylist/Util/Stream.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
namespace Jellyfin.Plugin.SmartPlaylist.Util {
|
||||
public interface IStream<T> {
|
||||
int available();
|
||||
T get();
|
||||
int commit();
|
||||
int rewind();
|
||||
int rewind(int n);
|
||||
}
|
||||
|
||||
public class Stream<T> : IStream<T> {
|
||||
private readonly IList<T> _items;
|
||||
private int _cursor;
|
||||
private int _ephemeral_cursor;
|
||||
|
||||
protected Stream(IList<T> items) {
|
||||
_items = items;
|
||||
_cursor = 0;
|
||||
_ephemeral_cursor = 0;
|
||||
}
|
||||
|
||||
public int available() {
|
||||
return _items.Count - _ephemeral_cursor;
|
||||
}
|
||||
public T get() {
|
||||
return _items[_ephemeral_cursor++];
|
||||
}
|
||||
public int commit() {
|
||||
int diff = _ephemeral_cursor - _cursor;
|
||||
_cursor = _ephemeral_cursor;
|
||||
return diff;
|
||||
}
|
||||
public int rewind() {
|
||||
int diff = _ephemeral_cursor - _cursor;
|
||||
_ephemeral_cursor = _cursor;
|
||||
return diff;
|
||||
}
|
||||
public int rewind(int n) {
|
||||
int diff = _ephemeral_cursor - _cursor;
|
||||
if (diff < n) {
|
||||
n = diff;
|
||||
}
|
||||
_ephemeral_cursor -= n;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
79
Jellyfin.Plugin.SmartPlaylist/configPage.html
Normal file
79
Jellyfin.Plugin.SmartPlaylist/configPage.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form id="TemplateConfigForm">
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="Options">Several Options</label>
|
||||
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
|
||||
<option id="optOneOption" value="OneOption">One Option</option>
|
||||
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
|
||||
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">A Description</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
|
||||
<span>A Checkbox</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AString">A String</label>
|
||||
<input id="AString" name="AString" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">Another Description</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var TemplateConfig = {
|
||||
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
||||
};
|
||||
|
||||
document.querySelector('#TemplateConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
document.querySelector('#Options').value = config.Options;
|
||||
document.querySelector('#AnInteger').value = config.AnInteger;
|
||||
document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting;
|
||||
document.querySelector('#AString').value = config.AString;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#TemplateConfigForm')
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
config.Options = document.querySelector('#Options').value;
|
||||
config.AnInteger = document.querySelector('#AnInteger').value;
|
||||
config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked;
|
||||
config.AString = document.querySelector('#AString').value;
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
15
Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj
Normal file
15
Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj
Normal file
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>jellyfin_smart_playlist</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.9.7" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.9.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
17
Test/test.sh
Executable file
17
Test/test.sh
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
pwd
|
||||
(
|
||||
cd ..
|
||||
dotnet build
|
||||
)
|
||||
pwd
|
||||
mkdir -p ./cache ./config/plugins/jellyfin-smart-playlist
|
||||
cp ../bin/Debug/net8.0/jellyfin-smart-playlist.dll ./config/plugins/jellyfin-smart-playlist/
|
||||
docker run --rm --user "$(id -u):$(id -g)" \
|
||||
-v ./cache:/cache \
|
||||
-v ./config:/config \
|
||||
-p 8096:8096 \
|
||||
jellyfin/jellyfin
|
1
Tests/GlobalUsings.cs
Normal file
1
Tests/GlobalUsings.cs
Normal file
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
68
Tests/Tests.cs
Normal file
68
Tests/Tests.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
using Xunit;
|
||||
using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment;
|
||||
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
|
||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
public class Test {
|
||||
[Fact]
|
||||
public static void StringTokenStreamTest() {
|
||||
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, "some");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "literal");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "string");
|
||||
Assert.Equal(sts.get().value, "\"");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "def");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "ghj");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "+");
|
||||
Assert.Equal(sts.get().value, "100");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "-");
|
||||
Assert.Equal(sts.get().value, "+");
|
||||
Assert.Equal(sts.get().value, "300");
|
||||
Assert.Equal(sts.get().value, " ");
|
||||
Assert.Equal(sts.get().value, "1");
|
||||
Assert.Equal(sts.get().value, ")");
|
||||
sts.commit();
|
||||
Assert.Equal(sts.available(), 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void ParserTest() {
|
||||
string program = "( + 1 ( * 2 3))";
|
||||
StringTokenStream sts = StringTokenStream.generate(program);
|
||||
Parser p = new Parser(sts);
|
||||
Assert.Equal(program, string.Format("{0}", p.parse()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void EvaluateTest() {
|
||||
Expression p = new Parser(StringTokenStream.generate("(+ 5 (+ 1 2 3))")).parse();
|
||||
var e = Lisp_Environment.create();
|
||||
Entry r = (Entry) e.eval(p);
|
||||
Assert.Equal(((Integer) r.expression).value, 11);
|
||||
|
||||
r = (Entry) Lisp_Environment.create().eval("(> 1 2)");
|
||||
Assert.Equal(((Lisp_Boolean) r.expression).value, false);
|
||||
|
||||
r = (Entry) Lisp_Environment.create().eval("(if (> 1 2) 3 4)");
|
||||
Assert.Equal(((Integer) r.expression).value, 4);
|
||||
|
||||
r = (Entry) Lisp_Environment.create().eval("(begin (define x 1) 4)");
|
||||
Assert.Equal(((Integer) r.expression).value, 4);
|
||||
|
||||
|
||||
r = (Entry) Lisp_Environment.create().eval("(apply + (1 2))");
|
||||
Assert.Equal(((Integer) r.expression).value, 3);
|
||||
}
|
||||
}
|
||||
}
|
27
Tests/Tests.csproj
Normal file
27
Tests/Tests.csproj
Normal file
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
Loading…
Reference in a new issue