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 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 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 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 FilterPlaylistItems(IEnumerable items, User user, SmartPlaylistDto smartPlaylist) { List results = new List(); 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)); 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 = SetupExecutor(); 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) { 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 GetAllUserMedia(User user) { var req = new InternalItemsQuery(user) { IncludeItemTypes = AvailableFilterItems, Recursive = true, }; return _libraryManager.GetItemsResult(req).Items; } public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { _logger.LogInformation("Started regenerate Smart Playlists"); _logger.LogDebug("Loaded Assemblies:"); foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { _logger.LogDebug("- {0}", asm); } var i = 0; foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) { if (!dto.Enabled) { i += 1; continue; } 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); } 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))); } } }