diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs
index 03f22e79..33d1dd76 100644
--- a/Kyoo.Common/Controllers/IFileManager.cs
+++ b/Kyoo.Common/Controllers/IFileManager.cs
@@ -54,8 +54,10 @@ namespace Kyoo.Controllers
/// List files in a directory.
///
/// The path of the directory
+ /// Should the search be recursive or not.
/// A list of files's path.
- public Task> ListFiles([NotNull] string path);
+ public Task> ListFiles([NotNull] string path,
+ SearchOption options = SearchOption.TopDirectoryOnly);
///
/// Check if a file exists at the given path.
diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs
index 7f1bac18..33e4cd2b 100644
--- a/Kyoo.Common/Controllers/IMetadataProvider.cs
+++ b/Kyoo.Common/Controllers/IMetadataProvider.cs
@@ -1,5 +1,4 @@
-using System;
-using Kyoo.Models;
+using Kyoo.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -31,10 +30,7 @@ namespace Kyoo.Controllers
/// Merging metadata is the job of Kyoo, a complex is given
/// to make a precise search and give you every available properties, not to discard properties.
///
- ///
- /// If this metadata provider does not support .
- ///
- /// A new containing metadata from your provider
+ /// A new containing metadata from your provider or null
[ItemCanBeNull]
Task Get([NotNull] T item)
where T : class, IResource;
@@ -43,15 +39,10 @@ namespace Kyoo.Controllers
/// Search for a specific type of items with a given query.
///
/// The search query to use.
- ///
- /// If this metadata provider does not support .
- ///
/// The list of items that could be found on this specific provider.
[ItemNotNull]
Task> Search(string query)
where T : class, IResource;
-
- Task> GetPeople(Show show);
}
///
diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs
index 83cc70a4..ea072c39 100644
--- a/Kyoo.Common/Controllers/IPlugin.cs
+++ b/Kyoo.Common/Controllers/IPlugin.cs
@@ -11,8 +11,10 @@ namespace Kyoo.Controllers
///
/// A common interface used to discord plugins
///
- /// You can inject services in the IPlugin constructor.
- /// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.
+ ///
+ /// You can inject services in the IPlugin constructor.
+ /// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.
+ ///
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IPlugin
{
@@ -84,7 +86,10 @@ namespace Kyoo.Controllers
/// An optional configuration step to allow a plugin to change asp net configurations.
/// WARNING: This is only called on Kyoo's startup so you must restart the app to apply this changes.
///
- /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.
+ ///
+ /// The Asp.Net application builder. On most case it is not needed but you can use it to
+ /// add asp net functionalities.
+ ///
void ConfigureAspNet(IApplicationBuilder app)
{
// Skipped
diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs
index 75277dd2..3267837b 100644
--- a/Kyoo.Common/Controllers/ITask.cs
+++ b/Kyoo.Common/Controllers/ITask.cs
@@ -3,12 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Controllers
{
///
- /// A single task parameter. This struct contains metadata to display and utility functions to get them in the taks.
+ /// A single task parameter. This struct contains metadata to display and utility functions to get them in the task.
///
/// This struct will be used to generate the swagger documentation of the task.
public record TaskParameter
@@ -60,6 +61,24 @@ namespace Kyoo.Controllers
};
}
+ ///
+ /// Create a new required task parameter.
+ ///
+ /// The name of the parameter
+ /// The description of the parameter
+ /// The type of the parameter.
+ /// A new task parameter.
+ public static TaskParameter CreateRequired(string name, string description)
+ {
+ return new()
+ {
+ Name = name,
+ Description = description,
+ Type = typeof(T),
+ IsRequired = true
+ };
+ }
+
///
/// Create a parameter's value to give to a task.
///
@@ -162,27 +181,37 @@ namespace Kyoo.Controllers
public int Priority { get; }
///
- /// Start this task.
+ /// true if this task should not be displayed to the user, false otherwise.
///
- /// The list of parameters.
- /// A token to request the task's cancellation.
- /// If this task is not cancelled quickly, it might be killed by the runner.
- ///
- /// Your task can have any service as a public field and use the ,
- /// they will be set to an available service from the service container before calling this method.
- ///
- public Task Run(TaskParameters arguments, CancellationToken cancellationToken);
-
+ public bool IsHidden { get; }
+
///
/// The list of parameters
///
- /// All parameters that this task as. Every one of them will be given to the run function with a value.
+ ///
+ /// All parameters that this task as. Every one of them will be given to the run function with a value.
+ ///
public TaskParameters GetParameters();
///
- /// If this task is running, return the percentage of completion of this task or null if no percentage can be given.
+ /// Start this task.
///
- /// The percentage of completion of the task.
- public int? Progress();
+ ///
+ /// The list of parameters.
+ ///
+ ///
+ /// The progress reporter. Used to inform the sender the percentage of completion of this task
+ /// .
+ /// A token to request the task's cancellation.
+ /// If this task is not cancelled quickly, it might be killed by the runner.
+ ///
+ ///
+ /// Your task can have any service as a public field and use the ,
+ /// they will be set to an available service from the service container before calling this method.
+ /// They also will be removed after this method return (or throw) to prevent dangling services.
+ ///
+ public Task Run([NotNull] TaskParameters arguments,
+ [NotNull] IProgress progress,
+ CancellationToken cancellationToken);
}
}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs
index 392355d3..ffcb1c75 100644
--- a/Kyoo.Common/Controllers/ITaskManager.cs
+++ b/Kyoo.Common/Controllers/ITaskManager.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
@@ -13,11 +15,56 @@ namespace Kyoo.Controllers
///
/// Start a new task (or queue it).
///
- /// The slug of the task to run
- /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
- /// If the number of arguments is invalid or if an argument can't be converted.
- /// The task could not be found.
- void StartTask(string taskSlug, Dictionary arguments = null);
+ ///
+ /// The slug of the task to run.
+ ///
+ ///
+ /// A progress reporter to know the percentage of completion of the task.
+ ///
+ ///
+ /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
+ ///
+ ///
+ /// A custom cancellation token for the task.
+ ///
+ ///
+ /// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
+ /// invalid.
+ ///
+ ///
+ /// The task could not be found.
+ ///
+ void StartTask(string taskSlug,
+ [NotNull] IProgress progress,
+ Dictionary arguments = null,
+ CancellationToken? cancellationToken = null);
+
+ ///
+ /// Start a new task (or queue it).
+ ///
+ ///
+ /// A progress reporter to know the percentage of completion of the task.
+ ///
+ ///
+ /// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
+ ///
+ ///
+ /// The type of the task to start.
+ ///
+ ///
+ /// A custom cancellation token for the task.
+ ///
+ ///
+ /// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
+ /// invalid.
+ ///
+ ///
+ /// The task could not be found.
+ ///
+ void StartTask([NotNull] IProgress progress,
+ Dictionary arguments = null,
+ CancellationToken? cancellationToken = null)
+ where T : ITask, new();
///
/// Get all currently running tasks
diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs
index a1f35756..cd860ea3 100644
--- a/Kyoo.Common/Utility/Merger.cs
+++ b/Kyoo.Common/Utility/Merger.cs
@@ -112,7 +112,7 @@ namespace Kyoo
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// Fields of T will be merged
///
- [ContractAnnotation("=> null; first:notnull => notnull; second:notnull => notnull")]
+ [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T Merge([CanBeNull] T first, [CanBeNull] T second)
{
if (first == null)
diff --git a/Kyoo.TheTvdb/ProviderTVDB.cs b/Kyoo.TheTvdb/ProviderTVDB.cs
index 44841326..5497cac1 100644
--- a/Kyoo.TheTvdb/ProviderTVDB.cs
+++ b/Kyoo.TheTvdb/ProviderTVDB.cs
@@ -56,7 +56,7 @@ namespace Kyoo.TheTvdb
{
Show show => await _GetShow(show) as T,
Episode episode => await _GetEpisode(episode) as T,
- _ => throw new NotSupportedException()
+ _ => null
};
}
@@ -66,7 +66,11 @@ namespace Kyoo.TheTvdb
if (!int.TryParse(show.GetID(Provider.Slug), out int id))
return (await _SearchShow(show.Title)).FirstOrDefault();
TvDbResponse series = await _client.Series.GetAsync(id);
- return series.Data.ToShow(Provider);
+ Show ret = series.Data.ToShow(Provider);
+
+ TvDbResponse people = await _client.Series.GetActorsAsync(id);
+ ret.People = people.Data.Select(x => x.ToPeopleRole(Provider)).ToArray();
+ return ret;
}
[ItemCanBeNull]
@@ -88,7 +92,7 @@ namespace Kyoo.TheTvdb
await _Authenticate();
if (typeof(T) == typeof(Show))
return (await _SearchShow(query) as ICollection)!;
- throw new NotImplementedException();
+ return ArraySegment.Empty;
}
[ItemNotNull]
@@ -97,15 +101,5 @@ namespace Kyoo.TheTvdb
TvDbResponse shows = await _client.Search.SearchSeriesByNameAsync(query);
return shows.Data.Select(x => x.ToShow(Provider)).ToArray();
}
-
- ///
- public async Task> GetPeople(Show show)
- {
- if (!int.TryParse(show?.GetID(Provider.Name), out int id))
- return null;
- await _Authenticate();
- TvDbResponse people = await _client.Series.GetActorsAsync(id);
- return people.Data.Select(x => x.ToPeopleRole(Provider)).ToArray();
- }
}
}
\ No newline at end of file
diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileManager.cs
index 43b808b8..f6669c32 100644
--- a/Kyoo/Controllers/FileManager.cs
+++ b/Kyoo/Controllers/FileManager.cs
@@ -70,13 +70,14 @@ namespace Kyoo.Controllers
}
///
- public Task> ListFiles(string path)
+ public Task> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
- return Task.FromResult>(Directory.Exists(path)
- ? Directory.GetFiles(path)
- : Array.Empty());
+ string[] ret = Directory.Exists(path)
+ ? Directory.GetFiles(path, "*", options)
+ : Array.Empty();
+ return Task.FromResult>(ret);
}
///
diff --git a/Kyoo/Controllers/ProviderComposite.cs b/Kyoo/Controllers/ProviderComposite.cs
index 7d62266e..88ad7529 100644
--- a/Kyoo/Controllers/ProviderComposite.cs
+++ b/Kyoo/Controllers/ProviderComposite.cs
@@ -68,17 +68,13 @@ namespace Kyoo.Controllers
public async Task Get(T item)
where T : class, IResource
{
- T ret = null;
-
+ T ret = item;
+
foreach (IMetadataProvider provider in _GetProviders())
{
try
{
- ret = Merger.Merge(ret, await provider.Get(ret ?? item));
- }
- catch (NotSupportedException)
- {
- // Silenced
+ ret = Merger.Merge(ret, await provider.Get(ret));
}
catch (Exception ex)
{
@@ -87,7 +83,7 @@ namespace Kyoo.Controllers
}
}
- return Merger.Merge(ret, item);
+ return ret;
}
///
@@ -102,10 +98,6 @@ namespace Kyoo.Controllers
{
ret.AddRange(await provider.Search(query));
}
- catch (NotSupportedException)
- {
- // Silenced
- }
catch (Exception ex)
{
_logger.LogError(ex, "The provider {Provider} could not search for {Type}",
@@ -115,108 +107,5 @@ namespace Kyoo.Controllers
return ret;
}
-
- public Task> GetPeople(Show show)
- {
- throw new NotImplementedException();
- }
-
- // public async Task GetCollectionFromName(string name, Library library)
- // {
- // Collection collection = await GetMetadata(
- // provider => provider.GetCollectionFromName(name),
- // library,
- // $"the collection {name}");
- // collection.Name ??= name;
- // collection.Slug ??= Utility.ToSlug(name);
- // return collection;
- // }
- //
- // public async Task CompleteShow(Show show, Library library)
- // {
- // return await GetMetadata(provider => provider.GetShowByID(show), library, $"the show {show.Title}");
- // }
- //
- // public async Task SearchShow(string showName, bool isMovie, Library library)
- // {
- // Show show = await GetMetadata(async provider =>
- // {
- // Show searchResult = (await provider.SearchShows(showName, isMovie))?.FirstOrDefault();
- // if (searchResult == null)
- // return null;
- // return await provider.GetShowByID(searchResult);
- // }, library, $"the show {showName}");
- // show.Slug = Utility.ToSlug(showName);
- // show.Title ??= showName;
- // show.IsMovie = isMovie;
- // show.Genres = show.Genres?.GroupBy(x => x.Slug).Select(x => x.First()).ToList();
- // show.People = show.People?.GroupBy(x => x.Slug).Select(x => x.First()).ToList();
- // return show;
- // }
- //
- // public async Task> SearchShows(string showName, bool isMovie, Library library)
- // {
- // IEnumerable shows = await GetMetadata(
- // provider => provider.SearchShows(showName, isMovie),
- // library,
- // $"the show {showName}");
- // return shows.Select(show =>
- // {
- // show.Slug = Utility.ToSlug(showName);
- // show.Title ??= showName;
- // show.IsMovie = isMovie;
- // return show;
- // });
- // }
- //
- // public async Task GetSeason(Show show, int seasonNumber, Library library)
- // {
- // Season season = await GetMetadata(
- // provider => provider.GetSeason(show, seasonNumber),
- // library,
- // $"the season {seasonNumber} of {show.Title}");
- // season.Show = show;
- // season.ShowID = show.ID;
- // season.ShowSlug = show.Slug;
- // season.Title ??= $"Season {season.SeasonNumber}";
- // return season;
- // }
- //
- // public async Task GetEpisode(Show show,
- // string episodePath,
- // int? seasonNumber,
- // int? episodeNumber,
- // int? absoluteNumber,
- // Library library)
- // {
- // Episode episode = await GetMetadata(
- // provider => provider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber),
- // library,
- // "an episode");
- // episode.Show = show;
- // episode.ShowID = show.ID;
- // episode.ShowSlug = show.Slug;
- // episode.Path = episodePath;
- // episode.SeasonNumber ??= seasonNumber;
- // episode.EpisodeNumber ??= episodeNumber;
- // episode.AbsoluteNumber ??= absoluteNumber;
- // return episode;
- // }
- //
- // public async Task> GetPeople(Show show, Library library)
- // {
- // List people = await GetMetadata(
- // provider => provider.GetPeople(show),
- // library,
- // $"a cast member of {show.Title}");
- // return people?.GroupBy(x => x.Slug)
- // .Select(x => x.First())
- // .Select(x =>
- // {
- // x.Show = show;
- // x.ShowID = show.ID;
- // return x;
- // }).ToList();
- // }
}
}
diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs
index caa7a01d..c850a516 100644
--- a/Kyoo/Controllers/TaskManager.cs
+++ b/Kyoo/Controllers/TaskManager.cs
@@ -41,7 +41,7 @@ namespace Kyoo.Controllers
///
/// The queue of tasks that should be run as soon as possible.
///
- private readonly Queue<(ITask, Dictionary)> _queuedTasks = new();
+ private readonly Queue<(ITask, IProgress, Dictionary)> _queuedTasks = new();
///
/// The currently running task.
///
@@ -106,11 +106,11 @@ namespace Kyoo.Controllers
{
if (_queuedTasks.Any())
{
- (ITask task, Dictionary arguments) = _queuedTasks.Dequeue();
+ (ITask task, IProgress progress, Dictionary args) = _queuedTasks.Dequeue();
_runningTask = task;
try
{
- await RunTask(task, arguments);
+ await RunTask(task, progress, args);
}
catch (Exception e)
{
@@ -129,9 +129,15 @@ namespace Kyoo.Controllers
/// Parse parameters, inject a task and run it.
///
/// The task to run
+ /// A progress reporter to know the percentage of completion of the task.
/// The arguments to pass to the function
- /// There was an invalid argument or a required argument was not found.
- private async Task RunTask(ITask task, Dictionary arguments)
+ ///
+ /// 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,
+ [NotNull] IProgress progress,
+ Dictionary arguments)
{
_logger.LogInformation("Task starting: {Task}", task.Name);
@@ -160,7 +166,7 @@ namespace Kyoo.Controllers
using IServiceScope scope = _provider.CreateScope();
InjectServices(task, x => scope.ServiceProvider.GetRequiredService(x));
- await task.Run(args, _taskToken.Token);
+ await task.Run(args, progress, _taskToken.Token);
InjectServices(task, _ => null);
_logger.LogInformation("Task finished: {Task}", task.Name);
}
@@ -190,7 +196,7 @@ namespace Kyoo.Controllers
foreach (string task in tasksToQueue)
{
_logger.LogDebug("Queuing task scheduled for running: {Task}", task);
- StartTask(task, new Dictionary());
+ StartTask(task, new Progress(), new Dictionary());
}
}
@@ -203,21 +209,33 @@ namespace Kyoo.Controllers
.Where(x => x.RunOnStartup)
.OrderByDescending(x => x.Priority);
foreach (ITask task in startupTasks)
- _queuedTasks.Enqueue((task, new Dictionary()));
+ _queuedTasks.Enqueue((task, new Progress(), new Dictionary()));
}
///
- public void StartTask(string taskSlug, Dictionary arguments = null)
+ public void StartTask(string taskSlug,
+ IProgress progress,
+ Dictionary arguments = null,
+ CancellationToken? cancellationToken = null)
{
arguments ??= new Dictionary();
int index = _tasks.FindIndex(x => x.task.Slug == taskSlug);
if (index == -1)
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
- _queuedTasks.Enqueue((_tasks[index].task, arguments));
+ _queuedTasks.Enqueue((_tasks[index].task, progress, arguments));
_tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug));
}
+ ///
+ public void StartTask(IProgress progress,
+ Dictionary arguments = null,
+ CancellationToken? cancellationToken = null)
+ where T : ITask, new()
+ {
+ StartTask(new T().Slug, progress, arguments, cancellationToken);
+ }
+
///
/// Get the next date of the execution of the given task.
///
diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs
index e9d99010..a96f470e 100644
--- a/Kyoo/Tasks/Crawler.cs
+++ b/Kyoo/Tasks/Crawler.cs
@@ -1,6 +1,5 @@
using System;
using Kyoo.Models;
-using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -11,26 +10,56 @@ using Kyoo.Controllers;
using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace Kyoo.Tasks
{
+ ///
+ /// A task to add new video files.
+ ///
public class Crawler : ITask
{
+ ///
public string Slug => "scan";
+
+ ///
public string Name => "Scan libraries";
- public string Description => "Scan your libraries, load data for new shows and remove shows that don't exist anymore.";
- 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 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;
-
- [Injected] public IServiceProvider ServiceProvider { private get; set; }
- [Injected] public IThumbnailsManager ThumbnailsManager { private get; set; }
- [Injected] public IMetadataProvider MetadataProvider { private get; set; }
- [Injected] public ITranscoder Transcoder { private get; set; }
- [Injected] public IConfiguration Config { private get; set; }
- private int _parallelTasks;
+ ///
+ public bool IsHidden => false;
+
+ ///
+ /// The library manager used to get libraries and providers to use.
+ ///
+ [Injected] public ILibraryManager LibraryManager { private get; set; }
+ ///
+ /// The file manager used walk inside directories and check they existences.
+ ///
+ [Injected] public IFileManager FileManager { private get; set; }
+ ///
+ /// A task manager used to create sub tasks for each episode to add to the database.
+ ///
+ [Injected] public ITaskManager TaskManager { private get; set; }
+ ///
+ /// The logger used to inform the current status to the console.
+ ///
+ [Injected] public ILogger Logger { private get; set; }
+
+ ///
public TaskParameters GetParameters()
{
return new()
@@ -39,354 +68,83 @@ namespace Kyoo.Tasks
};
}
- public int? Progress()
+ ///
+ public async Task Run(TaskParameters arguments, IProgress progress, CancellationToken cancellationToken)
{
- // TODO implement this later.
- return null;
- }
-
- public async Task Run(TaskParameters parameters,
- CancellationToken cancellationToken)
- {
- string argument = parameters["slug"].As();
-
- _parallelTasks = Config.GetValue("parallelTasks");
- if (_parallelTasks <= 0)
- _parallelTasks = 30;
-
- using IServiceScope serviceScope = ServiceProvider.CreateScope();
- ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService();
-
- foreach (Show show in await libraryManager!.GetAll())
- if (!Directory.Exists(show.Path))
- await libraryManager.Delete(show);
-
- ICollection episodes = await libraryManager.GetAll();
- foreach (Episode episode in episodes)
- if (!File.Exists(episode.Path))
- await libraryManager.Delete(episode);
-
- ICollection