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; using Jellyfin.Data.Entities; using Jellyfin.Plugin.SmartPlaylist.Lisp; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; 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 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 GetDefaultTriggers() { return new[] { new TaskTriggerInfo { IntervalTicks = TimeSpan.FromHours(24).Ticks, Type = TaskTriggerInfo.TriggerInterval, } }; } private async Task CreateNewCollection(string name) { _logger.LogDebug("Creating collection '{0}'", name); return (await _collectionManager.CreateCollectionAsync( new CollectionCreationOptions { Name = name, } )).Id; } public IEnumerable FilterCollectionItems(IEnumerable items, User? user, string name, string program, string sortProgram) { List results = new List(); 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(); 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); } private IEnumerable GetAllMedia() { var req = new InternalItemsQuery() { IncludeItemTypes = AvailableFilterItems, Recursive = true, }; return _libraryManager.GetItemsResult(req).Items; } public async Task ExecuteAsync(IProgress 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); } } }