diff --git a/Jellyfin.Plugin.SmartPlaylist/Api/SmartCollectionController.cs b/Jellyfin.Plugin.SmartPlaylist/Api/SmartCollectionController.cs new file mode 100644 index 0000000..4a597a1 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Api/SmartCollectionController.cs @@ -0,0 +1,62 @@ +using System.Net.Mime; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Providers; +using System.ComponentModel.DataAnnotations; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Common.Api; + + +namespace Jellyfin.Plugin.SmartPlaylist.Api { + [ApiController] + [Authorize(Policy = Policies.RequiresElevation)] + [Route("SmartCollection")] + [Produces(MediaTypeNames.Application.Json)] + public class SmartCollectionController : ControllerBase { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly ICollectionManager _collectionManager; + + private readonly IStore _store; + + public SmartCollectionController( + ILogger logger, + ILibraryManager libraryManager, + IUserManager userManager, + IProviderManager providerManager, + IFileSystem fileSystem, + ICollectionManager collectionManager, + IServerApplicationPaths serverApplicationPaths + ) { + _logger = logger; + _libraryManager = libraryManager; + _userManager = userManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + _collectionManager = collectionManager; + + _store = new Store(new SmartFileSystem(serverApplicationPaths)); + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetCollections() { + return Ok(await _store.GetAllSmartCollectionsAsync()); + } + + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task SetCollection([FromRoute, Required] CollectionId collectionId, [FromBody] SmartCollectionDto smartCollection) { + await _store.SaveSmartCollectionAsync(smartCollection); + return Created(); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Api/SmartPlaylistController.cs b/Jellyfin.Plugin.SmartPlaylist/Api/SmartPlaylistController.cs new file mode 100644 index 0000000..0e722cf --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Api/SmartPlaylistController.cs @@ -0,0 +1,61 @@ +using System.Net.Mime; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using System.ComponentModel.DataAnnotations; +using MediaBrowser.Common.Api; + +namespace Jellyfin.Plugin.SmartPlaylist.Api { + [ApiController] + [Authorize(Policy = Policies.RequiresElevation)] + [Route("SmartPlaylist")] + [Produces(MediaTypeNames.Application.Json)] + public class SmartPlaylistController : ControllerBase { + 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; + + public SmartPlaylistController( + 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 SmartFileSystem(serverApplicationPaths)); + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetPlaylists() { + return Ok(await _store.GetAllSmartPlaylistsAsync()); + } + + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task SetPlaylist([FromBody, Required] SmartPlaylistDto smartPlaylist) { + await _store.SaveSmartPlaylistAsync(smartPlaylist); + return Created(); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs b/Jellyfin.Plugin.SmartPlaylist/Configuration/PluginConfiguration.cs similarity index 56% rename from Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs rename to Jellyfin.Plugin.SmartPlaylist/Configuration/PluginConfiguration.cs index b3a5cfc..aa35a00 100644 --- a/Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Configuration/PluginConfiguration.cs @@ -42,45 +42,7 @@ namespace Jellyfin.Plugin.SmartPlaylist { (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 SmartFileSystem(Plugin.Instance.ServerApplicationPaths)); } - private Store store { 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 SmartCollectionDto[] Collections { - get { - return store.GetAllSmartCollectionsAsync().GetAwaiter().GetResult(); - } - set { - var existing = store.GetAllSmartCollectionsAsync().GetAwaiter().GetResult().Select(x => x.Id).ToList(); - foreach (var p in value) { - existing.Remove(p.Id); - store.SaveSmartCollectionAsync(p).GetAwaiter().GetResult(); - } - foreach (var p in existing) { - store.DeleteSmartCollectionById(p); - } - } - } - public object[][] Users { - get { - return Plugin.Instance.UserManager.Users.Select(x => new object[]{x.Id, x.Username}).ToArray(); - } - set { } - } } } diff --git a/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.html b/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.html new file mode 100644 index 0000000..c12584f --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.html @@ -0,0 +1,32 @@ + + + + + SmartPlaylist + + +
+
+
+
+
+ +
A program which can set up the environment
+ +
+
+ +
+
+
+
+ +
+ + diff --git a/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.js b/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.js new file mode 100644 index 0000000..70fb8be --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.js @@ -0,0 +1,23 @@ +var SmartPlaylistConfig = { + pluginUniqueId: 'dd2326e3-4d3e-4bfc-80e6-28502c1131df' +}; + +document.querySelector('#SmartPlaylistConfigPage') + .addEventListener('pageshow', function() { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) { + document.querySelector('#InitialProgram').value = config.InitialProgram; + Dashboard.hideLoadingMsg(); + }); + }); + +document.querySelector('#SmartPlaylistConfigForm') + .addEventListener('submit', function(e) { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) { + config.InitialProgram = document.querySelector('#InitialProgram').value; + }); + e.preventDefault(); + return false; +}); + diff --git a/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.html b/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.html new file mode 100644 index 0000000..2d93b39 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.html @@ -0,0 +1,57 @@ + + + + + SmartPlaylist + + +
+
+
+
+
+ + +
+
+ + +
+
+ +
A program which should return t or nil to include or exclude the provided item.
+ +
+
+ +
A program which should return a list of items to include in the playlist, sorted however you like.
+ +
+
+ +
Which users should get access to the playlist.
+ +
+
+ +
Is the playlist enabled.
+ +
+
+ +
+
+
+
+ +
+ + diff --git a/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.js b/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.js new file mode 100644 index 0000000..a0fb77b --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.js @@ -0,0 +1,144 @@ + +var PLAYLISTS = [ + { + Id: 'My New Smartplaylist', + Name: 'My New Smartplaylist', + Program: '(is-favourite)', + SortProgram: '(begin *items*)', + Playlists: [], + Enabled: true, + } +]; +var USERS = []; + +function fillForm(playlist, users) { + 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'); + editName.value = playlist.Name; + editProgram.value = playlist.Program; + editSortProgram.value = playlist.SortProgram; + editUsers.innerHTML = ''; + + var added_users = [] + for (const p of playlist.Playlists) { + var o = document.createElement('option'); + o.value = btoa(JSON.stringify(p)); + o.innerHTML = users.filter(u => u.Id == p.UserId).map(u => u.Name)[0]; + o.setAttribute('selected', 'selected'); + editUsers.appendChild(o); + added_users.push(p.UserId); + } + for (const u of users) { + if (added_users.includes(u.Id)) { + continue; + } + var o = document.createElement('option'); + o.value = btoa(JSON.stringify({ + 'PlaylistId': '00000000000000000000000000000000', + 'UserId': u.Id, + })); + o.innerHTML = u.Name; + editUsers.appendChild(o); + } + editEnabled.checked = playlist.Enabled; +} + +function fillPlaylistSelect(playlists) { + 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 playlists.slice(1)) { + var o = document.createElement('option'); + o.value = i.Id; + o.innerHTML = i.Name; + selection.appendChild(o); + } +} + +function jsonFromForm() { + 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'); + return { + Id: editName.value, + Name: editName.value, + Program: editProgram.value, + SortProgram: editSortProgram.value, + Playlists: Array.from(editUsers.options).filter((x) => x.selected).map(x => JSON.parse(atob(x.value))), + Enabled: editEnabled.checked, + }; +} + +ApiClient.getSmartPlaylists = function () { + const url = ApiClient.getUrl('SmartPlaylist'); + return this.ajax({ + type: 'GET', + url: url, + dataType: 'json', + }); +} + +ApiClient.setSmartPlaylist = function (p) { + const url = ApiClient.getUrl('SmartPlaylist'); + return this.ajax({ + type: 'POST', + url: url, + dataType: 'json', + contentType: 'application/json; charset=UTF-8', + data: JSON.stringify(p), + }); +} + +document.querySelector('#SmartPlaylistConfigPage') + .addEventListener('pageshow', function() { + Dashboard.showLoadingMsg(); + ApiClient.getSmartPlaylists().then(function (playlists) { + PLAYLISTS = [PLAYLISTS[0]].concat(playlists); + ApiClient.getUsers().then(function (users) { + USERS = users; + const selection = document.querySelector('#SmartplaylistSelection'); + fillPlaylistSelect(PLAYLISTS); + console.log('selectedIndex =', selection.selectedIndex); + console.log('selectedIndex =', PLAYLISTS[selection.selectedIndex]); + fillForm(PLAYLISTS[selection.selectedIndex], USERS); + Dashboard.hideLoadingMsg(); + }); + }); + }); + +document.querySelector('#SmartplaylistSelection') + .addEventListener('change', function() { + const selection = document.querySelector('#SmartplaylistSelection'); + console.log('p =', PLAYLISTS[selection.selectedIndex]); + fillForm(PLAYLISTS[selection.selectedIndex], USERS); + }); + +document.querySelector('#SmartPlaylistConfigForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + const selection = document.querySelector('#SmartplaylistSelection'); + const selectedid = PLAYLISTS[selection.selectedIndex].Id; + ApiClient.setSmartPlaylist(jsonFromForm()).then(function () { + ApiClient.getSmartPlaylists().then(function (playlists) { + PLAYLISTS = [PLAYLISTS[0]].concat(playlists); + ApiClient.getUsers().then(function (users) { + USERS = users; + fillPlaylistSelect(PLAYLISTS); + const idx = PLAYLISTS.map(x => x.Id).indexOf(selectedid); + selection.selectedIndex = idx; + fillForm(PLAYLISTS[selection.selectedIndex], USERS); + Dashboard.hideLoadingMsg(); + }); + }); + }); + e.preventDefault(); + }) + diff --git a/Jellyfin.Plugin.SmartPlaylist/Plugin.cs b/Jellyfin.Plugin.SmartPlaylist/Plugin.cs index 72d7763..12046e3 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Plugin.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Plugin.cs @@ -27,8 +27,30 @@ namespace Jellyfin.Plugin.SmartPlaylist { return new[] { new PluginPageInfo { Name = this.Name, - EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.configPage.html", GetType().Namespace) - } + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace), + }, + new PluginPageInfo { + Name = "configPage.js", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.js", GetType().Namespace), + }, + new PluginPageInfo { + Name = "Smart Playlists", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartPlaylists.html", GetType().Namespace), + EnableInMainMenu = true, + }, + new PluginPageInfo { + Name = "smartPlaylists.js", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartPlaylists.js", GetType().Namespace), + }, + new PluginPageInfo { + Name = "Smart Collections", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartCollections.html", GetType().Namespace), + //EnableInMainMenu = true, + }, + new PluginPageInfo { + Name = "smartCollections.js", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.smartCollections.js", GetType().Namespace), + }, }; } } diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GenerateCollection.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GenerateCollection.cs index 480de70..fb672a5 100644 --- a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GenerateCollection.cs +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GenerateCollection.cs @@ -1,4 +1,3 @@ -using System.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; using MediaBrowser.Controller; @@ -6,12 +5,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Model.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Model.Collections; namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs index ed79e98..6029ac9 100644 --- a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs @@ -7,15 +7,12 @@ using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Playlists; using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; -using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object; -using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean; namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { @@ -78,30 +75,30 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { 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)); + executor.environment.Set("*user*", Lisp.Object.FromBase(user)); 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); _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); results.Add(i); } } executor = SetupExecutor(); - executor.environment.Set("*user*", Lisp_Object.FromBase(user)); - executor.environment.Set("*items*", Lisp_Object.FromBase(results)); + executor.environment.Set("*user*", Lisp.Object.FromBase(user)); + executor.environment.Set("*items*", Lisp.Object.FromBase(results)); results = new List(); 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) { + 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 if (sort_result == Lisp.Boolean.FALSE) { } else { throw new ApplicationException($"Did not return a list of items, returned {sort_result}"); } diff --git a/Jellyfin.Plugin.SmartPlaylist/configPage.html b/Jellyfin.Plugin.SmartPlaylist/configPage.html deleted file mode 100644 index cd597d2..0000000 --- a/Jellyfin.Plugin.SmartPlaylist/configPage.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - SmartPlaylist - - -
-
-
-
-
- -
A program which can set up the environment
- -
-
- - -
-
- - -
-
- -
A program which should return t or nil to include or exclude the provided item.
- -
-
- -
A program which should return a list of items to include in the playlist, sorted however you like.
- -
-
- -
Which users should get access to the playlist.
- -
-
- -
Is the playlist enabled.
- -
-
- -
-
-
-
- - -
- - diff --git a/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj index a35388f..85efc5b 100644 --- a/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj +++ b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj @@ -15,8 +15,18 @@ - - + + + + + + + + + + + +