Compare commits
17 commits
collection
...
main
Author | SHA1 | Date | |
---|---|---|---|
64ae51ba71 | |||
d8145565cd | |||
5689d0424c | |||
eef2f32e14 | |||
2f07efd215 | |||
1aeb4d3cff | |||
fef10b5736 | |||
49bacbffde | |||
49298a3ca2 | |||
aea313a813 | |||
a569d863ae | |||
001aad5ed9 | |||
614c0a0cb1 | |||
f25eafd186 | |||
d2a10a967e | |||
a89b4606c5 | |||
4bc3b463cb |
14 changed files with 370 additions and 128 deletions
101
Jellyfin.Plugin.SmartPlaylist/Api/LispPlaygroundController.cs
Normal file
101
Jellyfin.Plugin.SmartPlaylist/Api/LispPlaygroundController.cs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using MediaBrowser.Common.Api;
|
||||||
|
|
||||||
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.SmartPlaylist.Api {
|
||||||
|
[Serializable]
|
||||||
|
public class ProgramOutputDto {
|
||||||
|
public string Output { get; set; }
|
||||||
|
public string FinalExpression { get; set; }
|
||||||
|
public string Traceback { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[Route("LispPlayground")]
|
||||||
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
|
public class LispPlaygroundController : ControllerBase {
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public LispPlaygroundController(
|
||||||
|
ILogger<LispPlaygroundController> logger
|
||||||
|
) {
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Executor SetupExecutor(StringBuilder sb) {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
executor.builtins["print"] = (x) => {
|
||||||
|
sb.Append(string.Join(" ", x.Select((i) => {
|
||||||
|
if (i is Lisp.String i_s) {
|
||||||
|
return i_s.Value();
|
||||||
|
}
|
||||||
|
return i.ToString();
|
||||||
|
})));
|
||||||
|
return Lisp.Boolean.TRUE;
|
||||||
|
};
|
||||||
|
executor.builtins["print"] = (x) => {
|
||||||
|
sb.Append(string.Join(" ", x.Select((i) => {
|
||||||
|
if (i is Lisp.String i_s) {
|
||||||
|
return i_s.Value();
|
||||||
|
}
|
||||||
|
return i.ToString();
|
||||||
|
})));
|
||||||
|
sb.Append("\n");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<ProgramOutputDto>> SetPlaylist() {
|
||||||
|
try {
|
||||||
|
string program;
|
||||||
|
using (StreamReader reader = new StreamReader(Request.Body)) {
|
||||||
|
program = await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
var e = SetupExecutor(output);
|
||||||
|
var r = e.eval(program).ToString();
|
||||||
|
return Ok(new ProgramOutputDto() {
|
||||||
|
FinalExpression = r,
|
||||||
|
Output = output.ToString(),
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return Ok(new ProgramOutputDto() {
|
||||||
|
Traceback = ex.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,9 @@ document.querySelector('#SmartPlaylistConfigForm')
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
||||||
config.InitialProgram = document.querySelector('#InitialProgram').value;
|
config.InitialProgram = document.querySelector('#InitialProgram').value;
|
||||||
|
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -47,6 +47,40 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
this["any"] = e.eval("(lambda (fc l) (apply or (map fc 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["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["append"] = e.eval("(lambda (l i) (if (null l) i (cons (car l) (append (cdr l) i))))");
|
||||||
|
this["reverse"] = e.eval(
|
||||||
|
"""
|
||||||
|
(lambda
|
||||||
|
(lst)
|
||||||
|
(let
|
||||||
|
(rev-helper
|
||||||
|
(lambda
|
||||||
|
(l acc)
|
||||||
|
(if
|
||||||
|
(null l)
|
||||||
|
acc
|
||||||
|
(rev-helper (cdr l) (cons (car l) acc)))))
|
||||||
|
(rev-helper lst '())))
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
this["split"] = e.eval(
|
||||||
|
"""
|
||||||
|
(lambda
|
||||||
|
(lst n)
|
||||||
|
(if
|
||||||
|
(or (= n 0) (null lst))
|
||||||
|
(cons '() lst)
|
||||||
|
(let
|
||||||
|
(s (split (cdr lst) (- n 1)))
|
||||||
|
(cons (cons (car lst) (car s)) (cdr s)))))
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
this["length"] = e.eval(
|
||||||
|
"""
|
||||||
|
(lambda
|
||||||
|
(lst n)
|
||||||
|
(if (null lst) n (length (cdr lst) (+ n 1))))
|
||||||
|
"""
|
||||||
|
);
|
||||||
this["qsort"] = e.eval(
|
this["qsort"] = e.eval(
|
||||||
"""
|
"""
|
||||||
(lambda
|
(lambda
|
||||||
|
@ -56,25 +90,50 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
(lambda
|
(lambda
|
||||||
(list0)
|
(list0)
|
||||||
(car 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)))))
|
||||||
(split
|
(split
|
||||||
(lambda
|
(lambda
|
||||||
(list0 pivot fc h0 h1)
|
(list0 pivot fc h0 h1 heq)
|
||||||
(cond
|
(cond
|
||||||
((null list0) (list list0 pivot fc h0 h1))
|
((null list0) (list list0 pivot fc h0 h1 heq))
|
||||||
((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
|
((and
|
||||||
(t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
|
(fc (car list0) pivot)
|
||||||
|
(fc pivot (car list0))) (split (cdr list0) pivot fc h0 h1 (cons (car list0) heq)))
|
||||||
|
((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1) heq))
|
||||||
|
((fc pivot (car list0)) (split (cdr list0) pivot fc (cons (car list0) h0) h1 heq))
|
||||||
|
((= (car list0) pivot) (split (cdr list0) pivot fc h0 h1 (cons (car list0) heq)))
|
||||||
|
(t (split (cdr list0) pivot fc h0 h1 heq)))))
|
||||||
|
;(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
|
(sort
|
||||||
(lambda
|
(lambda
|
||||||
(fc list0)
|
(fc list0)
|
||||||
(cond
|
(begin t (cond
|
||||||
((null list0) nil)
|
((null list0) nil)
|
||||||
((null (cdr list0)) list0)
|
((null (cdr list0)) list0)
|
||||||
(t
|
(t
|
||||||
(let*
|
(let*
|
||||||
(halves (split list0 (getpivot list0) fc nil nil))
|
(halves (split list0 (getpivot list0) fc nil nil nil))
|
||||||
(h0 (car (cdr (cdr (cdr halves)))))
|
(h0 (car (cdr (cdr (cdr halves)))))
|
||||||
(h1 (car (cdr (cdr (cdr (cdr halves))))))
|
(h1 (car (cdr (cdr (cdr (cdr halves))))))
|
||||||
(append (sort fc h0) (sort fc h1)))))))
|
(heq (car (cdr (cdr (cdr (cdr (cdr halves)))))))
|
||||||
|
(append (append (sort fc h0) heq) (sort fc h1))))))))
|
||||||
(sort fc list00)))
|
(sort fc list00)))
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
@ -418,7 +477,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
||||||
foreach (var pair in args.SkipLast(1)) {
|
foreach (var pair in args.SkipLast(1)) {
|
||||||
if (pair is not Cons pair_cons) {
|
if (pair is not Cons pair_cons) {
|
||||||
throw new ApplicationException("No expression for let*");
|
throw new ApplicationException($"All arguments but the last have to be a list of two items, got {pair}");
|
||||||
}
|
}
|
||||||
Symbol refname = (Symbol) pair_cons.Item1;
|
Symbol refname = (Symbol) pair_cons.Item1;
|
||||||
Expression exp = ((Cons) pair_cons.Item2).Item1;
|
Expression exp = ((Cons) pair_cons.Item2).Item1;
|
||||||
|
@ -431,7 +490,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
List<(Symbol, Expression)> vars = new List<(Symbol, Expression)>();
|
List<(Symbol, Expression)> vars = new List<(Symbol, Expression)>();
|
||||||
foreach (var pair in args.SkipLast(1)) {
|
foreach (var pair in args.SkipLast(1)) {
|
||||||
if (pair is not Cons pair_cons) {
|
if (pair is not Cons pair_cons) {
|
||||||
throw new ApplicationException("");
|
throw new ApplicationException($"All arguments but the last have to be a list of two items, got {pair}");
|
||||||
}
|
}
|
||||||
Symbol refname = (Symbol) pair_cons.Item1;
|
Symbol refname = (Symbol) pair_cons.Item1;
|
||||||
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
|
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
|
||||||
|
@ -447,7 +506,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
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 if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||||
else {
|
else {
|
||||||
throw new ApplicationException("");
|
throw new ApplicationException($"Expexted an argument list, but got {args.First()}");
|
||||||
}
|
}
|
||||||
return new Procedure(proc_args, args.Skip(1).First(), true);
|
return new Procedure(proc_args, args.Skip(1).First(), true);
|
||||||
}
|
}
|
||||||
|
@ -456,7 +515,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
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 if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||||
else {
|
else {
|
||||||
throw new ApplicationException("");
|
throw new ApplicationException($"Expexted an argument list, but got {args.First()}");
|
||||||
}
|
}
|
||||||
return new Procedure(proc_args, args.Skip(1).First(), false);
|
return new Procedure(proc_args, args.Skip(1).First(), false);
|
||||||
}
|
}
|
||||||
|
|
42
Jellyfin.Plugin.SmartPlaylist/Pages/lispPlayground.html
Normal file
42
Jellyfin.Plugin.SmartPlaylist/Pages/lispPlayground.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>LispPlayground</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="LispPlaygroundConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox" data-controller="__plugin/lispPlayground.js">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<form id="LispPlaygroundConfigForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundEditProgram">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="LispPlaygroundEditProgram" class="emby-input smartcollection-monospace" name="Program" rows="16" cols="120"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundOutput">Output</label>
|
||||||
|
<div class="fieldDescription">The output of the program.</div>
|
||||||
|
<textarea id="LispPlaygroundOutput" class="emby-input smartcollection-monospace" name="Output" rows="16" cols="120"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundReturn">Final expression</label>
|
||||||
|
<div class="fieldDescription">The final expression the program has been reduced to.</div>
|
||||||
|
<textarea id="LispPlaygroundReturn" class="emby-input smartcollection-monospace" name="Output" rows="1" cols="120"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>Run</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartcollection-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
46
Jellyfin.Plugin.SmartPlaylist/Pages/lispPlayground.js
Normal file
46
Jellyfin.Plugin.SmartPlaylist/Pages/lispPlayground.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
function fillForm(o) {
|
||||||
|
const output = document.querySelector('#LispPlaygroundOutput');
|
||||||
|
const return_ = document.querySelector('#LispPlaygroundReturn');
|
||||||
|
output.value = (o.hasOwnProperty("Output")) ? o.Output : o.Traceback;
|
||||||
|
return_.value = (o.hasOwnProperty("FinalExpression")) ? o.FinalExpression : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiClient.runLispProgram = function (program) {
|
||||||
|
const url = ApiClient.getUrl('LispPlayground');
|
||||||
|
return this.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'text/plain; charset=UTF-8',
|
||||||
|
data: program,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function initial_load() {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (view, params) {
|
||||||
|
view.addEventListener('viewshow', function() {
|
||||||
|
initial_load(null);
|
||||||
|
});
|
||||||
|
view.addEventListener('viewhide', function (_e) {});
|
||||||
|
view.addEventListener('viewdestroy', function (_e) {});
|
||||||
|
|
||||||
|
document.querySelector('#LispPlaygroundConfigPage')
|
||||||
|
.addEventListener('pageshow', function() {
|
||||||
|
initial_load();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#LispPlaygroundConfigForm')
|
||||||
|
.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
const editProgram = document.querySelector('#LispPlaygroundEditProgram');
|
||||||
|
ApiClient.runLispProgram(editProgram.value).then(function (r) {
|
||||||
|
fillForm(r);
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -88,15 +88,13 @@ function initial_load(selectedId) {
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.querySelector('#SmartCollectionConfigPage')
|
|
||||||
.addEventListener('viewshow', function() {
|
|
||||||
initial_load(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#SmartCollectionConfigPage')
|
export default function (view, params) {
|
||||||
.addEventListener('pageshow', function() {
|
view.addEventListener('viewshow', function() {
|
||||||
initial_load(null);
|
initial_load(null);
|
||||||
});
|
});
|
||||||
|
view.addEventListener('viewhide', function (_e) {});
|
||||||
|
view.addEventListener('viewdestroy', function (_e) {});
|
||||||
|
|
||||||
document.querySelector('#SmartcollectionSelection')
|
document.querySelector('#SmartcollectionSelection')
|
||||||
.addEventListener('change', function() {
|
.addEventListener('change', function() {
|
||||||
|
@ -114,3 +112,4 @@ document.querySelector('#SmartCollectionConfigForm')
|
||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -116,14 +116,12 @@ function initial_load(selectedId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector('#SmartPlaylistConfigPage')
|
export default function (view, params) {
|
||||||
.addEventListener('viewshow', function() {
|
view.addEventListener('viewshow', function() {
|
||||||
initial_load(null);
|
|
||||||
});
|
|
||||||
document.querySelector('#SmartPlaylistConfigPage')
|
|
||||||
.addEventListener('pageshow', function() {
|
|
||||||
initial_load(null);
|
initial_load(null);
|
||||||
});
|
});
|
||||||
|
view.addEventListener('viewhide', function (_e) {});
|
||||||
|
view.addEventListener('viewdestroy', function (_e) {});
|
||||||
|
|
||||||
document.querySelector('#SmartplaylistSelection')
|
document.querySelector('#SmartplaylistSelection')
|
||||||
.addEventListener('change', function() {
|
.addEventListener('change', function() {
|
||||||
|
@ -141,3 +139,4 @@ document.querySelector('#SmartPlaylistConfigForm')
|
||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,16 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
Name = "smartCollections.js",
|
Name = "smartCollections.js",
|
||||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartCollections.js", GetType().Namespace),
|
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartCollections.js", GetType().Namespace),
|
||||||
},
|
},
|
||||||
|
new PluginPageInfo {
|
||||||
|
Name = "Lisp Playground",
|
||||||
|
DisplayName = "Lisp Playground",
|
||||||
|
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.lispPlayground.html", GetType().Namespace),
|
||||||
|
EnableInMainMenu = true,
|
||||||
|
},
|
||||||
|
new PluginPageInfo {
|
||||||
|
Name = "lispPlayground.js",
|
||||||
|
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.lispPlayground.js", GetType().Namespace),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,39 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
public class Common {
|
public class Common {
|
||||||
public static readonly BaseItemKind[] AvailableFilterItems = {
|
public static readonly BaseItemKind[] AvailableFilterItems = {
|
||||||
BaseItemKind.Audio,
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.AudioBook,
|
||||||
|
BaseItemKind.Book,
|
||||||
|
BaseItemKind.BoxSet,
|
||||||
|
BaseItemKind.Channel,
|
||||||
|
// BaseItemKind.ChannelFolderItem,
|
||||||
|
// BaseItemKind.CollectionFolder,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
// BaseItemKind.Folder,
|
||||||
|
BaseItemKind.Genre,
|
||||||
|
// BaseItemKind.ManualPlaylistsFolder,
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.LiveTvChannel,
|
||||||
|
BaseItemKind.LiveTvProgram,
|
||||||
BaseItemKind.MusicAlbum,
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicGenre,
|
||||||
|
BaseItemKind.MusicVideo,
|
||||||
|
BaseItemKind.Person,
|
||||||
|
BaseItemKind.Photo,
|
||||||
|
BaseItemKind.PhotoAlbum,
|
||||||
BaseItemKind.Playlist,
|
BaseItemKind.Playlist,
|
||||||
|
// BaseItemKind.PlaylistsFolder,
|
||||||
|
BaseItemKind.Program,
|
||||||
|
BaseItemKind.Recording,
|
||||||
|
BaseItemKind.Season,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Studio,
|
||||||
|
BaseItemKind.Trailer,
|
||||||
|
BaseItemKind.TvChannel,
|
||||||
|
BaseItemKind.TvProgram,
|
||||||
|
BaseItemKind.UserView,
|
||||||
|
BaseItemKind.Video,
|
||||||
|
BaseItemKind.Year
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Smart Playlist
|
name: Smart Playlist
|
||||||
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
||||||
version: 0.4.0.0
|
version: 0.5.2.0
|
||||||
targetAbi: 10.10.3.0
|
targetAbi: 10.10.5.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.
|
||||||
|
@ -14,66 +14,8 @@ artifacts:
|
||||||
- jellyfin-smart-playlist.dll
|
- jellyfin-smart-playlist.dll
|
||||||
- YamlDotNet.dll
|
- YamlDotNet.dll
|
||||||
changelog: |
|
changelog: |
|
||||||
## v0.4.0.0
|
## v0.5.2.0
|
||||||
- Add a basic UI to configure the playlists.
|
- bump Jellyfin ABI version to 10.10.5
|
||||||
- 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**:
|
**Fixes**:
|
||||||
- The initialization of the executor now contains the same default definitions for the SortProgram and the normal Program.
|
- the config pages will always load, not only the first time
|
||||||
- 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.
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.4.0.0</Version>
|
<Version>0.5.2.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.5" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.5" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -21,12 +21,16 @@
|
||||||
<None Remove="Pages\smartPlaylists.js"/>
|
<None Remove="Pages\smartPlaylists.js"/>
|
||||||
<None Remove="Pages\smartCollections.html"/>
|
<None Remove="Pages\smartCollections.html"/>
|
||||||
<None Remove="Pages\smartCollections.js"/>
|
<None Remove="Pages\smartCollections.js"/>
|
||||||
|
<None Remove="Pages\lispPlayground.html"/>
|
||||||
|
<None Remove="Pages\lispPlayground.js"/>
|
||||||
<EmbeddedResource Include="Configuration\configPage.html"/>
|
<EmbeddedResource Include="Configuration\configPage.html"/>
|
||||||
<EmbeddedResource Include="Configuration\configPage.js"/>
|
<EmbeddedResource Include="Configuration\configPage.js"/>
|
||||||
<EmbeddedResource Include="Pages\smartPlaylists.html"/>
|
<EmbeddedResource Include="Pages\smartPlaylists.html"/>
|
||||||
<EmbeddedResource Include="Pages\smartPlaylists.js"/>
|
<EmbeddedResource Include="Pages\smartPlaylists.js"/>
|
||||||
<EmbeddedResource Include="Pages\smartCollections.html"/>
|
<EmbeddedResource Include="Pages\smartCollections.html"/>
|
||||||
<EmbeddedResource Include="Pages\smartCollections.js"/>
|
<EmbeddedResource Include="Pages\smartCollections.js"/>
|
||||||
|
<EmbeddedResource Include="Pages\lispPlayground.html"/>
|
||||||
|
<EmbeddedResource Include="Pages\lispPlayground.js"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -5,7 +5,7 @@ Smart playlists with Lisp filter engine.
|
||||||
This readme contains instructions for the most recent changes in
|
This readme contains instructions for the most recent changes in
|
||||||
the development branch (`main`). To view the file appropriate
|
the development branch (`main`). To view the file appropriate
|
||||||
for your version select the tag corresponding to your version.
|
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).
|
The latest version is [v0.5.2.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.5.2.0).
|
||||||
|
|
||||||
![configuration page](config.png)
|
![configuration page](config.png)
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,16 @@ JELLYFIN=jellyfin/jellyfin
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
pwd
|
mkdir -p ./cache ./media ./config/plugins/jellyfin-smart-playlist
|
||||||
|
if [ "$#" -eq 1 ] && [ "$1" = '--skip-build' ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
(
|
(
|
||||||
cd ../Jellyfin.Plugin.SmartPlaylist/
|
cd ../Jellyfin.Plugin.SmartPlaylist/
|
||||||
dotnet build
|
dotnet build
|
||||||
)
|
)
|
||||||
pwd
|
|
||||||
mkdir -p ./cache ./media ./config/plugins/jellyfin-smart-playlist
|
|
||||||
cp ../Jellyfin.Plugin.SmartPlaylist/bin/Debug/net8.0/jellyfin-smart-playlist.dll ./config/plugins/jellyfin-smart-playlist/
|
cp ../Jellyfin.Plugin.SmartPlaylist/bin/Debug/net8.0/jellyfin-smart-playlist.dll ./config/plugins/jellyfin-smart-playlist/
|
||||||
|
fi
|
||||||
docker pull "$JELLYFIN"
|
docker pull "$JELLYFIN"
|
||||||
docker run --rm --user "$(id -u):$(id -g)" \
|
docker run --rm --user "$(id -u):$(id -g)" \
|
||||||
-v ./cache:/cache \
|
-v ./cache:/cache \
|
||||||
|
|
|
@ -267,6 +267,10 @@ namespace Tests
|
||||||
//Assert.Equal("", e.eval("(shuf (list 0 1 2 3 4 5 6))").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 (lambda (a b) (> a b)) '(5 4 7 3 2 6 1))").ToString());
|
||||||
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort > (list 5 4 7 3 2 6 1))").ToString());
|
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort > (list 5 4 7 3 2 6 1))").ToString());
|
||||||
|
Assert.Equal("(0 1 2 4 5 5)", e.eval("(qsort > '(2 4 1 5 5 0))").ToString());
|
||||||
|
Assert.Equal("(5 5 4 2 1 0)", e.eval("(qsort < '(2 4 1 5 5 0))").ToString());
|
||||||
|
Assert.Equal("(5 5 4 2 1 0)", e.eval("(qsort <= '(2 4 1 5 5 0))").ToString());
|
||||||
|
Assert.Equal("(0 1 2 4 5 5)", e.eval("(qsort >= '(2 4 1 5 5 0))").ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue