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