Compare commits
No commits in common. "main" and "v0.2.2.0" have entirely different histories.
7 changed files with 16 additions and 125 deletions
|
@ -157,35 +157,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
return new List<Expression>();
|
return new List<Expression>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class String: Scalar<string>, ISortable<String, Boolean> {
|
public class String: Scalar<string> {
|
||||||
public String(string value) : base(value) {}
|
public String(string value) : base(value) {}
|
||||||
public override string? ToString() {
|
public override string? ToString() {
|
||||||
return $"\"{base.ToString()}\"";
|
return $"\"{base.ToString()}\"";
|
||||||
}
|
}
|
||||||
public static Boolean operator <(String a, String b) {
|
|
||||||
return (a.Value().CompareTo(b.Value()) < 0) ? Boolean.TRUE : Boolean.FALSE;
|
|
||||||
}
|
|
||||||
public static Boolean operator >(String a, String b) {
|
|
||||||
return b < a;
|
|
||||||
}
|
|
||||||
public static Boolean operator <=(String a, String b) {
|
|
||||||
return (a.Value().CompareTo(b.Value()) <= 0) ? Boolean.TRUE : Boolean.FALSE;
|
|
||||||
}
|
|
||||||
public static Boolean operator >=(String a, String b) {
|
|
||||||
return b <= a;
|
|
||||||
}
|
|
||||||
public override int GetHashCode() {
|
|
||||||
return base.GetHashCode();
|
|
||||||
}
|
|
||||||
public override bool Equals(object? other) {
|
|
||||||
return base.Equals(other);
|
|
||||||
}
|
|
||||||
public static Boolean operator ==(String a, String b) {
|
|
||||||
return (a._value == b._value) ? Boolean.TRUE : Boolean.FALSE;
|
|
||||||
}
|
|
||||||
public static Boolean operator !=(String a, String b) {
|
|
||||||
return (a._value != b._value) ? Boolean.TRUE : Boolean.FALSE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public class Cons: Expression {
|
public class Cons: Expression {
|
||||||
public Expression Item1;
|
public Expression Item1;
|
||||||
|
@ -248,7 +224,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Object : Scalar<object> {
|
public class Object : Scalar<object> {
|
||||||
internal Object(object value) : base(value) {}
|
public Object(object value) : base(value) { }
|
||||||
public static Expression FromBase(object? o) {
|
public static Expression FromBase(object? o) {
|
||||||
if (o == null) {
|
if (o == null) {
|
||||||
return Boolean.FALSE;
|
return Boolean.FALSE;
|
||||||
|
|
|
@ -46,38 +46,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
this["fold"] = e.eval("(lambda (fc i l) (if (null l) i (fold fc (fc i (car l)) (cdr l))))");
|
this["fold"] = e.eval("(lambda (fc i l) (if (null l) i (fold fc (fc i (car l)) (cdr l))))");
|
||||||
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["qsort"] = e.eval(
|
|
||||||
"""
|
|
||||||
(lambda
|
|
||||||
(fc list00)
|
|
||||||
(let
|
|
||||||
(getpivot
|
|
||||||
(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)))))
|
|
||||||
(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 fc list00)))
|
|
||||||
"""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,21 +95,16 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
this["/"] = (x) => _agg((Integer a, Integer b) => a / b, x);
|
this["/"] = (x) => _agg((Integer a, Integer b) => a / b, x);
|
||||||
this["%"] = (x) => _agg((Integer a, Integer b) => a % b, x);
|
this["%"] = (x) => _agg((Integer a, Integer b) => a % b, x);
|
||||||
|
|
||||||
this["="] = (x) => _cmp((Atom a, Atom b) => (a == b)? Boolean.TRUE : Boolean.FALSE, x);
|
this["="] = (x) => _cmp((Integer a, Integer b) => a == b, x);
|
||||||
this["!="] = (x) => _cmp((Atom a, Atom b) => (a != b)? Boolean.TRUE : Boolean.FALSE, x);
|
this["eq?"] = (x) => _cmp((Integer a, Integer b) => a == b, x);
|
||||||
this["<"] = (x) => _cmp((Integer a, Integer b) => a < b, x);
|
this["<"] = (x) => _cmp((Integer a, Integer b) => a < b, x);
|
||||||
this["<="] = (x) => _cmp((Integer a, Integer b) => a <= b, x);
|
this["<="] = (x) => _cmp((Integer a, Integer b) => a <= b, x);
|
||||||
this[">"] = (x) => _cmp((Integer a, Integer b) => a > b, x);
|
this[">"] = (x) => _cmp((Integer a, Integer b) => a > b, x);
|
||||||
this[">="] = (x) => _cmp((Integer a, Integer b) => a >= b, x);
|
this[">="] = (x) => _cmp((Integer a, Integer b) => a >= b, x);
|
||||||
|
this["!="] = (x) => _cmp((Integer a, Integer b) => a != b, x);
|
||||||
this["not"] = (x) => {
|
this["not"] = (x) => {
|
||||||
return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
|
return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
};
|
};
|
||||||
this["string="] = (x) => _cmp((String a, String b) => a == b, x);
|
|
||||||
this["string!="] = (x) => _cmp((String a, String b) => a != b, x);
|
|
||||||
this["string>"] = (x) => _cmp((String a, String b) => a > b, x);
|
|
||||||
this["string>="] = (x) => _cmp((String a, String b) => a >= b, x);
|
|
||||||
this["string<"] = (x) => _cmp((String a, String b) => a < b, x);
|
|
||||||
this["string<="] = (x) => _cmp((String a, String b) => a <= b, x);
|
|
||||||
|
|
||||||
|
|
||||||
this["haskeys"] = _haskeys;
|
this["haskeys"] = _haskeys;
|
||||||
|
|
|
@ -95,42 +95,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
||||||
List<BaseItem> results = new List<BaseItem>();
|
List<Guid> results = new List<Guid>();
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
||||||
Executor executor = new Executor(new DefaultEnvironment());
|
Executor executor = new Executor(new DefaultEnvironment());
|
||||||
executor.environment.Set("user", Lisp_Object.FromBase(user));
|
executor.environment.Set("user", new Lisp_Object(user));
|
||||||
if (Plugin.Instance is not null) {
|
if (Plugin.Instance is not null) {
|
||||||
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
||||||
} else {
|
} else {
|
||||||
throw new ApplicationException("Plugin Instance is not yet initialized");
|
throw new ApplicationException("Plugin Instance is not yet initialized");
|
||||||
}
|
}
|
||||||
foreach (var i in items) {
|
foreach (var i in items) {
|
||||||
executor.environment.Set("item", Lisp_Object.FromBase(i));
|
executor.environment.Set("item", new Lisp_Object(i));
|
||||||
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.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
executor = new Executor(new DefaultEnvironment());
|
return results;
|
||||||
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(smartPlaylist.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> GetAllUserMedia(User user) {
|
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
|
||||||
|
@ -179,7 +162,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
await ClearPlaylist(playlist);
|
await ClearPlaylist(playlist);
|
||||||
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
||||||
i += 1;
|
i += 1;
|
||||||
progress.Report(100 * ((double)i)/dto.Playlists.Count());
|
progress.Report(((double)i)/dto.Playlists.Count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,12 +42,10 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class SmartPlaylistDto : ISerializable {
|
public class SmartPlaylistDto : ISerializable {
|
||||||
private static string DEFAULT_PROGRAM = "(begin (invoke item \"IsFavoriteOrLiked\" (list user)))";
|
private static string DEFAULT_PROGRAM = "(begin (invoke item \"IsFavoriteOrLiked\" (list user)))";
|
||||||
private static string DEFAULT_SORT_PROGRAM = "(begin items)";
|
|
||||||
public SmartPlaylistId Id { get; set; }
|
public SmartPlaylistId Id { get; set; }
|
||||||
public SmartPlaylistLinkDto[] Playlists { get; set; }
|
public SmartPlaylistLinkDto[] Playlists { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Program { get; set; }
|
public string Program { get; set; }
|
||||||
public string SortProgram { get; set; }
|
|
||||||
public string? Filename { get; set; }
|
public string? Filename { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
@ -56,7 +54,6 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
Playlists = [];
|
Playlists = [];
|
||||||
Name = Id.ToString();
|
Name = Id.ToString();
|
||||||
Program = DEFAULT_PROGRAM;
|
Program = DEFAULT_PROGRAM;
|
||||||
SortProgram = DEFAULT_SORT_PROGRAM;
|
|
||||||
Filename = null;
|
Filename = null;
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
}
|
}
|
||||||
|
@ -82,11 +79,6 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
} else {
|
} else {
|
||||||
Program = DEFAULT_PROGRAM;
|
Program = DEFAULT_PROGRAM;
|
||||||
}
|
}
|
||||||
if (info.GetValue("SortProgram", typeof(string)) is string _SortProgram) {
|
|
||||||
SortProgram = _SortProgram;
|
|
||||||
} else {
|
|
||||||
SortProgram = DEFAULT_SORT_PROGRAM;
|
|
||||||
}
|
|
||||||
if (info.GetValue("Filename", typeof(string)) is string _Filename) {
|
if (info.GetValue("Filename", typeof(string)) is string _Filename) {
|
||||||
Filename = _Filename;
|
Filename = _Filename;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.2" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.2" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
23
README.md
23
README.md
|
@ -29,7 +29,6 @@ Playlists:
|
||||||
UserId: 6eec632a-ff0d-4d09-aad0-bf9e90b14bc6
|
UserId: 6eec632a-ff0d-4d09-aad0-bf9e90b14bc6
|
||||||
Name: Rock
|
Name: Rock
|
||||||
Program: (begin (invoke item "IsFavoriteOrLiked" (user)))
|
Program: (begin (invoke item "IsFavoriteOrLiked" (user)))
|
||||||
SortProgram: (begin items)
|
|
||||||
Filename: /config/data/smartplaylists/Rock.yaml
|
Filename: /config/data/smartplaylists/Rock.yaml
|
||||||
Enabled: true
|
Enabled: true
|
||||||
```
|
```
|
||||||
|
@ -85,10 +84,7 @@ still work and remember the correct playlist.
|
||||||
|
|
||||||
A lisp program to decide on a per item basis if it should be included in
|
A lisp program to decide on a per item basis if it should be included in
|
||||||
the playlist, return `nil` to not include items, return any other value
|
the playlist, return `nil` to not include items, return any other value
|
||||||
to include them. Global variables `user` and `item` are predefined
|
to include them.
|
||||||
and contain a [User](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Entities/User.cs) and
|
|
||||||
[BaseItem](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Controller/Entities/BaseItem.cs)
|
|
||||||
respectively.
|
|
||||||
|
|
||||||
**!!! The filter expression will include all items matching, if you do
|
**!!! The filter expression will include all items matching, if you do
|
||||||
not specify the kind of item to include/exclude all of them will be
|
not specify the kind of item to include/exclude all of them will be
|
||||||
|
@ -102,23 +98,6 @@ The configuration page defines some useful functions to make it easier
|
||||||
to create filters. The above filter for liked items could be simplified
|
to create filters. The above filter for liked items could be simplified
|
||||||
to: `(is-favourite)`.
|
to: `(is-favourite)`.
|
||||||
|
|
||||||
### SortProgram
|
|
||||||
|
|
||||||
This works exactly like [Program](#program), but the input is the
|
|
||||||
user and a list of items (`items`) matched by [Program](#program).
|
|
||||||
The default is `(begin items)`, which doesn't sort at all. To sort
|
|
||||||
the items by name you could use the following program:
|
|
||||||
|
|
||||||
```lisp
|
|
||||||
(qsort
|
|
||||||
(lambda
|
|
||||||
(a b)
|
|
||||||
(string>
|
|
||||||
(car (getitems a "Name"))
|
|
||||||
(car (getitems b "Name"))))
|
|
||||||
items)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Available definitions
|
#### Available definitions
|
||||||
|
|
||||||
- **lower**: lowercases a string (`(eq (lower "SomeString") "somestring")`)
|
- **lower**: lowercases a string (`(eq (lower "SomeString") "somestring")`)
|
||||||
|
|
|
@ -195,7 +195,7 @@ namespace Tests
|
||||||
public static void ObjectTest() {
|
public static void ObjectTest() {
|
||||||
Executor e = new Executor();
|
Executor e = new Executor();
|
||||||
Expression r;
|
Expression r;
|
||||||
e.environment.Set("o", Lisp_Object.FromBase(new O(5, false)));
|
e.environment.Set("o", new Lisp_Object(new O(5, false)));
|
||||||
r = e.eval("""(haskeys o "i" "b")""");
|
r = e.eval("""(haskeys o "i" "b")""");
|
||||||
Assert.True(((Lisp_Boolean)r).Value());
|
Assert.True(((Lisp_Boolean)r).Value());
|
||||||
r = e.eval("""(getitems o "i" "b")""");
|
r = e.eval("""(getitems o "i" "b")""");
|
||||||
|
@ -226,8 +226,6 @@ namespace Tests
|
||||||
Assert.Equal("nil", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 4 5))").ToString());
|
Assert.Equal("nil", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 4 5))").ToString());
|
||||||
Assert.Equal("nil", e.eval("(all (lambda (x) (= x 2)) nil)").ToString());
|
Assert.Equal("nil", e.eval("(all (lambda (x) (= x 2)) nil)").ToString());
|
||||||
Assert.Equal("10", e.eval("(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))").ToString());
|
Assert.Equal("10", e.eval("(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))").ToString());
|
||||||
Assert.Equal("(2 3 4 5 6 7)", e.eval("(append (list 2 3 4) (list 5 6 7))").ToString());
|
|
||||||
Assert.Equal("(1 2 3 4 5 6 7)", e.eval("(qsort (lambda (a b) (> a b)) (list 5 4 7 3 2 6 1))").ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue