Using task metadata as an attribute

This commit is contained in:
Zoe Roux 2021-07-22 19:33:21 +02:00
parent fd37cede9d
commit 48e81dfd92
20 changed files with 476 additions and 366 deletions

View File

@ -18,7 +18,7 @@ namespace Kyoo.Controllers
/// <param name="relativePath">
/// The path of the episode file relative to the library root. It starts with a <c>/</c>.
/// </param>
/// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception>
/// <exception cref="IdentificationFailedException">The identifier could not work for the given path.</exception>
/// <returns>
/// 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
/// <param name="relativePath">
/// The path of the episode file relative to the library root. It starts with a <c>/</c>.
/// </param>
/// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception>
/// <exception cref="IdentificationFailedException">The identifier could not work for the given path.</exception>
/// <returns>
/// The metadata of the track identified.
/// </returns>

View File

@ -114,6 +114,8 @@ namespace Kyoo.Controllers
/// <returns>The value of this parameter.</returns>
public T As<T>()
{
if (typeof(T) == typeof(object))
return (T)Value;
return (T)Convert.ChangeType(Value, typeof(T));
}
}
@ -150,42 +152,6 @@ namespace Kyoo.Controllers
/// </summary>
public interface ITask
{
/// <summary>
/// The slug of the task, used to start it.
/// </summary>
public string Slug { get; }
/// <summary>
/// The name of the task that will be displayed to the user.
/// </summary>
public string Name { get; }
/// <summary>
/// A quick description of what this task will do.
/// </summary>
public string Description { get; }
/// <summary>
/// An optional message to display to help the user.
/// </summary>
public string HelpMessage { get; }
/// <summary>
/// Should this task be automatically run at app startup?
/// </summary>
public bool RunOnStartup { get; }
/// <summary>
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
/// </summary>
public int Priority { get; }
/// <summary>
/// <c>true</c> if this task should not be displayed to the user, <c>false</c> otherwise.
/// </summary>
public bool IsHidden { get; }
/// <summary>
/// The list of parameters
/// </summary>

View File

@ -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
@ -64,18 +65,18 @@ namespace Kyoo.Controllers
void StartTask<T>([NotNull] IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null)
where T : ITask, new();
where T : ITask;
/// <summary>
/// Get all currently running tasks
/// </summary>
/// <returns>A list of currently running tasks.</returns>
ICollection<ITask> GetRunningTasks();
ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks();
/// <summary>
/// Get all available tasks
/// </summary>
/// <returns>A list of every tasks that this instance know.</returns>
ICollection<ITask> GetAllTasks();
ICollection<TaskMetadataAttribute> GetAllTasks();
}
}

View File

@ -8,8 +8,8 @@ namespace Kyoo.Models.Attributes
/// An attribute to inform that the service will be injected automatically by a service provider.
/// </summary>
/// <remarks>
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>.
/// It can also be used on <see cref="IPlugin"/> and it will be injected before calling <see cref="IPlugin.ConfigureAspNet"/>.
/// It should only be used on <see cref="IPlugin"/> and it will be injected before
/// calling <see cref="IPlugin.ConfigureAspNet"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Kyoo.Controllers;
namespace Kyoo.Common.Models.Attributes
{
/// <summary>
/// An attribute to inform how a <see cref="IFileSystem"/> works.
/// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public class TaskMetadataAttribute : Attribute
{
/// <summary>
/// The slug of the task, used to start it.
/// </summary>
public string Slug { get; }
/// <summary>
/// The name of the task that will be displayed to the user.
/// </summary>
public string Name { get; }
/// <summary>
/// A quick description of what this task will do.
/// </summary>
public string Description { get; }
/// <summary>
/// Should this task be automatically run at app startup?
/// </summary>
public bool RunOnStartup { get; set; }
/// <summary>
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
/// </summary>
public int Priority { get; set; }
/// <summary>
/// <c>true</c> if this task should not be displayed to the user, <c>false</c> otherwise.
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Create a new <see cref="TaskMetadataAttribute"/> with the given slug, name and description.
/// </summary>
/// <param name="slug">The slug of the task, used to start it.</param>
/// <param name="name">The name of the task that will be displayed to the user.</param>
/// <param name="description">A quick description of what this task will do.</param>
public TaskMetadataAttribute(string slug, string name, string description)
{
Slug = slug;
Name = name;
Description = description;
}
/// <summary>
/// Create a new <see cref="TaskMetadataAttribute"/> using a dictionary of metadata.
/// </summary>
/// <param name="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.
/// </param>
public TaskMetadataAttribute(IDictionary<string, object> 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)];
}
}
}

