Compare commits

..

No commits in common. "main" and "v0.5.2.0" have entirely different histories.

21 changed files with 80 additions and 256 deletions

View file

@ -1,26 +1,15 @@
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Web;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Library;
using YamlDotNet.Serialization;
using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp;
namespace Jellyfin.Plugin.SmartPlaylist.Api { namespace Jellyfin.Plugin.SmartPlaylist.Api {
[Serializable]
public class ProgramInputDto {
public string InputLinks { get; set; }
public string Program { get; set; }
}
[Serializable] [Serializable]
public class ProgramOutputDto { public class ProgramOutputDto {
public string Output { get; set; } public string Output { get; set; }
@ -34,21 +23,16 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
[Produces(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)]
public class LispPlaygroundController : ControllerBase { public class LispPlaygroundController : ControllerBase {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
public LispPlaygroundController( public LispPlaygroundController(
ILogger<LispPlaygroundController> logger, ILogger<LispPlaygroundController> logger
ILibraryManager libraryManager
) { ) {
_logger = logger; _logger = logger;
_libraryManager = libraryManager;
} }
private Executor SetupExecutor(StringBuilder sb, IList<string> inputs) { private Executor SetupExecutor(StringBuilder sb) {
var env = new DefaultEnvironment(); var env = new DefaultEnvironment();
var executor = new Executor(env); 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) => { executor.builtins["logd"] = (x) => {
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray()); _logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
return Lisp.Boolean.TRUE; return Lisp.Boolean.TRUE;
@ -74,7 +58,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
}))); })));
return Lisp.Boolean.TRUE; return Lisp.Boolean.TRUE;
}; };
executor.builtins["println"] = (x) => { executor.builtins["print"] = (x) => {
sb.Append(string.Join(" ", x.Select((i) => { sb.Append(string.Join(" ", x.Select((i) => {
if (i is Lisp.String i_s) { if (i is Lisp.String i_s) {
return i_s.Value(); return i_s.Value();
@ -92,32 +76,17 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
return executor; 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] [HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ProgramOutputDto>> SetPlaylist() { public async Task<ActionResult<ProgramOutputDto>> SetPlaylist() {
try { try {
string input; string program;
using (StreamReader reader = new StreamReader(Request.Body)) { using (StreamReader reader = new StreamReader(Request.Body)) {
input = await reader.ReadToEndAsync(); program = await reader.ReadToEndAsync();
} }
var dto = new DeserializerBuilder().Build().Deserialize<ProgramInputDto>(input);
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
var e = SetupExecutor(output, extractItemIds(dto.InputLinks)); var e = SetupExecutor(output);
var r = e.eval(dto.Program).ToString(); var r = e.eval(program).ToString();
return Ok(new ProgramOutputDto() { return Ok(new ProgramOutputDto() {
FinalExpression = r, FinalExpression = r,
Output = output.ToString(), Output = output.ToString(),

View file

@ -58,13 +58,5 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
await _store.SaveSmartCollectionAsync(smartCollection); await _store.SaveSmartCollectionAsync(smartCollection);
return Created(); return Created();
} }
[HttpDelete]
[Route("{smartCollectionId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteCollection([FromRoute, Required] SmartCollectionId smartCollectionId) {
_store.DeleteSmartCollectionById(smartCollectionId);
return NoContent();
}
} }
} }

View file

@ -57,13 +57,5 @@ namespace Jellyfin.Plugin.SmartPlaylist.Api {
await _store.SaveSmartPlaylistAsync(smartPlaylist); await _store.SaveSmartPlaylistAsync(smartPlaylist);
return Created(); return Created();
} }
[HttpDelete]
[Route("{smartPlaylistId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeletePlaylist([FromRoute, Required] SmartPlaylistId smartPlaylistId) {
_store.DeleteSmartPlaylistById(smartPlaylistId);
return NoContent();
}
} }
} }

View file

@ -3,7 +3,7 @@ using Jellyfin.Plugin.SmartPlaylist.Lisp;
namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
public class Parser { public class Parser {
internal StringTokenStream _sts; private StringTokenStream _sts;
public Parser(StringTokenStream tokens) { public Parser(StringTokenStream tokens) {
_sts = tokens; _sts = tokens;
} }
@ -29,18 +29,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
Debug.Assert(start.value == end.value); Debug.Assert(start.value == end.value);
Debug.Assert("\"".Contains(start.value)); Debug.Assert("\"".Contains(start.value));
string r = ""; string r = "";
bool exit_ok = false;
while (_sts.Available() > 0) { while (_sts.Available() > 0) {
Token<string> t = _sts.Get(); Token<string> t = _sts.Get();
if (t.value == end.value) { if (t.value == end.value) {
exit_ok = true;
break; break;
} }
r += t.value; r += t.value;
} }
if (!exit_ok) {
throw new ApplicationException($"Failed to parse string, are you missing the closing quotes? String is: {r}");
}
_sts.Commit(); _sts.Commit();
return new String(r); return new String(r);
} }
@ -74,16 +69,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
if (";".Contains(start.value)) { if (";".Contains(start.value)) {
return parse_comment(start, end); return parse_comment(start, end);
} }
if (end == null) { Debug.Assert(end != null);
throw new ApplicationException($"Don't know how to parse grouping starting with token '{start.value}'");
}
IList<Expression> expressions = new List<Expression>(); IList<Expression> expressions = new List<Expression>();
bool exit_ok = false;
while (_sts.Available() > 0) { while (_sts.Available() > 0) {
Token<string> t = _sts.Get(); Token<string> t = _sts.Get();
if (t.value == end.value) { if (t.value == end.value) {
exit_ok = true;
_sts.Commit(); _sts.Commit();
break; break;
} }
@ -97,11 +87,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
_sts.Rewind(1); _sts.Rewind(1);
expressions.Add(parse()); expressions.Add(parse());
} }
var r = Cons.FromList(expressions); return Cons.FromList(expressions);
if (!exit_ok) {
throw new ApplicationException($"Failed to parse grouping, are you missing some closing braces? Parsed expressions: {r}");
}
return r;
} }
Expression parse_atom(AtomToken at) { Expression parse_atom(AtomToken at) {

View file

@ -90,50 +90,25 @@ 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 heq) (list0 pivot fc h0 h1)
(cond (cond
((null list0) (list list0 pivot fc h0 h1 heq)) ((null list0) (list list0 pivot fc h0 h1))
((and ((fc (car list0) pivot) (split (cdr list0) pivot fc h0 (cons (car list0) h1)))
(fc (car list0) pivot) (t (split (cdr list0) pivot fc (cons (car list0) h0) h1)))))
(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)
(begin t (cond (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 nil)) (halves (split list0 (getpivot list0) fc 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))))))
(heq (car (cdr (cdr (cdr (cdr (cdr halves))))))) (append (sort fc h0) (sort fc h1)))))))
(append (append (sort fc h0) heq) (sort fc h1))))))))
(sort fc list00))) (sort fc list00)))
""" """
); );
@ -601,11 +576,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
throw new ApplicationException($"Not handled case '{expression}'"); throw new ApplicationException($"Not handled case '{expression}'");
} }
public Expression eval(Parser p) { public Expression eval(Parser p) {
Expression r = eval(p.parse()); return 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;
} }
public Expression eval(StringTokenStream sts) { public Expression eval(StringTokenStream sts) {
return eval(new Parser(sts)); return eval(new Parser(sts));

View file

@ -9,34 +9,20 @@
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<form id="LispPlaygroundConfigForm"> <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"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundEditProgram">Program</label> <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> <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> <textarea id="LispPlaygroundEditProgram" class="emby-input smartcollection-monospace" name="Program" rows="16" cols="120"></textarea>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundOutput">Output</label> <label class="inputLabel inputLabelUnfocused" for="LispPlaygroundOutput">Output</label>
<div class="fieldDescription">The output of the program.</div> <div class="fieldDescription">The output of the program.</div>
<textarea id="LispPlaygroundOutput" class="emby-input smartplayground-monospace" name="Output" rows="16" cols="120"></textarea> <textarea id="LispPlaygroundOutput" class="emby-input smartcollection-monospace" name="Output" rows="16" cols="120"></textarea>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="LispPlaygroundReturn">Final expression</label> <label class="inputLabel inputLabelUnfocused" for="LispPlaygroundReturn">Final expression</label>
<div class="fieldDescription">The final expression the program has been reduced to.</div> <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> <textarea id="LispPlaygroundReturn" class="emby-input smartcollection-monospace" name="Output" rows="1" cols="120"></textarea>
</div> </div>
<div> <div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button"> <button is="emby-button" type="submit" class="raised button-submit block emby-button">
@ -47,7 +33,7 @@
</div> </div>
</div> </div>
<style> <style>
.smartplayground-monospace { .smartcollection-monospace {
font-family: monospace; font-family: monospace;
} }
</style> </style>

View file

@ -6,17 +6,14 @@ function fillForm(o) {
return_.value = (o.hasOwnProperty("FinalExpression")) ? o.FinalExpression : ''; return_.value = (o.hasOwnProperty("FinalExpression")) ? o.FinalExpression : '';
} }
ApiClient.runLispProgram = function (inputLinks, program) { ApiClient.runLispProgram = function (program) {
const url = ApiClient.getUrl('LispPlayground'); const url = ApiClient.getUrl('LispPlayground');
return this.ajax({ return this.ajax({
type: 'POST', type: 'POST',
url: url, url: url,
dataType: 'json', dataType: 'json',
contentType: 'application/json; charset=UTF-8', contentType: 'text/plain; charset=UTF-8',
data: JSON.stringify({ data: program,
InputLinks: inputLinks,
Program: program,
}),
}); });
} }
function initial_load() { function initial_load() {
@ -40,9 +37,8 @@ export default function (view, params) {
.addEventListener('submit', function (e) { .addEventListener('submit', function (e) {
e.preventDefault(); e.preventDefault();
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
const editInputLinks = document.querySelector('#LispPlaygroundInputLinks');
const editProgram = document.querySelector('#LispPlaygroundEditProgram'); const editProgram = document.querySelector('#LispPlaygroundEditProgram');
ApiClient.runLispProgram(editInputLinks.value, editProgram.value).then(function (r) { ApiClient.runLispProgram(editProgram.value).then(function (r) {
fillForm(r); fillForm(r);
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });

View file

@ -37,9 +37,6 @@
<button is="emby-button" type="submit" class="raised button-submit block emby-button"> <button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span> <span>Save</span>
</button> </button>
<button is="emby-button" type="reset" class="raised button-reset block emby-button">
<span>Delete</span>
</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -73,15 +73,6 @@ ApiClient.setSmartCollection = function (c) {
data: JSON.stringify(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) { function initial_load(selectedId) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getSmartCollections().then(function (collections) { ApiClient.getSmartCollections().then(function (collections) {
@ -121,14 +112,4 @@ export default function (view, params) {
}); });
e.preventDefault(); 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();
});
} }

View file

@ -43,9 +43,6 @@
<button is="emby-button" type="submit" class="raised button-submit block emby-button"> <button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span> <span>Save</span>
</button> </button>
<button is="emby-button" type="reset" class="raised button-reset block emby-button">
<span>Delete</span>
</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -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) { function initial_load(selectedId) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getSmartPlaylists().then(function (playlists) { ApiClient.getSmartPlaylists().then(function (playlists) {
@ -147,14 +139,4 @@ export default function (view, params) {
}); });
e.preventDefault(); 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();
});
} }

View file

@ -77,5 +77,39 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
} }
return executor; 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);
}
} }
} }

