Initial commit.

This commit is contained in:
redxef 2024-06-27 01:47:44 +02:00
commit 3365521283
Signed by: redxef
GPG key ID: 7DAC3AA211CBD921
14 changed files with 918 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
**/obj/
**/bin/
cache/
config/

View 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);
}
}
}

View 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));
}
}
}

View 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));
}
}
}

View 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)
}
};
}
}
}

View 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(
) {
}
}
}

View file

@ -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");
}
}
}

View 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;
}
}
}

View 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>

View 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
View 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
View file

@ -0,0 +1 @@
global using Xunit;

68
Tests/Tests.cs Normal file
View 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
View 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>