diff --git a/Kyoo.Common/Controllers/ICrawler.cs b/Kyoo.Common/Controllers/ICrawler.cs deleted file mode 100644 index 7373ca94..00000000 --- a/Kyoo.Common/Controllers/ICrawler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Kyoo.Controllers -{ - public interface ICrawler - { - Task StartAsync(CancellationToken cancellationToken); - } -} diff --git a/Kyoo.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs index 815c8395..643cce7a 100644 --- a/Kyoo.Common/Controllers/IPluginManager.cs +++ b/Kyoo.Common/Controllers/IPluginManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Kyoo.Models; namespace Kyoo.Controllers { @@ -6,6 +7,7 @@ namespace Kyoo.Controllers { public T GetPlugin(string name); public IEnumerable GetPlugins(); + public IEnumerable GetAllPlugins(); public void ReloadPlugins(); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITaskManager.cs b/Kyoo.Common/Controllers/ITaskManager.cs new file mode 100644 index 00000000..7dbe36ea --- /dev/null +++ b/Kyoo.Common/Controllers/ITaskManager.cs @@ -0,0 +1,9 @@ +namespace Kyoo.Controllers +{ + public interface ITaskManager + { + bool StartTask(string taskSlug); + + void ReloadTask(); + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Plugin.cs b/Kyoo.Common/Models/Plugin.cs index c79de89c..9965d1ba 100644 --- a/Kyoo.Common/Models/Plugin.cs +++ b/Kyoo.Common/Models/Plugin.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; + namespace Kyoo.Models { public interface IPlugin { public string Name { get; } + public IEnumerable Tasks { get; } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Task.cs b/Kyoo.Common/Models/Task.cs new file mode 100644 index 00000000..55820483 --- /dev/null +++ b/Kyoo.Common/Models/Task.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Kyoo.Models +{ + public interface ITask + { + public string Slug { get; } + public string Name { get; } + public string Description { get; } + public string HelpMessage { get; } + public bool RunOnStartup { get; } + public int Priority { get; } + public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index a49b55c6..3db6de80 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -62,6 +62,11 @@ namespace Kyoo.Controllers return from plugin in _plugins where plugin is T select (T)plugin; } + public IEnumerable GetAllPlugins() + { + return _plugins ?? new List(); + } + public void ReloadPlugins() { string pluginFolder = _config.GetValue("plugins"); @@ -86,6 +91,12 @@ namespace Kyoo.Controllers return null; } }).Where(x => x != null).ToList(); + if (!_plugins.Any()) + { + Console.WriteLine("No plugin enabled."); + return; + } + Console.WriteLine("Plugin enabled:"); foreach (IPlugin plugin in _plugins) Console.WriteLine($"\t{plugin.Name}"); diff --git a/Kyoo/Controllers/StartupCode.cs b/Kyoo/Controllers/StartupCode.cs deleted file mode 100644 index f0b28f4c..00000000 --- a/Kyoo/Controllers/StartupCode.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using IdentityServer4.EntityFramework.DbContexts; -using IdentityServer4.EntityFramework.Mappers; -using IdentityServer4.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Kyoo.Controllers -{ - public class StartupCode : BackgroundService - { - private readonly IServiceProvider _serviceProvider; - - public StartupCode(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - using (IServiceScope serviceScope = _serviceProvider.CreateScope()) - { - serviceScope.ServiceProvider.GetService().Database.Migrate(); - - ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService(); - identityContext.Database.Migrate(); - if (!identityContext.Clients.Any()) - { - foreach (Client client in IdentityContext.GetClients()) - identityContext.Clients.Add(client.ToEntity()); - identityContext.SaveChanges(); - } - if (!identityContext.IdentityResources.Any()) - { - foreach (IdentityResource resource in IdentityContext.GetIdentityResources()) - identityContext.IdentityResources.Add(resource.ToEntity()); - identityContext.SaveChanges(); - } - if (!identityContext.ApiResources.Any()) - { - foreach (ApiResource resource in IdentityContext.GetApis()) - identityContext.ApiResources.Add(resource.ToEntity()); - identityContext.SaveChanges(); - } - - IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); - pluginManager.ReloadPlugins(); - - ICrawler crawler = serviceScope.ServiceProvider.GetService(); - await crawler.StartAsync(stoppingToken); - } - } - } -} \ No newline at end of file diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs new file mode 100644 index 00000000..958394f8 --- /dev/null +++ b/Kyoo/Controllers/TaskManager.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Models; +using Kyoo.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Kyoo.Controllers +{ + public class TaskManager : BackgroundService, ITaskManager + { + private readonly IServiceProvider _serviceProvider; + private readonly IPluginManager _pluginManager; + + private List _tasks = new List(); + private CancellationTokenSource _taskToken = new CancellationTokenSource(); + private Queue _queuedTasks = new Queue(); + + public TaskManager(IServiceProvider serviceProvider, IPluginManager pluginManager) + { + _serviceProvider = serviceProvider; + _pluginManager = pluginManager; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + if (_queuedTasks.Any()) + await _queuedTasks.Dequeue().Run(_serviceProvider, _taskToken.Token); + else + await Task.Delay(10); + } + } + + public override Task StartAsync(CancellationToken cancellationToken) + { + ReloadTask(); + foreach (ITask task in _tasks.Where(x => x.RunOnStartup && x.Priority != Int32.MaxValue).OrderByDescending(x => x.Priority)) + _queuedTasks.Enqueue(task); + return base.StartAsync(cancellationToken); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _taskToken.Cancel(); + return base.StopAsync(cancellationToken); + } + + public bool StartTask(string taskSlug) + { + ITask task = _tasks.FirstOrDefault(x => x.Slug == taskSlug); + if (task == null) + return false; + _queuedTasks.Enqueue(task); + return true; + } + + public void ReloadTask() + { + _tasks.Clear(); + _tasks.AddRange(CoreTaskHolder.Tasks); + foreach (ITask task in _tasks.Where(x => x.RunOnStartup && x.Priority == Int32.MaxValue)) + task.Run(_serviceProvider, _taskToken.Token); + foreach (IPlugin plugin in _pluginManager.GetAllPlugins()) + _tasks.AddRange(plugin.Tasks); + } + } +} \ No newline at end of file diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 0c0e3131..f0999afc 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -114,13 +114,13 @@ namespace Kyoo }); services.AddScoped(); - services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); - services.AddHostedService(); + services.AddHostedService(provider => (TaskManager)provider.GetService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Kyoo/Tasks/CoreTaskHolder.cs b/Kyoo/Tasks/CoreTaskHolder.cs new file mode 100644 index 00000000..afe56737 --- /dev/null +++ b/Kyoo/Tasks/CoreTaskHolder.cs @@ -0,0 +1,15 @@ +using Kyoo.Controllers; +using Kyoo.Models; + +namespace Kyoo.Tasks +{ + public static class CoreTaskHolder + { + public static ITask[] Tasks = + { + new CreateDatabase(), + new PluginLoader(), + new Crawler() + }; + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Crawler.cs b/Kyoo/Tasks/Crawler.cs similarity index 86% rename from Kyoo/Controllers/Crawler.cs rename to Kyoo/Tasks/Crawler.cs index 1447125a..e6f2cbb0 100644 --- a/Kyoo/Controllers/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -8,26 +8,33 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Kyoo.Models.Watch; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class Crawler : ICrawler + public class Crawler : ITask { - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _metadataProvider; - private readonly ITranscoder _transcoder; - private readonly IConfiguration _config; - - public Crawler(ILibraryManager libraryManager, IProviderManager metadataProvider, ITranscoder transcoder, IConfiguration configuration) + 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 bool RunOnStartup => true; + public int Priority => 0; + + private ILibraryManager _libraryManager; + private IProviderManager _metadataProvider; + private ITranscoder _transcoder; + private IConfiguration _config; + + + public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken) { - _libraryManager = libraryManager; - _metadataProvider = metadataProvider; - _transcoder = transcoder; - _config = configuration; - } + using IServiceScope serviceScope = serviceProvider.CreateScope(); + _libraryManager = serviceScope.ServiceProvider.GetService(); + _metadataProvider = serviceScope.ServiceProvider.GetService(); + _transcoder = serviceScope.ServiceProvider.GetService(); + _config = serviceScope.ServiceProvider.GetService(); - public async Task StartAsync(CancellationToken cancellationToken) - { try { IEnumerable episodes = _libraryManager.GetAllEpisodes(); diff --git a/Kyoo/Tasks/CreateDatabase.cs b/Kyoo/Tasks/CreateDatabase.cs new file mode 100644 index 00000000..cd5394fb --- /dev/null +++ b/Kyoo/Tasks/CreateDatabase.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using IdentityServer4.EntityFramework.DbContexts; +using IdentityServer4.EntityFramework.Mappers; +using IdentityServer4.Models; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Tasks +{ + public class CreateDatabase : ITask + { + public string Slug => "create-database"; + public string Name => "Create the database"; + public string Description => "Create the database if it does not exit and initialize it with defaults value."; + public string HelpMessage => null; + public bool RunOnStartup => true; + public int Priority => Int32.MaxValue; + + public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + using IServiceScope serviceScope = serviceProvider.CreateScope(); + DatabaseContext databaseContext = serviceScope.ServiceProvider.GetService(); + ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService(); + + databaseContext.Database.Migrate(); + identityContext.Database.Migrate(); + + if (!identityContext.Clients.Any()) + { + foreach (Client client in IdentityContext.GetClients()) + identityContext.Clients.Add(client.ToEntity()); + identityContext.SaveChanges(); + } + if (!identityContext.IdentityResources.Any()) + { + foreach (IdentityResource resource in IdentityContext.GetIdentityResources()) + identityContext.IdentityResources.Add(resource.ToEntity()); + identityContext.SaveChanges(); + } + if (!identityContext.ApiResources.Any()) + { + foreach (ApiResource resource in IdentityContext.GetApis()) + identityContext.ApiResources.Add(resource.ToEntity()); + identityContext.SaveChanges(); + } + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Kyoo/Tasks/PluginLoader.cs b/Kyoo/Tasks/PluginLoader.cs new file mode 100644 index 00000000..8efdcabd --- /dev/null +++ b/Kyoo/Tasks/PluginLoader.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Tasks +{ + public class PluginLoader : ITask + { + public string Slug => "reload-plugin"; + public string Name => "Reload plugins"; + public string Description => "Reload all plugins from the plugin folder."; + public string HelpMessage => null; + public bool RunOnStartup => true; + public int Priority => Int32.MaxValue; + public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + using IServiceScope serviceScope = serviceProvider.CreateScope(); + IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); + pluginManager.ReloadPlugins(); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/AdminAPI.cs b/Kyoo/Views/API/AdminAPI.cs deleted file mode 100644 index 6e2af871..00000000 --- a/Kyoo/Views/API/AdminAPI.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading; -using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - [Route("api/[controller]")] - [ApiController] - public class AdminController : ControllerBase - { - [HttpGet("scan")] - [Authorize(Policy="Admin")] - public IActionResult ScanLibrary([FromServices] ICrawler crawler) - { - // The crawler is destroyed before the completion of this task. - // TODO implement an hosted service that can queue tasks from the controller. - crawler.StartAsync(new CancellationToken()); - return Ok("Scanning"); - } - } -} diff --git a/Kyoo/Views/API/TaskAPI.cs b/Kyoo/Views/API/TaskAPI.cs new file mode 100644 index 00000000..67f210a3 --- /dev/null +++ b/Kyoo/Views/API/TaskAPI.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Kyoo.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace Kyoo.Api +{ + [Route("api/[controller]")] + [ApiController] + public class TaskController : ControllerBase + { + private readonly ITaskManager _taskManager; + + public TaskController(ITaskManager taskManager) + { + _taskManager = taskManager; + } + + + [HttpGet("{taskSlug}")] + [Authorize(Policy="Admin")] + public IActionResult RunTask(string taskSlug) + { + if (_taskManager.StartTask(taskSlug)) + return Ok(); + return NotFound(); + } + } +}