commit
614c0a0cb1
20 changed files with 896 additions and 295 deletions
|
@ -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<SmartCollectionController> 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<ActionResult<IReadOnlyList<SmartCollectionDto>>> GetCollections() {
|
||||||
|
return Ok(await _store.GetAllSmartCollectionsAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||||
|
public async Task<ActionResult> SetCollection([FromBody, Required] SmartCollectionDto smartCollection) {
|
||||||
|
await _store.SaveSmartCollectionAsync(smartCollection);
|
||||||
|
return Created();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
Jellyfin.Plugin.SmartPlaylist/Api/SmartPlaylistController.cs
Normal file
61
Jellyfin.Plugin.SmartPlaylist/Api/SmartPlaylistController.cs
Normal file
|
@ -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<SmartPlaylistController> 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<ActionResult<IReadOnlyList<SmartPlaylistDto>>> GetPlaylists() {
|
||||||
|
return Ok(await _store.GetAllSmartPlaylistsAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||||
|
public async Task<ActionResult> SetPlaylist([FromBody, Required] SmartPlaylistDto smartPlaylist) {
|
||||||
|
await _store.SaveSmartPlaylistAsync(smartPlaylist);
|
||||||
|
return Created();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
@ -43,30 +42,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
(define find-parent (lambda (typename) (invoke-generic *item* "FindParent" nil (list typename))))
|
(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"))))
|
(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 { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
32
Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.html
Normal file
32
Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SmartPlaylist</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox" data-controller="__plugin/configPage.js">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<form id="SmartPlaylistConfigForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartplaylist-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.js
Normal file
23
Jellyfin.Plugin.SmartPlaylist/Configuration/configPage.js
Normal file
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
@ -279,7 +279,7 @@ 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()} ({o.Value().GetType()}) has no method {s.Value()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.FromBase(mi.Invoke(o.Value(), l_));
|
return Object.FromBase(mi.Invoke(o.Value(), l_));
|
||||||
|
|
51
Jellyfin.Plugin.SmartPlaylist/Pages/smartCollections.html
Normal file
51
Jellyfin.Plugin.SmartPlaylist/Pages/smartCollections.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SmartCollection</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="SmartCollectionConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox" data-controller="__plugin/smartCollections.js">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<form id="SmartCollectionConfigForm">
|
||||||
|
<div>
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="SmartcollectionSelection">Choose a collection to edit</label>
|
||||||
|
<select id="SmartcollectionSelection" class="emby-select">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="SmartcollectionEditName">Name</label>
|
||||||
|
<input id="SmartcollectionEditName" type="text" class="emby-input"/>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="SmartcollectionEditProgram">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="SmartcollectionEditProgram" class="emby-input smartcollection-monospace" name="Program" rows="16" cols="120"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="SmartcollectionEditSortProgram">Sort Program</label>
|
||||||
|
<div class="fieldDescription">A program which should return a list of items to include in the collection, sorted however you like.</div>
|
||||||
|
<textarea id="SmartcollectionEditSortProgram" class="emby-input smartcollection-monospace" name="SortProgram" rows="16" cols="120"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="SmartcollectionEditEnabled">Enabled</label>
|
||||||
|
<div class="fieldDescription">Is the collection enabled.</div>
|
||||||
|
<input id="SmartcollectionEditEnabled" type="checkbox" class="emby-input"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartcollection-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
116
Jellyfin.Plugin.SmartPlaylist/Pages/smartCollections.js
Normal file
116
Jellyfin.Plugin.SmartPlaylist/Pages/smartCollections.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
var COLLECTIONS = [
|
||||||
|
{
|
||||||
|
Id: 'My New Smartcollection',
|
||||||
|
Name: 'My New Smartcollection',
|
||||||
|
Program: 'nil',
|
||||||
|
SortProgram: '(begin *items*)',
|
||||||
|
CollectionId: '00000000000000000000000000000000',
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function fillForm(collection) {
|
||||||
|
const editName = document.querySelector('#SmartcollectionEditName');
|
||||||
|
const editProgram = document.querySelector('#SmartcollectionEditProgram');
|
||||||
|
const editSortProgram = document.querySelector('#SmartcollectionEditSortProgram');
|
||||||
|
const editEnabled = document.querySelector('#SmartcollectionEditEnabled');
|
||||||
|
editName.value = collection.Name;
|
||||||
|
editProgram.value = collection.Program;
|
||||||
|
editSortProgram.value = collection.SortProgram;
|
||||||
|
editEnabled.checked = collection.Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillCollectionSelect(collections) {
|
||||||
|
const selection = document.querySelector('#SmartcollectionSelection');
|
||||||
|
selection.innerHTML = '';
|
||||||
|
var o = document.createElement('option');
|
||||||
|
o.value = null;
|
||||||
|
o.innerHTML = 'Create new collection ...';
|
||||||
|
selection.appendChild(o);
|
||||||
|
for (const i of collections.slice(1)) {
|
||||||
|
var o = document.createElement('option');
|
||||||
|
o.value = i.Id;
|
||||||
|
o.innerHTML = i.Name;
|
||||||
|
selection.appendChild(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonFromForm(collectionId) {
|
||||||
|
const editName = document.querySelector('#SmartcollectionEditName');
|
||||||
|
const editProgram = document.querySelector('#SmartcollectionEditProgram');
|
||||||
|
const editSortProgram = document.querySelector('#SmartcollectionEditSortProgram');
|
||||||
|
const editEnabled = document.querySelector('#SmartcollectionEditEnabled');
|
||||||
|
if (collectionId === null) {
|
||||||
|
collectionId = '00000000000000000000000000000000'
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Id: editName.value,
|
||||||
|
Name: editName.value,
|
||||||
|
Program: editProgram.value,
|
||||||
|
SortProgram: editSortProgram.value,
|
||||||
|
CollectionId: collectionId,
|
||||||
|
Enabled: editEnabled.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiClient.getSmartCollections = function () {
|
||||||
|
const url = ApiClient.getUrl('SmartCollection');
|
||||||
|
return this.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiClient.setSmartCollection = function (c) {
|
||||||
|
const url = ApiClient.getUrl('SmartCollection');
|
||||||
|
return this.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json; charset=UTF-8',
|
||||||
|
data: JSON.stringify(c),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function initial_load(selectedId) {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
ApiClient.getSmartCollections().then(function (collections) {
|
||||||
|
COLLECTIONS = [COLLECTIONS[0]].concat(collections);
|
||||||
|
const selection = document.querySelector('#SmartcollectionSelection');
|
||||||
|
fillCollectionSelect(COLLECTIONS);
|
||||||
|
if (selectedId === null) {
|
||||||
|
selection.selectedIndex = 0;
|
||||||
|
} else {
|
||||||
|
selection.selectedIndex = COLLECTIONS.map(x => x.Id).indexOf(selectedId);
|
||||||
|
}
|
||||||
|
fillForm(COLLECTIONS[selection.selectedIndex]);
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.querySelector('#SmartCollectionConfigPage')
|
||||||
|
.addEventListener('viewshow', function() {
|
||||||
|
initial_load(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#SmartCollectionConfigPage')
|
||||||
|
.addEventListener('pageshow', function() {
|
||||||
|
initial_load(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#SmartcollectionSelection')
|
||||||
|
.addEventListener('change', function() {
|
||||||
|
const selection = document.querySelector('#SmartcollectionSelection');
|
||||||
|
fillForm(COLLECTIONS[selection.selectedIndex]);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#SmartCollectionConfigForm')
|
||||||
|
.addEventListener('submit', function (e) {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
const selection = document.querySelector('#SmartcollectionSelection');
|
||||||
|
const selectedId = COLLECTIONS[selection.selectedIndex].Id;
|
||||||
|
ApiClient.setSmartCollection(jsonFromForm(COLLECTIONS[selection.selectedIndex].CollectionId)).then(function () {
|
||||||
|
initial_load(selectedId);
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
57
Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.html
Normal file
57
Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SmartPlaylist</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox" data-controller="__plugin/smartPlaylists.js">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<form id="SmartPlaylistConfigForm">
|
||||||
|
<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>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.smartplaylist-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
143
Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.js
Normal file
143
Jellyfin.Plugin.SmartPlaylist/Pages/smartPlaylists.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initial_load(selectedId) {
|
||||||
|
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);
|
||||||
|
if (selectedId === null) {
|
||||||
|
selection.selectedIndex = 0;
|
||||||
|
} else {
|
||||||
|
selection.selectedIndex = PLAYLISTS.map(x => x.Id).indexOf(selectedId);
|
||||||
|
}
|
||||||
|
fillForm(PLAYLISTS[selection.selectedIndex], USERS);
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#SmartPlaylistConfigPage')
|
||||||
|
.addEventListener('viewshow', function() {
|
||||||
|
initial_load(null);
|
||||||
|
});
|
||||||
|
document.querySelector('#SmartPlaylistConfigPage')
|
||||||
|
.addEventListener('pageshow', function() {
|
||||||
|
initial_load(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#SmartplaylistSelection')
|
||||||
|
.addEventListener('change', function() {
|
||||||
|
const selection = document.querySelector('#SmartplaylistSelection');
|
||||||
|
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 () {
|
||||||
|
initial_load(selectedId);
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
|
@ -27,8 +27,32 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
return new[] {
|
return new[] {
|
||||||
new PluginPageInfo {
|
new PluginPageInfo {
|
||||||
Name = this.Name,
|
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",
|
||||||
|
DisplayName = "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",
|
||||||
|
DisplayName = "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),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
84
Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/Common.cs
Normal file
84
Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/Common.cs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
|
||||||
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
|
|
||||||
|
public class Common {
|
||||||
|
public static readonly BaseItemKind[] AvailableFilterItems = {
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.Playlist,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public readonly ILogger _logger;
|
||||||
|
public Common(ILogger logger) {
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
public Executor SetupExecutor() {
|
||||||
|
var env = new DefaultEnvironment();
|
||||||
|
var executor = new Executor(env);
|
||||||
|
executor.builtins["logd"] = (x) => {
|
||||||
|
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
||||||
|
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) {
|
||||||
|
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
||||||
|
} else {
|
||||||
|
throw new ApplicationException("Plugin Instance is not yet initialized");
|
||||||
|
}
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
public IEnumerable<Guid> FilterCollectionItems(IEnumerable<BaseItem> items, User? user, string name, string program, string sortProgram) {
|
||||||
|
List<BaseItem> results = new List<BaseItem>();
|
||||||
|
Expression expression = new Parser(StringTokenStream.generate(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) {
|
||||||
|
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())) {
|
||||||
|
_logger.LogDebug("Added '{0}' to Smart Collection {1}", i, name);
|
||||||
|
results.Add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executor = SetupExecutor();
|
||||||
|
executor.environment.Set("*user*", Lisp.Object.FromBase(user));
|
||||||
|
executor.environment.Set("*items*", Lisp.Object.FromBase(results));
|
||||||
|
results = new List<BaseItem>();
|
||||||
|
var sort_result = executor.eval(sortProgram);
|
||||||
|
if (sort_result is Cons sorted_items) {
|
||||||
|
foreach (var i in sorted_items.ToList()) {
|
||||||
|
if (i is Lisp.Object iObject && iObject.Value() is BaseItem iBaseItem) {
|
||||||
|
results.Add(iBaseItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
|
||||||
|
}
|
||||||
|
} else if (sort_result == Lisp.Boolean.FALSE) {
|
||||||
|
} else {
|
||||||
|
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
||||||
|
}
|
||||||
|
return results.Select(x => x.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
using MediaBrowser.Model.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Collections;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
|
public class GenerateCollection: Common, IScheduledTask {
|
||||||
|
|
||||||
|
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 GenerateCollection(
|
||||||
|
ILogger<Plugin> logger,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ICollectionManager collectionManager,
|
||||||
|
IServerApplicationPaths serverApplicationPaths
|
||||||
|
) : base(logger) {
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_collectionManager = collectionManager;
|
||||||
|
|
||||||
|
_store = new Store(new SmartFileSystem(serverApplicationPaths));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Category => "Library";
|
||||||
|
public string Name => "(re)generate Smart Collections";
|
||||||
|
public string Description => "Generate or regenerate all Smart Collections";
|
||||||
|
public string Key => nameof(GenerateCollection);
|
||||||
|
|
||||||
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
|
||||||
|
return new[] {
|
||||||
|
new TaskTriggerInfo {
|
||||||
|
IntervalTicks = TimeSpan.FromHours(24).Ticks,
|
||||||
|
Type = TaskTriggerInfo.TriggerInterval,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CollectionId> CreateNewCollection(string name) {
|
||||||
|
_logger.LogDebug("Creating collection '{0}'", name);
|
||||||
|
return (await _collectionManager.CreateCollectionAsync(
|
||||||
|
new CollectionCreationOptions {
|
||||||
|
Name = name,
|
||||||
|
}
|
||||||
|
)).Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<BaseItem> GetAllMedia() {
|
||||||
|
var req = new InternalItemsQuery() {
|
||||||
|
IncludeItemTypes = AvailableFilterItems,
|
||||||
|
Recursive = true,
|
||||||
|
};
|
||||||
|
return _libraryManager.GetItemsResult(req).Items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
||||||
|
_logger.LogInformation("Started regenerate Smart Collections");
|
||||||
|
_logger.LogDebug("Loaded Assemblies:");
|
||||||
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
|
||||||
|
_logger.LogDebug("- {0}", asm);
|
||||||
|
}
|
||||||
|
var i = 0;
|
||||||
|
var smartCollections = await _store.GetAllSmartCollectionsAsync();
|
||||||
|
foreach (SmartCollectionDto dto in smartCollections) {
|
||||||
|
if (!dto.Enabled) {
|
||||||
|
progress.Report(100 * ((double)i)/smartCollections.Count());
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dto.CollectionId == Guid.Empty) {
|
||||||
|
dto.CollectionId = await CreateNewCollection(dto.Name);
|
||||||
|
_store.DeleteSmartCollection(dto); // delete in case the file was not the canonical one.
|
||||||
|
await _store.SaveSmartCollectionAsync(dto);
|
||||||
|
}
|
||||||
|
var insertItems = FilterCollectionItems(GetAllMedia(), null, dto.Name, dto.Program, dto.SortProgram).ToArray();
|
||||||
|
await ClearCollection(dto.CollectionId);
|
||||||
|
await _collectionManager.AddToCollectionAsync(dto.CollectionId, insertItems);
|
||||||
|
i += 1;
|
||||||
|
progress.Report(100 * ((double)i)/smartCollections.Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearCollection(CollectionId collectionId) {
|
||||||
|
// fuck if I know
|
||||||
|
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) {
|
||||||
|
throw new ArgumentException("");
|
||||||
|
}
|
||||||
|
var existingItems = collection.LinkedChildren.Select(x => x.ItemId).Where(x => x != null).Select(x => x.Value);
|
||||||
|
await _collectionManager.RemoveFromCollectionAsync(collectionId, existingItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,27 +7,17 @@ using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Playlists;
|
using MediaBrowser.Model.Playlists;
|
||||||
|
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
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 {
|
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
public class GeneratePlaylist : IScheduledTask {
|
public class GeneratePlaylist : Common, IScheduledTask {
|
||||||
|
|
||||||
public static readonly BaseItemKind[] AvailableFilterItems = {
|
|
||||||
BaseItemKind.Audio,
|
|
||||||
BaseItemKind.MusicAlbum,
|
|
||||||
BaseItemKind.Playlist,
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
|
@ -44,15 +34,14 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IPlaylistManager playlistManager,
|
IPlaylistManager playlistManager,
|
||||||
IServerApplicationPaths serverApplicationPaths
|
IServerApplicationPaths serverApplicationPaths
|
||||||
) {
|
) : base(logger) {
|
||||||
_logger = logger;
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_playlistManager = playlistManager;
|
_playlistManager = playlistManager;
|
||||||
|
|
||||||
_store = new Store(new SmartPlaylistFileSystem(serverApplicationPaths));
|
_store = new Store(new SmartFileSystem(serverApplicationPaths));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Category => "Library";
|
public string Category => "Library";
|
||||||
|
@ -81,62 +70,35 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
return playlistGuid;
|
return playlistGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Executor SetupExecutor() {
|
|
||||||
var env = new DefaultEnvironment();
|
|
||||||
var executor = new Executor(env);
|
|
||||||
executor.builtins["logd"] = (x) => {
|
|
||||||
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
|
||||||
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) {
|
|
||||||
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
|
||||||
} else {
|
|
||||||
throw new ApplicationException("Plugin Instance is not yet initialized");
|
|
||||||
}
|
|
||||||
return executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
||||||
List<BaseItem> results = new List<BaseItem>();
|
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
|
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 executor = SetupExecutor();
|
||||||
|
|
||||||
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
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())) {
|
||||||
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
||||||
results.Add(i);
|
results.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
executor = SetupExecutor();
|
executor = SetupExecutor();
|
||||||
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) {
|
||||||
foreach (var i in sorted_items.ToList()) {
|
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);
|
results.Add(iBaseItem);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
|
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 {
|
} else {
|
||||||
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
||||||
}
|
}
|
||||||
|
@ -162,6 +124,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
foreach (SmartPlaylistDto dto in all_playlists) {
|
foreach (SmartPlaylistDto dto in all_playlists) {
|
||||||
if (!dto.Enabled) {
|
if (!dto.Enabled) {
|
||||||
i += 1;
|
i += 1;
|
||||||
|
progress.Report(100 * ((double)i)/all_playlists.Count());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var changedDto = false;
|
var changedDto = false;
|
||||||
|
|
|
@ -108,4 +108,15 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
info.AddValue("Enabled", Enabled);
|
info.AddValue("Enabled", Enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SmartCollectionDto {
|
||||||
|
public SmartCollectionId Id { get; set; }
|
||||||
|
public CollectionId CollectionId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Program { get; set; }
|
||||||
|
public string SortProgram { get; set; }
|
||||||
|
public string? Filename { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,34 +3,61 @@ using MediaBrowser.Controller;
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
|
|
||||||
public interface ISmartPlaylistFileSystem {
|
public interface ISmartPlaylistFileSystem {
|
||||||
public string StoragePath { get; }
|
public string PlaylistStoragePath { get; }
|
||||||
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId);
|
public string CollectionStoragePath { get; }
|
||||||
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId);
|
|
||||||
|
public string GetSmartPlaylistFilePath(string id);
|
||||||
|
public string FindSmartPlaylistFilePath(string id);
|
||||||
public string[] FindAllSmartPlaylistFilePaths();
|
public string[] FindAllSmartPlaylistFilePaths();
|
||||||
|
|
||||||
|
public string GetSmartCollectionFilePath(string id);
|
||||||
|
public string FindSmartCollectionFilePath(string id);
|
||||||
|
public string[] FindAllSmartCollectionFilePaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SmartPlaylistFileSystem : ISmartPlaylistFileSystem {
|
public class SmartFileSystem : ISmartPlaylistFileSystem {
|
||||||
public SmartPlaylistFileSystem(IServerApplicationPaths serverApplicationPaths) {
|
public SmartFileSystem(IServerApplicationPaths serverApplicationPaths) {
|
||||||
StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists");
|
PlaylistStoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists");
|
||||||
if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); }
|
CollectionStoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartcollections");
|
||||||
|
if (!Directory.Exists(PlaylistStoragePath)) { Directory.CreateDirectory(PlaylistStoragePath); }
|
||||||
|
if (!Directory.Exists(CollectionStoragePath)) { Directory.CreateDirectory(CollectionStoragePath); }
|
||||||
}
|
}
|
||||||
public string StoragePath { get; }
|
public string PlaylistStoragePath { get; }
|
||||||
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
public string CollectionStoragePath { get; }
|
||||||
return Path.Combine(StoragePath, $"{smartPlaylistId}.yaml");
|
public string GetSmartPlaylistFilePath(string id) {
|
||||||
|
return Path.Combine(PlaylistStoragePath, $"{id}.yaml");
|
||||||
}
|
}
|
||||||
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
|
public string FindSmartPlaylistFilePath(string id) {
|
||||||
return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yaml", SearchOption.AllDirectories).Concat(
|
return Directory.GetFiles(PlaylistStoragePath, $"{id}.yaml", SearchOption.AllDirectories).Concat(
|
||||||
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.yml", SearchOption.AllDirectories)
|
Directory.GetFiles(PlaylistStoragePath, $"{id}.yml", SearchOption.AllDirectories)
|
||||||
).Concat(
|
).Concat(
|
||||||
Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories)
|
Directory.GetFiles(PlaylistStoragePath, $"{id}.json", SearchOption.AllDirectories)
|
||||||
).First();
|
).First();
|
||||||
}
|
}
|
||||||
public string[] FindAllSmartPlaylistFilePaths() {
|
public string[] FindAllSmartPlaylistFilePaths() {
|
||||||
return Directory.GetFiles(StoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
|
return Directory.GetFiles(PlaylistStoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
|
||||||
Directory.GetFiles(StoragePath, "*.yml", SearchOption.AllDirectories)
|
Directory.GetFiles(PlaylistStoragePath, "*.yml", SearchOption.AllDirectories)
|
||||||
).Concat(
|
).Concat(
|
||||||
Directory.GetFiles(StoragePath, "*.json", SearchOption.AllDirectories)
|
Directory.GetFiles(PlaylistStoragePath, "*.json", SearchOption.AllDirectories)
|
||||||
).ToArray();
|
).ToArray();
|
||||||
}
|
}
|
||||||
|
public string GetSmartCollectionFilePath(string id) {
|
||||||
|
return Path.Combine(CollectionStoragePath, $"{id}.yaml");
|
||||||
|
}
|
||||||
|
public string FindSmartCollectionFilePath(string id) {
|
||||||
|
return Directory.GetFiles(CollectionStoragePath, $"{id}.yaml", SearchOption.AllDirectories).Concat(
|
||||||
|
Directory.GetFiles(CollectionStoragePath, $"{id}.yml", SearchOption.AllDirectories)
|
||||||
|
).Concat(
|
||||||
|
Directory.GetFiles(CollectionStoragePath, $"{id}.json", SearchOption.AllDirectories)
|
||||||
|
).First();
|
||||||
|
}
|
||||||
|
public string[] FindAllSmartCollectionFilePaths() {
|
||||||
|
return Directory.GetFiles(CollectionStoragePath, "*.yaml", SearchOption.AllDirectories).Concat(
|
||||||
|
Directory.GetFiles(CollectionStoragePath, "*.yml", SearchOption.AllDirectories)
|
||||||
|
).Concat(
|
||||||
|
Directory.GetFiles(CollectionStoragePath, "*.json", SearchOption.AllDirectories)
|
||||||
|
).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,12 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
||||||
void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId);
|
void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId);
|
||||||
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
||||||
|
|
||||||
|
Task<SmartCollectionDto> GetSmartCollectionAsync(SmartCollectionId smartCollectionId);
|
||||||
|
Task<SmartCollectionDto[]> GetAllSmartCollectionsAsync();
|
||||||
|
Task SaveSmartCollectionAsync(SmartCollectionDto smartCollection);
|
||||||
|
void DeleteSmartCollectionById(SmartCollectionId smartCollectionId);
|
||||||
|
void DeleteSmartCollection(SmartCollectionDto smartCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Store : IStore {
|
public class Store : IStore {
|
||||||
|
@ -59,5 +65,51 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
if (File.Exists(smartPlaylist.Filename)) { File.Delete(smartPlaylist.Filename); }
|
if (File.Exists(smartPlaylist.Filename)) { File.Delete(smartPlaylist.Filename); }
|
||||||
DeleteSmartPlaylistById(smartPlaylist.Id);
|
DeleteSmartPlaylistById(smartPlaylist.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<SmartCollectionDto> LoadCollectionAsync(string filename) {
|
||||||
|
var r = await File.ReadAllTextAsync(filename);
|
||||||
|
if (r.Equals("")) {
|
||||||
|
r = "{}";
|
||||||
|
}
|
||||||
|
var dto = new DeserializerBuilder().Build().Deserialize<SmartCollectionDto>(r);
|
||||||
|
if (dto == null)
|
||||||
|
{
|
||||||
|
throw new ApplicationException("");
|
||||||
|
}
|
||||||
|
if (dto.Id != Path.GetFileNameWithoutExtension(filename)) {
|
||||||
|
dto.Id = Path.GetFileNameWithoutExtension(filename);
|
||||||
|
}
|
||||||
|
if (dto.Name != Path.GetFileNameWithoutExtension(filename)) {
|
||||||
|
dto.Name = Path.GetFileNameWithoutExtension(filename);
|
||||||
|
}
|
||||||
|
if (dto.Filename != filename) {
|
||||||
|
dto.Filename = filename;
|
||||||
|
}
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
public async Task<SmartCollectionDto> GetSmartCollectionAsync(SmartCollectionId smartCollectionId) {
|
||||||
|
string filename = _fileSystem.FindSmartCollectionFilePath(smartCollectionId);
|
||||||
|
return await LoadCollectionAsync(filename).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task<SmartCollectionDto[]> GetAllSmartCollectionsAsync() {
|
||||||
|
var t = _fileSystem.FindAllSmartCollectionFilePaths().Select(LoadCollectionAsync).ToArray();
|
||||||
|
await Task.WhenAll(t).ConfigureAwait(false);
|
||||||
|
return t.Where(x => x != null).Select(x => x.Result).ToArray();
|
||||||
|
}
|
||||||
|
public async Task SaveSmartCollectionAsync(SmartCollectionDto smartCollection) {
|
||||||
|
string filename = _fileSystem.GetSmartCollectionFilePath(smartCollection.Id);
|
||||||
|
var text = new SerializerBuilder().Build().Serialize(smartCollection);
|
||||||
|
await File.WriteAllTextAsync(filename, text);
|
||||||
|
}
|
||||||
|
public void DeleteSmartCollectionById(SmartCollectionId smartCollectionId) {
|
||||||
|
try {
|
||||||
|
string filename = _fileSystem.FindSmartCollectionFilePath(smartCollectionId);
|
||||||
|
if (File.Exists(filename)) { File.Delete(filename); }
|
||||||
|
} catch (System.InvalidOperationException) {}
|
||||||
|
}
|
||||||
|
public void DeleteSmartCollection(SmartCollectionDto smartCollection) {
|
||||||
|
if (File.Exists(smartCollection.Filename)) { File.Delete(smartCollection.Filename); }
|
||||||
|
DeleteSmartCollectionById(smartCollection.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,6 @@ global using System;
|
||||||
|
|
||||||
global using UserId = System.Guid;
|
global using UserId = System.Guid;
|
||||||
global using PlaylistId = System.Guid;
|
global using PlaylistId = System.Guid;
|
||||||
|
global using CollectionId = System.Guid;
|
||||||
global using SmartPlaylistId = string;
|
global using SmartPlaylistId = string;
|
||||||
|
global using SmartCollectionId = string;
|
||||||
|
|
|
@ -1,201 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>SmartPlaylist</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="SmartPlaylistConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
|
||||||
<div data-role="content">
|
|
||||||
<div class="content-primary">
|
|
||||||
<form id="SmartPlaylistConfigForm">
|
|
||||||
<div class="inputContainer">
|
|
||||||
<label class="inputLabel inputLabelUnfocused" for="InitialProgram">Initial Program</label>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
|
||||||
<span>Save</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
.smartplaylist-monospace {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script type="text/javascript">
|
|
||||||
var SmartPlaylistConfig = {
|
|
||||||
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')
|
|
||||||
.addEventListener('pageshow', function() {
|
|
||||||
Dashboard.showLoadingMsg();
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#SmartPlaylistConfigForm')
|
|
||||||
.addEventListener('submit', function(e) {
|
|
||||||
Dashboard.showLoadingMsg();
|
|
||||||
ApiClient.getPluginConfiguration(SmartPlaylistConfig.pluginUniqueId).then(function (config) {
|
|
||||||
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) {
|
|
||||||
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();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -15,8 +15,18 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="configPage.html"/>
|
<None Remove="Configuration\configPage.html"/>
|
||||||
<EmbeddedResource Include="configPage.html"/>
|
<None Remove="Configuration\configPage.js"/>
|
||||||
|
<None Remove="Pages\smartPlaylists.html"/>
|
||||||
|
<None Remove="Pages\smartPlaylists.js"/>
|
||||||
|
<None Remove="Pages\smartCollections.html"/>
|
||||||
|
<None Remove="Pages\smartCollections.js"/>
|
||||||
|
<EmbeddedResource Include="Configuration\configPage.html"/>
|
||||||
|
<EmbeddedResource Include="Configuration\configPage.js"/>
|
||||||
|
<EmbeddedResource Include="Pages\smartPlaylists.html"/>
|
||||||
|
<EmbeddedResource Include="Pages\smartPlaylists.js"/>
|
||||||
|
<EmbeddedResource Include="Pages\smartCollections.html"/>
|
||||||
|
<EmbeddedResource Include="Pages\smartCollections.js"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Add table
Reference in a new issue