diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs index c716805..a768129 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs @@ -31,8 +31,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { E Equals(T other); } - public abstract class Expression: IFormattable, IComparable { - public abstract string ToString(string? format, IFormatProvider? provider); + public abstract class Expression: IComparable { + public override abstract string ToString(); public abstract override int GetHashCode(); public abstract bool Equals(Expression other); public override bool Equals(object? other) { @@ -47,6 +47,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { public static bool operator !=(Expression left, Expression right) { return !left.Equals(right); } + public abstract object Inner(); } public abstract class Atom : Expression {} public class Symbol : Atom { @@ -67,7 +68,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { + public override string ToString() { + return _name; + } + public override object Inner() { return _name; } } @@ -90,9 +94,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { + public override string ToString() { return _value? "t" : "nil"; } + public override object Inner() { + return _value; + } } public class Integer : Atom, IAddable, ISubtractable, IMultiplicatable, IDivisible, ISortable { @@ -113,9 +120,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { + public override string ToString() { return _value.ToString(); - //return _value.ToString("0", provider); } public static Integer operator +(Integer a, Integer b) { return new Integer(a.value + b.value); @@ -150,6 +156,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { public static Boolean operator !=(Integer a, Integer b) { return new Boolean(a.value != b.value); } + public override object Inner() { + return _value; + } } public class String : Atom, IAddable { @@ -170,12 +179,15 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { + public override string ToString() { return "\"" + _value + "\""; } public static String operator +(String a, String b) { return new String (a.value + b.value); } + public override object Inner() { + return _value; + } } public class Object : Atom { @@ -196,7 +208,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { + public override string ToString() { return _value.ToString(); } public static Atom FromBase(object o) { @@ -211,6 +223,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { return new Object(o); } } + public override object Inner() { + return _value; + } } public class List : Expression { @@ -233,8 +248,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { } return false; } - public override string ToString(string? format, IFormatProvider? provider) { - return "(" + string.Join(" ", _expressions.Select(x => x.ToString("0", provider))) + ")"; + public override string ToString() { + return "(" + string.Join(" ", _expressions.Select(x => x.ToString())) + ")"; } public static List operator +(List a, List b) { List r = new List(); @@ -242,6 +257,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { r.AddRange(b.expressions); return new List(r); } + public override object Inner() { + return _expressions.Select(x => x.Inner()).ToArray(); + } } public class Parser { diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs index 365e475..ff9c17c 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { this["length"] = _length; this["haskeys"] = _haskeys; this["getitems"] = _getitems; + this["invoke"] = _invoke; //this[new Symbol("!=")] = _ne; } @@ -202,20 +203,27 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { r.Add(Compiler.Object.FromBase(pi.GetValue(o.value))); continue; } - MethodInfo? mi = o.value.GetType().GetMethod(s.value); - if (mi != null) { - r.Add(Compiler.Object.FromBase(mi.Invoke(o.value, null))); - continue; - } FieldInfo? fi = o.value.GetType().GetField(s.value); if (fi != null) { r.Add(Compiler.Object.FromBase(fi.GetValue(o.value))); continue; } - throw new ApplicationException(); + throw new ApplicationException($"{o.value} has no property or field {s.value}"); } return new Compiler.List(r); } + + private static Expression _invoke(IList args) { + Compiler.Object o = (Compiler.Object) args[0]; + Compiler.String s = (Compiler.String) args[1]; + Compiler.List l = (Compiler.List) args[2]; + IList r = new List(); + MethodInfo? mi = o.value.GetType().GetMethod(s.value); + if (mi == null) { + throw new ApplicationException($"{o.value} has not method {s.value}"); + } + return Compiler.Object.FromBase(mi.Invoke(o.value, (object?[]?) l.Inner())); + } } public class BuiltinsLater : Dictionary { diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs index c7dd6a2..198bdb0 100644 --- a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs @@ -38,6 +38,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; private readonly IPlaylistManager _playlistManager; private readonly IStore _store; @@ -46,15 +48,19 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { ILogger logger, ILibraryManager libraryManager, IUserManager userManager, + IProviderManager providerManager, + IFileSystem fileSystem, IPlaylistManager playlistManager, IServerApplicationPaths serverApplicationPaths ) { _logger = logger; _libraryManager = libraryManager; _userManager = userManager; + _providerManager = providerManager; + _fileSystem = fileSystem; _playlistManager = playlistManager; - _store = new Store(new FileSystem(serverApplicationPaths)); + _store = new Store(new SmartPlaylistFileSystem(serverApplicationPaths)); } public string Category => "Library"; @@ -84,24 +90,29 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { } } - private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto, User user) { + private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto) { var req = new PlaylistCreationRequest { Name = dto.Name, - UserId = user.Id + UserId = dto.User }; - return Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id); + var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id); + return playlistGuid; } private IEnumerable FilterPlaylistItems(IEnumerable items, User user, SmartPlaylistDto smartPlaylist) { List results = new List(); Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); Executor executor = new Executor(); + executor.environment["user"] = new Lisp_Object(user); foreach (var i in items) { executor.environment["item"] = new Lisp_Object(i); var r = executor.eval(expression); - _logger.LogInformation("Item {0} evaluated to {1}", i, r.ToString()); + _logger.LogDebug("Item {0} evaluated to {1}", i, r.ToString()); if (r is Lisp_Boolean r_bool) { - if (r_bool.value) { results.Add(i.Id); } + if (r_bool.value) { + _logger.LogDebug("Added "{0}" to Smart Playlist {1}", i, smartPlaylist.Name); + results.Add(i.Id); + } } else { _logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString()); } @@ -124,8 +135,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { List playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList(); if ((dto.Id == null) || !playlists.Any()) { _logger.LogInformation("Generating new smart playlist (dto.Id = {0}, playlists.Any() = {1})", dto.Id, playlists.Any()); - _store.DeleteSmartPlaylist(dto.Id); - dto.Id = CreateNewPlaylist(dto, user); + _store.DeleteSmartPlaylist(dto); + dto.Id = CreateNewPlaylist(dto); await _store.SaveSmartPlaylistAsync(dto); playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList(); } @@ -137,13 +148,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { } private async Task ClearPlaylist(SmartPlaylistDto smartPlaylist, Playlist playlist, User user) { - var req = new InternalItemsQuery(user) - { - IncludeItemTypes = AvailableFilterItems, - Recursive = true - }; - var existingItems = playlist.GetChildren(user, false, req).Select(x => x.Id.ToString()).ToList(); - await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems); + // fuck if I know + if (_libraryManager.GetItemById(playlist.Id) is not Playlist playlist_new) { + throw new ArgumentException(""); + } + var existingItems = playlist_new.GetManageableItems().ToList(); + await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id)); } } } diff --git a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs index 3c81ad2..ca024dd 100644 --- a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs +++ b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs @@ -1,11 +1,28 @@ +using System.Runtime.Serialization; namespace Jellyfin.Plugin.SmartPlaylist { [Serializable] public class SmartPlaylistDto { + private string DEFAULT_PROGRAM = "(begin (invoke item 'IsFavoriteOrLiked' (user)))"; public SmartPlaylistId Id { get; set; } - public string Name { get; set; } - public string FileName { get; set; } + public string? Name { get; set; } public UserId User { get; set; } - public string Program { get; set; } - public int MaxItems { get; set; } + public string? Program { get; set; } + public string? Filename { get; set; } + public int MaxItems { get; set; } = -1; + + public void Fill(string filename) { + if (Id == Guid.Empty) { + Id = Guid.NewGuid(); + } + if (Name == null) { + Name = Id.ToString(); + } + if (Program == null) { + Program = DEFAULT_PROGRAM; + } + if (Filename == null) { + Filename = filename; + } + } } } diff --git a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs index f871fb3..27a88fa 100644 --- a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs +++ b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs @@ -9,8 +9,8 @@ namespace Jellyfin.Plugin.SmartPlaylist { public string[] FindAllSmartPlaylistFilePaths(); } - public class FileSystem : ISmartPlaylistFileSystem { - public FileSystem(IServerApplicationPaths serverApplicationPaths) { + public class SmartPlaylistFileSystem : ISmartPlaylistFileSystem { + public SmartPlaylistFileSystem(IServerApplicationPaths serverApplicationPaths) { StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists"); if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); } } diff --git a/Jellyfin.Plugin.SmartPlaylist/Store.cs b/Jellyfin.Plugin.SmartPlaylist/Store.cs index ce37a91..2a9b609 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Store.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Store.cs @@ -5,7 +5,7 @@ namespace Jellyfin.Plugin.SmartPlaylist { Task GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId); Task GetAllSmartPlaylistsAsync(); Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist); - void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId); + void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist); } public class Store : IStore { @@ -15,7 +15,9 @@ namespace Jellyfin.Plugin.SmartPlaylist { } private async Task LoadPlaylistAsync(string filename) { await using var r = File.OpenRead(filename); - return await JsonSerializer.DeserializeAsync(r).ConfigureAwait(false); + var dto = (await JsonSerializer.DeserializeAsync(r).ConfigureAwait(false)); + dto.Fill(filename); + return dto; } public async Task GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) { string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId); @@ -31,9 +33,15 @@ namespace Jellyfin.Plugin.SmartPlaylist { await using var w = File.Create(filename); await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false); } - public void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId) { - string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId); - if (File.Exists(filename)) { File.Delete(filename); } + private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) { + try { + string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId); + if (File.Exists(filename)) { File.Delete(filename); } + } catch (System.InvalidOperationException) {} + } + public void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist) { + if (File.Exists(smartPlaylist.Filename)) { File.Delete(smartPlaylist.Filename); } + DeleteSmartPlaylistById(smartPlaylist.Id); } } }