View File

@ -8,20 +8,20 @@ namespace Kyoo.Models.Exceptions
/// An exception raised when an <see cref="IIdentifier"/> failed.
/// </summary>
[Serializable]
public class IdentificationFailed : Exception
public class IdentificationFailedException : Exception
{
/// <summary>
/// Create a new <see cref="IdentificationFailed"/> with a default message.
/// Create a new <see cref="IdentificationFailedException"/> with a default message.
/// </summary>
public IdentificationFailed()
public IdentificationFailedException()
: base("An identification failed.")
{}
/// <summary>
/// Create a new <see cref="IdentificationFailed"/> with a custom message.
/// Create a new <see cref="IdentificationFailedException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use.</param>
public IdentificationFailed(string message)
public IdentificationFailedException(string message)
: base(message)
{}
@ -30,7 +30,7 @@ namespace Kyoo.Models.Exceptions
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected IdentificationFailed(SerializationInfo info, StreamingContext context)
protected IdentificationFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}

View File

@ -75,7 +75,7 @@ namespace Kyoo.Postgresql
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
context.Database.Migrate();
using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
conn.Open();
conn.ReloadTypes();
}

View File

@ -43,7 +43,7 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
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
};
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
/// <inheritdoc />

View File

@ -81,17 +81,28 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
public override Task<LibraryItem> Create(LibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException();
public override Task<LibraryItem> Edit(LibraryItem obj, bool resetOld)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(int id) => throw new InvalidOperationException();
public override Task Delete(int id)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(string slug) => throw new InvalidOperationException();
public override Task Delete(string slug)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(LibraryItem obj) => throw new InvalidOperationException();
public override Task Delete(LibraryItem obj)
=> throw new InvalidOperationException();
/// <summary>
/// Get a basic queryable for a library with the right mapping from shows & collections.

View File

@ -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
{
/// <summary>
/// The service provider used to activate
/// The class representing task under this <see cref="TaskManager"/> jurisdiction.
/// </summary>
private readonly IServiceProvider _provider;
private class ManagedTask
{
/// <summary>
/// The metadata for this task (the slug, and other useful information).
/// </summary>
public TaskMetadataAttribute Metadata { get; set; }
/// <summary>
/// The function used to create the task object.
/// </summary>
public Func<Owned<ITask>> Factory { get; init; }
/// <summary>
/// The next scheduled date for this task
/// </summary>
public DateTime ScheduledDate { get; set; }
}
/// <summary>
/// A class representing a task inside the <see cref="TaskManager._queuedTasks"/> list.
/// </summary>
private class QueuedTask
{
/// <summary>
/// The task currently queued.
/// </summary>
public ManagedTask Task { get; init; }
/// <summary>
/// The progress reporter that this task should use.
/// </summary>
public IProgress<float> ProgressReporter { get; init; }
/// <summary>
/// The arguments to give to run the task with.
/// </summary>
public Dictionary<string, object> Arguments { get; init; }
/// <summary>
/// A token informing the task that it should be cancelled or not.
/// </summary>
public CancellationToken? CancellationToken { get; init; }
}
/// <summary>
/// The configuration instance used to get schedule information
/// </summary>
@ -35,15 +81,15 @@ namespace Kyoo.Controllers
/// <summary>
/// The list of tasks and their next scheduled run.
/// </summary>
private readonly List<(ITask task, DateTime scheduledDate)> _tasks;
private readonly List<ManagedTask> _tasks;
/// <summary>
/// The queue of tasks that should be run as soon as possible.
/// </summary>
private readonly Queue<(ITask, IProgress<float>, Dictionary<string, object>)> _queuedTasks = new();
private readonly Queue<QueuedTask> _queuedTasks = new();
/// <summary>
/// The currently running task.
/// </summary>
private ITask _runningTask;
private (TaskMetadataAttribute, ITask)? _runningTask;
/// <summary>
/// The cancellation token used to cancel the running task when the runner should shutdown.
/// </summary>
@ -53,22 +99,24 @@ namespace Kyoo.Controllers
/// <summary>
/// Create a new <see cref="TaskManager"/>.
/// </summary>
/// <param name="tasks">The list of tasks to manage</param>
/// <param name="provider">The service provider to request services for tasks</param>
/// <param name="tasks">The list of tasks to manage with their metadata</param>
/// <param name="options">The configuration to load schedule information.</param>
/// <param name="logger">The logger.</param>
public TaskManager(IEnumerable<ITask> tasks,
IServiceProvider provider,
public TaskManager(IEnumerable<Meta<Func<Owned<ITask>>, TaskMetadataAttribute>> tasks,
IOptionsMonitor<TaskOptions> options,
ILogger<TaskManager> 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
/// <param name="cancellationToken">A token to stop the runner</param>
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
EnqueueStartupTasks();
_EnqueueStartupTasks();
while (!cancellationToken.IsCancellationRequested)
{
if (_queuedTasks.Any())
{
(ITask task, IProgress<float> progress, Dictionary<string, object> 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
/// <param name="task">The task to run</param>
/// <param name="progress">A progress reporter to know the percentage of completion of the task.</param>
/// <param name="arguments">The arguments to pass to the function</param>
/// <param name="cancellationToken">An optional cancellation token that will be passed to the task.</param>
/// <exception cref="ArgumentException">
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
/// invalid.
/// </exception>
private async Task RunTask(ITask task,
private async Task _RunTask(ManagedTask task,
[NotNull] IProgress<float> progress,
Dictionary<string, object> arguments)
Dictionary<string, object> arguments,
CancellationToken? cancellationToken = null)
{
_logger.LogInformation("Task starting: {Task}", task.Name);
ICollection<TaskParameter> all = task.GetParameters();
ICollection<string> 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}");
}
await using Owned<ITask> taskObj = task.Factory.Invoke();
ICollection<TaskParameter> all = taskObj.Value.GetParameters();
TaskParameters args = new(all
.Select(x =>
_runningTask = (task.Metadata, taskObj.Value);
ICollection<string> invalids = arguments.Keys
.Where(x => all.All(y => x != y.Name))
.ToArray();
if (invalids.Any())
{
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);
}));
throw new ArgumentException($"{string.Join(", ", invalids)} are " +
$"invalid arguments for the task {task.Metadata.Name}");
}
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);
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<object>()));
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;
}
}
/// <summary>
@ -178,8 +237,8 @@ namespace Kyoo.Controllers
/// </summary>
private void QueueScheduledTasks()
{
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.scheduledDate <= DateTime.Now)
.Select(x => x.task.Slug);
IEnumerable<string> 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
/// <summary>
/// Queue startup tasks with respect to the priority rules.
/// </summary>
private void EnqueueStartupTasks()
private void _EnqueueStartupTasks()
{
IEnumerable<ITask> startupTasks = _tasks.Select(x => x.task)
.Where(x => x.RunOnStartup)
.OrderByDescending(x => x.Priority);
foreach (ITask task in startupTasks)
_queuedTasks.Enqueue((task, new Progress<float>(), new Dictionary<string, object>()));
IEnumerable<string> 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<float>(), new Dictionary<string, object>());
}
/// <inheritdoc />
@ -207,20 +267,29 @@ namespace Kyoo.Controllers
{
arguments ??= new Dictionary<string, object>();
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);
}
/// <inheritdoc />
public void StartTask<T>(IProgress<float> progress,
Dictionary<string, object> arguments = null,
CancellationToken? cancellationToken = null)
where T : ITask, new()
where T : ITask
{
StartTask(new T().Slug, progress, arguments, cancellationToken);
TaskMetadataAttribute metadata = typeof(T).GetCustomAttribute<TaskMetadataAttribute>();
if (metadata == null)
throw new ArgumentException($"No metadata found on the given task (type: {typeof(T).Name}).");
StartTask(metadata.Slug, progress, arguments, cancellationToken);
}
/// <summary>
@ -236,15 +305,17 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public ICollection<ITask> GetRunningTasks()
public ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks()
{
return new[] {_runningTask};
return _runningTask == null
? ArraySegment<(TaskMetadataAttribute, ITask)>.Empty
: new[] { _runningTask.Value };
}
/// <inheritdoc />
public ICollection<ITask> GetAllTasks()
public ICollection<TaskMetadataAttribute> GetAllTasks()
{
return _tasks.Select(x => x.task).ToArray();
return _tasks.Select(x => x.Metadata).ToArray();
}
}
}

