Compare commits

..

9 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
9 changed files with 147 additions and 151 deletions

View file

@ -16,6 +16,9 @@ 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;

View file

@ -47,6 +47,40 @@ 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
@ -56,25 +90,50 @@ 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)
(list0 pivot fc h0 h1 heq)
(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)))))
((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)))))))
(sort
(lambda
(fc list0)
(cond
(begin t (cond
((null list0) nil)
((null (cdr list0)) list0)
(t
(let*
(halves (split list0 (getpivot list0) fc nil nil))
(halves (split list0 (getpivot list0) fc nil nil nil))
(h0 (car (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)))
"""
);
@ -418,7 +477,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("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;
Expression exp = ((Cons) pair_cons.Item2).Item1;
@ -431,7 +490,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("");
throw new ApplicationException($"All arguments but the last have to be a list of two items, got {pair}");
}
Symbol refname = (Symbol) pair_cons.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); }
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
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);
}
@ -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); }
else if (args.First() == Boolean.FALSE) { proc_args = new List<Symbol>(); }
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);
}

View file

@ -21,17 +21,19 @@ function initial_load() {
Dashboard.hideLoadingMsg();
}
document.querySelector('#LispPlaygroundConfigPage')
.addEventListener('viewshow', function() {
initial_load();
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')
document.querySelector('#LispPlaygroundConfigPage')
.addEventListener('pageshow', function() {
initial_load();
});
document.querySelector('#LispPlaygroundConfigForm')
document.querySelector('#LispPlaygroundConfigForm')
.addEventListener('submit', function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
@ -41,3 +43,4 @@ document.querySelector('#LispPlaygroundConfigForm')
Dashboard.hideLoadingMsg();
});
});
}

View file

@ -88,23 +88,21 @@ function initial_load(selectedId) {
Dashboard.hideLoadingMsg();
});
}
document.querySelector('#SmartCollectionConfigPage')
.addEventListener('viewshow', function() {
export default function (view, params) {
view.addEventListener('viewshow', function() {
initial_load(null);
});
view.addEventListener('viewhide', function (_e) {});
view.addEventListener('viewdestroy', function (_e) {});
document.querySelector('#SmartCollectionConfigPage')
.addEventListener('pageshow', function() {
initial_load(null);
});
document.querySelector('#SmartcollectionSelection')
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');
@ -114,3 +112,4 @@ document.querySelector('#SmartCollectionConfigForm')
});
e.preventDefault();
});
}

View file

@ -116,22 +116,20 @@ function initial_load(selectedId) {
});
}
document.querySelector('#SmartPlaylistConfigPage')
.addEventListener('viewshow', function() {
initial_load(null);
});
document.querySelector('#SmartPlaylistConfigPage')
.addEventListener('pageshow', function() {
export default function (view, params) {
view.addEventListener('viewshow', 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');
@ -141,3 +139,4 @@ document.querySelector('#SmartPlaylistConfigForm')
});
e.preventDefault();
});
}

View file

@ -1,7 +1,7 @@
name: Smart Playlist
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
version: 0.5.0.0
targetAbi: 10.10.3.0
version: 0.5.2.0
targetAbi: 10.10.5.0
framework: net8.0
owner: redxef
overview: Smart playlists with Lisp filter engine.
@ -14,79 +14,8 @@ artifacts:
- jellyfin-smart-playlist.dll
- YamlDotNet.dll
changelog: |
## v0.5.0.0
- Add support for collections
- Add a playground to test lisp code, the playground has
two additional functions available: `print` and `println`
to fill the output window
- Separate the config pages
- Enable nearly all item types
## v0.4.1.0
- improve defaults for new playlists
## v0.5.2.0
- bump Jellyfin ABI version to 10.10.5
**Fixes**:
- finally get the percentage indicator of the scheduled task right
## 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**:
- 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.
- the config pages will always load, not only the first time

View file

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

View file

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

View file

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