Compare commits

..

17 commits

Author SHA1 Message Date
64ae51ba71
fix: qsort ran into an infinite loop when encountering duplicate values. 2025-01-26 22:38:52 +01:00
d8145565cd
ci: prepare for release. 2025-01-26 00:26:03 +01:00
5689d0424c
chore: bump jellyfin ABI version. 2025-01-26 00:21:59 +01:00
eef2f32e14
fix: navigating to the config pages allways loads, not only on the first attempt. 2025-01-21 22:51:25 +01:00
2f07efd215
ci: prepare for release. 2025-01-20 21:18:07 +01:00
1aeb4d3cff
fix: readd updating path of plugin config.
That got lost during the restructuring for smart collections.
2025-01-20 21:13:24 +01:00
fef10b5736
feat: add length function. 2025-01-20 20:59:47 +01:00
49bacbffde
feat: implement split function.
Takes a list and an integer, splits off n elements from the list
and returns a list where the first element contains the first n
elements of the original list, the cdr of that list contains the
rest.
2025-01-20 20:52:11 +01:00
49298a3ca2
feat: add "reverse" function definition. 2025-01-20 18:05:26 +01:00
aea313a813
ci: prepare for release 0.5.0.0. 2025-01-20 00:06:08 +01:00
a569d863ae
feat: add lisp playground.
Refs: #4
2025-01-20 00:00:15 +01:00
001aad5ed9
Merge branch 'enable-all-item-kinds'
Refs: #2
2025-01-19 22:43:57 +01:00
614c0a0cb1
Merge branch 'collections'
Refs: #1
2025-01-19 22:40:27 +01:00
f25eafd186
test: add option to startup script to not load the newest plugin. 2025-01-19 15:32:25 +01:00
d2a10a967e
docs: fix typo. 2025-01-17 18:33:21 +01:00
a89b4606c5
ci: prepare release 0.4.1.0. 2025-01-16 23:35:25 +01:00
4bc3b463cb
feat: enable more item kinds.
Refs: #2
2025-01-16 18:56:53 +01:00
14 changed files with 370 additions and 128 deletions

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

View file

@ -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;

View file

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

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

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

View file

@ -88,29 +88,28 @@ function initial_load(selectedId) {
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
} }
document.querySelector('#SmartCollectionConfigPage')
.addEventListener('viewshow', function() { export default function (view, params) {
view.addEventListener('viewshow', function() {
initial_load(null); initial_load(null);
}); });
view.addEventListener('viewhide', function (_e) {});
view.addEventListener('viewdestroy', function (_e) {});
document.querySelector('#SmartCollectionConfigPage') document.querySelector('#SmartcollectionSelection')
.addEventListener('pageshow', function() { .addEventListener('change', function() {
initial_load(null); const selection = document.querySelector('#SmartcollectionSelection');
}); fillForm(COLLECTIONS[selection.selectedIndex]);
document.querySelector('#SmartcollectionSelection')
.addEventListener('change', function() {
const selection = document.querySelector('#SmartcollectionSelection');
fillForm(COLLECTIONS[selection.selectedIndex]);
});
document.querySelector('#SmartCollectionConfigForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
const selection = document.querySelector('#SmartcollectionSelection');
const selectedId = COLLECTIONS[selection.selectedIndex].Id;
ApiClient.setSmartCollection(jsonFromForm(COLLECTIONS[selection.selectedIndex].CollectionId)).then(function () {
initial_load(selectedId);
}); });
e.preventDefault();
}); document.querySelector('#SmartCollectionConfigForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
const selection = document.querySelector('#SmartcollectionSelection');
const selectedId = COLLECTIONS[selection.selectedIndex].Id;
ApiClient.setSmartCollection(jsonFromForm(COLLECTIONS[selection.selectedIndex].CollectionId)).then(function () {
initial_load(selectedId);
});
e.preventDefault();
});
}

View file

@ -116,28 +116,27 @@ 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() {
const selection = document.querySelector('#SmartplaylistSelection'); const selection = document.querySelector('#SmartplaylistSelection');
fillForm(PLAYLISTS[selection.selectedIndex], USERS); fillForm(PLAYLISTS[selection.selectedIndex], USERS);
});
document.querySelector('#SmartPlaylistConfigForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
const selection = document.querySelector('#SmartplaylistSelection');
const selectedId = PLAYLISTS[selection.selectedIndex].Id;
ApiClient.setSmartPlaylist(jsonFromForm()).then(function () {
initial_load(selectedId);
}); });
e.preventDefault();
}); document.querySelector('#SmartPlaylistConfigForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
const selection = document.querySelector('#SmartplaylistSelection');
const selectedId = PLAYLISTS[selection.selectedIndex].Id;
ApiClient.setSmartPlaylist(jsonFromForm()).then(function () {
initial_load(selectedId);
});
e.preventDefault();
});
}

View file

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

View file

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

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

View file

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

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.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)

View file

@ -4,14 +4,16 @@ JELLYFIN=jellyfin/jellyfin
set -eu set -eu
cd "$(dirname "$0")" cd "$(dirname "$0")"
pwd
(
cd ../Jellyfin.Plugin.SmartPlaylist/
dotnet build
)
pwd
mkdir -p ./cache ./media ./config/plugins/jellyfin-smart-playlist 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/ if [ "$#" -eq 1 ] && [ "$1" = '--skip-build' ]; then
:
else
(
cd ../Jellyfin.Plugin.SmartPlaylist/
dotnet build
)
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 \

View file

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