Compare commits
No commits in common. "main" and "collections" have entirely different histories.
main
...
collection
24 changed files with 175 additions and 564 deletions
|
@ -1,132 +0,0 @@
|
|||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
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 MediaBrowser.Controller.Library;
|
||||
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.Api {
|
||||
|
||||
[Serializable]
|
||||
public class ProgramInputDto {
|
||||
public string InputLinks { get; set; }
|
||||
public string Program { get; set; }
|
||||
}
|
||||
|
||||
[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;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public LispPlaygroundController(
|
||||
ILogger<LispPlaygroundController> logger,
|
||||
ILibraryManager libraryManager
|
||||
) {
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
private Executor SetupExecutor(StringBuilder sb, IList<string> inputs) {
|
||||
var env = new DefaultEnvironment();
|
||||
var executor = new Executor(env);
|
||||
|
||||
env["*items*"] = Lisp.Cons.FromList(inputs.Select(x => _libraryManager.GetItemById(x)).Select(x => Lisp.Object.FromBase(x)).ToList());
|
||||
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["println"] = (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;
|
||||
}
|
||||
|
||||
private List<string> extractItemIds(string s) {
|
||||
List<string> r = new List<string>();
|
||||
foreach (string line in s.Split("\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)) {
|
||||
var uri = new Uri(line);
|
||||
uri = new Uri("http://some-domain.tld" + uri.Fragment.Substring(1));
|
||||
var id = HttpUtility.ParseQueryString(uri.Query).Get("id");
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
r.Add(id);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ProgramOutputDto>> SetPlaylist() {
|
||||
try {
|
||||
string input;
|
||||
using (StreamReader reader = new StreamReader(Request.Body)) {
|
||||
input = await reader.ReadToEndAsync();
|
||||
}
|
||||
var dto = new DeserializerBuilder().Build().Deserialize<ProgramInputDto>(input);
|
||||
StringBuilder output = new StringBuilder();
|
||||
var e = SetupExecutor(output, extractItemIds(dto.InputLinks));
|
||||
var r = e.eval(dto.Program).ToString();
|
||||
return Ok(new ProgramOutputDto() {
|
||||
FinalExpression = r,
|
||||
Output = output.ToString(),
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
return Ok(new ProgramOutputDto() {
|
||||
Traceback = ex.ToString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,13 +58,5 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
|
|||
await _store.SaveSmartCollectionAsync(smartCollection);
|
||||
return Created();
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("{smartCollectionId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeleteCollection([FromRoute, Required] SmartCollectionId smartCollectionId) {
|
||||
_store.DeleteSmartCollectionById(smartCollectionId);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,13 +57,5 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
|
|||
await _store.SaveSmartPlaylistAsync(smartPlaylist);
|
||||
return Created();
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("{smartPlaylistId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeletePlaylist([FromRoute, Required] SmartPlaylistId smartPlaylistId) {
|
||||
_store.DeleteSmartPlaylistById(smartPlaylistId);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,6 @@ document.querySelector('#SmartPlaylistConfigForm')
|
|||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
||||
config.InitialProgram = document.querySelector('#InitialProgram').value;
|
||||
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
|
|
@ -3,7 +3,7 @@ using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
|||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||
public class Parser {
|
||||
internal StringTokenStream _sts;
|
||||
private StringTokenStream _sts;
|
||||
public Parser(StringTokenStream tokens) {
|
||||
_sts = tokens;
|
||||
}
|
||||
|
@ -29,18 +29,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
Debug.Assert(start.value == end.value);
|
||||
Debug.Assert("\"".Contains(start.value));
|
||||
string r = "";
|
||||
bool exit_ok = false;
|
||||
while (_sts.Available() > 0) {
|
||||
Token<string> t = _sts.Get();
|
||||
if (t.value == end.value) {
|
||||
exit_ok = true;
|
||||
break;
|
||||
}
|
||||
r += t.value;
|
||||
}
|
||||
if (!exit_ok) {
|
||||
throw new ApplicationException($"Failed to parse string, are you missing the closing quotes? String is: {r}");
|
||||
}
|
||||
_sts.Commit();
|
||||
return new String(r);
|
||||
}
|
||||
|
@ -74,16 +69,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
if (";".Contains(start.value)) {
|
||||
return parse_comment(start, end);
|
||||
}
|
||||
if (end == null) {
|
||||
throw new ApplicationException($"Don't know how to parse grouping starting with token '{start.value}'");
|
||||
}
|
||||
|
||||
Debug.Assert(end != null);
|
||||
IList<Expression> expressions = new List<Expression>();
|
||||
bool exit_ok = false;
|
||||
while (_sts.Available() > 0) {
|
||||
Token<string> t = _sts.Get();
|
||||
if (t.value == end.value) {
|
||||
exit_ok = true;
|
||||
_sts.Commit();
|
||||
break;
|
||||
}
|
||||
|
@ -97,11 +87,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
_sts.Rewind(1);
|
||||
expressions.Add(parse());
|
||||
}
|
||||
var r = Cons.FromList(expressions);
|
||||
if (!exit_ok) {
|
||||
throw new ApplicationException($"Failed to parse grouping, are you missing some closing braces? Parsed expressions: {r}");
|
||||
}
|
||||
return r;
|
||||
return Cons.FromList(expressions);
|
||||
}
|
||||
|
||||
Expression parse_atom(AtomToken at) {
|
||||
|
|
|
@ -47,40 +47,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
this["any"] = e.eval("(lambda (fc l) (apply or (map fc l)))");
|
||||
this["all"] = e.eval("(lambda (fc l) (apply and (map fc l)))");
|
||||
this["append"] = e.eval("(lambda (l i) (if (null l) i (cons (car l) (append (cdr l) i))))");
|
||||
this["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(
|
||||
"""
|
||||
(lambda
|
||||
|
@ -90,50 +56,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
(lambda
|
||||
(list0)
|
||||
(car list0)))
|
||||
;(split
|
||||
; (lambda
|
||||
; (list0 pivot fc h0 h1)
|
||||
; (cond
|
||||
; ((null list0) (list list0 pivot fc h0 h1))
|
||||
; ((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
|
||||
; (t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
|
||||
(split
|
||||
(lambda
|
||||
(list0 pivot fc h0 h1 heq)
|
||||
(list0 pivot fc h0 h1)
|
||||
(cond
|
||||
((null list0) (list list0 pivot fc h0 h1 heq))
|
||||
((and
|
||||
(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)))))))
|
||||
((null list0) (list list0 pivot fc h0 h1))
|
||||
((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
|
||||
(t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
|
||||
(sort
|
||||
(lambda
|
||||
(fc list0)
|
||||
(begin t (cond
|
||||
(cond
|
||||
((null list0) nil)
|
||||
((null (cdr list0)) list0)
|
||||
(t
|
||||
(let*
|
||||
(halves (split list0 (getpivot list0) fc nil nil nil))
|
||||
(halves (split list0 (getpivot list0) fc nil nil))
|
||||
(h0 (car (cdr (cdr (cdr halves)))))
|
||||
(h1 (car (cdr (cdr (cdr (cdr halves))))))
|
||||
(heq (car (cdr (cdr (cdr (cdr (cdr halves)))))))
|
||||
(append (append (sort fc h0) heq) (sort fc h1))))))))
|
||||
(append (sort fc h0) (sort fc h1)))))))
|
||||
(sort fc list00)))
|
||||
"""
|
||||
);
|
||||
|
@ -477,7 +418,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater);
|
||||
foreach (var pair in args.SkipLast(1)) {
|
||||
if (pair is not Cons pair_cons) {
|
||||
throw new ApplicationException($"All arguments but the last have to be a list of two items, got {pair}");
|
||||
throw new ApplicationException("No expression for let*");
|
||||
}
|
||||
Symbol refname = (Symbol) pair_cons.Item1;
|
||||
Expression exp = ((Cons) pair_cons.Item2).Item1;
|
||||
|
@ -490,7 +431,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
List<(Symbol, Expression)> vars = new List<(Symbol, Expression)>();
|
||||
foreach (var pair in args.SkipLast(1)) {
|
||||
if (pair is not Cons pair_cons) {
|
||||
throw new ApplicationException($"All arguments but the last have to be a list of two items, got {pair}");
|
||||
throw new ApplicationException("");
|
||||
}
|
||||
Symbol refname = (Symbol) pair_cons.Item1;
|
||||
Expression exp_ = ((Cons) pair_cons.Item2).Item1;
|
||||
|
@ -506,7 +447,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
||||
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||
else {
|
||||
throw new ApplicationException($"Expexted an argument list, but got {args.First()}");
|
||||
throw new ApplicationException("");
|
||||
}
|
||||
return new Procedure(proc_args, args.Skip(1).First(), true);
|
||||
}
|
||||
|
@ -515,7 +456,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
if (args.First() is Cons proc_args_) { proc_args = proc_args_.ToList().Select(x => (Symbol) x); }
|
||||
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
|
||||
else {
|
||||
throw new ApplicationException($"Expexted an argument list, but got {args.First()}");
|
||||
throw new ApplicationException("");
|
||||
}
|
||||
return new Procedure(proc_args, args.Skip(1).First(), false);
|
||||
}
|
||||
|
@ -601,11 +542,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
|||
throw new ApplicationException($"Not handled case '{expression}'");
|
||||
}
|
||||
public Expression eval(Parser p) {
|
||||
Expression r = eval(p.parse());
|
||||
if (p._sts.Available() > 0) {
|
||||
throw new ApplicationException($"Did not consume all tokens, remaining program is {string.Join(" ", p._sts.Remainder())}");
|
||||
}
|
||||
return r;
|
||||
return eval(p.parse());
|
||||
}
|
||||
public Expression eval(StringTokenStream sts) {
|
||||
return eval(new Parser(sts));
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<!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="LispPlaygroundInputLinks">Input Items</label>
|
||||
<div class="fieldDescription">
|
||||
Items which should be given to the filter program, one link per line.
|
||||
This list is provided to the program in the
|
||||
<code>*items*</code> variable.<br>
|
||||
Hint: Just copy the link to the media item,
|
||||
it should contain a <code>id</code> in
|
||||
the query part of the URL's
|
||||
fragment.<br>
|
||||
Example:
|
||||
<code>https://jellyfin.example.com/web/#/details?id=9f63ee779963588ed2ad94d37d6abe4e&serverId=dc0683d7a413473eb8246d21eaea2f99</code><br>
|
||||
<textarea id="LispPlaygroundInputLinks" class="emby-input smartplayground-monospace" name="InputLinks" rows="6" cols="120"></textarea>
|
||||
</div>
|
||||
<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 smartplayground-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 smartplayground-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 smartplayground-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>
|
||||
.smartplayground-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
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 (inputLinks, program) {
|
||||
const url = ApiClient.getUrl('LispPlayground');
|
||||
return this.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json; charset=UTF-8',
|
||||
data: JSON.stringify({
|
||||
InputLinks: inputLinks,
|
||||
Program: 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 editInputLinks = document.querySelector('#LispPlaygroundInputLinks');
|
||||
const editProgram = document.querySelector('#LispPlaygroundEditProgram');
|
||||
ApiClient.runLispProgram(editInputLinks.value, editProgram.value).then(function (r) {
|
||||
fillForm(r);
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -37,9 +37,6 @@
|
|||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<button is="emby-button" type="reset" class="raised button-reset block emby-button">
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -73,15 +73,6 @@ ApiClient.setSmartCollection = function (c) {
|
|||
data: JSON.stringify(c),
|
||||
});
|
||||
}
|
||||
|
||||
ApiClient.deleteSmartCollection = function (id) {
|
||||
const url = ApiClient.getUrl(`SmartCollection/${id}`)
|
||||
return this.ajax({
|
||||
type: 'DELETE',
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
|
||||
function initial_load(selectedId) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getSmartCollections().then(function (collections) {
|
||||
|
@ -97,21 +88,23 @@ function initial_load(selectedId) {
|
|||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
}
|
||||
|
||||
export default function (view, params) {
|
||||
view.addEventListener('viewshow', function() {
|
||||
document.querySelector('#SmartCollectionConfigPage')
|
||||
.addEventListener('viewshow', function() {
|
||||
initial_load(null);
|
||||
});
|
||||
view.addEventListener('viewhide', function (_e) {});
|
||||
view.addEventListener('viewdestroy', function (_e) {});
|
||||
|
||||
document.querySelector('#SmartcollectionSelection')
|
||||
document.querySelector('#SmartCollectionConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
initial_load(null);
|
||||
});
|
||||
|
||||
document.querySelector('#SmartcollectionSelection')
|
||||
.addEventListener('change', function() {
|
||||
const selection = document.querySelector('#SmartcollectionSelection');
|
||||
fillForm(COLLECTIONS[selection.selectedIndex]);
|
||||
});
|
||||
|
||||
document.querySelector('#SmartCollectionConfigForm')
|
||||
document.querySelector('#SmartCollectionConfigForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
const selection = document.querySelector('#SmartcollectionSelection');
|
||||
|
@ -121,14 +114,3 @@ export default function (view, params) {
|
|||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
document.querySelector('#SmartCollectionConfigForm')
|
||||
.addEventListener('reset', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
const selection = document.querySelector('#SmartcollectionSelection');
|
||||
const selectedId = COLLECTIONS[selection.selectedIndex].Id;
|
||||
ApiClient.deleteSmartCollection(selectedId).then(function () {
|
||||
initial_load(null);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,9 +43,6 @@
|
|||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<button is="emby-button" type="reset" class="raised button-reset block emby-button">
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -97,14 +97,6 @@ ApiClient.setSmartPlaylist = function (p) {
|
|||
});
|
||||
}
|
||||
|
||||
ApiClient.deleteSmartPlaylist = function (id) {
|
||||
const url = ApiClient.getUrl(`SmartPlaylist/${id}`)
|
||||
return this.ajax({
|
||||
type: 'DELETE',
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
|
||||
function initial_load(selectedId) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getSmartPlaylists().then(function (playlists) {
|
||||
|
@ -124,20 +116,22 @@ function initial_load(selectedId) {
|
|||
});
|
||||
}
|
||||
|
||||
export default function (view, params) {
|
||||
view.addEventListener('viewshow', function() {
|
||||
document.querySelector('#SmartPlaylistConfigPage')
|
||||
.addEventListener('viewshow', function() {
|
||||
initial_load(null);
|
||||
});
|
||||
document.querySelector('#SmartPlaylistConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
initial_load(null);
|
||||
});
|
||||
view.addEventListener('viewhide', function (_e) {});
|
||||
view.addEventListener('viewdestroy', function (_e) {});
|
||||
|
||||
document.querySelector('#SmartplaylistSelection')
|
||||
document.querySelector('#SmartplaylistSelection')
|
||||
.addEventListener('change', function() {
|
||||
const selection = document.querySelector('#SmartplaylistSelection');
|
||||
fillForm(PLAYLISTS[selection.selectedIndex], USERS);
|
||||
});
|
||||
|
||||
document.querySelector('#SmartPlaylistConfigForm')
|
||||
document.querySelector('#SmartPlaylistConfigForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
const selection = document.querySelector('#SmartplaylistSelection');
|
||||
|
@ -147,14 +141,3 @@ export default function (view, params) {
|
|||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
document.querySelector('#SmartPlaylistConfigForm')
|
||||
.addEventListener('reset', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
const selection = document.querySelector('#SmartplaylistSelection');
|
||||
const selectedId = PLAYLISTS[selection.selectedIndex].Id;
|
||||
ApiClient.deleteSmartPlaylist(selectedId).then(function () {
|
||||
initial_load(null);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -53,16 +53,6 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
|||
Name = "smartCollections.js",
|
||||
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,39 +11,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
public class Common {
|
||||
public static readonly BaseItemKind[] AvailableFilterItems = {
|
||||
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.MusicArtist,
|
||||
BaseItemKind.MusicGenre,
|
||||
BaseItemKind.MusicVideo,
|
||||
BaseItemKind.Person,
|
||||
BaseItemKind.Photo,
|
||||
BaseItemKind.PhotoAlbum,
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
|
@ -77,5 +46,39 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
}
|
||||
return executor;
|
||||
}
|
||||
public IEnumerable<Guid> FilterCollectionItems(IEnumerable<BaseItem> items, User? user, string name, string program, string sortProgram) {
|
||||
List<BaseItem> results = new List<BaseItem>();
|
||||
Expression expression = new Parser(StringTokenStream.generate(program)).parse(); // parse here, so that we don't repeat the work for each item
|
||||
Executor executor = SetupExecutor();
|
||||
|
||||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
foreach (var i in items) {
|
||||
executor.environment.Set("*item*", Lisp.Object.FromBase(i));
|
||||
var r = executor.eval(expression);
|
||||
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
||||
if ((r is not Lisp.Boolean r_bool) || (r_bool.Value())) {
|
||||
_logger.LogDebug("Added '{0}' to Smart Collection {1}", i, name);
|
||||
results.Add(i);
|
||||
}
|
||||
}
|
||||
executor = SetupExecutor();
|
||||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
executor.environment.Set("*items*", Lisp.Object.FromBase(results));
|
||||
results = new List<BaseItem>();
|
||||
var sort_result = executor.eval(sortProgram);
|
||||
if (sort_result is Cons sorted_items) {
|
||||
foreach (var i in sorted_items.ToList()) {
|
||||
if (i is Lisp.Object iObject && iObject.Value() is BaseItem iBaseItem) {
|
||||
results.Add(iBaseItem);
|
||||
continue;
|
||||
}
|
||||
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
|
||||
}
|
||||
} else if (sort_result == Lisp.Boolean.FALSE) {
|
||||
} else {
|
||||
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
||||
}
|
||||
return results.Select(x => x.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,7 @@ using MediaBrowser.Controller.Providers;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||
|
||||
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||
public class GenerateCollection: Common, IScheduledTask {
|
||||
|
@ -64,47 +61,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
)).Id;
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> FilterCollectionItems(IEnumerable<BaseItem> items, User? user, string name, string program, string sortProgram) {
|
||||
List<BaseItem> results = new List<BaseItem>();
|
||||
Expression expression = new Parser(StringTokenStream.generate(program)).parse(); // parse here, so that we don't repeat the work for each item
|
||||
Executor executor = SetupExecutor();
|
||||
|
||||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
foreach (var i in items) {
|
||||
executor.environment.Set("*item*", Lisp.Object.FromBase(i));
|
||||
try {
|
||||
var r = executor.eval(expression);
|
||||
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
||||
if ((r is not Lisp.Boolean r_bool) || (r_bool.Value())) {
|
||||
_logger.LogDebug("Added '{0}' to Smart Collection {1}", i, name);
|
||||
results.Add(i);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_logger.LogError("Program crashed on item {0}:\n{1}", i.Id, expression.ToString());
|
||||
_logger.LogError("Environment:\n{0}", executor.environment);
|
||||
_logger.LogError("Traceback:\n{0}", e.ToString());
|
||||
}
|
||||
}
|
||||
executor = SetupExecutor();
|
||||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
executor.environment.Set("*items*", Lisp.Object.FromBase(results));
|
||||
results = new List<BaseItem>();
|
||||
var sort_result = executor.eval(sortProgram);
|
||||
if (sort_result is Cons sorted_items) {
|
||||
foreach (var i in sorted_items.ToList()) {
|
||||
if (i is Lisp.Object iObject && iObject.Value() is BaseItem iBaseItem) {
|
||||
results.Add(iBaseItem);
|
||||
continue;
|
||||
}
|
||||
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
|
||||
}
|
||||
} else if (sort_result == Lisp.Boolean.FALSE) {
|
||||
} else {
|
||||
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
||||
}
|
||||
return results.Select(x => x.Id);
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> GetAllMedia() {
|
||||
var req = new InternalItemsQuery() {
|
||||
IncludeItemTypes = AvailableFilterItems,
|
||||
|
|
|
@ -78,18 +78,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
foreach (var i in items) {
|
||||
executor.environment.Set("*item*", Lisp.Object.FromBase(i));
|
||||
try {
|
||||
var r = executor.eval(expression);
|
||||
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
||||
if ((r is not Lisp.Boolean r_bool) || (r_bool.Value())) {
|
||||
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
||||
results.Add(i);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_logger.LogError("Program crashed on item {0}:\n{1}", i.Id, expression.ToString());
|
||||
_logger.LogError("Environment:\n{0}", executor.environment);
|
||||
_logger.LogError("Traceback:\n{0}", e.ToString());
|
||||
}
|
||||
}
|
||||
executor = SetupExecutor();
|
||||
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||
|
|
|
@ -56,9 +56,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Util {
|
|||
public IStream<T> Copy() {
|
||||
return new Stream<T>(_items, _cursor, _ephemeralCursor);
|
||||
}
|
||||
public IList<T> Remainder() {
|
||||
return _items.Skip(_cursor).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: Smart Playlist
|
||||
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
||||
version: 0.5.3.0
|
||||
targetAbi: 10.10.6.0
|
||||
version: 0.4.0.0
|
||||
targetAbi: 10.10.3.0
|
||||
framework: net8.0
|
||||
owner: redxef
|
||||
overview: Smart playlists with Lisp filter engine.
|
||||
|
@ -14,9 +14,66 @@ artifacts:
|
|||
- jellyfin-smart-playlist.dll
|
||||
- YamlDotNet.dll
|
||||
changelog: |
|
||||
## v0.5.3.0
|
||||
- bump Jellyfin ABI version to 10.10.6
|
||||
- bump yamldotnet to 16.3.0
|
||||
## v0.4.0.0
|
||||
- Add a basic UI to configure the playlists.
|
||||
- It's now possible to print log messages to the jellyfin log by calling `logd`, `logi`, `logw` or `loge`
|
||||
for the respective levels `debug`, `info`, `warning` or `error`.
|
||||
- Allow calling generic methods via `(invoke-generic object methodname args list-of-types)`.
|
||||
- Add quoting via single quote: `'`.
|
||||
- Add special case for `(quote <form>)` to be rendered as `'<form>`.
|
||||
- It is now possible to include comments in the source via a semicolon (`;`).
|
||||
- Respect the `Enabled` flag and only process the playlists that are enabled.
|
||||
- New methods have been added: `rand`, `shuf`.
|
||||
- Add `find-artist`, `get-name` and `find-parent` default definitions.
|
||||
- Update YamlDotNet to v16.2.1.
|
||||
|
||||
**Breaking changes**:
|
||||
- Rename global environment variables to be enclosed by `*`.
|
||||
|
||||
**Fixes**:
|
||||
- qsort doesn't loop endlessly with duplicate values in import anymore
|
||||
- The initialization of the executor now contains the same default definitions for the SortProgram and the normal Program.
|
||||
- The progress report now considers the SmartPlaylists and not the individual playlists per user.
|
||||
- It is now possible to pass builtins as arguments. Previously to get `(qsort > (list 1 2 3))` one had to write
|
||||
something like this: `(qsort (lambda (a b) (> a b)) (list 1 2 3))`.
|
||||
- A program no longer has to be a list, `t` is a valid program.
|
||||
- Fix list parsing in cases where a space was before the closing parenthesis.
|
||||
|
||||
|
||||
## v0.3.0.0
|
||||
- Add a second program (`SortProgram`) which is run after the filtering, this
|
||||
program should return the list of items, but in the order in which they should appear in
|
||||
the playlist. The default is `(begin items)` which returns the list as is.
|
||||
- Extend builtin lisp definitions: add `qsort` and string comparison methods
|
||||
- Extend default program definitions: add `all-genres` and `any-genres` to quickly specify a list of genres which to include (or excluding when negating)
|
||||
- Update Jellyfin to v 10.10.3
|
||||
|
||||
**Fixes**:
|
||||
- The progress report now correctly gives a percentage in the range [0, 100].
|
||||
|
||||
## v0.2.2.0
|
||||
- Update Jellyfin to v 10.10.2
|
||||
|
||||
## v0.2.1.0
|
||||
- Make default program configuration a textarea in the settings page
|
||||
- Add convinience definitions: `is-type`, `name-contains`
|
||||
- Update YamlDotNet to v 16.2.0
|
||||
|
||||
**Fixes**:
|
||||
- The default program was malformed, a closing bracket was at the wrong position
|
||||
- The `haskeys` function could only be called on Objects
|
||||
|
||||
## v0.2.0.0
|
||||
- Switch to yaml loading, old json files are still accepted
|
||||
- Rework lisp interpreter to be more conventional
|
||||
- Use arbitrary strings as ids for playlists
|
||||
- Add configuration page with some default definitions for
|
||||
the filter expressions.
|
||||
|
||||
**Breaking Changes**:
|
||||
- The lisp interpreter will now only detect strings in double quotes (`"`).
|
||||
- The interpreter will also not allow specifying lists without quoting them.
|
||||
`(1 2 3)` ... used to work but will no longer, replace by either specifying
|
||||
the list as `(list 1 2 3)` or `(quote (1 2 3))`.
|
||||
|
||||
## v0.1.1.0
|
||||
- Initial Alpha release.
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.5.3.0</Version>
|
||||
<Version>0.4.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.6" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.10.6" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -21,16 +21,12 @@
|
|||
<None Remove="Pages\smartPlaylists.js"/>
|
||||
<None Remove="Pages\smartCollections.html"/>
|
||||
<None Remove="Pages\smartCollections.js"/>
|
||||
<None Remove="Pages\lispPlayground.html"/>
|
||||
<None Remove="Pages\lispPlayground.js"/>
|
||||
<EmbeddedResource Include="Configuration\configPage.html"/>
|
||||
<EmbeddedResource Include="Configuration\configPage.js"/>
|
||||
<EmbeddedResource Include="Pages\smartPlaylists.html"/>
|
||||
<EmbeddedResource Include="Pages\smartPlaylists.js"/>
|
||||
<EmbeddedResource Include="Pages\smartCollections.html"/>
|
||||
<EmbeddedResource Include="Pages\smartCollections.js"/>
|
||||
<EmbeddedResource Include="Pages\lispPlayground.html"/>
|
||||
<EmbeddedResource Include="Pages\lispPlayground.js"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -5,7 +5,7 @@ Smart playlists with Lisp filter engine.
|
|||
This readme contains instructions for the most recent changes in
|
||||
the development branch (`main`). To view the file appropriate
|
||||
for your version select the tag corresponding to your version.
|
||||
The latest version is [v0.5.3.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.5.3.0).
|
||||
The latest version is [v0.4.0.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.4.0.0).
|
||||
|
||||

|
||||
|
||||
|
|
14
Test/test.sh
14
Test/test.sh
|
@ -4,16 +4,14 @@ JELLYFIN=jellyfin/jellyfin
|
|||
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
mkdir -p ./cache ./media ./config/plugins/jellyfin-smart-playlist
|
||||
if [ "$#" -eq 1 ] && [ "$1" = '--skip-build' ]; then
|
||||
:
|
||||
else
|
||||
(
|
||||
pwd
|
||||
(
|
||||
cd ../Jellyfin.Plugin.SmartPlaylist/
|
||||
dotnet build
|
||||
)
|
||||
cp ../Jellyfin.Plugin.SmartPlaylist/bin/Debug/net8.0/jellyfin-smart-playlist.dll ./config/plugins/jellyfin-smart-playlist/
|
||||
fi
|
||||
)
|
||||
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/
|
||||
docker pull "$JELLYFIN"
|
||||
docker run --rm --user "$(id -u):$(id -g)" \
|
||||
-v ./cache:/cache \
|
||||
|
|
|
@ -267,10 +267,6 @@ namespace Tests
|
|||
//Assert.Equal("", e.eval("(shuf (list 0 1 2 3 4 5 6))").ToString());
|
||||
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort (lambda (a b) (> a b)) '(5 4 7 3 2 6 1))").ToString());
|
||||
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort > (list 5 4 7 3 2 6 1))").ToString());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ resources:
|
|||
basic:
|
||||
username: ((gitea.username))
|
||||
password: ((gitea.token))
|
||||
version_check_method: none
|
||||
- name: artifact
|
||||
type: http-resource
|
||||
source:
|
||||
|
|
10
examples.md
10
examples.md
|
@ -26,19 +26,19 @@
|
|||
(is-genre "swing" g)))
|
||||
```
|
||||
|
||||
- `All of Haller`: A playlist containing all items by artist `Haller`,
|
||||
- `All of Seeed`: A playlist containing all items by artist `Seeed`,
|
||||
useless, since you also can just navigate to the artist to play all
|
||||
their songs, but a good example.
|
||||
```
|
||||
Id: Haller
|
||||
Name: Haller
|
||||
Id: Seeed
|
||||
Name: Seeed
|
||||
Program: |
|
||||
(let
|
||||
(parent
|
||||
(invoke-generic *item* "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller")))
|
||||
(cond
|
||||
((null parent) nil)
|
||||
(t (string= (lower (car (getitems parent "Name"))) (lower "Haller")))))
|
||||
(t (string= (car (getitems parent "Name")) "Haller"))))
|
||||
```
|
||||
or simplified with definitions contained in the preamble:
|
||||
```
|
||||
|
@ -91,7 +91,7 @@
|
|||
(lambda
|
||||
(i)
|
||||
;; the `(and (find-artist)` is here to prevent null violations.
|
||||
(and (find-artist) (string= (lower i) (lower (get-name (find-artist))))))
|
||||
(and (find-artist) (string= i (lower (get-name (find-artist))))))
|
||||
*include-artists*)))
|
||||
SortProgram: (begin (shuf *items*))
|
||||
Filename: /config/data/smartplaylists/German.yaml
|
||||
|
|
Loading…
Add table
Reference in a new issue