Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
96ebc366b6 | |||
05742dd17c | |||
52b270a8d8 | |||
1193ca3005 | |||
8371dc8536 | |||
1b0c5455dd | |||
f479c93c5c | |||
0844cebd88 | |||
24b3d41df5 | |||
45844cafec | |||
bf286d4ece | |||
bfcf854d38 | |||
0ccefa3b58 | |||
1f961ccb0c | |||
12d98c46cb | |||
3c0d8a3809 | |||
8f832ed224 | |||
67cffd98ff | |||
df2e07e519 | |||
74486640d8 | |||
af63a8a696 | |||
f39633d7c5 | |||
b23587d721 | |||
7bf2923ad1 | |||
5cfb35a239 |
13 changed files with 263 additions and 47 deletions
|
@ -126,6 +126,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
public static Boolean operator <=(Integer a, Integer b) {
|
public static Boolean operator <=(Integer a, Integer b) {
|
||||||
return (a._value <= b._value) ? Boolean.TRUE : Boolean.FALSE;
|
return (a._value <= b._value) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
}
|
}
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return base.GetHashCode();
|
||||||
|
}
|
||||||
|
public override bool Equals(object? other) {
|
||||||
|
return base.Equals(other);
|
||||||
|
}
|
||||||
public static Boolean operator ==(Integer a, Integer b) {
|
public static Boolean operator ==(Integer a, Integer b) {
|
||||||
return (a._value == b._value) ? Boolean.TRUE : Boolean.FALSE;
|
return (a._value == b._value) ? Boolean.TRUE : Boolean.FALSE;
|
||||||
}
|
}
|
||||||
|
@ -151,11 +157,35 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
return new List<Expression>();
|
return new List<Expression>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class String: Scalar<string> {
|
public class String: Scalar<string>, ISortable<String, Boolean> {
|
||||||
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;
|
||||||
|
@ -218,7 +248,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Object : Scalar<object> {
|
public class Object : Scalar<object> {
|
||||||
public Object(object value) : base(value) { }
|
internal 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;
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
using FunctionLater = Func<Executor, IEnumerable<Expression>, Expression>;
|
using FunctionLater = Func<Executor, IEnumerable<Expression>, Expression>;
|
||||||
|
|
||||||
public interface IEnvironment<K, V> {
|
public interface IEnvironment<K, V> {
|
||||||
public V Get(K k);
|
public V? Get(K k);
|
||||||
public void Set(K k, V v);
|
public void Set(K k, V v);
|
||||||
public IEnvironment<K, V>? Find(K k);
|
public IEnvironment<K, V>? Find(K k);
|
||||||
public IEnvironment<K, V> Parent(bool recursive);
|
public IEnvironment<K, V> Parent(bool recursive);
|
||||||
|
@ -46,6 +46,38 @@ 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)))
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,16 +127,21 @@ 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((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((Atom a, Atom b) => (a != b)? Boolean.TRUE : Boolean.FALSE, 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;
|
||||||
|
@ -141,7 +178,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
return args.Last();
|
return args.Last();
|
||||||
}
|
}
|
||||||
private static Expression _haskeys(IEnumerable<Expression> args) {
|
private static Expression _haskeys(IEnumerable<Expression> args) {
|
||||||
Object o = (Object) args.First();
|
Object o = new Object(((IInner) args.First()).Inner());
|
||||||
foreach (var e in args.Skip(1)) {
|
foreach (var e in args.Skip(1)) {
|
||||||
String s = (String) e;
|
String s = (String) e;
|
||||||
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
|
PropertyInfo? pi = o.Value().GetType().GetProperty(s.Value());
|
||||||
|
@ -377,7 +414,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
|
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
|
||||||
throw new ApplicationException($"Could not find '{s.Name()}'");
|
throw new ApplicationException($"Could not find '{s.Name()}'");
|
||||||
}
|
}
|
||||||
return env.Get(s.Name());
|
var r_ = env.Get(s.Name());
|
||||||
|
if (r_ is null) {
|
||||||
|
throw new ApplicationException($"Could not find '{s.Name()}'");
|
||||||
|
}
|
||||||
|
return r_;
|
||||||
case Boolean b:
|
case Boolean b:
|
||||||
return b;
|
return b;
|
||||||
case Integer i:
|
case Integer i:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
|
|
|
@ -5,14 +5,39 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public PluginConfiguration() {
|
public PluginConfiguration() {
|
||||||
InitialProgram = """
|
InitialProgram = """
|
||||||
(begin
|
(begin
|
||||||
(define lower (lambda (s) (invoke s "ToLower" nil)))
|
(define lower
|
||||||
(define is-genre (lambda (g g-list) (any (lambda (x) (invoke (lower x) "Contains" (list (lower g)))) g-list)))
|
(lambda (s)
|
||||||
(define is-genre-exact (lambda (g g-list) (find g g-list)))
|
(invoke s "ToLower" nil)))
|
||||||
(define genre-list (lambda nil (let (_g (getitems item "Genres")) (if (null _g) nil (car _g)))))
|
(define is-genre
|
||||||
(define is-favorite (lambda nil (invoke item "IsFavoriteOrLiked" (list user)))))
|
(lambda (g g-list)
|
||||||
|
(any
|
||||||
|
(lambda (x)
|
||||||
|
(invoke (lower x) "Contains" (list (lower g))))
|
||||||
|
g-list)))
|
||||||
|
(define is-genre-exact
|
||||||
|
(lambda (g g-list)
|
||||||
|
(find g g-list)))
|
||||||
|
(define genre-list
|
||||||
|
(lambda nil
|
||||||
|
(let
|
||||||
|
(_g (getitems item "Genres"))
|
||||||
|
(if (null _g)
|
||||||
|
nil
|
||||||
|
(car _g)))))
|
||||||
|
(define is-favorite
|
||||||
|
(lambda nil
|
||||||
|
(invoke item "IsFavoriteOrLiked" (list user))))
|
||||||
|
(define is-type
|
||||||
|
(lambda (x)
|
||||||
|
(and
|
||||||
|
(haskeys item "GetClientTypeName")
|
||||||
|
(invoke (invoke item "GetClientTypeName" nil) "Equals" (list x)))))
|
||||||
|
(define name-contains
|
||||||
|
(lambda (x)
|
||||||
|
(invoke (lower (car (getitems item "Name"))) "Contains" (list (lower x)))))
|
||||||
(define is-favourite is-favorite)
|
(define is-favourite is-favorite)
|
||||||
|
(define all-genres (lambda (want have) (all (lambda (x) (is-genre x have)) want)))
|
||||||
|
(define any-genres (lambda (want have) (any (lambda (x) (is-genre x have)) want))))
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
public string InitialProgram { get; set; }
|
public string InitialProgram { get; set; }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -94,25 +95,42 @@ 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<Guid> results = new List<Guid>();
|
List<BaseItem> results = new List<BaseItem>();
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
||||||
Executor executor = new Executor(new DefaultEnvironment());
|
Executor executor = new Executor(new DefaultEnvironment());
|
||||||
executor.environment.Set("user", new Lisp_Object(user));
|
executor.environment.Set("user", Lisp_Object.FromBase(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", new Lisp_Object(i));
|
executor.environment.Set("item", Lisp_Object.FromBase(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.Id);
|
results.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
executor = new Executor(new DefaultEnvironment());
|
||||||
|
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) {
|
||||||
|
@ -161,7 +179,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(((double)i)/dto.Playlists.Count());
|
progress.Report(100 * ((double)i)/dto.Playlists.Count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +190,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
throw new ArgumentException("");
|
throw new ArgumentException("");
|
||||||
}
|
}
|
||||||
var existingItems = playlist_new.GetManageableItems().ToList();
|
var existingItems = playlist_new.GetManageableItems().ToList();
|
||||||
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id));
|
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,12 @@ 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; }
|
||||||
|
|
||||||
|
@ -54,6 +56,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -79,6 +82,11 @@ 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 {
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
}
|
}
|
||||||
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
|
private async Task<SmartPlaylistDto> LoadPlaylistAsync(string filename) {
|
||||||
var r = File.ReadAllText(filename);
|
var r = await File.ReadAllTextAsync(filename);
|
||||||
if (r.Equals("")) {
|
if (r.Equals("")) {
|
||||||
r = "{}";
|
r = "{}";
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
|
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
|
||||||
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
|
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
|
||||||
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
|
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
|
||||||
File.WriteAllText(filename, text);
|
await File.WriteAllTextAsync(filename, text);
|
||||||
}
|
}
|
||||||
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: Smart Playlist
|
name: Smart Playlist
|
||||||
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
guid: dd2326e3-4d3e-4bfc-80e6-28502c1131df
|
||||||
version: 0.2.0.0
|
version: 0.3.0.0
|
||||||
targetAbi: 10.10.0.0
|
targetAbi: 10.10.3.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,6 +14,29 @@ artifacts:
|
||||||
- jellyfin-smart-playlist.dll
|
- jellyfin-smart-playlist.dll
|
||||||
- YamlDotNet.dll
|
- YamlDotNet.dll
|
||||||
changelog: |
|
changelog: |
|
||||||
|
## 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
|
## v0.2.0.0
|
||||||
- Switch to yaml loading, old json files are still accepted
|
- Switch to yaml loading, old json files are still accepted
|
||||||
- Rework lisp interpreter to be more conventional
|
- Rework lisp interpreter to be more conventional
|
||||||
|
@ -21,11 +44,11 @@ changelog: |
|
||||||
- Add configuration page with some default definitions for
|
- Add configuration page with some default definitions for
|
||||||
the filter expressions.
|
the filter expressions.
|
||||||
|
|
||||||
**Breaking Changes:**
|
**Breaking Changes**:
|
||||||
- The lisp interpreter will no only detect strings in double quotes (`"`).
|
- The lisp interpreter will now only detect strings in double quotes (`"`).
|
||||||
- The interpreter will also not allow specifying lists without quoting them.
|
- 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
|
`(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)).
|
the list as `(list 1 2 3)` or `(quote (1 2 3))`.
|
||||||
|
|
||||||
## v0.1.1.0
|
## v0.1.1.0
|
||||||
- Initial Alpha release.
|
- Initial Alpha release.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Template</title>
|
<title>SmartPlaylist</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
|
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
|
||||||
<div class="fieldDescription">A program which can set up the environment</div>
|
<div class="fieldDescription">A program which can set up the environment</div>
|
||||||
<input id="InitialProgram" name="InitialProgram" type="text" is="emby-input" />
|
<textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" 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">
|
||||||
|
@ -22,6 +22,11 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartplaylist-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var SmartPlaylistConfig = {
|
var SmartPlaylistConfig = {
|
||||||
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
||||||
|
|
|
@ -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.2.0.0</Version>
|
<Version>0.3.0.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.0" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.0" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
53
README.md
53
README.md
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
Smart playlists with Lisp filter engine.
|
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.3.0.0](https://gitea.redxef.at/redxef/jellyfin-smart-playlist/src/tag/v0.3.0.0).
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
After [installing](#installation) the plugin and restarting Jellyfin
|
After [installing](#installation) the plugin and restarting Jellyfin
|
||||||
|
@ -24,6 +29,7 @@ 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
|
||||||
```
|
```
|
||||||
|
@ -79,12 +85,40 @@ 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.
|
to include them. Global variables `user` and `item` are predefined
|
||||||
|
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
|
||||||
|
not specify the kind of item to include/exclude all of them will be
|
||||||
|
added. Should you allow a playlist to be included all of it's items
|
||||||
|
will be added to the generated playlist !!!**
|
||||||
|
|
||||||
|
It's best to be explicit and always specify the item kinds you want to
|
||||||
|
include: `(and (or (is-type "MusicAlbum") (is-type "Audio")) . rest of filter)`.
|
||||||
|
|
||||||
The configuration page defines some useful functions to make it easier
|
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")`)
|
||||||
|
@ -92,6 +126,9 @@ to: `(is-favourite)`.
|
||||||
allowed, the example filter would match the genre "Nu-Metal" (`(is-genre "metal" (genre-list))`)
|
allowed, the example filter would match the genre "Nu-Metal" (`(is-genre "metal" (genre-list))`)
|
||||||
- **is-genre-exact**: the same as `is-genre`, but does not match paritally
|
- **is-genre-exact**: the same as `is-genre`, but does not match paritally
|
||||||
- **is-favorite**: matches a favorite item (`(is-favorite)`)
|
- **is-favorite**: matches a favorite item (`(is-favorite)`)
|
||||||
|
- **is-type**: matches the type of item look at
|
||||||
|
[BaseItemKind.cs](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/BaseItemKind.cs)
|
||||||
|
for a list of items. The plugin has enabled support for `Audio, MusicAlbum, Playlist` (`(is-type "Audio")`)
|
||||||
|
|
||||||
### Filename
|
### Filename
|
||||||
|
|
||||||
|
@ -111,3 +148,17 @@ to Jellyfin:
|
||||||
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
|
Go to `Dashboard>Catalog>(Gear)>(Plus)` and paste the provided link into
|
||||||
the field labeled `Repository URL`, give the plugin a descriptive name
|
the field labeled `Repository URL`, give the plugin a descriptive name
|
||||||
too.
|
too.
|
||||||
|
|
||||||
|
## Releasing a new version
|
||||||
|
|
||||||
|
1. Write the changelog: `git log --oneline $prev_version..`
|
||||||
|
2. Update the following files to include up-to-date version numbers
|
||||||
|
and changelogs, if applicable:
|
||||||
|
- `README.md`
|
||||||
|
- `Jellyfin.Plugin.SmartPlaylist/build.yaml`
|
||||||
|
- `Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj`
|
||||||
|
Don't forget to also bump the ABI version of Jellyfin.
|
||||||
|
3. Push the changes
|
||||||
|
4. Create a new release with the changelog, mark as pre-release if
|
||||||
|
applicable.
|
||||||
|
5. Done! The build pipeline will do the rest.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using Xunit;
|
|
||||||
using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment;
|
|
||||||
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
|
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
|
||||||
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
|
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
|
@ -197,13 +195,13 @@ 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", new Lisp_Object(new O(5, false)));
|
e.environment.Set("o", Lisp_Object.FromBase(new O(5, false)));
|
||||||
r = e.eval("""(haskeys o "i" "b")""");
|
r = e.eval("""(haskeys o "i" "b")""");
|
||||||
Assert.Equal(((Lisp_Boolean)r).Value(), true);
|
Assert.True(((Lisp_Boolean)r).Value());
|
||||||
r = e.eval("""(getitems o "i" "b")""");
|
r = e.eval("""(getitems o "i" "b")""");
|
||||||
Assert.Equal(string.Format("{0}", r), "(5 nil)");
|
Assert.Equal("(5 nil)", string.Format("{0}", r));
|
||||||
r = e.eval("""(invoke o "I" nil)""");
|
r = e.eval("""(invoke o "I" nil)""");
|
||||||
Assert.Equal(string.Format("{0}", r), "5");
|
Assert.Equal("5", string.Format("{0}", r));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -228,6 +226,8 @@ 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
examples.md
21
examples.md
|
@ -1,10 +1,27 @@
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
* `Favourite Pop`: A Playlist
|
- `Favourite Pop`: A playlist
|
||||||
containing all favourite items of the genre pop.
|
containing all favourite items of the genre pop.
|
||||||
```
|
```
|
||||||
Id: Favourite Pop
|
Id: Favourite Pop
|
||||||
Name: Favourite Pop
|
Name: Favourite Pop
|
||||||
Program: |
|
Program: |
|
||||||
(and (is-favorite) (is-genre "pop" (genre-list)))
|
(and (is-type "Audio") (is-favorite) (is-genre "pop" (genre-list)))
|
||||||
|
```
|
||||||
|
- `Electro Swing`: A playlist containing all items
|
||||||
|
which have a genre that contains "electro" and a
|
||||||
|
genre that contains "swing". It will only include
|
||||||
|
albums and single tracks.
|
||||||
|
```
|
||||||
|
Id: Electro Swing
|
||||||
|
Name: Electro Swing
|
||||||
|
Program: |
|
||||||
|
(let
|
||||||
|
(g (genre-list))
|
||||||
|
(and
|
||||||
|
(or
|
||||||
|
(is-type "Audio")
|
||||||
|
(is-type "MusicAlbum"))
|
||||||
|
(is-genre "electro" g)
|
||||||
|
(is-genre "swing" g)))
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in a new issue