diff --git a/Kyoo.Common/Controllers/IIdentifier.cs b/Kyoo.Common/Controllers/IIdentifier.cs index 5033bb77..11aeb65d 100644 --- a/Kyoo.Common/Controllers/IIdentifier.cs +++ b/Kyoo.Common/Controllers/IIdentifier.cs @@ -18,7 +18,7 @@ namespace Kyoo.Controllers /// /// The path of the episode file relative to the library root. It starts with a /. /// - /// The identifier could not work for the given path. + /// The identifier could not work for the given path. /// /// A tuple of models representing parsed metadata. /// If no metadata could be parsed for a type, null can be returned. @@ -34,7 +34,7 @@ namespace Kyoo.Controllers /// /// The path of the episode file relative to the library root. It starts with a /. /// - /// The identifier could not work for the given path. + /// The identifier could not work for the given path. /// /// The metadata of the track identified. /// diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs index 1896bbcd..e5fbce29 100644 --- a/Kyoo.Common/Controllers/ITask.cs +++ b/Kyoo.Common/Controllers/ITask.cs @@ -114,6 +114,8 @@ namespace Kyoo.Controllers /// The value of this parameter. public T As() { + if (typeof(T) == typeof(object)) + return (T)Value; return (T)Convert.ChangeType(Value, typeof(T)); } } @@ -150,42 +152,6 @@ namespace Kyoo.Controllers /// public interface ITask { - /// - /// The slug of the task, used to start it. - /// - public string Slug { get; } - - /// - /// The name of the task that will be displayed to the user. - /// - public string Name { get; } - - /// - /// A quick description of what this task will do. - /// - public string Description { get; } - - /// - /// An optional message to display to help the user. - /// - public string HelpMessage { get; } - - /// - /// Should this task be automatically run at app startup? - /// - public bool RunOnStartup { get; } - - /// - /// The priority of this task. Only used if is true. - /// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order. - /// - public int Priority { get; } - - /// - /// true if this task should not be displayed to the user, false otherwise. - /// - public bool IsHidden { get; } - /// /// The list of parameters /// diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs index ffcb1c75..37b40f12 100644 --- a/Kyoo.Common/Controllers/ITaskManager.cs +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Exceptions; namespace Kyoo.Controllers @@ -38,7 +39,7 @@ namespace Kyoo.Controllers [NotNull] IProgress progress, Dictionary arguments = null, CancellationToken? cancellationToken = null); - + /// /// Start a new task (or queue it). /// @@ -61,21 +62,21 @@ namespace Kyoo.Controllers /// /// The task could not be found. /// - void StartTask([NotNull] IProgress progress, + void StartTask([NotNull] IProgress progress, Dictionary arguments = null, CancellationToken? cancellationToken = null) - where T : ITask, new(); + where T : ITask; /// /// Get all currently running tasks /// /// A list of currently running tasks. - ICollection GetRunningTasks(); + ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks(); /// /// Get all available tasks /// /// A list of every tasks that this instance know. - ICollection GetAllTasks(); + ICollection GetAllTasks(); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs index b036acdf..d517300f 100644 --- a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -8,8 +8,8 @@ namespace Kyoo.Models.Attributes /// An attribute to inform that the service will be injected automatically by a service provider. /// /// - /// It should only be used on and will be injected before calling . - /// It can also be used on and it will be injected before calling . + /// It should only be used on and it will be injected before + /// calling . /// [AttributeUsage(AttributeTargets.Property)] [MeansImplicitUse(ImplicitUseKindFlags.Assign)] diff --git a/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs b/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs new file mode 100644 index 00000000..0ebe7a34 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using Kyoo.Controllers; + +namespace Kyoo.Common.Models.Attributes +{ + /// + /// An attribute to inform how a works. + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class)] + public class TaskMetadataAttribute : Attribute + { + /// + /// The slug of the task, used to start it. + /// + public string Slug { get; } + + /// + /// The name of the task that will be displayed to the user. + /// + public string Name { get; } + + /// + /// A quick description of what this task will do. + /// + public string Description { get; } + + /// + /// Should this task be automatically run at app startup? + /// + public bool RunOnStartup { get; set; } + + /// + /// The priority of this task. Only used if is true. + /// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order. + /// + public int Priority { get; set; } + + /// + /// true if this task should not be displayed to the user, false otherwise. + /// + public bool IsHidden { get; set; } + + + /// + /// Create a new with the given slug, name and description. + /// + /// The slug of the task, used to start it. + /// The name of the task that will be displayed to the user. + /// A quick description of what this task will do. + public TaskMetadataAttribute(string slug, string name, string description) + { + Slug = slug; + Name = name; + Description = description; + } + + /// + /// Create a new using a dictionary of metadata. + /// + /// + /// The dictionary of metadata. This method expect the dictionary to contain a field + /// per property in this attribute, with the same types as the properties of this attribute. + /// + public TaskMetadataAttribute(IDictionary metadata) + { + Slug = (string)metadata[nameof(Slug)]; + Name = (string)metadata[nameof(Name)]; + Description = (string)metadata[nameof(Description)]; + RunOnStartup = (bool)metadata[nameof(RunOnStartup)]; + Priority = (int)metadata[nameof(Priority)]; + IsHidden = (bool)metadata[nameof(IsHidden)]; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/IdentificationFailed.cs b/Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs similarity index 60% rename from Kyoo.Common/Models/Exceptions/IdentificationFailed.cs rename to Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs index a8838fea..7c820f5a 100644 --- a/Kyoo.Common/Models/Exceptions/IdentificationFailed.cs +++ b/Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs @@ -8,20 +8,20 @@ namespace Kyoo.Models.Exceptions /// An exception raised when an failed. /// [Serializable] - public class IdentificationFailed : Exception + public class IdentificationFailedException : Exception { /// - /// Create a new with a default message. + /// Create a new with a default message. /// - public IdentificationFailed() + public IdentificationFailedException() : base("An identification failed.") {} /// - /// Create a new with a custom message. + /// Create a new with a custom message. /// /// The message to use. - public IdentificationFailed(string message) + public IdentificationFailedException(string message) : base(message) {} @@ -30,7 +30,7 @@ namespace Kyoo.Models.Exceptions /// /// Serialization infos /// The serialization context - protected IdentificationFailed(SerializationInfo info, StreamingContext context) + protected IdentificationFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 124df770..70d62f74 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -75,7 +75,7 @@ namespace Kyoo.Postgresql DatabaseContext context = provider.GetRequiredService(); context.Database.Migrate(); - using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection(); + NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection(); conn.Open(); conn.ReloadTypes(); } diff --git a/Kyoo/Controllers/FileSystems/LocalFileSystem.cs b/Kyoo/Controllers/FileSystems/LocalFileSystem.cs index dc1c4b9d..3239c55f 100644 --- a/Kyoo/Controllers/FileSystems/LocalFileSystem.cs +++ b/Kyoo/Controllers/FileSystems/LocalFileSystem.cs @@ -43,7 +43,7 @@ namespace Kyoo.Controllers } /// - public IActionResult FileResult(string path, bool range = false, string type = null) + public IActionResult FileResult(string path, bool rangeSupport = false, string type = null) { if (path == null) return new NotFoundResult(); @@ -51,7 +51,7 @@ namespace Kyoo.Controllers return new NotFoundResult(); return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path)) { - EnableRangeProcessing = range + EnableRangeProcessing = rangeSupport }; } diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index 230480c6..46185f16 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -158,6 +158,7 @@ namespace Kyoo.Controllers using IServiceScope scope = _provider.CreateScope(); Helper.InjectServices(plugin, x => scope.ServiceProvider.GetRequiredService(x)); plugin.ConfigureAspNet(app); + Helper.InjectServices(plugin, _ => null); } } diff --git a/Kyoo/Controllers/RegexIdentifier.cs b/Kyoo/Controllers/RegexIdentifier.cs index f578aa30..3c09ae4a 100644 --- a/Kyoo/Controllers/RegexIdentifier.cs +++ b/Kyoo/Controllers/RegexIdentifier.cs @@ -36,7 +36,7 @@ namespace Kyoo.Controllers Match match = regex.Match(relativePath); if (!match.Success) - throw new IdentificationFailed($"The episode at {path} does not match the episode's regex."); + throw new IdentificationFailedException($"The episode at {path} does not match the episode's regex."); (Collection collection, Show show, Season season, Episode episode) ret = ( collection: new Collection @@ -90,7 +90,7 @@ namespace Kyoo.Controllers Match match = regex.Match(path); if (!match.Success) - throw new IdentificationFailed($"The subtitle at {path} does not match the subtitle's regex."); + throw new IdentificationFailedException($"The subtitle at {path} does not match the subtitle's regex."); string episodePath = match.Groups["Episode"].Value; return Task.FromResult(new Track diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 2cf7ff1f..3c1393eb 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -114,9 +114,6 @@ namespace Kyoo.Controllers obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); return await ValidateTracks(obj); - // TODO check if this is needed - // obj.Slug = await _database.Entry(obj).Property(x => x.Slug). - // return obj; } /// diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 37391c5d..b0ec916e 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -81,17 +81,28 @@ namespace Kyoo.Controllers } /// - public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); + public override Task Create(LibraryItem obj) + => throw new InvalidOperationException(); + /// - public override Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); + public override Task CreateIfNotExists(LibraryItem obj) + => throw new InvalidOperationException(); + /// - public override Task Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); + public override Task Edit(LibraryItem obj, bool resetOld) + => throw new InvalidOperationException(); + /// - public override Task Delete(int id) => throw new InvalidOperationException(); + public override Task Delete(int id) + => throw new InvalidOperationException(); + /// - public override Task Delete(string slug) => throw new InvalidOperationException(); + public override Task Delete(string slug) + => throw new InvalidOperationException(); + /// - public override Task Delete(LibraryItem obj) => throw new InvalidOperationException(); + public override Task Delete(LibraryItem obj) + => throw new InvalidOperationException(); /// /// Get a basic queryable for a library with the right mapping from shows & collections. diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 65d623dc..54d724f0 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Autofac.Features.Metadata; +using Autofac.Features.OwnedInstances; using JetBrains.Annotations; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Exceptions; using Kyoo.Models.Options; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,9 +23,52 @@ namespace Kyoo.Controllers public class TaskManager : BackgroundService, ITaskManager { /// - /// The service provider used to activate + /// The class representing task under this jurisdiction. /// - private readonly IServiceProvider _provider; + private class ManagedTask + { + /// + /// The metadata for this task (the slug, and other useful information). + /// + public TaskMetadataAttribute Metadata { get; set; } + + /// + /// The function used to create the task object. + /// + public Func> Factory { get; init; } + + /// + /// The next scheduled date for this task + /// + public DateTime ScheduledDate { get; set; } + } + + /// + /// A class representing a task inside the list. + /// + private class QueuedTask + { + /// + /// The task currently queued. + /// + public ManagedTask Task { get; init; } + + /// + /// The progress reporter that this task should use. + /// + public IProgress ProgressReporter { get; init; } + + /// + /// The arguments to give to run the task with. + /// + public Dictionary Arguments { get; init; } + + /// + /// A token informing the task that it should be cancelled or not. + /// + public CancellationToken? CancellationToken { get; init; } + } + /// /// The configuration instance used to get schedule information /// @@ -35,15 +81,15 @@ namespace Kyoo.Controllers /// /// The list of tasks and their next scheduled run. /// - private readonly List<(ITask task, DateTime scheduledDate)> _tasks; + private readonly List _tasks; /// /// The queue of tasks that should be run as soon as possible. /// - private readonly Queue<(ITask, IProgress, Dictionary)> _queuedTasks = new(); + private readonly Queue _queuedTasks = new(); /// /// The currently running task. /// - private ITask _runningTask; + private (TaskMetadataAttribute, ITask)? _runningTask; /// /// The cancellation token used to cancel the running task when the runner should shutdown. /// @@ -53,22 +99,24 @@ namespace Kyoo.Controllers /// /// Create a new . /// - /// The list of tasks to manage - /// The service provider to request services for tasks + /// The list of tasks to manage with their metadata /// The configuration to load schedule information. /// The logger. - public TaskManager(IEnumerable tasks, - IServiceProvider provider, + public TaskManager(IEnumerable>, TaskMetadataAttribute>> tasks, IOptionsMonitor options, ILogger logger) { - _provider = provider; _options = options; _logger = logger; - _tasks = tasks.Select(x => (x, GetNextTaskDate(x.Slug))).ToList(); + _tasks = tasks.Select(x => new ManagedTask + { + Factory = x.Value, + Metadata = x.Metadata, + ScheduledDate = GetNextTaskDate(x.Metadata.Slug) + }).ToList(); if (_tasks.Any()) - _logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.task.Name)); + _logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.Metadata.Name)); else _logger.LogInformation("Task manager initiated without any tasks"); } @@ -98,25 +146,26 @@ namespace Kyoo.Controllers /// A token to stop the runner protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - EnqueueStartupTasks(); + _EnqueueStartupTasks(); while (!cancellationToken.IsCancellationRequested) { if (_queuedTasks.Any()) { - (ITask task, IProgress progress, Dictionary args) = _queuedTasks.Dequeue(); - _runningTask = task; + QueuedTask task = _queuedTasks.Dequeue(); try { - await RunTask(task, progress, args); + await _RunTask(task.Task, task.ProgressReporter, task.Arguments, task.CancellationToken); } catch (TaskFailedException ex) { - _logger.LogWarning("The task \"{Task}\" failed: {Message}", task.Name, ex.Message); + _logger.LogWarning("The task \"{Task}\" failed: {Message}", + task.Task.Metadata.Name, ex.Message); } catch (Exception e) { - _logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name); + _logger.LogError(e, "An unhandled exception occured while running the task {Task}", + task.Task.Metadata.Name); } } else @@ -133,44 +182,54 @@ namespace Kyoo.Controllers /// The task to run /// A progress reporter to know the percentage of completion of the task. /// The arguments to pass to the function + /// An optional cancellation token that will be passed to the task. /// /// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument /// invalid. /// - private async Task RunTask(ITask task, + private async Task _RunTask(ManagedTask task, [NotNull] IProgress progress, - Dictionary arguments) + Dictionary arguments, + CancellationToken? cancellationToken = null) { - _logger.LogInformation("Task starting: {Task}", task.Name); - - ICollection all = task.GetParameters(); - - ICollection invalids = arguments.Keys - .Where(x => all.All(y => x != y.Name)) - .ToArray(); - if (invalids.Any()) + using (_logger.BeginScope("Task: {Task}", task.Metadata.Name)) { - string invalidsStr = string.Join(", ", invalids); - throw new ArgumentException($"{invalidsStr} are invalid arguments for the task {task.Name}"); - } - - TaskParameters args = new(all - .Select(x => - { - object value = arguments - .FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase)) - .Value; - if (value == null && x.IsRequired) - throw new ArgumentException($"The argument {x.Name} is required to run {task.Name}" + - " but it was not specified."); - return x.CreateValue(value ?? x.DefaultValue); - })); + await using Owned taskObj = task.Factory.Invoke(); + ICollection all = taskObj.Value.GetParameters(); - using IServiceScope scope = _provider.CreateScope(); - Helper.InjectServices(task, x => scope.ServiceProvider.GetRequiredService(x)); - await task.Run(args, progress, _taskToken.Token); - Helper.InjectServices(task, _ => null); - _logger.LogInformation("Task finished: {Task}", task.Name); + _runningTask = (task.Metadata, taskObj.Value); + ICollection invalids = arguments.Keys + .Where(x => all.All(y => x != y.Name)) + .ToArray(); + if (invalids.Any()) + { + throw new ArgumentException($"{string.Join(", ", invalids)} are " + + $"invalid arguments for the task {task.Metadata.Name}"); + } + + TaskParameters args = new(all + .Select(x => + { + object value = arguments + .FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase)) + .Value; + if (value == null && x.IsRequired) + throw new ArgumentException($"The argument {x.Name} is required to run " + + $"{task.Metadata.Name} but it was not specified."); + return x.CreateValue(value ?? x.DefaultValue); + })); + + _logger.LogInformation("Task starting: {Task} ({Parameters})", + task.Metadata.Name, args.ToDictionary(x => x.Name, x => x.As())); + + CancellationToken token = cancellationToken != null + ? CancellationTokenSource.CreateLinkedTokenSource(_taskToken.Token, cancellationToken.Value).Token + : _taskToken.Token; + await taskObj.Value.Run(args, progress, token); + + _logger.LogInformation("Task finished: {Task}", task.Metadata.Name); + _runningTask = null; + } } /// @@ -178,8 +237,8 @@ namespace Kyoo.Controllers /// private void QueueScheduledTasks() { - IEnumerable tasksToQueue = _tasks.Where(x => x.scheduledDate <= DateTime.Now) - .Select(x => x.task.Slug); + IEnumerable tasksToQueue = _tasks.Where(x => x.ScheduledDate <= DateTime.Now) + .Select(x => x.Metadata.Slug); foreach (string task in tasksToQueue) { _logger.LogDebug("Queuing task scheduled for running: {Task}", task); @@ -190,13 +249,14 @@ namespace Kyoo.Controllers /// /// Queue startup tasks with respect to the priority rules. /// - private void EnqueueStartupTasks() + private void _EnqueueStartupTasks() { - IEnumerable startupTasks = _tasks.Select(x => x.task) - .Where(x => x.RunOnStartup) - .OrderByDescending(x => x.Priority); - foreach (ITask task in startupTasks) - _queuedTasks.Enqueue((task, new Progress(), new Dictionary())); + IEnumerable startupTasks = _tasks + .Where(x => x.Metadata.RunOnStartup) + .OrderByDescending(x => x.Metadata.Priority) + .Select(x => x.Metadata.Slug); + foreach (string task in startupTasks) + StartTask(task, new Progress(), new Dictionary()); } /// @@ -207,20 +267,29 @@ namespace Kyoo.Controllers { arguments ??= new Dictionary(); - int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); + int index = _tasks.FindIndex(x => x.Metadata.Slug == taskSlug); if (index == -1) throw new ItemNotFoundException($"No task found with the slug {taskSlug}"); - _queuedTasks.Enqueue((_tasks[index].task, progress, arguments)); - _tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug)); + _queuedTasks.Enqueue(new QueuedTask + { + Task = _tasks[index], + ProgressReporter = progress, + Arguments = arguments, + CancellationToken = cancellationToken + }); + _tasks[index].ScheduledDate = GetNextTaskDate(taskSlug); } /// public void StartTask(IProgress progress, Dictionary arguments = null, CancellationToken? cancellationToken = null) - where T : ITask, new() + where T : ITask { - StartTask(new T().Slug, progress, arguments, cancellationToken); + TaskMetadataAttribute metadata = typeof(T).GetCustomAttribute(); + if (metadata == null) + throw new ArgumentException($"No metadata found on the given task (type: {typeof(T).Name})."); + StartTask(metadata.Slug, progress, arguments, cancellationToken); } /// @@ -236,15 +305,17 @@ namespace Kyoo.Controllers } /// - public ICollection GetRunningTasks() + public ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks() { - return new[] {_runningTask}; + return _runningTask == null + ? ArraySegment<(TaskMetadataAttribute, ITask)>.Empty + : new[] { _runningTask.Value }; } /// - public ICollection GetAllTasks() + public ICollection GetAllTasks() { - return _tasks.Select(x => x.task).ToArray(); + return _tasks.Select(x => x.Metadata).ToArray(); } } } \ No newline at end of file diff --git a/Kyoo/Models/FileExtensions.cs b/Kyoo/Models/FileExtensions.cs index d023c039..5b29fe08 100644 --- a/Kyoo/Models/FileExtensions.cs +++ b/Kyoo/Models/FileExtensions.cs @@ -12,8 +12,7 @@ namespace Kyoo.Models.Watch /// /// The list of known video extensions /// - public static readonly ImmutableArray VideoExtensions = new() - { + public static readonly ImmutableArray VideoExtensions = ImmutableArray.Create( ".webm", ".mkv", ".flv", @@ -38,7 +37,7 @@ namespace Kyoo.Models.Watch ".m2v", ".3gp", ".3g2" - }; + ); /// /// Check if a file represent a video file (only by checking the extension of the file) diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 43567bec..191fa27d 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -5,8 +5,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; -using Kyoo.Models.Attributes; using Kyoo.Models.Watch; using Microsoft.Extensions.Logging; @@ -15,46 +15,43 @@ namespace Kyoo.Tasks /// /// A task to add new video files. /// + [TaskMetadata("scan", "Scan libraries", "Scan your libraries and load data for new shows.", RunOnStartup = true)] public class Crawler : ITask { - /// - public string Slug => "scan"; - - /// - public string Name => "Scan libraries"; - - /// - public string Description => "Scan your libraries and load data for new shows."; - - /// - public string HelpMessage => "Reloading all libraries is a long process and may take up to" + - " 24 hours if it is the first scan in a while."; - - /// - public bool RunOnStartup => true; - - /// - public int Priority => 0; - - /// - public bool IsHidden => false; - /// /// The library manager used to get libraries and providers to use. /// - [Injected] public ILibraryManager LibraryManager { private get; set; } + private readonly ILibraryManager _libraryManager; /// /// The file manager used walk inside directories and check they existences. /// - [Injected] public IFileSystem FileSystem { private get; set; } + private readonly IFileSystem _fileSystem; /// /// A task manager used to create sub tasks for each episode to add to the database. /// - [Injected] public ITaskManager TaskManager { private get; set; } + private readonly ITaskManager _taskManager; /// /// The logger used to inform the current status to the console. /// - [Injected] public ILogger Logger { private get; set; } + private readonly ILogger _logger; + + /// + /// Create a new . + /// + /// The library manager to retrieve existing episodes/library/tracks + /// The file system to glob files + /// The task manager used to start . + /// The logger used print messages. + public Crawler(ILibraryManager libraryManager, + IFileSystem fileSystem, + ITaskManager taskManager, + ILogger logger) + { + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _taskManager = taskManager; + _logger = logger; + } /// @@ -71,20 +68,20 @@ namespace Kyoo.Tasks { string argument = arguments["slug"].As(); ICollection libraries = argument == null - ? await LibraryManager.GetAll() - : new [] { await LibraryManager.GetOrDefault(argument)}; + ? await _libraryManager.GetAll() + : new [] { await _libraryManager.GetOrDefault(argument)}; if (argument != null && libraries.First() == null) throw new ArgumentException($"No library found with the name {argument}"); foreach (Library library in libraries) - await LibraryManager.Load(library, x => x.Providers); + await _libraryManager.Load(library, x => x.Providers); progress.Report(0); float percent = 0; - ICollection episodes = await LibraryManager.GetAll(); - ICollection tracks = await LibraryManager.GetAll(); + ICollection episodes = await _libraryManager.GetAll(); + ICollection tracks = await _libraryManager.GetAll(); foreach (Library library in libraries) { IProgress reporter = new Progress(x => @@ -108,10 +105,10 @@ namespace Kyoo.Tasks IProgress progress, CancellationToken cancellationToken) { - Logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths); + _logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths); foreach (string path in library.Paths) { - ICollection files = await FileSystem.ListFiles(path, SearchOption.AllDirectories); + ICollection files = await _fileSystem.ListFiles(path, SearchOption.AllDirectories); if (cancellationToken.IsCancellationRequested) return; @@ -138,7 +135,7 @@ namespace Kyoo.Tasks foreach (string episodePath in paths) { - TaskManager.StartTask(reporter, new Dictionary + _taskManager.StartTask(reporter, new Dictionary { ["path"] = episodePath, ["relativePath"] = episodePath[path.Length..], @@ -162,7 +159,7 @@ namespace Kyoo.Tasks foreach (string trackPath in subtitles) { - TaskManager.StartTask(reporter, new Dictionary + _taskManager.StartTask(reporter, new Dictionary { ["path"] = trackPath, ["relativePath"] = trackPath[path.Length..] diff --git a/Kyoo/Tasks/Housekeeping.cs b/Kyoo/Tasks/Housekeeping.cs index 1d7789aa..b98989f5 100644 --- a/Kyoo/Tasks/Housekeeping.cs +++ b/Kyoo/Tasks/Housekeeping.cs @@ -1,83 +1,43 @@ using System; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; using Kyoo.Models; -using Kyoo.Models.Attributes; using Microsoft.Extensions.Logging; namespace Kyoo.Tasks { + /// + /// A task to remove orphaned episode and series. + /// + [TaskMetadata("housekeeping", "Housekeeping", "Remove orphaned episode and series.", RunOnStartup = true)] public class Housekeeping : ITask { - /// - public string Slug => "housekeeping"; - - /// - public string Name => "Housekeeping"; - - /// - public string Description => "Remove orphaned episode and series."; - - /// - public string HelpMessage => null; - - /// - public bool RunOnStartup => true; - - /// - public int Priority => 0; - - /// - public bool IsHidden => false; - - /// - /// The library manager used to get libraries or remove deleted episodes + /// The library manager used to get libraries or remove deleted episodes. /// - [Injected] public ILibraryManager LibraryManager { private get; set; } + private readonly ILibraryManager _libraryManager; /// /// The file manager used walk inside directories and check they existences. /// - [Injected] public IFileSystem FileSystem { private get; set; } + private readonly IFileSystem _fileSystem; /// /// The logger used to inform the user that episodes has been removed. /// - [Injected] public ILogger Logger { private get; set; } + private readonly ILogger _logger; - - /// - public async Task Run(TaskParameters arguments, IProgress progress, CancellationToken cancellationToken) + /// + /// Create a new task. + /// + /// The library manager used to get libraries or remove deleted episodes. + /// The file manager used walk inside directories and check they existences. + /// The logger used to inform the user that episodes has been removed. + public Housekeeping(ILibraryManager libraryManager, IFileSystem fileSystem, ILogger logger) { - int count = 0; - int delCount = await LibraryManager.GetCount() + await LibraryManager.GetCount(); - progress.Report(0); - - foreach (Show show in await LibraryManager.GetAll()) - { - progress.Report(count / delCount * 100); - count++; - - if (await FileSystem.Exists(show.Path)) - continue; - Logger.LogWarning("Show {Name}'s folder has been deleted (was {Path}), removing it from kyoo", - show.Title, show.Path); - await LibraryManager.Delete(show); - } - - foreach (Episode episode in await LibraryManager.GetAll()) - { - progress.Report(count / delCount * 100); - count++; - - if (await FileSystem.Exists(episode.Path)) - continue; - Logger.LogWarning("Episode {Slug}'s file has been deleted (was {Path}), removing it from kyoo", - episode.Slug, episode.Path); - await LibraryManager.Delete(episode); - } - - progress.Report(100); + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _logger = logger; } /// @@ -85,5 +45,39 @@ namespace Kyoo.Tasks { return new(); } + + /// + public async Task Run(TaskParameters arguments, IProgress progress, CancellationToken cancellationToken) + { + int count = 0; + int delCount = await _libraryManager.GetCount() + await _libraryManager.GetCount(); + progress.Report(0); + + foreach (Show show in await _libraryManager.GetAll()) + { + progress.Report(count / delCount * 100); + count++; + + if (await _fileSystem.Exists(show.Path)) + continue; + _logger.LogWarning("Show {Name}'s folder has been deleted (was {Path}), removing it from kyoo", + show.Title, show.Path); + await _libraryManager.Delete(show); + } + + foreach (Episode episode in await _libraryManager.GetAll()) + { + progress.Report(count / delCount * 100); + count++; + + if (await _fileSystem.Exists(episode.Path)) + continue; + _logger.LogWarning("Episode {Slug}'s file has been deleted (was {Path}), removing it from kyoo", + episode.Slug, episode.Path); + await _libraryManager.Delete(episode); + } + + progress.Report(100); + } } } \ No newline at end of file diff --git a/Kyoo/Tasks/MetadataProviderLoader.cs b/Kyoo/Tasks/MetadataProviderLoader.cs index db50f9ec..42342434 100644 --- a/Kyoo/Tasks/MetadataProviderLoader.cs +++ b/Kyoo/Tasks/MetadataProviderLoader.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; -using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; namespace Kyoo.Tasks @@ -11,43 +11,45 @@ namespace Kyoo.Tasks /// /// A task that download metadata providers images. /// + [TaskMetadata("reload-metadata", "Reload Metadata Providers", "Add every loaded metadata provider to the database.", + RunOnStartup = true, Priority = 1000, IsHidden = true)] public class MetadataProviderLoader : ITask { - /// - public string Slug => "reload-metdata"; - - /// - public string Name => "Reload Metadata Providers"; - - /// - public string Description => "Add every loaded metadata provider to the database."; - - /// - public string HelpMessage => null; - - /// - public bool RunOnStartup => true; - - /// - public int Priority => 1000; - - /// - public bool IsHidden => true; - /// /// The provider repository used to create in-db providers from metadata providers. /// - [Injected] public IProviderRepository Providers { private get; set; } + private readonly IProviderRepository _providers; /// /// The thumbnail manager used to download providers logo. /// - [Injected] public IThumbnailsManager Thumbnails { private get; set; } + private readonly IThumbnailsManager _thumbnails; /// /// The list of metadata providers to register. /// - [Injected] public ICollection MetadataProviders { private get; set; } - - + private readonly ICollection _metadataProviders; + + /// + /// Create a new task. + /// + /// + /// The provider repository used to create in-db providers from metadata providers. + /// + /// + /// The thumbnail manager used to download providers logo. + /// + /// + /// The list of metadata providers to register. + /// + public MetadataProviderLoader(IProviderRepository providers, + IThumbnailsManager thumbnails, + ICollection metadataProviders) + { + _providers = providers; + _thumbnails = thumbnails; + _metadataProviders = metadataProviders; + } + + /// public TaskParameters GetParameters() { @@ -60,13 +62,13 @@ namespace Kyoo.Tasks float percent = 0; progress.Report(0); - foreach (IMetadataProvider provider in MetadataProviders) + foreach (IMetadataProvider provider in _metadataProviders) { if (string.IsNullOrEmpty(provider.Provider.Slug)) throw new TaskFailedException($"Empty provider slug (name: {provider.Provider.Name})."); - await Providers.CreateIfNotExists(provider.Provider); - await Thumbnails.DownloadImages(provider.Provider); - percent += 100f / MetadataProviders.Count; + await _providers.CreateIfNotExists(provider.Provider); + await _thumbnails.DownloadImages(provider.Provider); + percent += 100f / _metadataProviders.Count; progress.Report(percent); } progress.Report(100); diff --git a/Kyoo/Tasks/PluginInitializer.cs b/Kyoo/Tasks/PluginInitializer.cs index 31dd324e..c9f814a6 100644 --- a/Kyoo/Tasks/PluginInitializer.cs +++ b/Kyoo/Tasks/PluginInitializer.cs @@ -2,57 +2,55 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; -using Kyoo.Models.Attributes; namespace Kyoo.Tasks { /// /// A task run on Kyoo's startup to initialize plugins /// + [TaskMetadata("plugin-init", "Plugin Initializer", "A task to initialize plugins.", + RunOnStartup = true, Priority = int.MaxValue, IsHidden = true)] public class PluginInitializer : ITask { - /// - public string Slug => "plugin-init"; - - /// - public string Name => "PluginInitializer"; - - /// - public string Description => "A task to initialize plugins."; - - /// - public string HelpMessage => null; - - /// - public bool RunOnStartup => true; - - /// - public int Priority => int.MaxValue; - - /// - public bool IsHidden => true; - - /// /// The plugin manager used to retrieve plugins to initialize them. /// - [Injected] public IPluginManager PluginManager { private get; set; } + private readonly IPluginManager _pluginManager; /// /// The service provider given to each method. /// - [Injected] public IServiceProvider Provider { private get; set; } + private readonly IServiceProvider _provider; + + /// + /// Create a new task + /// + /// The plugin manager used to retrieve plugins to initialize them. + /// The service provider given to each method. + public PluginInitializer(IPluginManager pluginManager, IServiceProvider provider) + { + _pluginManager = pluginManager; + _provider = provider; + } + + + /// + public TaskParameters GetParameters() + { + return new(); + } /// public Task Run(TaskParameters arguments, IProgress progress, CancellationToken cancellationToken) { - ICollection plugins = PluginManager.GetAllPlugins(); + ICollection plugins = _pluginManager.GetAllPlugins(); int count = 0; progress.Report(0); foreach (IPlugin plugin in plugins) { - plugin.Initialize(Provider); + plugin.Initialize(_provider); progress.Report(count / plugins.Count * 100); count++; @@ -61,11 +59,5 @@ namespace Kyoo.Tasks progress.Report(100); return Task.CompletedTask; } - - /// - public TaskParameters GetParameters() - { - return new(); - } } } \ No newline at end of file diff --git a/Kyoo/Tasks/RegisterEpisode.cs b/Kyoo/Tasks/RegisterEpisode.cs index 674ae477..dea3bf40 100644 --- a/Kyoo/Tasks/RegisterEpisode.cs +++ b/Kyoo/Tasks/RegisterEpisode.cs @@ -2,9 +2,9 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; using Kyoo.Models; -using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; namespace Kyoo.Tasks @@ -12,49 +12,60 @@ namespace Kyoo.Tasks /// /// A task to register a new episode /// + [TaskMetadata("register", "Register episode", "Register a new episode")] public class RegisterEpisode : ITask { - /// - public string Slug => "register"; - - /// - public string Name => "Register episode"; - - /// - public string Description => "Register a new episode"; - - /// - public string HelpMessage => null; - - /// - public bool RunOnStartup => false; - - /// - public int Priority => 0; - - /// - public bool IsHidden => false; - /// /// An identifier to extract metadata from paths. /// - [Injected] public IIdentifier Identifier { private get; set; } + private readonly IIdentifier _identifier; /// - /// The library manager used to register the episode + /// The library manager used to register the episode. /// - [Injected] public ILibraryManager LibraryManager { private get; set; } + private readonly ILibraryManager _libraryManager; /// /// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist). /// - [Injected] public AProviderComposite MetadataProvider { private get; set; } + private readonly AProviderComposite _metadataProvider; /// /// The thumbnail manager used to download images. /// - [Injected] public IThumbnailsManager ThumbnailsManager { private get; set; } + private readonly IThumbnailsManager _thumbnailsManager; /// /// The transcoder used to extract subtitles and metadata. /// - [Injected] public ITranscoder Transcoder { private get; set; } + private readonly ITranscoder _transcoder; + + /// + /// Create a new task. + /// + /// + /// An identifier to extract metadata from paths. + /// + /// + /// The library manager used to register the episode. + /// + /// + /// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist). + /// + /// + /// The thumbnail manager used to download images. + /// + /// + /// The transcoder used to extract subtitles and metadata. + /// + public RegisterEpisode(IIdentifier identifier, + ILibraryManager libraryManager, + AProviderComposite metadataProvider, + IThumbnailsManager thumbnailsManager, + ITranscoder transcoder) + { + _identifier = identifier; + _libraryManager = libraryManager; + _metadataProvider = metadataProvider; + _thumbnailsManager = thumbnailsManager; + _transcoder = transcoder; + } /// public TaskParameters GetParameters() @@ -77,11 +88,11 @@ namespace Kyoo.Tasks progress.Report(0); if (library.Providers == null) - await LibraryManager.Load(library, x => x.Providers); - MetadataProvider.UseProviders(library.Providers); + await _libraryManager.Load(library, x => x.Providers); + _metadataProvider.UseProviders(library.Providers); try { - (Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path, + (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(path, relativePath); progress.Report(15); @@ -94,7 +105,7 @@ namespace Kyoo.Tasks if (show.StartAir.HasValue) { show.Slug += $"-{show.StartAir.Value.Year}"; - show = await LibraryManager.Create(show); + show = await _libraryManager.Create(show); } else { @@ -107,7 +118,7 @@ namespace Kyoo.Tasks // If they are not already loaded, load external ids to allow metadata providers to use them. if (show.ExternalIDs == null) - await LibraryManager.Load(show, x => x.ExternalIDs); + await _libraryManager.Load(show, x => x.ExternalIDs); progress.Report(50); if (season != null) @@ -119,20 +130,20 @@ namespace Kyoo.Tasks episode.Show = show; episode.Season = season; - episode = await MetadataProvider.Get(episode); + episode = await _metadataProvider.Get(episode); progress.Report(70); - episode.Tracks = (await Transcoder.ExtractInfos(episode, false)) + episode.Tracks = (await _transcoder.ExtractInfos(episode, false)) .Where(x => x.Type != StreamType.Attachment) .ToArray(); - await ThumbnailsManager.DownloadImages(episode); + await _thumbnailsManager.DownloadImages(episode); progress.Report(90); - await LibraryManager.Create(episode); + await _libraryManager.Create(episode); progress.Report(95); - await LibraryManager.AddShowLink(show, library, collection); + await _libraryManager.AddShowLink(show, library, collection); progress.Report(100); } - catch (IdentificationFailed ex) + catch (IdentificationFailedException ex) { throw new TaskFailedException(ex); } @@ -156,12 +167,12 @@ namespace Kyoo.Tasks if (item == null || string.IsNullOrEmpty(item.Slug)) return null; - T existing = await LibraryManager.GetOrDefault(item.Slug); + T existing = await _libraryManager.GetOrDefault(item.Slug); if (existing != null) return existing; - item = await MetadataProvider.Get(item); - await ThumbnailsManager.DownloadImages(item); - return await LibraryManager.CreateIfNotExists(item); + item = await _metadataProvider.Get(item); + await _thumbnailsManager.DownloadImages(item); + return await _libraryManager.CreateIfNotExists(item); } } } \ No newline at end of file diff --git a/Kyoo/Tasks/RegisterSubtitle.cs b/Kyoo/Tasks/RegisterSubtitle.cs index 07620e6c..23a7d425 100644 --- a/Kyoo/Tasks/RegisterSubtitle.cs +++ b/Kyoo/Tasks/RegisterSubtitle.cs @@ -1,9 +1,9 @@ using System; using System.Threading; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Controllers; using Kyoo.Models; -using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; namespace Kyoo.Tasks @@ -11,37 +11,28 @@ namespace Kyoo.Tasks /// /// A task to register a new episode /// + [TaskMetadata("register-sub", "Register subtitle", "Register a new subtitle")] public class RegisterSubtitle : ITask { - /// - public string Slug => "register-sub"; - - /// - public string Name => "Register subtitle"; - - /// - public string Description => "Register a new subtitle"; - - /// - public string HelpMessage => null; - - /// - public bool RunOnStartup => false; - - /// - public int Priority => 0; - - /// - public bool IsHidden => false; - /// /// An identifier to extract metadata from paths. /// - [Injected] public IIdentifier Identifier { private get; set; } + private readonly IIdentifier _identifier; /// - /// The library manager used to register the episode + /// The library manager used to register the episode. /// - [Injected] public ILibraryManager LibraryManager { private get; set; } + private readonly ILibraryManager _libraryManager; + + /// + /// Create a new task. + /// + /// An identifier to extract metadata from paths. + /// The library manager used to register the episode. + public RegisterSubtitle(IIdentifier identifier, ILibraryManager libraryManager) + { + _identifier = identifier; + _libraryManager = libraryManager; + } /// public TaskParameters GetParameters() @@ -63,7 +54,7 @@ namespace Kyoo.Tasks try { progress.Report(0); - Track track = await Identifier.IdentifyTrack(path, relativePath); + Track track = await _identifier.IdentifyTrack(path, relativePath); progress.Report(25); if (track.Episode == null) @@ -71,10 +62,10 @@ namespace Kyoo.Tasks if (track.Episode.ID == 0) { if (track.Episode.Slug != null) - track.Episode = await LibraryManager.Get(track.Episode.Slug); + track.Episode = await _libraryManager.Get(track.Episode.Slug); else if (track.Episode.Path != null) { - track.Episode = await LibraryManager.GetOrDefault(x => x.Path.StartsWith(track.Episode.Path)); + track.Episode = await _libraryManager.GetOrDefault(x => x.Path.StartsWith(track.Episode.Path)); if (track.Episode == null) throw new TaskFailedException($"No episode found for the track at: {path}."); } @@ -83,10 +74,10 @@ namespace Kyoo.Tasks } progress.Report(50); - await LibraryManager.Create(track); + await _libraryManager.Create(track); progress.Report(100); } - catch (IdentificationFailed ex) + catch (IdentificationFailedException ex) { throw new TaskFailedException(ex); }