View file

@ -7,10 +7,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; 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 { namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
public class GenerateCollection: Common, IScheduledTask { public class GenerateCollection: Common, IScheduledTask {
@ -64,47 +61,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
)).Id; )).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() { private IEnumerable<BaseItem> GetAllMedia() {
var req = new InternalItemsQuery() { var req = new InternalItemsQuery() {
IncludeItemTypes = AvailableFilterItems, IncludeItemTypes = AvailableFilterItems,

View file

@ -78,17 +78,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
executor.environment.Set("*user*", Lisp.Object.FromBase(user)); executor.environment.Set("*user*", Lisp.Object.FromBase(user));
foreach (var i in items) { foreach (var i in items) {
executor.environment.Set("*item*", Lisp.Object.FromBase(i)); executor.environment.Set("*item*", Lisp.Object.FromBase(i));
try { var r = executor.eval(expression);
var r = executor.eval(expression); _logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString()); if ((r is not Lisp.Boolean r_bool) || (r_bool.Value())) {
if ((r is not Lisp.Boolean r_bool) || (r_bool.Value())) { _logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name); results.Add(i);
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 = SetupExecutor();

View file

@ -56,9 +56,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Util {
public IStream<T> Copy() { public IStream<T> Copy() {
return new Stream<T>(_items, _cursor, _ephemeralCursor); return new Stream<T>(_items, _cursor, _ephemeralCursor);
} }
public IList<T> Remainder() {
return _items.Skip(_cursor).ToList();
}
} }
} }

View file

@ -1,7 +1,7 @@
name: Smart Playlist name: Smart Playlist
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
version: 0.5.3.0 version: 0.5.2.0
targetAbi: 10.10.6.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,9 +14,8 @@ artifacts:
- jellyfin-smart-playlist.dll - jellyfin-smart-playlist.dll
- YamlDotNet.dll - YamlDotNet.dll
changelog: | changelog: |
## v0.5.3.0 ## v0.5.2.0
- bump Jellyfin ABI version to 10.10.6 - bump Jellyfin ABI version to 10.10.5
- bump yamldotnet to 16.3.0
**Fixes**: **Fixes**:
- qsort doesn't loop endlessly with duplicate values in import anymore - the config pages will always load, not only the first time

View file

@ -5,13 +5,13 @@
<RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace> <RootNamespace>Jellyfin.Plugin.SmartPlaylist</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.5.3.0</Version> <Version>0.5.2.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.10.6" /> <PackageReference Include="Jellyfin.Controller" Version="10.10.5" />
<PackageReference Include="Jellyfin.Model" Version="10.10.6" /> <PackageReference Include="Jellyfin.Model" Version="10.10.5" />
<PackageReference Include="YamlDotNet" Version="16.3.0" /> <PackageReference Include="YamlDotNet" Version="16.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -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.5.3.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.5.3.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)

View file

@ -267,10 +267,6 @@ 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());
} }
} }
} }

View file

@ -29,7 +29,6 @@ resources:
basic: basic:
username: ((gitea.username)) username: ((gitea.username))
password: ((gitea.token)) password: ((gitea.token))
version_check_method: none
- name: artifact - name: artifact
type: http-resource type: http-resource
source: source:

View file

@ -26,19 +26,19 @@
(is-genre "swing" g))) (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 useless, since you also can just navigate to the artist to play all
their songs, but a good example. their songs, but a good example.
``` ```
Id: Haller Id: Seeed
Name: Haller Name: Seeed
Program: | Program: |
(let (let
(parent (parent
(invoke-generic *item* "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))) (invoke-generic *item* "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller")))
(cond (cond
((null parent) nil) ((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: or simplified with definitions contained in the preamble:
``` ```
@ -91,7 +91,7 @@
(lambda (lambda
(i) (i)
;; the `(and (find-artist)` is here to prevent null violations. ;; 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*))) *include-artists*)))
SortProgram: (begin (shuf *items*)) SortProgram: (begin (shuf *items*))
Filename: /config/data/smartplaylists/German.yaml Filename: /config/data/smartplaylists/German.yaml