Compare commits
No commits in common. "main" and "v0.3.0.0" have entirely different histories.
17 changed files with 118 additions and 662 deletions
|
@ -40,36 +40,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new String(r);
|
return new String(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parse_comment(GroupingToken start, GroupingToken? end) {
|
|
||||||
Debug.Assert(end != null);
|
|
||||||
Debug.Assert(";".Contains(start.value));
|
|
||||||
while (_sts.Available() > 0) {
|
|
||||||
Token<string> t = _sts.Get();
|
|
||||||
if (t.value == end.value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_sts.Commit();
|
|
||||||
return parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression parse_quote(GroupingToken start, GroupingToken? end) {
|
|
||||||
Debug.Assert(end == null);
|
|
||||||
Debug.Assert("'".Contains(start.value));
|
|
||||||
return Cons.FromList(new Expression[]{ new Symbol("quote"), parse()});
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression parse_grouping(GroupingToken start, GroupingToken? end) {
|
Expression parse_grouping(GroupingToken start, GroupingToken? end) {
|
||||||
|
Debug.Assert(end != null);
|
||||||
if ("\"".Contains(start.value)) {
|
if ("\"".Contains(start.value)) {
|
||||||
return parse_string(start, end);
|
return parse_string(start, end);
|
||||||
}
|
}
|
||||||
if ("'".Contains(start.value)) {
|
|
||||||
return parse_quote(start, end);
|
|
||||||
}
|
|
||||||
if (";".Contains(start.value)) {
|
|
||||||
return parse_comment(start, end);
|
|
||||||
}
|
|
||||||
Debug.Assert(end != null);
|
|
||||||
IList<Expression> expressions = new List<Expression>();
|
IList<Expression> expressions = new List<Expression>();
|
||||||
while (_sts.Available() > 0) {
|
while (_sts.Available() > 0) {
|
||||||
Token<string> t = _sts.Get();
|
Token<string> t = _sts.Get();
|
||||||
|
@ -77,13 +52,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
_sts.Commit();
|
_sts.Commit();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (t is SpaceToken) {
|
|
||||||
// need this here because those tokens can never
|
|
||||||
// return an expression and trying to parse the last
|
|
||||||
// expression will not work if its only spaces and a
|
|
||||||
// closing parentheses.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_sts.Rewind(1);
|
_sts.Rewind(1);
|
||||||
expressions.Add(parse());
|
expressions.Add(parse());
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
char t = program.Get();
|
char t = program.Get();
|
||||||
if ("()\"';".Contains(t)) {
|
if ("()\"".Contains(t)) {
|
||||||
return new GroupingToken(t.ToString());
|
return new GroupingToken(t.ToString());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -53,10 +53,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new GroupingToken(")");
|
return new GroupingToken(")");
|
||||||
} else if (_value == ")") {
|
} else if (_value == ")") {
|
||||||
return null;
|
return null;
|
||||||
} else if (_value == "'") {
|
|
||||||
return null;
|
|
||||||
} else if (_value == ";") {
|
|
||||||
return new GroupingToken("\n");
|
|
||||||
}
|
}
|
||||||
return new GroupingToken(_value);
|
return new GroupingToken(_value);
|
||||||
}
|
}
|
||||||
|
@ -78,11 +74,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
value += t;
|
value += t;
|
||||||
}
|
}
|
||||||
if (value.Equals("")) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new AtomToken(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CharStream: Stream<char> {
|
class CharStream: Stream<char> {
|
||||||
|
@ -104,7 +97,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
if (prev_avail == program.Available() && prev_avail == 0) {
|
if (prev_avail == program.Available() && prev_avail == 0) {
|
||||||
break;
|
break;
|
||||||
} else if (prev_avail == program.Available()) {
|
} else if (prev_avail == program.Available()) {
|
||||||
throw new ApplicationException($"Program is invalid, still available: {program.Available()}");
|
throw new ApplicationException("Program is invalid");
|
||||||
}
|
}
|
||||||
prev_avail = program.Available();
|
prev_avail = program.Available();
|
||||||
foreach (Type c in _classes) {
|
foreach (Type c in _classes) {
|
||||||
|
|
|
@ -243,13 +243,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
return $"{Item1} . {Item2}";
|
return $"{Item1} . {Item2}";
|
||||||
}
|
}
|
||||||
public override string? ToString() {
|
public override string? ToString() {
|
||||||
if (Item1 is Symbol SymbolItem1
|
|
||||||
&& SymbolItem1.Name() == "quote"
|
|
||||||
&& Item2 is Cons ConsItem2
|
|
||||||
&& ConsItem2.Item2.Equals(Boolean.FALSE)
|
|
||||||
) {
|
|
||||||
return $"'{ConsItem2.Item1}";
|
|
||||||
}
|
|
||||||
return $"({ToStringSimple()})";
|
return $"({ToStringSimple()})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,23 +78,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
(sort fc list00)))
|
(sort fc list00)))
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
this["rand"] = e.eval(
|
|
||||||
"""
|
|
||||||
(lambda
|
|
||||||
(. a)
|
|
||||||
(cond
|
|
||||||
((null a) (random))
|
|
||||||
((null (cdr a)) (% (random) (car a)))
|
|
||||||
(t (+
|
|
||||||
(car a)
|
|
||||||
(%
|
|
||||||
(random)
|
|
||||||
(-
|
|
||||||
(car (cdr a))
|
|
||||||
(car a)))))))
|
|
||||||
"""
|
|
||||||
);
|
|
||||||
this["shuf"] = new Symbol("shuffle");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,12 +112,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Builtins : Dictionary<string, Function> {
|
public class Builtins : Dictionary<string, Function> {
|
||||||
|
|
||||||
private static Dictionary<string, Type?> ResolvedTypes = new Dictionary<string, Type?>();
|
|
||||||
private Random Random;
|
|
||||||
|
|
||||||
public Builtins() : base() {
|
public Builtins() : base() {
|
||||||
Random = new Random();
|
|
||||||
this["atom"] = _atom;
|
this["atom"] = _atom;
|
||||||
this["eq"] = _eq;
|
this["eq"] = _eq;
|
||||||
this["car"] = _car;
|
this["car"] = _car;
|
||||||
|
@ -165,17 +143,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
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;
|
||||||
this["getitems"] = _getitems;
|
this["getitems"] = _getitems;
|
||||||
this["invoke"] = _invoke;
|
this["invoke"] = _invoke;
|
||||||
this["invoke-generic"] = _invoke_generic;
|
|
||||||
|
|
||||||
this["random"] = (x) => new Lisp.Integer(Random.Next());
|
|
||||||
this["shuffle"] = (x) => {
|
|
||||||
var newx = ((Lisp.Cons) x.First()).ToList().ToArray();
|
|
||||||
Random.Shuffle<Expression>(newx);
|
|
||||||
return Lisp.Cons.FromList(newx);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
private static T _agg<T>(Func<T, T, T> op, IEnumerable<Expression> args) where T : Expression {
|
private static T _agg<T>(Func<T, T, T> op, IEnumerable<Expression> args) where T : Expression {
|
||||||
T agg = (T) args.First();
|
T agg = (T) args.First();
|
||||||
|
@ -279,73 +250,11 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
IList<Expression> r = new List<Expression>();
|
IList<Expression> r = new List<Expression>();
|
||||||
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
|
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
|
||||||
if (mi == null) {
|
if (mi == null) {
|
||||||
throw new ApplicationException($"{o.Value()} has no method {s.Value()}");
|
throw new ApplicationException($"{o.Value()} has not method {s.Value()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.FromBase(mi.Invoke(o.Value(), l_));
|
return Object.FromBase(mi.Invoke(o.Value(), l_));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type? GetType(string name) {
|
|
||||||
if (ResolvedTypes.ContainsKey(name)) {
|
|
||||||
return ResolvedTypes[name];
|
|
||||||
}
|
|
||||||
var t = Type.GetType(
|
|
||||||
name,
|
|
||||||
(name) => { return AppDomain.CurrentDomain.GetAssemblies().Where(z => (z.FullName != null) && z.FullName.StartsWith(name.FullName)).FirstOrDefault(); },
|
|
||||||
null,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
ResolvedTypes[name] = t;
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression _invoke_generic(IEnumerable<Expression> args) {
|
|
||||||
Object o = new Object(((IInner) args.First()).Inner());
|
|
||||||
String s = (String) args.Skip(1).First();
|
|
||||||
IEnumerable<Expression> l;
|
|
||||||
if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) {
|
|
||||||
l = new List<Expression>();
|
|
||||||
} else if (args.Skip(2).First() is Cons lc) {
|
|
||||||
l = lc.ToList();
|
|
||||||
} else {
|
|
||||||
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}");
|
|
||||||
}
|
|
||||||
IEnumerable<Type> types;
|
|
||||||
if (args.Skip(3).First() is Boolean lb_ && lb_ == Boolean.FALSE) {
|
|
||||||
types = new List<Type>();
|
|
||||||
} else if (args.Skip(3).First() is Cons lc) {
|
|
||||||
types = lc.ToList().Select(x => GetType(((String) x).Value())).ToList();
|
|
||||||
} else {
|
|
||||||
throw new ApplicationException($"Expected a list of arguments, got {args.Skip(3).First()}");
|
|
||||||
}
|
|
||||||
object[]? l_ = l.Select<Expression, object>(x => {
|
|
||||||
switch (x) {
|
|
||||||
case Integer s:
|
|
||||||
return s.Value();
|
|
||||||
case Boolean b:
|
|
||||||
return b.Value();
|
|
||||||
case String s:
|
|
||||||
return s.Value();
|
|
||||||
case Object o:
|
|
||||||
return o.Value();
|
|
||||||
case Cons c:
|
|
||||||
return c.ToList().ToList();
|
|
||||||
}
|
|
||||||
throw new ApplicationException($"Unhandled value {x} (type {x.GetType()})");
|
|
||||||
}).ToArray();
|
|
||||||
Type[] l_types = l_.Select( x => {
|
|
||||||
return x.GetType();
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
IList<Expression> r = new List<Expression>();
|
|
||||||
MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types);
|
|
||||||
if (mi == null) {
|
|
||||||
throw new ApplicationException($"{o.Value()} has no method {s.Value()}");
|
|
||||||
}
|
|
||||||
mi = mi.MakeGenericMethod(types.ToArray());
|
|
||||||
|
|
||||||
return Object.FromBase(mi.Invoke(o.Value(), l_));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
||||||
|
@ -503,9 +412,6 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
switch (expression) {
|
switch (expression) {
|
||||||
case Symbol s:
|
case Symbol s:
|
||||||
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
|
if (_environment.Find(s.Name()) is not IEnvironment<string, Expression> env) {
|
||||||
if (builtins.ContainsKey(s.Name()) || builtinsLater.ContainsKey(s.Name())) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
throw new ApplicationException($"Could not find '{s.Name()}'");
|
throw new ApplicationException($"Could not find '{s.Name()}'");
|
||||||
}
|
}
|
||||||
var r_ = env.Get(s.Name());
|
var r_ = env.Get(s.Name());
|
||||||
|
|
|
@ -3,22 +3,14 @@ using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages {
|
||||||
public IServerApplicationPaths ServerApplicationPaths;
|
|
||||||
public IUserManager UserManager;
|
|
||||||
public Plugin(
|
public Plugin(
|
||||||
IApplicationPaths applicationPaths,
|
IApplicationPaths applicationPaths,
|
||||||
IXmlSerializer xmlSerializer,
|
IXmlSerializer xmlSerializer
|
||||||
IServerApplicationPaths serverApplicationPaths,
|
|
||||||
IUserManager userManager
|
|
||||||
) : base (applicationPaths, xmlSerializer) {
|
) : base (applicationPaths, xmlSerializer) {
|
||||||
Instance = this;
|
Instance = this;
|
||||||
ServerApplicationPaths = serverApplicationPaths;
|
|
||||||
UserManager = userManager;
|
|
||||||
}
|
}
|
||||||
public static Plugin? Instance {get; private set; }
|
public static Plugin? Instance {get; private set; }
|
||||||
public override string Name => "Smart Playlist";
|
public override string Name => "Smart Playlist";
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Controller;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public class PluginConfiguration : BasePluginConfiguration {
|
public class PluginConfiguration : BasePluginConfiguration {
|
||||||
|
@ -21,52 +20,26 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
(define genre-list
|
(define genre-list
|
||||||
(lambda nil
|
(lambda nil
|
||||||
(let
|
(let
|
||||||
(_g (getitems *item* "Genres"))
|
(_g (getitems item "Genres"))
|
||||||
(if (null _g)
|
(if (null _g)
|
||||||
nil
|
nil
|
||||||
(car _g)))))
|
(car _g)))))
|
||||||
(define is-favorite
|
(define is-favorite
|
||||||
(lambda nil
|
(lambda nil
|
||||||
(invoke *item* "IsFavoriteOrLiked" (list *user*))))
|
(invoke item "IsFavoriteOrLiked" (list user))))
|
||||||
(define is-type
|
(define is-type
|
||||||
(lambda (x)
|
(lambda (x)
|
||||||
(and
|
(and
|
||||||
(haskeys *item* "GetClientTypeName")
|
(haskeys item "GetClientTypeName")
|
||||||
(invoke (invoke *item* "GetClientTypeName" nil) "Equals" (list x)))))
|
(invoke (invoke item "GetClientTypeName" nil) "Equals" (list x)))))
|
||||||
(define name-contains
|
(define name-contains
|
||||||
(lambda (x)
|
(lambda (x)
|
||||||
(invoke (lower (car (getitems *item* "Name"))) "Contains" (list (lower 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 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)))
|
(define any-genres (lambda (want have) (any (lambda (x) (is-genre x have)) want))))
|
||||||
(define get-name (lambda (x) (car (getitems x "Name"))))
|
|
||||||
(define find-parent (lambda (typename) (invoke-generic *item* "FindParent" nil (list typename))))
|
|
||||||
(define find-artist (lambda nil (find-parent "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))))
|
|
||||||
""";
|
""";
|
||||||
store = new Store(new SmartPlaylistFileSystem(Plugin.Instance.ServerApplicationPaths));
|
|
||||||
}
|
}
|
||||||
private Store store { get; set; }
|
|
||||||
public string InitialProgram { get; set; }
|
public string InitialProgram { get; set; }
|
||||||
public SmartPlaylistDto[] Playlists {
|
|
||||||
get {
|
|
||||||
return store.GetAllSmartPlaylistsAsync().GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
var existing = store.GetAllSmartPlaylistsAsync().GetAwaiter().GetResult().Select(x => x.Id).ToList();
|
|
||||||
foreach (var p in value) {
|
|
||||||
existing.Remove(p.Id);
|
|
||||||
store.SaveSmartPlaylistAsync(p).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
foreach (var p in existing) {
|
|
||||||
store.DeleteSmartPlaylistById(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public object[][] Users {
|
|
||||||
get {
|
|
||||||
return Plugin.Instance.UserManager.Users.Select(x => new object[]{x.Id, x.Username}).ToArray();
|
|
||||||
}
|
|
||||||
set { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,41 +94,18 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
return playlistGuid;
|
return playlistGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Executor SetupExecutor() {
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
||||||
var env = new DefaultEnvironment();
|
List<BaseItem> results = new List<BaseItem>();
|
||||||
var executor = new Executor(env);
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
||||||
executor.builtins["logd"] = (x) => {
|
Executor executor = new Executor(new DefaultEnvironment());
|
||||||
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
executor.environment.Set("user", Lisp_Object.FromBase(user));
|
||||||
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;
|
|
||||||
};
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
return executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
|
||||||
List<BaseItem> results = new List<BaseItem>();
|
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
|
||||||
Executor executor = SetupExecutor();
|
|
||||||
|
|
||||||
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
|
||||||
foreach (var i in items) {
|
foreach (var i in items) {
|
||||||
executor.environment.Set("*item*", Lisp_Object.FromBase(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())) {
|
||||||
|
@ -136,9 +113,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
results.Add(i);
|
results.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
executor = SetupExecutor();
|
executor = new Executor(new DefaultEnvironment());
|
||||||
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
executor.environment.Set("user", Lisp_Object.FromBase(user));
|
||||||
executor.environment.Set("*items*", Lisp_Object.FromBase(results));
|
executor.environment.Set("items", Lisp_Object.FromBase(results));
|
||||||
results = new List<BaseItem>();
|
results = new List<BaseItem>();
|
||||||
var sort_result = executor.eval(smartPlaylist.SortProgram);
|
var sort_result = executor.eval(smartPlaylist.SortProgram);
|
||||||
if (sort_result is Cons sorted_items) {
|
if (sort_result is Cons sorted_items) {
|
||||||
|
@ -166,17 +143,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
|
|
||||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
||||||
_logger.LogInformation("Started regenerate Smart Playlists");
|
_logger.LogInformation("Started regenerate Smart Playlists");
|
||||||
_logger.LogDebug("Loaded Assemblies:");
|
foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) {
|
||||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
|
|
||||||
_logger.LogDebug("- {0}", asm);
|
|
||||||
}
|
|
||||||
var i = 0;
|
|
||||||
var all_playlists = await _store.GetAllSmartPlaylistsAsync();
|
|
||||||
foreach (SmartPlaylistDto dto in all_playlists) {
|
|
||||||
if (!dto.Enabled) {
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var changedDto = false;
|
var changedDto = false;
|
||||||
if (dto.Playlists.Length == 0) {
|
if (dto.Playlists.Length == 0) {
|
||||||
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
||||||
|
@ -201,6 +168,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
_store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one.
|
_store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one.
|
||||||
await _store.SaveSmartPlaylistAsync(dto);
|
await _store.SaveSmartPlaylistAsync(dto);
|
||||||
}
|
}
|
||||||
|
var i = 0;
|
||||||
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
||||||
User? user = _userManager.GetUserById(playlistLink.UserId);
|
User? user = _userManager.GetUserById(playlistLink.UserId);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -210,9 +178,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First();
|
var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First();
|
||||||
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)/all_playlists.Count());
|
progress.Report(100 * ((double)i)/dto.Playlists.Count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,8 @@ 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*)";
|
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; }
|
||||||
|
|
|
@ -5,7 +5,6 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
Task<SmartPlaylistDto> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
||||||
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
||||||
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
||||||
void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId);
|
|
||||||
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
|
var text = new SerializerBuilder().Build().Serialize(smartPlaylist);
|
||||||
await File.WriteAllTextAsync(filename, text);
|
await File.WriteAllTextAsync(filename, text);
|
||||||
}
|
}
|
||||||
public void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
||||||
try {
|
try {
|
||||||
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
||||||
if (File.Exists(filename)) { File.Delete(filename); }
|
if (File.Exists(filename)) { File.Delete(filename); }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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.3.0.0
|
||||||
targetAbi: 10.10.3.0
|
targetAbi: 10.10.3.0
|
||||||
framework: net8.0
|
framework: net8.0
|
||||||
owner: redxef
|
owner: redxef
|
||||||
|
@ -14,31 +14,6 @@ artifacts:
|
||||||
- jellyfin-smart-playlist.dll
|
- jellyfin-smart-playlist.dll
|
||||||
- YamlDotNet.dll
|
- YamlDotNet.dll
|
||||||
changelog: |
|
changelog: |
|
||||||
## 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
|
## v0.3.0.0
|
||||||
- Add a second program (`SortProgram`) which is run after the filtering, this
|
- 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
|
program should return the list of items, but in the order in which they should appear in
|
||||||
|
|
|
@ -14,36 +14,6 @@
|
||||||
<div class="fieldDescription">A program which can set up the environment</div>
|
<div class="fieldDescription">A program which can set up the environment</div>
|
||||||
<textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" cols="120"></textarea>
|
<textarea id="InitialProgram" class="emby-input smartplaylist-monospace" name="InitialProgram" rows="16" cols="120"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistSelection">Choose a playlist to edit</label>
|
|
||||||
<select id="SmartplaylistSelection" class="emby-select">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditName">Name</label>
|
|
||||||
<input id="SmartplaylistEditName" type="text" class="emby-input"/>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditProgram">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="SmartplaylistEditProgram" class="emby-input smartplaylist-monospace" name="Program" rows="16" cols="120"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditSortProgram">Sort Program</label>
|
|
||||||
<div class="fieldDescription">A program which should return a list of items to include in the playlist, sorted however you like.</div>
|
|
||||||
<textarea id="SmartplaylistEditSortProgram" class="emby-input smartplaylist-monospace" name="SortProgram" rows="16" cols="120"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartplaylistEditUsers">Users</label>
|
|
||||||
<div class="fieldDescription">Which users should get access to the playlist.</div>
|
|
||||||
<select multiple id="SmartplaylistEditUsers" class="emby-select">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="SmartPlaylistEditEnabled">Enabled</label>
|
|
||||||
<div class="fieldDescription">Is the playlist enabled.</div>
|
|
||||||
<input id="SmartplaylistEditEnabled" type="checkbox" class="emby-input"/>
|
|
||||||
</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">
|
||||||
<span>Save</span>
|
<span>Save</span>
|
||||||
|
@ -62,83 +32,11 @@
|
||||||
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df'
|
||||||
};
|
};
|
||||||
|
|
||||||
function changeEditBox(config, id) {
|
|
||||||
const selection = document.querySelector('#SmartplaylistSelection');
|
|
||||||
const editName = document.querySelector('#SmartplaylistEditName');
|
|
||||||
const editProgram = document.querySelector('#SmartplaylistEditProgram');
|
|
||||||
const editSortProgram = document.querySelector('#SmartplaylistEditSortProgram');
|
|
||||||
const editUsers = document.querySelector('#SmartplaylistEditUsers');
|
|
||||||
const editEnabled = document.querySelector('#SmartplaylistEditEnabled');
|
|
||||||
if (id === null) {
|
|
||||||
selection.selectedIndex = 0;
|
|
||||||
editName.value = 'My New Smartplaylist';
|
|
||||||
editProgram.value = '(is-favourite)';
|
|
||||||
editSortProgram.value = '(begin *items*)';
|
|
||||||
editUsers.innerHTML = '';
|
|
||||||
for (const u of config.Users) {
|
|
||||||
var o = document.createElement('option');
|
|
||||||
o.value = u[0];
|
|
||||||
o.innerHTML = u[1];
|
|
||||||
o.setAttribute('selected', 'selected');
|
|
||||||
editUsers.appendChild(o);
|
|
||||||
}
|
|
||||||
editEnabled.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function matchId(p) {
|
|
||||||
return p.Id == id;
|
|
||||||
}
|
|
||||||
const index = config.Playlists.map(function (x) { return x.Id }).indexOf(id);
|
|
||||||
selection.selectedIndex = index + 1;
|
|
||||||
const p = config.Playlists[index];
|
|
||||||
editName.value = p.Name;
|
|
||||||
editProgram.value = p.Program;
|
|
||||||
editSortProgram.value = p.SortProgram;
|
|
||||||
editUsers.innerHTML = '';
|
|
||||||
for (const u of config.Users) {
|
|
||||||
var o = document.createElement('option');
|
|
||||||
o.value = u[0];
|
|
||||||
o.innerHTML = u[1];
|
|
||||||
if (p.Playlists.map((x) => x.UserId).includes(u[0])) {
|
|
||||||
o.setAttribute('selected', 'selected');
|
|
||||||
}
|
|
||||||
editUsers.appendChild(o);
|
|
||||||
}
|
|
||||||
editEnabled.checked = p.Enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillPlaylistSelect(config) {
|
|
||||||
const selection = document.querySelector('#SmartplaylistSelection');
|
|
||||||
selection.innerHTML = '';
|
|
||||||
var o = document.createElement('option');
|
|
||||||
o.value = null;
|
|
||||||
o.innerHTML = 'Create new playlist ...';
|
|
||||||
selection.appendChild(o);
|
|
||||||
for (const i of config.Playlists) {
|
|
||||||
var o = document.createElement('option');
|
|
||||||
o.value = i.Id;
|
|
||||||
o.innerHTML = i.Name;
|
|
||||||
selection.appendChild(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector('#SmartPlaylistConfigPage')
|
document.querySelector('#SmartPlaylistConfigPage')
|
||||||
.addEventListener('pageshow', function() {
|
.addEventListener('pageshow', function() {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
||||||
document.querySelector('#InitialProgram').value = config.InitialProgram;
|
document.querySelector('#InitialProgram').value = config.InitialProgram;
|
||||||
fillPlaylistSelect(config);
|
|
||||||
changeEditBox(config, (config.Playlists.length > 0) ? config.Playlists[0].Id : null);
|
|
||||||
Dashboard.hideLoadingMsg();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#SmartplaylistSelection')
|
|
||||||
.addEventListener('change', function() {
|
|
||||||
Dashboard.showLoadingMsg();
|
|
||||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
|
||||||
const selection = document.querySelector('#SmartplaylistSelection');
|
|
||||||
changeEditBox(config, (selection.selectedIndex > 0) ? config.Playlists[selection.selectedIndex - 1].Id : null);
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -148,50 +46,11 @@
|
||||||
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;
|
||||||
const selection = document.querySelector('#SmartplaylistSelection');
|
|
||||||
const editName = document.querySelector('#SmartplaylistEditName');
|
|
||||||
const editProgram = document.querySelector('#SmartplaylistEditProgram');
|
|
||||||
const editSortProgram = document.querySelector('#SmartplaylistEditSortProgram');
|
|
||||||
const editUsers = document.querySelector('#SmartplaylistEditUsers');
|
|
||||||
const editEnabled = document.querySelector('#SmartplaylistEditEnabled');
|
|
||||||
var index = selection.selectedIndex;
|
|
||||||
if (index === 0) {
|
|
||||||
const o = {
|
|
||||||
Id: editName.value,
|
|
||||||
Name: editName.value,
|
|
||||||
Program: editProgram.value,
|
|
||||||
SortProgram: editSortProgram.value,
|
|
||||||
Playlists: Array.from(editUsers.options).filter((x) => x.selected).map((x) => {
|
|
||||||
const m = {UserId: x.value, PlaylistId: "00000000-0000-0000-0000-000000000000"};
|
|
||||||
return m;
|
|
||||||
}),
|
|
||||||
Enabled: editEnabled.checked,
|
|
||||||
};
|
|
||||||
config.Playlists.push(o);
|
|
||||||
} else {
|
|
||||||
config.Playlists[index-1].Id = editName.value;
|
|
||||||
config.Playlists[index-1].Name = editName.value;
|
|
||||||
config.Playlists[index-1].Program = editProgram.value;
|
|
||||||
config.Playlists[index-1].SortProgram = editSortProgram.value;
|
|
||||||
config.Playlists[index-1].Playlists = Array.from(editUsers.options).filter((x) => x.selected).map((x) => {
|
|
||||||
const existing = config.Playlists[index-1].Playlists.filter((x_) => x_.UserId === x.value).map((x_) => x_.PlaylistId);
|
|
||||||
const m = {UserId: x.value, PlaylistId: ((existing.length > 0) ? existing[0] : "00000000-0000-0000-0000-000000000000")};
|
|
||||||
return m;
|
|
||||||
}),
|
|
||||||
config.Playlists[index-1].Enabled = editEnabled.checked;
|
|
||||||
}
|
|
||||||
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
|
ApiClient.updatePluginConfiguration(SmartPlaylistConfig.pluginUniqueId, config).then(function (result) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
|
||||||
document.querySelector('#InitialProgram').value = config.InitialProgram;
|
|
||||||
fillPlaylistSelect(config);
|
|
||||||
changeEditBox(config, (config.Playlists.length > 0) ? config.Playlists[0].Id : null);
|
|
||||||
Dashboard.hideLoadingMsg();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.4.0.0</Version>
|
<Version>0.3.0.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
<PackageReference Include="Jellyfin.Model" Version="10.10.3" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
94
README.md
94
README.md
|
@ -5,18 +5,75 @@ 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.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
|
||||||
go to the plugin settings and below the `Initial Program` configuration
|
create a empty file in `config/data/smartplaylists` like this, maybe
|
||||||
choose the smart playlist you want to edit, or `Create new playlist ...`
|
you want to generate a playlist of your favourite rock songs:
|
||||||
to create a new one.
|
|
||||||
|
|
||||||
[Go here](examples.md) to see some example configurations.
|
```
|
||||||
|
$ touch config/data/smartplaylists/Rock.yaml
|
||||||
|
```
|
||||||
|
|
||||||
Below are all the configuration values for a smart playlist.
|
Afterwards run the Task `(re)generate Smart Playlists`, this will rename
|
||||||
|
the `yaml` file and populate it with some default values. You can now
|
||||||
|
adjust the file to your liking. [Go here](examples.md) to see more
|
||||||
|
examples.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Id: Rock
|
||||||
|
Playlists:
|
||||||
|
- PlaylistId: 24f12e1e-3278-d6d6-0ca4-066e93296c95
|
||||||
|
UserId: 6eec632a-ff0d-4d09-aad0-bf9e90b14bc6
|
||||||
|
Name: Rock
|
||||||
|
Program: (begin (invoke item "IsFavoriteOrLiked" (user)))
|
||||||
|
SortProgram: (begin items)
|
||||||
|
Filename: /config/data/smartplaylists/Rock.yaml
|
||||||
|
Enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the default configuration and will always match all your
|
||||||
|
favorite songs (and songs which are in favourited albums).
|
||||||
|
|
||||||
|
To change the filter you can append a `|` (pipe) to the Program
|
||||||
|
line and write multiline filters like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Porgram: |
|
||||||
|
(begin
|
||||||
|
(invoke item "IsFavoriteOrLiked" (list user)))
|
||||||
|
```
|
||||||
|
|
||||||
|
This is equivalent to the above example (not counting the other
|
||||||
|
fields obviously).
|
||||||
|
|
||||||
|
|
||||||
|
### Id
|
||||||
|
|
||||||
|
Arbitrary Id assigned to this playlist, can usually be left alone.
|
||||||
|
|
||||||
|
### Playlists
|
||||||
|
|
||||||
|
A list of Playlist/User mappings. By default all users get an entry.
|
||||||
|
|
||||||
|
The ids must have the dashes in them as of now. To convert a id
|
||||||
|
from without dashes to the canonical form run this command:
|
||||||
|
|
||||||
|
`echo '<your id here>' | python3 -c 'import uuid; import sys; print(uuid.UUID(sys.stdin.read().strip()))'`
|
||||||
|
|
||||||
|
To get your user id navigate to your user profile and copy the part
|
||||||
|
after `userId` in the address bar.
|
||||||
|
|
||||||
|
#### PlaylistId
|
||||||
|
|
||||||
|
The id of the playlist that should be managed, must be owned by the
|
||||||
|
corresponding user.
|
||||||
|
|
||||||
|
#### UserId
|
||||||
|
|
||||||
|
The user associated with this playlist.
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
|
@ -28,7 +85,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. Global variables `user` and `item` are predefined
|
||||||
and contain a [User](https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Entities/User.cs) and
|
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)
|
[BaseItem](https://github.com/jellyfin/jellyfin/blob/master/MediaBrowser.Controller/Entities/BaseItem.cs)
|
||||||
respectively.
|
respectively.
|
||||||
|
@ -45,13 +102,11 @@ 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)`.
|
||||||
|
|
||||||
*Go [here](lisp.md) to get an overview of the built-in functions.*
|
|
||||||
|
|
||||||
### SortProgram
|
### SortProgram
|
||||||
|
|
||||||
This works exactly like [Program](#program), but the input is the
|
This works exactly like [Program](#program), but the input is the
|
||||||
user and a list of items (`*items*`) matched by [Program](#program).
|
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 default is `(begin items)`, which doesn't sort at all. To sort
|
||||||
the items by name you could use the following program:
|
the items by name you could use the following program:
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
|
@ -61,9 +116,24 @@ the items by name you could use the following program:
|
||||||
(string>
|
(string>
|
||||||
(car (getitems a "Name"))
|
(car (getitems a "Name"))
|
||||||
(car (getitems b "Name"))))
|
(car (getitems b "Name"))))
|
||||||
*items*)
|
items)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Available definitions
|
||||||
|
|
||||||
|
- **lower**: lowercases a string (`(eq (lower "SomeString") "somestring")`)
|
||||||
|
- **is-genre**: check if the item is of this genre, partial matches
|
||||||
|
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-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
|
||||||
|
|
||||||
|
The path to this file, only used internally and updated by the program.
|
||||||
|
|
||||||
### Enabled
|
### Enabled
|
||||||
|
|
||||||
Enable this playlist, currently ignored.
|
Enable this playlist, currently ignored.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 Lisp_Integer = Jellyfin.Plugin.SmartPlaylist.Lisp.Integer;
|
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
|
|
||||||
|
@ -18,9 +17,6 @@ namespace Tests
|
||||||
public int I() {
|
public int I() {
|
||||||
return _i;
|
return _i;
|
||||||
}
|
}
|
||||||
public string G<E>() {
|
|
||||||
return typeof(E).FullName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Test {
|
public class Test {
|
||||||
|
@ -67,26 +63,6 @@ namespace Tests
|
||||||
sts = StringTokenStream.generate(program);
|
sts = StringTokenStream.generate(program);
|
||||||
p = new Parser(sts);
|
p = new Parser(sts);
|
||||||
Assert.Equal(program, string.Format("{0}", p.parse()));
|
Assert.Equal(program, string.Format("{0}", p.parse()));
|
||||||
|
|
||||||
program = "(abc '(1 2 3))";
|
|
||||||
sts = StringTokenStream.generate(program);
|
|
||||||
p = new Parser(sts);
|
|
||||||
Assert.Equal(program, string.Format("{0}", p.parse()));
|
|
||||||
|
|
||||||
program = "(abc \"'(1 2 3)\")";
|
|
||||||
sts = StringTokenStream.generate(program);
|
|
||||||
p = new Parser(sts);
|
|
||||||
Assert.Equal(program, string.Format("{0}", p.parse()));
|
|
||||||
|
|
||||||
program = """
|
|
||||||
(begin ;this too
|
|
||||||
;;; this is a comment
|
|
||||||
t
|
|
||||||
)
|
|
||||||
""";
|
|
||||||
sts = StringTokenStream.generate(program);
|
|
||||||
p = new Parser(sts);
|
|
||||||
Assert.Equal("(begin t)", string.Format("{0}", p.parse()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -149,7 +125,6 @@ namespace Tests
|
||||||
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
(if (> 0 1) (car (quote (1 2 3))) (cdr (quote (2 3 4)))))
|
||||||
""").ToString());
|
""").ToString());
|
||||||
|
|
||||||
Assert.Equal("a", e.eval("'a").ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -227,15 +202,6 @@ namespace Tests
|
||||||
Assert.Equal("(5 nil)", string.Format("{0}", r));
|
Assert.Equal("(5 nil)", string.Format("{0}", r));
|
||||||
r = e.eval("""(invoke o "I" nil)""");
|
r = e.eval("""(invoke o "I" nil)""");
|
||||||
Assert.Equal("5", string.Format("{0}", r));
|
Assert.Equal("5", string.Format("{0}", r));
|
||||||
r = e.eval("""(invoke-generic o "G" nil ((lambda (. args) args) "System.String"))""");
|
|
||||||
Assert.Equal("\"System.String\"", string.Format("{0}", r));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public static void GlobalVariableTest() {
|
|
||||||
Executor e = new Executor();
|
|
||||||
e.environment.Set("*o*", new Lisp_Integer(5));
|
|
||||||
Assert.Equal("10", e.eval("(* *o* 2)").ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -262,11 +228,6 @@ namespace Tests
|
||||||
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("(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());
|
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());
|
||||||
|
|
||||||
//Assert.Equal("", e.eval("(rand)").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 > (list 5 4 7 3 2 6 1))").ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,20 +10,20 @@ resources:
|
||||||
- name: source
|
- name: source
|
||||||
type: git
|
type: git
|
||||||
source:
|
source:
|
||||||
uri: https://git.redxef.at/redxef/jellyfin-smart-playlist
|
uri: https://gitea.redxef.at/redxef/jellyfin-smart-playlist
|
||||||
branch: main
|
branch: main
|
||||||
fetch_tags: true
|
fetch_tags: true
|
||||||
tag_regex: 'v.*'
|
tag_regex: 'v.*'
|
||||||
- name: manifest
|
- name: manifest
|
||||||
type: git
|
type: git
|
||||||
source:
|
source:
|
||||||
uri: ssh://git@git.redxef.at:8022/redxef/jellyfin-smart-playlist.git
|
uri: ssh://git@gitea.redxef.at:8022/redxef/jellyfin-smart-playlist.git
|
||||||
branch: manifest
|
branch: manifest
|
||||||
private_key: ((gitea.id_ed25519))
|
private_key: ((gitea.id_ed25519))
|
||||||
- name: releases
|
- name: releases
|
||||||
type: http-resource
|
type: http-resource
|
||||||
source:
|
source:
|
||||||
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases
|
url: https://gitea.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases
|
||||||
method: get
|
method: get
|
||||||
auth:
|
auth:
|
||||||
basic:
|
basic:
|
||||||
|
@ -32,7 +32,7 @@ resources:
|
||||||
- name: artifact
|
- name: artifact
|
||||||
type: http-resource
|
type: http-resource
|
||||||
source:
|
source:
|
||||||
url: https://git.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases/((.:release_id))/assets
|
url: https://gitea.redxef.at/api/v1/repos/redxef/jellyfin-smart-playlist/releases/((.:release_id))/assets
|
||||||
method: get
|
method: get
|
||||||
auth:
|
auth:
|
||||||
basic:
|
basic:
|
||||||
|
|
72
examples.md
72
examples.md
|
@ -25,75 +25,3 @@
|
||||||
(is-genre "electro" g)
|
(is-genre "electro" g)
|
||||||
(is-genre "swing" g)))
|
(is-genre "swing" g)))
|
||||||
```
|
```
|
||||||
|
|
||||||
- `All of Seeed`: A playlist containing all items by artist `Seeed`,
|
|
||||||
useless, since you also can just navigate to the artist to play all
|
|
||||||
their songs, but a good example.
|
|
||||||
```
|
|
||||||
Id: Seeed
|
|
||||||
Name: Seeed
|
|
||||||
Program: |
|
|
||||||
(let
|
|
||||||
(parent
|
|
||||||
(invoke-generic *item* "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller")))
|
|
||||||
(cond
|
|
||||||
((null parent) nil)
|
|
||||||
(t (string= (car (getitems parent "Name")) "Haller"))))
|
|
||||||
```
|
|
||||||
or simplified with definitions contained in the preamble:
|
|
||||||
```
|
|
||||||
Id: Seeed
|
|
||||||
Name: Seeed
|
|
||||||
Program: |
|
|
||||||
(let
|
|
||||||
(parent
|
|
||||||
(find-parent "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))
|
|
||||||
(cond
|
|
||||||
((null parent) nil)
|
|
||||||
(t (string= (car (getitems parent "Name")) "Seeed"))))
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
- `Artists`: A playlist including everything from a list of artists.
|
|
||||||
```
|
|
||||||
Id: Artists
|
|
||||||
Name: Artists
|
|
||||||
Program: >-
|
|
||||||
(begin
|
|
||||||
(define *include-artists* '(
|
|
||||||
"seeed"
|
|
||||||
"juli"
|
|
||||||
"falco"
|
|
||||||
"peter fox"
|
|
||||||
"alligatoah"
|
|
||||||
"kraftklub"
|
|
||||||
"wanda"
|
|
||||||
"annennaykantereit"
|
|
||||||
"feine sahne fischfilet"
|
|
||||||
"madsen"
|
|
||||||
"wiener blond"
|
|
||||||
"die toten hosen"
|
|
||||||
"die ärzte"
|
|
||||||
"wir sind helden"
|
|
||||||
))
|
|
||||||
(define my-prefilter-only-tracks
|
|
||||||
(lambda nil
|
|
||||||
(and
|
|
||||||
(is-type "Audio")
|
|
||||||
(not (name-contains "live"))
|
|
||||||
(not (name-contains "acoustic"))
|
|
||||||
(not (name-contains "instrumental"))
|
|
||||||
(not (name-contains "remix")))))
|
|
||||||
(and
|
|
||||||
(my-prefilter-only-tracks)
|
|
||||||
(is-favourite)
|
|
||||||
(any
|
|
||||||
(lambda
|
|
||||||
(i)
|
|
||||||
;; the `(and (find-artist)` is here to prevent null violations.
|
|
||||||
(and (find-artist) (string= i (lower (get-name (find-artist))))))
|
|
||||||
*include-artists*)))
|
|
||||||
SortProgram: (begin (shuf *items*))
|
|
||||||
Filename: /config/data/smartplaylists/German.yaml
|
|
||||||
Enabled: true
|
|
||||||
```
|
|
||||||
|
|
129
lisp.md
129
lisp.md
|
@ -1,129 +0,0 @@
|
||||||
# The lisp interpreter
|
|
||||||
|
|
||||||
The interpreter is a lisp-like language used to build the filter
|
|
||||||
expressions.
|
|
||||||
|
|
||||||
## Builtins
|
|
||||||
|
|
||||||
**atom**: check if a receive value is a atom. `(atom 1)`
|
|
||||||
|
|
||||||
**eq**: check if two values are equal. `(eq (quote a) (quote a))`
|
|
||||||
|
|
||||||
**car**: get the first item of the cons.
|
|
||||||
|
|
||||||
**cdr**: get the remainder of the list.
|
|
||||||
|
|
||||||
**cons**: create a new cons. `(cons 1 2)`
|
|
||||||
|
|
||||||
**begin**: evaluate a series of statements, returning the result of the
|
|
||||||
last one. `(begin t nil)`
|
|
||||||
|
|
||||||
**+-\*/%**: arithmetic operations for integers. `(+ 1 2)`
|
|
||||||
|
|
||||||
**=**, **!=**, **<**, **<=**, **>**, **>=**: compare two integers. `(> 1 2)`
|
|
||||||
|
|
||||||
**not**: negate the given value. `nil -> t`, everything else will be `nil`. `(not (quote a))`
|
|
||||||
|
|
||||||
**string=**, **string!=**, **string<**, **string<=**, **string>**, **string>=**: compare two strings. `(> "1" "2")`
|
|
||||||
|
|
||||||
**haskeys**: takes any object and a variadic number of arguments (strings) and
|
|
||||||
returns a list with either `t` or `nil` describing if a corresponding property/field/method
|
|
||||||
with that name exists on the object. `(haskeys mystring "Length")`
|
|
||||||
|
|
||||||
**getitems**: takes any object and a variadic number of arguments
|
|
||||||
(strings) and returns the values of the fields/properties. `(getitems mystring "Length" "Chars")`
|
|
||||||
|
|
||||||
**invoke**: takes 3 arguments and invokes the method defined on the
|
|
||||||
object.
|
|
||||||
|
|
||||||
The first argument is the object on which to invoke the method, the
|
|
||||||
second one is the name of the method and the third one is a list of
|
|
||||||
arguments to pass to the method. `(invoke mystring "Lower" nil)`
|
|
||||||
|
|
||||||
**invoke-generic**: the same as **invoke**, but takes a fourth
|
|
||||||
argument, a list of string describing the types for the generic method.
|
|
||||||
`(invoke-generic mybaseitem "FindParent" nil (list "MediaBrowser.Controller.Entities.Audio.MusicArtist, MediaBrowser.Controller"))`
|
|
||||||
|
|
||||||
**random**: gives a random integer. `(random)`
|
|
||||||
|
|
||||||
**shuffle**: shuffles a list. `(shuffle (list 1 2 3 4))`
|
|
||||||
|
|
||||||
**quote**: quotes a value. `(quote a)`
|
|
||||||
|
|
||||||
**eval**: evaluates a expression. `(eval (quote a))`
|
|
||||||
|
|
||||||
**cond**: checks conditions and evaluates the corresponding expression.
|
|
||||||
```
|
|
||||||
(cond
|
|
||||||
((> 1 2) t)
|
|
||||||
(t f))
|
|
||||||
```
|
|
||||||
|
|
||||||
**if**: a conditional. `(if t 1 2)`
|
|
||||||
|
|
||||||
**define**: defines a new symbol. `(define foo 1)`, `(define add (lambda (a b) (+ a b)))`
|
|
||||||
|
|
||||||
**let**: define variables in the let context and evaluate the last
|
|
||||||
expression. `(let (a 1) (b 2) (+ a b))`
|
|
||||||
|
|
||||||
**let\***: the same as **let**, but allows to reference variables
|
|
||||||
defined earlier in the let statement. `(let* (a 1) (b (+ 2 a)) (+ a b))`
|
|
||||||
|
|
||||||
**apply**: call a function with the specified arguments. `(apply + (list 1 2))`
|
|
||||||
|
|
||||||
**and**: evaluate the given expressions in order, if any one of them
|
|
||||||
evaluates to `nil` return early with that value, otherwise return the
|
|
||||||
last value. `(and 1 2 nil 3)`
|
|
||||||
|
|
||||||
**or**: return `nil` if all arguments evaluate to `nil` otherwise the
|
|
||||||
first non-nil value.
|
|
||||||
|
|
||||||
## Derived builtins
|
|
||||||
|
|
||||||
**null**: the same as **not**. Can be useful to indicate semantics of a
|
|
||||||
program.
|
|
||||||
|
|
||||||
**list**: create a list from the given arguments. `(list 1 2 3)`
|
|
||||||
|
|
||||||
**find**: find if an item is in the given list and return it, otherwise
|
|
||||||
return `nil`. `(find 4 (list 1 2 3 4 5 6 7))`
|
|
||||||
|
|
||||||
**map**: apply a function to every item in the list. `(map (lambda (x) (* 2 x)) (list 1 2 3))`
|
|
||||||
|
|
||||||
**fold**: also known as reduce. Apply the function to a sequence of
|
|
||||||
values, reducing the sequence to a single item. It takes a initial value
|
|
||||||
which is returned for empty lists. `(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))`
|
|
||||||
|
|
||||||
**any**: equivalent to `(apply or (map function list))`.
|
|
||||||
`(any (lambda (a) (% a 2)) (list 2 4 6 7 8))`
|
|
||||||
|
|
||||||
**all**: equivalent to `(apply and (map function list))`.
|
|
||||||
`(all (lambda (a) (% a 2)) (list 2 4 6 7 8))`
|
|
||||||
|
|
||||||
**append**: append an item to the given list. `(append (list 1 2 3) 4)`
|
|
||||||
|
|
||||||
**qsort**: quicksort, takes a comparison function and the list.
|
|
||||||
`(qsort (lambda (a b) (> a b)) (list 1 2 6 4 9 1 19 0))`
|
|
||||||
|
|
||||||
**rand**: get a random integer. Takes either zero, one or two arguments.
|
|
||||||
If zero arguments are given it gives a random integer from all possibly
|
|
||||||
representable integers. If one argument is given it gives a integer
|
|
||||||
between `0` (inclusive) and `n` (exclusive). If two arguments are given
|
|
||||||
it gives a integer between `a` (inclusive) and `b` (exclusive).
|
|
||||||
|
|
||||||
**shuf**: same as **shuffle**.
|
|
||||||
|
|
||||||
## Pre-defined convenience functions
|
|
||||||
|
|
||||||
- **lower**: lowercases a string (`(eq (lower "SomeString") "somestring")`)
|
|
||||||
- **is-genre**: check if the item is of this genre, partial matches
|
|
||||||
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-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")`)
|
|
||||||
- **logd**, **logi**, **logw**, **loge**: write to the logger with the respective levels (`debug`, `information`, `warning`, `error`).
|
|
||||||
Takes the same arguments as `Logger.LogInformation(...)`.
|
|
||||||
|
|
||||||
And many more.
|
|
Loading…
Reference in a new issue