View File

@ -12,8 +12,7 @@ namespace Kyoo.Models.Watch
/// <summary>
/// The list of known video extensions
/// </summary>
public static readonly ImmutableArray<string> VideoExtensions = new()
{
public static readonly ImmutableArray<string> VideoExtensions = ImmutableArray.Create(
".webm",
".mkv",
".flv",
@ -38,7 +37,7 @@ namespace Kyoo.Models.Watch
".m2v",
".3gp",
".3g2"
};
);
/// <summary>
/// Check if a file represent a video file (only by checking the extension of the file)

View File

@ -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
/// <summary>
/// A task to add new video files.
/// </summary>
[TaskMetadata("scan", "Scan libraries", "Scan your libraries and load data for new shows.", RunOnStartup = true)]
public class Crawler : ITask
{
/// <inheritdoc />
public string Slug => "scan";
/// <inheritdoc />
public string Name => "Scan libraries";
/// <inheritdoc />
public string Description => "Scan your libraries and load data for new shows.";
/// <inheritdoc />
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.";
/// <inheritdoc />
public bool RunOnStartup => true;
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool IsHidden => false;
/// <summary>
/// The library manager used to get libraries and providers to use.
/// </summary>
[Injected] public ILibraryManager LibraryManager { private get; set; }
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used walk inside directories and check they existences.
/// </summary>
[Injected] public IFileSystem FileSystem { private get; set; }
private readonly IFileSystem _fileSystem;
/// <summary>
/// A task manager used to create sub tasks for each episode to add to the database.
/// </summary>
[Injected] public ITaskManager TaskManager { private get; set; }
private readonly ITaskManager _taskManager;
/// <summary>
/// The logger used to inform the current status to the console.
/// </summary>
[Injected] public ILogger<Crawler> Logger { private get; set; }
private readonly ILogger<Crawler> _logger;
/// <summary>
/// Create a new <see cref="Crawler"/>.
/// </summary>
/// <param name="libraryManager">The library manager to retrieve existing episodes/library/tracks</param>
/// <param name="fileSystem">The file system to glob files</param>
/// <param name="taskManager">The task manager used to start <see cref="RegisterEpisode"/>.</param>
/// <param name="logger">The logger used print messages.</param>
public Crawler(ILibraryManager libraryManager,
IFileSystem fileSystem,
ITaskManager taskManager,
ILogger<Crawler> logger)
{
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_taskManager = taskManager;
_logger = logger;
}
/// <inheritdoc />
@ -71,20 +68,20 @@ namespace Kyoo.Tasks
{
string argument = arguments["slug"].As<string>();
ICollection<Library> libraries = argument == null
? await LibraryManager.GetAll<Library>()
: new [] { await LibraryManager.GetOrDefault<Library>(argument)};
? await _libraryManager.GetAll<Library>()
: new [] { await _libraryManager.GetOrDefault<Library>(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<Episode> episodes = await LibraryManager.GetAll<Episode>();
ICollection<Track> tracks = await LibraryManager.GetAll<Track>();
ICollection<Episode> episodes = await _libraryManager.GetAll<Episode>();
ICollection<Track> tracks = await _libraryManager.GetAll<Track>();
foreach (Library library in libraries)
{
IProgress<float> reporter = new Progress<float>(x =>
@ -108,10 +105,10 @@ namespace Kyoo.Tasks
IProgress<float> 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<string> files = await FileSystem.ListFiles(path, SearchOption.AllDirectories);
ICollection<string> 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<RegisterEpisode>(reporter, new Dictionary<string, object>
_taskManager.StartTask<RegisterEpisode>(reporter, new Dictionary<string, object>
{
["path"] = episodePath,
["relativePath"] = episodePath[path.Length..],
@ -162,7 +159,7 @@ namespace Kyoo.Tasks
foreach (string trackPath in subtitles)
{
TaskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
_taskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
{
["path"] = trackPath,
["relativePath"] = trackPath[path.Length..]

View File

@ -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
{
/// <summary>
/// A task to remove orphaned episode and series.
/// </summary>
[TaskMetadata("housekeeping", "Housekeeping", "Remove orphaned episode and series.", RunOnStartup = true)]
public class Housekeeping : ITask
{
/// <inheritdoc />
public string Slug => "housekeeping";
/// <inheritdoc />
public string Name => "Housekeeping";
/// <inheritdoc />
public string Description => "Remove orphaned episode and series.";
/// <inheritdoc />
public string HelpMessage => null;
/// <inheritdoc />
public bool RunOnStartup => true;
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool IsHidden => false;
/// <summary>
/// The library manager used to get libraries or remove deleted episodes
/// The library manager used to get libraries or remove deleted episodes.
/// </summary>
[Injected] public ILibraryManager LibraryManager { private get; set; }
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The file manager used walk inside directories and check they existences.
/// </summary>
[Injected] public IFileSystem FileSystem { private get; set; }
private readonly IFileSystem _fileSystem;
/// <summary>
/// The logger used to inform the user that episodes has been removed.
/// </summary>
[Injected] public ILogger<Housekeeping> Logger { private get; set; }
private readonly ILogger<Housekeeping> _logger;
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
/// <summary>
/// Create a new <see cref="Housekeeping"/> task.
/// </summary>
/// <param name="libraryManager">The library manager used to get libraries or remove deleted episodes.</param>
/// <param name="fileSystem">The file manager used walk inside directories and check they existences.</param>
/// <param name="logger">The logger used to inform the user that episodes has been removed.</param>
public Housekeeping(ILibraryManager libraryManager, IFileSystem fileSystem, ILogger<Housekeeping> logger)
{
int count = 0;
int delCount = await LibraryManager.GetCount<Show>() + await LibraryManager.GetCount<Episode>();
progress.Report(0);
foreach (Show show in await LibraryManager.GetAll<Show>())
{
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<Episode>())
{
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;
}
/// <inheritdoc />
@ -85,5 +45,39 @@ namespace Kyoo.Tasks
{
return new();
}
/// <inheritdoc />
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
int count = 0;
int delCount = await _libraryManager.GetCount<Show>() + await _libraryManager.GetCount<Episode>();
progress.Report(0);
foreach (Show show in await _libraryManager.GetAll<Show>())
{
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<Episode>())
{
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);
}
}
}

View File

@ -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,41 +11,43 @@ namespace Kyoo.Tasks
/// <summary>
/// A task that download metadata providers images.
/// </summary>
[TaskMetadata("reload-metadata", "Reload Metadata Providers", "Add every loaded metadata provider to the database.",
RunOnStartup = true, Priority = 1000, IsHidden = true)]
public class MetadataProviderLoader : ITask
{
/// <inheritdoc />
public string Slug => "reload-metdata";
/// <inheritdoc />
public string Name => "Reload Metadata Providers";
/// <inheritdoc />
public string Description => "Add every loaded metadata provider to the database.";
/// <inheritdoc />
public string HelpMessage => null;
/// <inheritdoc />
public bool RunOnStartup => true;
/// <inheritdoc />
public int Priority => 1000;
/// <inheritdoc />
public bool IsHidden => true;
/// <summary>
/// The provider repository used to create in-db providers from metadata providers.
/// </summary>
[Injected] public IProviderRepository Providers { private get; set; }
private readonly IProviderRepository _providers;
/// <summary>
/// The thumbnail manager used to download providers logo.
/// </summary>
[Injected] public IThumbnailsManager Thumbnails { private get; set; }
private readonly IThumbnailsManager _thumbnails;
/// <summary>
/// The list of metadata providers to register.
/// </summary>
[Injected] public ICollection<IMetadataProvider> MetadataProviders { private get; set; }
private readonly ICollection<IMetadataProvider> _metadataProviders;
/// <summary>
/// Create a new <see cref="MetadataProviderLoader"/> task.
/// </summary>
/// <param name="providers">
/// The provider repository used to create in-db providers from metadata providers.
/// </param>
/// <param name="thumbnails">
/// The thumbnail manager used to download providers logo.
/// </param>
/// <param name="metadataProviders">
/// The list of metadata providers to register.
/// </param>
public MetadataProviderLoader(IProviderRepository providers,
IThumbnailsManager thumbnails,
ICollection<IMetadataProvider> metadataProviders)
{
_providers = providers;
_thumbnails = thumbnails;
_metadataProviders = metadataProviders;
}
/// <inheritdoc />
@ -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);

View File

@ -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
{
/// <summary>
/// A task run on Kyoo's startup to initialize plugins
/// </summary>
[TaskMetadata("plugin-init", "Plugin Initializer", "A task to initialize plugins.",
RunOnStartup = true, Priority = int.MaxValue, IsHidden = true)]
public class PluginInitializer : ITask
{
/// <inheritdoc />
public string Slug => "plugin-init";
/// <inheritdoc />
public string Name => "PluginInitializer";
/// <inheritdoc />
public string Description => "A task to initialize plugins.";
/// <inheritdoc />
public string HelpMessage => null;
/// <inheritdoc />
public bool RunOnStartup => true;
/// <inheritdoc />
public int Priority => int.MaxValue;
/// <inheritdoc />
public bool IsHidden => true;
/// <summary>
/// The plugin manager used to retrieve plugins to initialize them.
/// </summary>
[Injected] public IPluginManager PluginManager { private get; set; }
private readonly IPluginManager _pluginManager;
/// <summary>
/// The service provider given to each <see cref="IPlugin.Initialize"/> method.
/// </summary>
[Injected] public IServiceProvider Provider { private get; set; }
private readonly IServiceProvider _provider;
/// <summary>
/// Create a new <see cref="PluginInitializer"/> task
/// </summary>
/// <param name="pluginManager">The plugin manager used to retrieve plugins to initialize them.</param>
/// <param name="provider">The service provider given to each <see cref="IPlugin.Initialize"/> method.</param>
public PluginInitializer(IPluginManager pluginManager, IServiceProvider provider)
{
_pluginManager = pluginManager;
_provider = provider;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new();
}
/// <inheritdoc />
public Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{
ICollection<IPlugin> plugins = PluginManager.GetAllPlugins();
ICollection<IPlugin> 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;
}
/// <inheritdoc />
public TaskParameters GetParameters()
{
return new();
}
}
}

View File

@ -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
/// <summary>
/// A task to register a new episode
/// </summary>
[TaskMetadata("register", "Register episode", "Register a new episode")]
public class RegisterEpisode : ITask
{
/// <inheritdoc />
public string Slug => "register";
/// <inheritdoc />
public string Name => "Register episode";
/// <inheritdoc />
public string Description => "Register a new episode";
/// <inheritdoc />
public string HelpMessage => null;
/// <inheritdoc />
public bool RunOnStartup => false;
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool IsHidden => false;
/// <summary>
/// An identifier to extract metadata from paths.
/// </summary>
[Injected] public IIdentifier Identifier { private get; set; }
private readonly IIdentifier _identifier;
/// <summary>
/// The library manager used to register the episode
/// The library manager used to register the episode.
/// </summary>
[Injected] public ILibraryManager LibraryManager { private get; set; }
private readonly ILibraryManager _libraryManager;
/// <summary>
/// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist).
/// </summary>
[Injected] public AProviderComposite MetadataProvider { private get; set; }
private readonly AProviderComposite _metadataProvider;
/// <summary>
/// The thumbnail manager used to download images.
/// </summary>
[Injected] public IThumbnailsManager ThumbnailsManager { private get; set; }
private readonly IThumbnailsManager _thumbnailsManager;
/// <summary>
/// The transcoder used to extract subtitles and metadata.
/// </summary>
[Injected] public ITranscoder Transcoder { private get; set; }
private readonly ITranscoder _transcoder;
/// <summary>
/// Create a new <see cref="RegisterEpisode"/> task.
/// </summary>
/// <param name="identifier">
/// An identifier to extract metadata from paths.
/// </param>
/// <param name="libraryManager">
/// The library manager used to register the episode.
/// </param>
/// <param name="metadataProvider">
/// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist).
/// </param>
/// <param name="thumbnailsManager">
/// The thumbnail manager used to download images.
/// </param>
/// <param name="transcoder">
/// The transcoder used to extract subtitles and metadata.
/// </param>
public RegisterEpisode(IIdentifier identifier,
ILibraryManager libraryManager,
AProviderComposite metadataProvider,
IThumbnailsManager thumbnailsManager,
ITranscoder transcoder)
{
_identifier = identifier;
_libraryManager = libraryManager;
_metadataProvider = metadataProvider;
_thumbnailsManager = thumbnailsManager;
_transcoder = transcoder;
}
/// <inheritdoc />
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<T>(item.Slug);
T existing = await _libraryManager.GetOrDefault<T>(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);
}
}
}

View File

@ -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
/// <summary>
/// A task to register a new episode
/// </summary>
[TaskMetadata("register-sub", "Register subtitle", "Register a new subtitle")]
public class RegisterSubtitle : ITask
{
/// <inheritdoc />
public string Slug => "register-sub";
/// <inheritdoc />
public string Name => "Register subtitle";
/// <inheritdoc />
public string Description => "Register a new subtitle";
/// <inheritdoc />
public string HelpMessage => null;
/// <inheritdoc />
public bool RunOnStartup => false;
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool IsHidden => false;
/// <summary>
/// An identifier to extract metadata from paths.
/// </summary>
[Injected] public IIdentifier Identifier { private get; set; }
private readonly IIdentifier _identifier;
/// <summary>
/// The library manager used to register the episode
/// The library manager used to register the episode.
/// </summary>
[Injected] public ILibraryManager LibraryManager { private get; set; }
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="RegisterSubtitle"/> task.
/// </summary>
/// <param name="identifier">An identifier to extract metadata from paths.</param>
/// <param name="libraryManager">The library manager used to register the episode.</param>
public RegisterSubtitle(IIdentifier identifier, ILibraryManager libraryManager)
{
_identifier = identifier;
_libraryManager = libraryManager;
}
/// <inheritdoc />
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<Episode>(track.Episode.Slug);
track.Episode = await _libraryManager.Get<Episode>(track.Episode.Slug);
else if (track.Episode.Path != null)
{
track.Episode = await LibraryManager.GetOrDefault<Episode>(x => x.Path.StartsWith(track.Episode.Path));
track.Episode = await _libraryManager.GetOrDefault<Episode>(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);
}