diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs index 656d4e6..c716805 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs @@ -28,10 +28,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { interface IComparable where T : IComparable { static abstract E operator ==(T left, T right); static abstract E operator !=(T left, T right); + E Equals(T other); } - public abstract class Expression : IFormattable { + public abstract class Expression: IFormattable, IComparable { public abstract string ToString(string? format, IFormatProvider? provider); + public abstract override int GetHashCode(); + public abstract bool Equals(Expression other); + public override bool Equals(object? other) { + if (other is Expression other_e) { + return Equals(other_e); + } + return false; + } + public static bool operator ==(Expression left, Expression right) { + return left.Equals(right); + } + public static bool operator !=(Expression left, Expression right) { + return !left.Equals(right); + } } public abstract class Atom : Expression {} public class Symbol : Atom { @@ -40,34 +55,67 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { _name = name; } public string name { get => _name; } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _name.GetHashCode(); + return hash; + } + public override bool Equals(Expression? other) { + if (other is Symbol other_s) { + return _name == other_s._name; + } + return false; + } public override string ToString(string? format, IFormatProvider? provider) { return _name; } } - public class Boolean : Atom, IComparable { + + public class Boolean : Atom { private readonly bool _value; public Boolean(bool value) { _value = value; } public bool value { get => _value; } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _value.GetHashCode(); + return hash; + } + public override bool Equals(Expression other) { + if (other is Boolean other_b) { + return _value == other_b.value; + } + return false; + } public override string ToString(string? format, IFormatProvider? provider) { return _value? "t" : "nil"; } - public static Boolean operator ==(Boolean a, Boolean b) { - return new Boolean(a.value == b.value); - } - public static Boolean operator !=(Boolean a, Boolean b) { - return new Boolean(a.value != b.value); - } } - public class Integer : Atom, IAddable, ISubtractable, IMultiplicatable, IDivisible, ISortable, IComparable { + + public class Integer : Atom, IAddable, ISubtractable, IMultiplicatable, IDivisible, ISortable { private readonly int _value; public Integer(int value) { _value = value; } public int value { get => _value; } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _value.GetHashCode(); + return hash; + } + public override bool Equals(Expression other) { + if (other is Integer other_i) { + return _value == other_i._value; + } + return false; + } public override string ToString(string? format, IFormatProvider? provider) { - return _value.ToString("0", provider); + return _value.ToString(); + //return _value.ToString("0", provider); } public static Integer operator +(Integer a, Integer b) { return new Integer(a.value + b.value); @@ -103,12 +151,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { return new Boolean(a.value != b.value); } } + public class String : Atom, IAddable { private readonly string _value; public String(string value) { _value = value; } public string value { get => _value; } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _value.GetHashCode(); + return hash; + } + public override bool Equals(Expression other) { + if (other is String other_s) { + return _value == other_s._value; + } + return false; + } public override string ToString(string? format, IFormatProvider? provider) { return "\"" + _value + "\""; } @@ -116,12 +177,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { return new String (a.value + b.value); } } + public class Object : Atom { private readonly object _value; public Object(object value) { _value = value; } public object value { get => _value; } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _value.GetHashCode(); + return hash; + } + public override bool Equals(Expression other) { + if (other is Object other_o) { + return _value == other_o._value; + } + return false; + } public override string ToString(string? format, IFormatProvider? provider) { return _value.ToString(); } @@ -145,13 +219,22 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { _expressions = expressions; } public IList expressions { get => _expressions; } - public override string ToString(string? format, IFormatProvider? provider) { - string r = "("; - foreach (var e in _expressions) { - r += " "; - r += e.ToString("0", provider); + public override int GetHashCode() { + int hash = 17; + foreach (Expression i in _expressions) { + hash *= 23; + hash += i.GetHashCode(); } - return r + ")"; + return hash; + } + public override bool Equals(Expression other) { + if (other is List other_l) { + return _expressions == other_l._expressions; + } + return false; + } + public override string ToString(string? format, IFormatProvider? provider) { + return "(" + string.Join(" ", _expressions.Select(x => x.ToString("0", provider))) + ")"; } public static List operator +(List a, List b) { List r = new List(); diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs index 5b024f4..c7dd6a2 100644 --- a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs @@ -22,20 +22,39 @@ using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using Jellyfin.Plugin.SmartPlaylist.Lisp; +using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; +using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object; +using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean; + + namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { public class GeneratePlaylist : IScheduledTask { + + public static readonly BaseItemKind[] AvailableFilterItems = { + BaseItemKind.Audio + }; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; + private readonly IPlaylistManager _playlistManager; + + private readonly IStore _store; public GeneratePlaylist( ILogger logger, ILibraryManager libraryManager, - IUserManager userManager + IUserManager userManager, + IPlaylistManager playlistManager, + IServerApplicationPaths serverApplicationPaths ) { _logger = logger; _libraryManager = libraryManager; _userManager = userManager; + _playlistManager = playlistManager; + + _store = new Store(new FileSystem(serverApplicationPaths)); } public string Category => "Library"; @@ -46,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { public IEnumerable GetDefaultTriggers() { return new[] { new TaskTriggerInfo { - IntervalTicks = TimeSpan.FromMinutes(1).Ticks, + IntervalTicks = TimeSpan.FromHours(24).Ticks, Type = TaskTriggerInfo.TriggerInterval, } }; @@ -56,7 +75,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { foreach (var user in _userManager.Users) { _logger.LogInformation("User {0}", user); var query = new InternalItemsQuery(user) { - IncludeItemTypes = new[] {BaseItemKind.Audio}, + IncludeItemTypes = AvailableFilterItems, Recursive = true, }; foreach (BaseItem item in _libraryManager.GetItemsResult(query).Items) { @@ -65,9 +84,66 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { } } + private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto, User user) { + var req = new PlaylistCreationRequest { + Name = dto.Name, + UserId = user.Id + }; + return Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id); + } + + 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(); + 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()); + if (r is Lisp_Boolean r_bool) { + if (r_bool.value) { results.Add(i.Id); } + } else { + _logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString()); + } + } + return results; + } + + private IEnumerable GetAllUserMedia(User user) { + var req = new InternalItemsQuery(user) { + IncludeItemTypes = new[] {BaseItemKind.Audio}, + Recursive = true, + }; + return _libraryManager.GetItemsResult(req).Items; + } + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - _logger.LogInformation("This is a test"); - GetUsers(); + _logger.LogInformation("Started regenerate Smart Playlists"); + foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) { + var user = _userManager.GetUserById(dto.User); + 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); + await _store.SaveSmartPlaylistAsync(dto); + playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList(); + } + var insertItems = FilterPlaylistItems(GetAllUserMedia(user), user, dto); + Playlist playlist = playlists.First(); + await ClearPlaylist(dto, playlist, user); + await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems.ToArray(), user.Id); + } + } + + 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); } } } diff --git a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs new file mode 100644 index 0000000..3c81ad2 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs @@ -0,0 +1,11 @@ +namespace Jellyfin.Plugin.SmartPlaylist { + [Serializable] + public class SmartPlaylistDto { + public SmartPlaylistId Id { get; set; } + public string Name { get; set; } + public string FileName { get; set; } + public UserId User { get; set; } + public string Program { get; set; } + public int MaxItems { get; set; } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs new file mode 100644 index 0000000..f871fb3 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller; + +namespace Jellyfin.Plugin.SmartPlaylist { + + public interface ISmartPlaylistFileSystem { + public string StoragePath { get; } + public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId); + public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId); + public string[] FindAllSmartPlaylistFilePaths(); + } + + public class FileSystem : ISmartPlaylistFileSystem { + public FileSystem(IServerApplicationPaths serverApplicationPaths) { + StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists"); + if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); } + } + public string StoragePath { get; } + public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) { + return Path.Combine(StoragePath, $"{smartPlaylistId}.json"); + } + public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) { + return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories).First(); + } + public string[] FindAllSmartPlaylistFilePaths() { + return Directory.GetFiles(StoragePath); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Store.cs b/Jellyfin.Plugin.SmartPlaylist/Store.cs new file mode 100644 index 0000000..ce37a91 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Store.cs @@ -0,0 +1,39 @@ +using System.Text.Json; + +namespace Jellyfin.Plugin.SmartPlaylist { + public interface IStore { + Task GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId); + Task GetAllSmartPlaylistsAsync(); + Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist); + void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId); + } + + public class Store : IStore { + private readonly ISmartPlaylistFileSystem _fileSystem; + public Store(ISmartPlaylistFileSystem fileSystem) { + _fileSystem = fileSystem; + } + private async Task LoadPlaylistAsync(string filename) { + await using var r = File.OpenRead(filename); + return await JsonSerializer.DeserializeAsync(r).ConfigureAwait(false); + } + public async Task GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) { + string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId); + return await LoadPlaylistAsync(filename).ConfigureAwait(false); + } + public async Task GetAllSmartPlaylistsAsync() { + var t = _fileSystem.FindAllSmartPlaylistFilePaths().Select(LoadPlaylistAsync).ToArray(); + await Task.WhenAll(t).ConfigureAwait(false); + return t.Where(x => x != null).Select(x => x.Result).ToArray(); + } + public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) { + string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id); + 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); } + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Types.cs b/Jellyfin.Plugin.SmartPlaylist/Types.cs new file mode 100644 index 0000000..060a796 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Types.cs @@ -0,0 +1,4 @@ +global using System; + +global using UserId = System.Guid; +global using SmartPlaylistId = System.Guid;