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 System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object; using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean; namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { public class GeneratePlaylist : IScheduledTask { public static readonly BaseItemKind[] AvailableFilterItems = { BaseItemKind.Audio, 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 SmartPlaylistId CreateNewPlaylist(string name, UserId userId) { var req = new PlaylistCreationRequest { Name = name, UserId = userId, }; var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id); return playlistGuid; } private IEnumerable FilterPlaylistItems(IEnumerable items, User user, SmartPlaylistDto smartPlaylist) { List results = new List(); Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); Executor executor = new Executor(); executor.environment["user"] = new Lisp_Object(user); foreach (var i in items) { executor.environment["item"] = new Lisp_Object(i); var r = executor.eval(expression); _logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString()); if (r is Lisp_Boolean r_bool) { if (r_bool.value) { _logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name); results.Add(i.Id); } } else { _logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString()); } } return results; } private IEnumerable GetAllUserMedia(User user) { var req = new InternalItemsQuery(user) { IncludeItemTypes = AvailableFilterItems, Recursive = true, }; return _libraryManager.GetItemsResult(req).Items; } public async Task ExecuteAsync(IProgress 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); } 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); } } } 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.Id)); } } }