Compare commits

...

6 commits

11 changed files with 150 additions and 77 deletions

View file

@ -29,6 +29,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
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();
@ -48,7 +52,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
public abstract class Atom : Expression {}
public class Scalar<V> : Atom where V : notnull {
public class Scalar<V> : Atom, IInner where V : notnull {
protected V _value;
public Scalar(V value) {
_value = value;
@ -68,6 +72,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
public V Value() {
return _value;
}
public object Inner() {
return _value;
}
}
public class Symbol : Atom {
private string _name;
@ -276,18 +283,24 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
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;
}
new_e.environment.Set(name, _eval(e, args[i]));
name_args.Add((name, _eval(e, args[i])));
}
if (idx_rest > 0) {
new_e.environment.Set(_params[idx_rest], Cons.FromList(args.Skip(idx_rest - 1).Select(x => _eval(e, x))));
name_args.Add((_params[idx_rest], Cons.FromList(args.Skip(idx_rest - 1).Select(x => _eval(e, x)))));
}
return new_e.eval(_body);
foreach (var na in name_args) {
new_e.environment.Set(na.Item1, na.Item2);
}
var r = new_e.eval(_body);
return r;
}
}

View file

@ -39,15 +39,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
public class DefaultEnvironment: Environment {
public DefaultEnvironment() {
var e = new Executor();
this["if"] = e.eval("(lambda* (condition a b) ( cond ((eval condition) (eval a)) (t (eval b))))");
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["and"] = e.eval("(lambda (l) (if (null l) t (if (car l) (and (cdr l)) nil)))");
this["or"] = e.eval("(lambda (l) (if (null l) nil (if (car l) t (or (cdr l)))))");
this["any"] = e.eval("(lambda (fc l) (or (map fc l)))");
this["all"] = e.eval("(lambda (fc l) (and (map fc 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)))");
}
}
@ -104,7 +102,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
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) => (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
this["not"] = (x) => {
return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
};
this["haskeys"] = _haskeys;
this["getitems"] = _getitems;
@ -160,7 +161,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
return Boolean.TRUE;
}
private static Expression _getitems(IEnumerable<Expression> args) {
Object o = (Object) args.First();
Object o = new Object(((IInner) args.First()).Inner());
IList<Expression> r = new List<Expression>();
foreach (var e in args.Skip(1)) {
String s = (String) e;
@ -180,7 +181,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
private static Expression _invoke(IEnumerable<Expression> args) {
Object o = (Object) args.First();
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) {
@ -190,25 +191,31 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
} else {
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
}
IList<Expression> r = new List<Expression>();
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value());
if (mi == null) {
throw new ApplicationException($"{o.Value()} has not method {s.Value()}");
}
object?[]? l_ = l.Select<Expression, object?>(x => {
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();
}
return null;
throw new ApplicationException($"Unhandled value {x} (type {x.GetType()})");
}).ToArray();
Type[] l_types = l_.Select( x => {
return x.GetType();
}).ToArray();
IList<Expression> r = new List<Expression>();
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
if (mi == null) {
throw new ApplicationException($"{o.Value()} has not method {s.Value()}");
}
return Object.FromBase(mi.Invoke(o.Value(), l_));
}
}
@ -218,11 +225,36 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
this["quote"] = _quote;
this["eval"] = _eval;
this["cond"] = _cond;
this["if"] = _if;
this["define"] = _define;
this["let"] = _let;
this["let*"] = _let_star;
this["lambda"] = _lambda;
this["lambda*"] = _lambda_star;
this["apply"] = _apply;
this["and"] = (e, x) => {
Expression? r = null;
foreach (var xi in x) {
r = e.eval(xi);
if (r == Boolean.FALSE) {
return r;
}
}
if (r is null) {
return Boolean.FALSE;
}
return r;
};
this["or"] = (e, x) => {
foreach (var xi in x) {
var r = e.eval(xi);
if (r != Boolean.FALSE) {
return r;
}
}
return Boolean.FALSE;
};
}
private static Expression _quote(Executor e, IEnumerable<Expression> args) {
return args.First();
@ -243,6 +275,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
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());
@ -269,7 +307,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
Symbol refname = (Symbol) pair_cons.Item1;
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
vars.Add((refname, exp_));
vars.Add((refname, e.eval(exp_)));
}
foreach (var pair in vars) {
new_e.environment.Set(pair.Item1.Name(), pair.Item2);
@ -294,6 +332,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
}
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())));
}
}
public class Executor {

View file

@ -2,8 +2,19 @@ using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.SmartPlaylist {
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-favourite is-favorite)
""";
}
public string InitialProgram { get; set; }
}
}

View file

@ -98,6 +98,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
Executor executor = new Executor(new DefaultEnvironment());
executor.environment.Set("user", new Lisp_Object(user));
if (Plugin.Instance is not null) {
executor.eval(Plugin.Instance.Configuration.InitialProgram);
} else {
throw new ApplicationException("Plugin Instance is not yet initialized");
}
foreach (var i in items) {
executor.environment.Set("item", new Lisp_Object(i));
var r = executor.eval(expression);

View file

@ -41,7 +41,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
[Serializable]
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)))";
public SmartPlaylistId Id { get; set; }
public SmartPlaylistLinkDto[] Playlists { get; set; }
public string Name { get; set; }

View file

@ -16,13 +16,21 @@ namespace Jellyfin.Plugin.SmartPlaylist {
}
public string StoragePath { get; }
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
return Path.Combine(StoragePath, $"{smartPlaylistId}.json");
return Path.Combine(StoragePath, $"{smartPlaylistId}.yaml");
}
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() {
return Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories);
return Directory.GetFiles(StoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
Directory.GetFiles(StoragePath, "*.yml", SearchOption.AllDirectories)
).Concat(
Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories)
).ToArray();
}
}
}

View file

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

View file

@ -12,5 +12,7 @@ description: |
category: "General"
artifacts:
- jellyfin-smart-playlist.dll
- YamlDotNet.dll
changelog: |
- Switch to yaml loading.
- Initial Alpha release.

View file

@ -5,32 +5,14 @@
<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 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 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>
<form id="SmartPlaylistConfigForm">
<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>
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
<div class="fieldDescription">A program which can set up the environment</div>
<input id="InitialProgram" name="InitialProgram" type="text" is="emby-input" />
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
@ -41,31 +23,25 @@
</div>
</div>
<script type="text/javascript">
var TemplateConfig = {
var SmartPlaylistConfig = {
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
};
document.querySelector('#TemplateConfigPage')
document.querySelector('#SmartPlaylistConfigPage')
.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;
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
document.querySelector('#InitialProgram').value = config.InitialProgram;
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#TemplateConfigForm')
document.querySelector('#SmartPlaylistConfigForm')
.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) {
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
config.InitialProgram = document.querySelector('#InitialProgram').value;
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>jellyfin_smart_playlist</RootNamespace>
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.1.1.0</Version>
@ -11,6 +11,12 @@
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" />
<PackageReference Include="Jellyfin.Model" Version="10.10.0" />
<PackageReference Include="YamlDotNet" Version="16.1.3" />
</ItemGroup>
<ItemGroup>
<None Remove="configPage.html"/>
<EmbeddedResource Include="configPage.html"/>
</ItemGroup>
</Project>

View file

@ -192,7 +192,6 @@ namespace Tests
(define null (lambda* (x) (cond ((eval x) nil) (t t))))
(null (quote (1 2))))
""").ToString());
}
[Fact]
public static void ObjectTest() {
@ -212,18 +211,23 @@ namespace Tests
Executor e = new Executor(new DefaultEnvironment());
Assert.Equal("1", e.eval("(if nil 0 1)").ToString());
Assert.Equal("0", e.eval("(if t 0 1)").ToString());
Assert.Equal("5", e.eval("(if t (if t 5 nil) nil)").ToString());
Assert.Equal("nil", e.eval("(if t (if nil 5 nil) nil)").ToString());
Assert.Equal("(1 2 3)", e.eval("(list 1 2 3)").ToString());
Assert.Equal("3", e.eval("(find 3 (list 1 2 3 4))").ToString());
Assert.Equal("nil", e.eval("(find 0 (list 1 2 3 4))").ToString());
Assert.Equal("(2 4 6)", e.eval("(map (lambda (x) (* x 2)) (quote (1 2 3)))").ToString());
Assert.Equal("nil", e.eval("(and (quote (1 2 3 nil)))").ToString());
Assert.Equal("t", e.eval("(and (quote (1 2 3 4)))").ToString());
Assert.Equal("t", e.eval("(or (quote (nil nil 1 nil)))").ToString());
Assert.Equal("nil", e.eval("(or (quote (nil nil nil nil)))").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());
}
}
}