redxef
f479c93c5c
Works similar to Program, but receives the list of matched items and should return the same list but sorted. This can actually also be used for further filtering.
196 lines
9 KiB
C#
196 lines
9 KiB
C#
using System.Globalization;
|
|
using MediaBrowser.Model.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using MediaBrowser.Controller;
|
|
using MediaBrowser.Controller.Library;
|
|
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 {
|
|
public class GeneratePlaylist : IScheduledTask {
|
|
|
|
public static readonly BaseItemKind[] AvailableFilterItems = {
|
|
BaseItemKind.Audio,
|
|
BaseItemKind.MusicAlbum,
|
|
BaseItemKind.Playlist,
|
|
};
|
|
|
|
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 GeneratePlaylist(
|
|
ILogger<Plugin> 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 SmartPlaylistFileSystem(serverApplicationPaths));
|
|
}
|
|
|
|
public string Category => "Library";
|
|
public string Name => "(re)generate Smart Playlists";
|
|
public string Description => "Generate or regenerate all Smart Playlists";
|
|
public string Key => nameof(GeneratePlaylist);
|
|
|
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
|
|
return new[] {
|
|
new TaskTriggerInfo {
|
|
IntervalTicks = TimeSpan.FromHours(24).Ticks,
|
|
Type = TaskTriggerInfo.TriggerInterval,
|
|
}
|
|
};
|
|
}
|
|
|
|
private void GetUsers() {
|
|
foreach (var user in _userManager.Users) {
|
|
_logger.LogInformation("User {0}", user);
|
|
var query = new InternalItemsQuery(user) {
|
|
IncludeItemTypes = AvailableFilterItems,
|
|
Recursive = true,
|
|
};
|
|
foreach (BaseItem item in _libraryManager.GetItemsResult(query).Items) {
|
|
_logger.LogInformation("Item {0}", item);
|
|
}
|
|
}
|
|
}
|
|
|
|
private PlaylistId CreateNewPlaylist(string name, UserId userId) {
|
|
_logger.LogDebug("Creating playlist '{0}'", name);
|
|
var req = new PlaylistCreationRequest {
|
|
Name = name,
|
|
UserId = userId,
|
|
Users = [new PlaylistUserPermissions(userId)],
|
|
Public = false,
|
|
};
|
|
var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
|
|
return playlistGuid;
|
|
}
|
|
|
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
|
List<BaseItem> results = new List<BaseItem>();
|
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
|
Executor executor = new Executor(new DefaultEnvironment());
|
|
executor.environment.Set("user", Lisp_Object.FromBase(user));
|
|
if (Plugin.Instance is not null) {
|
|
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
|
} else {
|
|
throw new ApplicationException("Plugin Instance is not yet initialized");
|
|
}
|
|
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 Playlist {1}", i, smartPlaylist.Name);
|
|
results.Add(i);
|
|
}
|
|
}
|
|
executor = new Executor(new DefaultEnvironment());
|
|
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(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) {
|
|
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);
|
|
}
|
|
|
|
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
|
|
var req = new InternalItemsQuery(user) {
|
|
IncludeItemTypes = AvailableFilterItems,
|
|
Recursive = true,
|
|
};
|
|
return _libraryManager.GetItemsResult(req).Items;
|
|
}
|
|
|
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
|
_logger.LogInformation("Started regenerate Smart Playlists");
|
|
foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) {
|
|
var changedDto = false;
|
|
if (dto.Playlists.Length == 0) {
|
|
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
|
UserId = x,
|
|
PlaylistId = CreateNewPlaylist(dto.Name, x),
|
|
}).ToArray();
|
|
changedDto = true;
|
|
}
|
|
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
|
if (playlistLink.PlaylistId == Guid.Empty) {
|
|
// not initialized
|
|
playlistLink.PlaylistId = CreateNewPlaylist(dto.Name, playlistLink.UserId);
|
|
changedDto = true;
|
|
} else if (_playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).ToArray().Length == 0) {
|
|
// somehow the corresponding playlist doesnt
|
|
// exist anymore, did the user delete it?
|
|
playlistLink.PlaylistId = CreateNewPlaylist(dto.Name, playlistLink.UserId);
|
|
changedDto = true;
|
|
}
|
|
}
|
|
if (changedDto) {
|
|
_store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one.
|
|
await _store.SaveSmartPlaylistAsync(dto);
|
|
}
|
|
var i = 0;
|
|
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
|
User? user = _userManager.GetUserById(playlistLink.UserId);
|
|
if (user == null) {
|
|
continue;
|
|
}
|
|
var insertItems = FilterPlaylistItems(GetAllUserMedia(user), user, dto).ToArray();
|
|
var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First();
|
|
await ClearPlaylist(playlist);
|
|
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
|
i += 1;
|
|
progress.Report(100 * ((double)i)/dto.Playlists.Count());
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ClearPlaylist(Playlist playlist) {
|
|
// fuck if I know
|
|
if (_libraryManager.GetItemById(playlist.Id) is not Playlist playlist_new) {
|
|
throw new ArgumentException("");
|
|
}
|
|
var existingItems = playlist_new.GetManageableItems().ToList();
|
|
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
|
|
}
|
|
}
|
|
}
|