Reworking the plugin manager

This commit is contained in:
Zoe Roux 2021-04-29 23:59:46 +02:00
parent 833447ded8
commit 79995ea191
39 changed files with 394 additions and 253 deletions

View File

@ -18,7 +18,7 @@ namespace Kyoo.Controllers
/// Get the repository corresponding to the T item. /// Get the repository corresponding to the T item.
/// </summary> /// </summary>
/// <typeparam name="T">The type you want</typeparam> /// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The repository corresponding</returns> /// <returns>The repository corresponding</returns>
IRepository<T> GetRepository<T>() where T : class, IResource; IRepository<T> GetRepository<T>() where T : class, IResource;
@ -82,7 +82,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the resource</param> /// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get<T>(int id) where T : class, IResource; Task<T> Get<T>(int id) where T : class, IResource;
@ -91,7 +91,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get<T>(string slug) where T : class, IResource; Task<T> Get<T>(string slug) where T : class, IResource;
@ -100,7 +100,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="where">The filter function.</param> /// <param name="where">The filter function.</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The first resource found that match the where function</returns> /// <returns>The first resource found that match the where function</returns>
Task<T> Get<T>(Expression<Func<T, bool>> where) where T : class, IResource; Task<T> Get<T>(Expression<Func<T, bool>> where) where T : class, IResource;
@ -109,7 +109,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber); Task<Season> Get(int showID, int seasonNumber);
@ -118,7 +118,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber); Task<Season> Get(string showSlug, int seasonNumber);
@ -128,7 +128,7 @@ namespace Kyoo.Controllers
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber); Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
@ -138,7 +138,7 @@ namespace Kyoo.Controllers
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
@ -147,7 +147,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the track</param> /// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The tracl found</returns> /// <returns>The tracl found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown); Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
@ -505,7 +505,7 @@ namespace Kyoo.Controllers
/// <param name="item">The resourcce to edit, it's ID can't change.</param> /// <param name="item">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam> /// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource; Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource;
@ -514,7 +514,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="item">The resource to delete</param> /// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(T item) where T : class, IResource; Task Delete<T>(T item) where T : class, IResource;
/// <summary> /// <summary>
@ -522,7 +522,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the resource to delete</param> /// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(int id) where T : class, IResource; Task Delete<T>(int id) where T : class, IResource;
/// <summary> /// <summary>
@ -530,7 +530,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource to delete</param> /// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(string slug) where T : class, IResource; Task Delete<T>(string slug) where T : class, IResource;
} }
} }

View File

@ -1,6 +1,6 @@
using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Unity; using Unity;
namespace Kyoo.Controllers namespace Kyoo.Controllers
@ -8,6 +8,8 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// A common interface used to discord plugins /// A common interface used to discord plugins
/// </summary> /// </summary>
/// <remarks>You can inject services in the IPlugin constructor.
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.</remarks>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IPlugin public interface IPlugin
{ {
@ -30,30 +32,33 @@ namespace Kyoo.Controllers
/// A list of services that are provided by this service. This allow other plugins to declare dependencies. /// A list of services that are provided by this service. This allow other plugins to declare dependencies.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The format should be the name of the interface ':' and the name of the implementation. /// You should put directly the type that you will register in configure, Kyoo will detect by itself which
/// For a plugins that provide a new service named IService with a default implementation named Koala, that would /// interfaces are implemented by your type.
/// be "IService:Koala".
/// </remarks> /// </remarks>
string[] Provides { get; } Type[] Provides { get; }
/// <summary> /// <summary>
/// A list of services that are required by this service. /// A list of services that are required by this service.
/// The Core will warn the user that this plugin can't be loaded if a required service is not found. /// The Core will warn the user that this plugin can't be loaded if a required service is not found.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is the same format as <see cref="Provides"/> but you may leave a blank implementation's name if you don't need a special one. /// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager,
/// For example, if you need a service named IService but you don't care what implementation it will be, you can use /// put typeof(ILibraryManager).
/// "IService:"
/// </remarks> /// </remarks>
string[] Requires { get; } Type[] Requires { get; }
/// <summary> /// <summary>
/// A configure method that will be run on plugin's startup. /// A configure method that will be run on plugin's startup.
/// </summary> /// </summary>
/// <param name="container">A unity container to register new services.</param> /// <param name="container">A unity container to register new services.</param>
/// <param name="config">The configuration, if you need values at config time (database connection strings...)</param> void Configure(IUnityContainer container);
/// <summary>
/// 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.
/// </summary>
/// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param> /// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param>
/// <param name="debugMode">True if the app should run in debug mode.</param> void ConfigureAspNet(IApplicationBuilder app) {}
void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode);
} }
} }

View File

@ -1,13 +1,39 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Models; using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A manager to load plugins and retrieve information from them.
/// </summary>
public interface IPluginManager public interface IPluginManager
{ {
/// <summary>
/// Get a single plugin that match the type and name given.
/// </summary>
/// <param name="name">The name of the plugin</param>
/// <typeparam name="T">The type of the plugin</typeparam>
/// <exception cref="ItemNotFoundException">If no plugins match the query</exception>
/// <returns>A plugin that match the queries</returns>
public T GetPlugin<T>(string name); public T GetPlugin<T>(string name);
public IEnumerable<T> GetPlugins<T>();
public IEnumerable<IPlugin> GetAllPlugins(); /// <summary>
/// Get all plugins of the given type.
/// </summary>
/// <typeparam name="T">The type of plugins to get</typeparam>
/// <returns>A list of plugins matching the given type or an empty list of none match.</returns>
public ICollection<T> GetPlugins<T>();
/// <summary>
/// Get all plugins currently running on Kyoo. This also includes deleted plugins if the app as not been restarted.
/// </summary>
/// <returns>All plugins currently loaded.</returns>
public ICollection<IPlugin> GetAllPlugins();
/// <summary>
/// Load new plugins from the plugin directory.
/// </summary>
/// <exception cref="MissingDependencyException">If a plugin can't be loaded because a dependency can't be resolved.</exception>
public void ReloadPlugins(); public void ReloadPlugins();
} }
} }

View File

@ -127,21 +127,21 @@ namespace Kyoo.Controllers
/// Get a resource from it's ID. /// Get a resource from it's ID.
/// </summary> /// </summary>
/// <param name="id">The id of the resource</param> /// <param name="id">The id of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get(int id); Task<T> Get(int id);
/// <summary> /// <summary>
/// Get a resource from it's slug. /// Get a resource from it's slug.
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get(string slug); Task<T> Get(string slug);
/// <summary> /// <summary>
/// Get the first resource that match the predicate. /// Get the first resource that match the predicate.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter the resource.</param> /// <param name="where">A predicate to filter the resource.</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get(Expression<Func<T, bool>> where); Task<T> Get(Expression<Func<T, bool>> where);
@ -221,7 +221,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="edited">The resourcce to edit, it's ID can't change.</param> /// <param name="edited">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit([NotNull] T edited, bool resetOld); Task<T> Edit([NotNull] T edited, bool resetOld);
@ -229,62 +229,62 @@ namespace Kyoo.Controllers
/// Delete a resource by it's ID /// Delete a resource by it's ID
/// </summary> /// </summary>
/// <param name="id">The ID of the resource</param> /// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete(int id); Task Delete(int id);
/// <summary> /// <summary>
/// Delete a resource by it's slug /// Delete a resource by it's slug
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete(string slug); Task Delete(string slug);
/// <summary> /// <summary>
/// Delete a resource /// Delete a resource
/// </summary> /// </summary>
/// <param name="obj">The resource to delete</param> /// <param name="obj">The resource to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete([NotNull] T obj); Task Delete([NotNull] T obj);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="objs">One or multiple resources to delete</param> /// <param name="objs">One or multiple resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="objs">An enumerable of resources to delete</param> /// <param name="objs">An enumerable of resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<T> objs); Task DeleteRange(IEnumerable<T> objs);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">One or multiple resources's id</param> /// <param name="ids">One or multiple resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">An enumearble of resources's id</param> /// <param name="ids">An enumearble of resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids); Task DeleteRange(IEnumerable<int> ids);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">One or multiple resources's slug</param> /// <param name="slugs">One or multiple resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">An enumerable of resources's slug</param> /// <param name="slugs">An enumerable of resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs); Task DeleteRange(IEnumerable<string> slugs);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange([NotNull] Expression<Func<T, bool>> where); Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
} }
@ -306,7 +306,7 @@ namespace Kyoo.Controllers
/// Get a show's slug from it's ID. /// Get a show's slug from it's ID.
/// </summary> /// </summary>
/// <param name="showID">The ID of the show</param> /// <param name="showID">The ID of the show</param>
/// <exception cref="ItemNotFound">If a show with the given ID is not found.</exception> /// <exception cref="ItemNotFoundException">If a show with the given ID is not found.</exception>
/// <returns>The show's slug</returns> /// <returns>The show's slug</returns>
Task<string> GetSlug(int showID); Task<string> GetSlug(int showID);
} }
@ -321,7 +321,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber); Task<Season> Get(int showID, int seasonNumber);
@ -330,7 +330,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber); Task<Season> Get(string showSlug, int seasonNumber);
@ -362,7 +362,7 @@ namespace Kyoo.Controllers
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber); Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
@ -371,7 +371,7 @@ namespace Kyoo.Controllers
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
@ -397,7 +397,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param> /// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> GetAbsolute(int showID, int absoluteNumber); Task<Episode> GetAbsolute(int showID, int absoluteNumber);
/// <summary> /// <summary>
@ -405,7 +405,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param> /// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber); Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
} }
@ -420,7 +420,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the track</param> /// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The tracl found</returns> /// <returns>The tracl found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown); Task<Track> Get(string slug, StreamType type = StreamType.Unknown);

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
@ -17,7 +16,7 @@ namespace Kyoo.Controllers
/// <param name="taskSlug">The slug of the task to run</param> /// <param name="taskSlug">The slug of the task to run</param>
/// <param name="arguments">A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.</param> /// <param name="arguments">A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.</param>
/// <exception cref="ArgumentException">If the number of arguments is invalid or if an argument can't be converted.</exception> /// <exception cref="ArgumentException">If the number of arguments is invalid or if an argument can't be converted.</exception>
/// <exception cref="ItemNotFound">The task could not be found.</exception> /// <exception cref="ItemNotFoundException">The task could not be found.</exception>
void StartTask(string taskSlug, Dictionary<string, object> arguments = null); void StartTask(string taskSlug, Dictionary<string, object> arguments = null);
/// <summary> /// <summary>
@ -27,7 +26,7 @@ namespace Kyoo.Controllers
ICollection<ITask> GetRunningTasks(); ICollection<ITask> GetRunningTasks();
/// <summary> /// <summary>
/// Get all availables tasks /// Get all available tasks
/// </summary> /// </summary>
/// <returns>A list of every tasks that this instance know.</returns> /// <returns>A list of every tasks that this instance know.</returns>
ICollection<ITask> GetAllTasks(); ICollection<ITask> GetAllTasks();

View File

@ -1,5 +1,4 @@
using Kyoo.Models; using Kyoo.Models;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@ -66,7 +66,7 @@ namespace Kyoo.Controllers
{ {
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret) if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
return ret; return ret;
throw new ItemNotFound(); throw new ItemNotFoundException();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -2,18 +2,25 @@ using System;
namespace Kyoo.Models.Exceptions namespace Kyoo.Models.Exceptions
{ {
/// <summary>
/// An exception raised when an item already exists in the database.
/// </summary>
[Serializable]
public class DuplicatedItemException : Exception public class DuplicatedItemException : Exception
{ {
public override string Message { get; } /// <summary>
/// Create a new <see cref="DuplicatedItemException"/> with the default message.
/// </summary>
public DuplicatedItemException() public DuplicatedItemException()
{ : base("Already exists in the database.")
Message = "Already exists in the databse."; { }
}
/// <summary>
/// Create a new <see cref="DuplicatedItemException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use</param>
public DuplicatedItemException(string message) public DuplicatedItemException(string message)
{ : base(message)
Message = message; { }
}
} }
} }

View File

@ -1,16 +0,0 @@
using System;
namespace Kyoo.Models.Exceptions
{
public class ItemNotFound : Exception
{
public override string Message { get; }
public ItemNotFound() {}
public ItemNotFound(string message)
{
Message = message;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace Kyoo.Models.Exceptions
{
/// <summary>
/// An exception raised when an item could not be found.
/// </summary>
[Serializable]
public class ItemNotFoundException : Exception
{
/// <summary>
/// Create a default <see cref="ItemNotFoundException"/> with no message.
/// </summary>
public ItemNotFoundException() {}
/// <summary>
/// Create a new <see cref="ItemNotFoundException"/> with a message
/// </summary>
/// <param name="message">The message of the exception</param>
public ItemNotFoundException(string message)
: base(message)
{ }
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace Kyoo.Models.Exceptions
{
/// <summary>
/// An exception raised when a plugin requires dependencies that can't be found with the current configuration.
/// </summary>
[Serializable]
public class MissingDependencyException : Exception
{
/// <summary>
/// Create a new <see cref="MissingDependencyException"/> with a custom message
/// </summary>
/// <param name="plugin">The name of the plugin that can't be loaded.</param>
/// <param name="dependency">The name of the missing dependency.</param>
public MissingDependencyException(string plugin, string dependency)
: base($"No {dependency} are available in kyoo but the plugin {plugin} requires it.")
{}
}
}

View File

@ -33,7 +33,7 @@ namespace Kyoo.CommonApi
{ {
return await _repository.Get(id); return await _repository.Get(id);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -47,7 +47,7 @@ namespace Kyoo.CommonApi
{ {
return await _repository.Get(slug); return await _repository.Get(slug);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -129,7 +129,7 @@ namespace Kyoo.CommonApi
resource.ID = old.ID; resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -144,7 +144,7 @@ namespace Kyoo.CommonApi
{ {
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -160,7 +160,7 @@ namespace Kyoo.CommonApi
resource.ID = old.ID; resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -174,7 +174,7 @@ namespace Kyoo.CommonApi
{ {
await _repository.Delete(id); await _repository.Delete(id);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -190,7 +190,7 @@ namespace Kyoo.CommonApi
{ {
await _repository.Delete(slug); await _repository.Delete(slug);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -205,7 +205,7 @@ namespace Kyoo.CommonApi
{ {
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where)); await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -9,14 +9,15 @@ namespace Kyoo
public static class Extensions public static class Extensions
{ {
/// <summary> /// <summary>
/// Get a connection string from the Configuration's section "Databse" /// Get a connection string from the Configuration's section "Database"
/// </summary> /// </summary>
/// <param name="config">The IConfiguration instance to load.</param> /// <param name="config">The IConfiguration instance to load.</param>
/// <param name="database">The database's name.</param>
/// <returns>A parsed connection string</returns> /// <returns>A parsed connection string</returns>
public static string GetDatabaseConnection(this IConfiguration config) public static string GetDatabaseConnection(this IConfiguration config, string database)
{ {
DbConnectionStringBuilder builder = new(); DbConnectionStringBuilder builder = new();
IConfigurationSection section = config.GetSection("Database"); IConfigurationSection section = config.GetSection("Database").GetSection(database);
foreach (IConfigurationSection child in section.GetChildren()) foreach (IConfigurationSection child in section.GetChildren())
builder[child.Key] = child.Value; builder[child.Key] = child.Value;
return builder.ConnectionString; return builder.ConnectionString;

View File

@ -46,13 +46,13 @@ namespace Kyoo.Controllers
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it. /// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
/// </summary> /// </summary>
/// <param name="id">The ID of the resource</param> /// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The tracked resource with the given ID</returns> /// <returns>The tracked resource with the given ID</returns>
protected virtual async Task<T> GetWithTracking(int id) protected virtual async Task<T> GetWithTracking(int id)
{ {
T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id); T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}"); throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret; return ret;
} }
@ -61,7 +61,7 @@ namespace Kyoo.Controllers
{ {
T ret = await GetOrDefault(id); T ret = await GetOrDefault(id);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}"); throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret; return ret;
} }
@ -70,7 +70,7 @@ namespace Kyoo.Controllers
{ {
T ret = await GetOrDefault(slug); T ret = await GetOrDefault(slug);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the slug {slug}"); throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}");
return ret; return ret;
} }
@ -79,7 +79,7 @@ namespace Kyoo.Controllers
{ {
T ret = await GetOrDefault(where); T ret = await GetOrDefault(where);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the given predicate."); throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret; return ret;
} }

View File

@ -2,10 +2,18 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<OutputPath>$(SolutionDir)/Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="../Kyoo/Kyoo.csproj" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
@ -15,4 +23,15 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
</ProjectReference>
</ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,6 @@
using System; using System;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql; using Npgsql;
namespace Kyoo.Postgresql namespace Kyoo.Postgresql

View File

@ -1,12 +1,9 @@
using System; using System;
using Kyoo.Controllers; using Kyoo.Controllers;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Unity; using Unity;
using Unity.Injection;
using Unity.Lifetime;
using Unity.Resolution;
namespace Kyoo.Postgresql namespace Kyoo.Postgresql
{ {
@ -25,25 +22,42 @@ namespace Kyoo.Postgresql
public string Description => "A database context for postgresql."; public string Description => "A database context for postgresql.";
/// <inheritdoc /> /// <inheritdoc />
public string[] Provides => new[] public Type[] Provides => new[]
{ {
$"{nameof(DatabaseContext)}:{nameof(PostgresContext)}" typeof(PostgresContext)
}; };
/// <inheritdoc /> /// <inheritdoc />
public string[] Requires => Array.Empty<string>(); public Type[] Requires => Array.Empty<Type>();
/// <inheritdoc />
public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) /// <summary>
/// The configuration to use. The database connection string is pulled from it.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The host environment to check if the app is in debug mode.
/// </summary>
private readonly IWebHostEnvironment _environment;
/// <summary>
/// Create a new postgres module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="env">The environment that will be used (if the env is in development mode, more information will be displayed on errors.</param>
public PostgresModule(IConfiguration configuration, IWebHostEnvironment env)
{ {
// options.UseNpgsql(_configuration.GetDatabaseConnection()); _configuration = configuration;
// // // .EnableSensitiveDataLogging() _environment = env;
// // // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); }
container.RegisterFactory<DatabaseContext>(_ => /// <inheritdoc />
{ public void Configure(IUnityContainer container)
return new PostgresContext(config.GetDatabaseConnection(), debugMode); {
}); container.RegisterFactory<DatabaseContext>(_ => new PostgresContext(
_configuration.GetDatabaseConnection("postgres"),
_environment.IsDevelopment()));
} }
} }
} }

View File

@ -4,100 +4,152 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader; using System.Runtime.Loader;
using Kyoo.Models; using Kyoo.Models.Exceptions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging;
using Unity;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class PluginDependencyLoader : AssemblyLoadContext /// <summary>
{ /// An implementation of <see cref="IPluginManager"/>.
private readonly AssemblyDependencyResolver _resolver; /// This is used to load plugins and retrieve information from them.
/// </summary>
public PluginDependencyLoader(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
return base.Load(assemblyName);
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
return LoadUnmanagedDllFromPath(libraryPath);
return base.LoadUnmanagedDll(unmanagedDllName);
}
}
public class PluginManager : IPluginManager public class PluginManager : IPluginManager
{ {
private readonly IServiceProvider _provider; /// <summary>
/// The unity container. It is given to the Configure method of plugins.
/// </summary>
private readonly IUnityContainer _container;
/// <summary>
/// The configuration to get the plugin's directory.
/// </summary>
private readonly IConfiguration _config; private readonly IConfiguration _config;
private List<IPlugin> _plugins; /// <summary>
/// The logger used by this class.
/// </summary>
private readonly ILogger<PluginManager> _logger;
/// <summary>
/// The list of plugins that are currently loaded.
/// </summary>
private readonly List<IPlugin> _plugins = new();
public PluginManager(IServiceProvider provider, IConfiguration config) /// <summary>
/// Create a new <see cref="PluginManager"/> instance.
/// </summary>
/// <param name="container">A unity container to allow plugins to register new entries</param>
/// <param name="config">The configuration instance, to get the plugin's directory path.</param>
/// <param name="logger">The logger used by this class.</param>
public PluginManager(IUnityContainer container,
IConfiguration config,
ILogger<PluginManager> logger)
{ {
_provider = provider; _container = container;
_config = config; _config = config;
_logger = logger;
} }
/// <inheritdoc />
public T GetPlugin<T>(string name) public T GetPlugin<T>(string name)
{ {
return (T)_plugins?.FirstOrDefault(x => x.Name == name && x is T); return (T)_plugins?.FirstOrDefault(x => x.Name == name && x is T);
} }
public IEnumerable<T> GetPlugins<T>() /// <inheritdoc />
public ICollection<T> GetPlugins<T>()
{ {
return _plugins?.OfType<T>() ?? new List<T>(); return _plugins?.OfType<T>().ToArray();
} }
public IEnumerable<IPlugin> GetAllPlugins() /// <inheritdoc />
public ICollection<IPlugin> GetAllPlugins()
{ {
return _plugins ?? new List<IPlugin>(); return _plugins;
} }
/// <inheritdoc />
public void ReloadPlugins() public void ReloadPlugins()
{ {
string pluginFolder = _config.GetValue<string>("plugins"); string pluginFolder = _config.GetValue<string>("plugins");
if (!Directory.Exists(pluginFolder)) if (!Directory.Exists(pluginFolder))
Directory.CreateDirectory(pluginFolder); Directory.CreateDirectory(pluginFolder);
string[] pluginsPaths = Directory.GetFiles(pluginFolder); _logger.LogTrace("Loading new plugins...");
_plugins = pluginsPaths.SelectMany(path => string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories);
ICollection<IPlugin> newPlugins = pluginsPaths.SelectMany(path =>
{ {
path = Path.GetFullPath(path); path = Path.GetFullPath(path);
try try
{ {
PluginDependencyLoader loader = new(path); PluginDependencyLoader loader = new(path);
Assembly ass = loader.LoadFromAssemblyPath(path); Assembly assembly = loader.LoadFromAssemblyPath(path);
return ass.GetTypes() return assembly.GetTypes()
.Where(x => typeof(IPlugin).IsAssignableFrom(x)) .Where(x => typeof(IPlugin).IsAssignableFrom(x))
.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)); .Where(x => _plugins.All(y => y.GetType() != x))
.Select(x => (IPlugin)_container.Resolve(x));
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"\nError loading the plugin at {path}.\n{ex.GetType().Name}: {ex.Message}\n"); _logger.LogError(ex, "Could not load the plugin at {Path}", path);
return Array.Empty<IPlugin>(); return Array.Empty<IPlugin>();
} }
}).ToList(); }).ToArray();
_plugins.AddRange(newPlugins);
if (!_plugins.Any())
ICollection<Type> available = _plugins.SelectMany(x => x.Provides).ToArray();
foreach (IPlugin plugin in newPlugins)
{ {
Console.WriteLine("\nNo plugin enabled.\n"); Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x)));
return; if (missing != null)
throw new MissingDependencyException(plugin.Name, missing.Name);
plugin.Configure(_container);
} }
Console.WriteLine("\nPlugin enabled:"); if (!_plugins.Any())
foreach (IPlugin plugin in _plugins) _logger.LogInformation("No plugin enabled");
Console.WriteLine($"\t{plugin.Name}"); else
Console.WriteLine(); _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
}
/// <summary>
/// A custom <see cref="AssemblyLoadContext"/> to load plugin's dependency if they are on the same folder.
/// </summary>
private class PluginDependencyLoader : AssemblyLoadContext
{
/// <summary>
/// The basic resolver that will be used to load dlls.
/// </summary>
private readonly AssemblyDependencyResolver _resolver;
/// <summary>
/// Create a new <see cref="PluginDependencyLoader"/> for the given path.
/// </summary>
/// <param name="pluginPath">The path of the plugin and it's dependencies</param>
public PluginDependencyLoader(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
/// <inheritdoc />
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
return base.Load(assemblyName);
}
/// <inheritdoc />
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
return LoadUnmanagedDllFromPath(libraryPath);
return base.LoadUnmanagedDll(unmanagedDllName);
}
} }
} }
} }

View File

@ -108,7 +108,7 @@ namespace Kyoo.Controllers
{ {
Episode ret = await GetOrDefault(showID, seasonNumber, episodeNumber); Episode ret = await GetOrDefault(showID, seasonNumber, episodeNumber);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}."); throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}.");
return ret; return ret;
} }
@ -117,7 +117,7 @@ namespace Kyoo.Controllers
{ {
Episode ret = await GetOrDefault(showSlug, seasonNumber, episodeNumber); Episode ret = await GetOrDefault(showSlug, seasonNumber, episodeNumber);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}."); throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}.");
return ret; return ret;
} }

View File

@ -154,7 +154,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null) if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
return items; return items;
} }
@ -169,7 +169,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null) if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
return items; return items;
} }
} }

View File

@ -131,7 +131,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!people.Any() && await _shows.Value.Get(showID) == null) if (!people.Any() && await _shows.Value.Get(showID) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
foreach (PeopleRole role in people) foreach (PeopleRole role in people)
role.ForPeople = true; role.ForPeople = true;
return people; return people;
@ -153,7 +153,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!people.Any() && await _shows.Value.Get(showSlug) == null) if (!people.Any() && await _shows.Value.Get(showSlug) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
foreach (PeopleRole role in people) foreach (PeopleRole role in people)
role.ForPeople = true; role.ForPeople = true;
return people; return people;
@ -174,7 +174,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!roles.Any() && await Get(id) == null) if (!roles.Any() && await Get(id) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
return roles; return roles;
} }
@ -193,7 +193,7 @@ namespace Kyoo.Controllers
sort, sort,
limit); limit);
if (!roles.Any() && await Get(slug) == null) if (!roles.Any() && await Get(slug) == null)
throw new ItemNotFound(); throw new ItemNotFoundException();
return roles; return roles;
} }
} }

View File

@ -88,7 +88,7 @@ namespace Kyoo.Controllers
{ {
Season ret = await GetOrDefault(showID, seasonNumber); Season ret = await GetOrDefault(showID, seasonNumber);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No season {seasonNumber} found for the show {showID}"); throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID); ret.ShowSlug = await _shows.GetSlug(showID);
return ret; return ret;
} }
@ -98,7 +98,7 @@ namespace Kyoo.Controllers
{ {
Season ret = await GetOrDefault(showSlug, seasonNumber); Season ret = await GetOrDefault(showSlug, seasonNumber);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No season {seasonNumber} found for the show {showSlug}"); throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug; ret.ShowSlug = showSlug;
return ret; return ret;
} }

View File

@ -46,7 +46,7 @@ namespace Kyoo.Controllers
{ {
Track ret = await GetOrDefault(slug, type); Track ret = await GetOrDefault(slug, type);
if (ret == null) if (ret == null)
throw new ItemNotFound($"No track found with the slug {slug} and the type {type}."); throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}.");
return ret; return ret;
} }

View File

@ -185,7 +185,7 @@ namespace Kyoo.Controllers
int index = _tasks.FindIndex(x => x.task.Slug == taskSlug); int index = _tasks.FindIndex(x => x.task.Slug == taskSlug);
if (index == -1) if (index == -1)
throw new ItemNotFound($"No task found with the slug {taskSlug}"); throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
_queuedTasks.Enqueue((_tasks[index].task, arguments)); _queuedTasks.Enqueue((_tasks[index].task, arguments));
_tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug)); _tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug));
} }

View File

@ -1,7 +1,6 @@
using System;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Tasks; using Kyoo.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Unity; using Unity;
using Unity.Lifetime; using Unity.Lifetime;
@ -22,42 +21,40 @@ namespace Kyoo
public string Description => "The core module containing default implementations."; public string Description => "The core module containing default implementations.";
/// <inheritdoc /> /// <inheritdoc />
public string[] Provides => new[] public Type[] Provides => new[]
{ {
$"{nameof(IFileManager)}:file", typeof(FileManager),
$"{nameof(ITranscoder)}:{nameof(Transcoder)}", typeof(Transcoder),
$"{nameof(IThumbnailsManager)}:{nameof(ThumbnailsManager)}", typeof(ThumbnailsManager),
$"{nameof(IProviderManager)}:{nameof(ProviderManager)}", typeof(ProviderManager),
$"{nameof(IPluginManager)}:{nameof(PluginManager)}", typeof(TaskManager),
$"{nameof(ITaskManager)}:{nameof(TaskManager)}", typeof(LibraryManager),
$"{nameof(ILibraryManager)}:{nameof(LibraryManager)}", typeof(LibraryRepository),
$"{nameof(ILibraryRepository)}:{nameof(LibraryRepository)}", typeof(LibraryItemRepository),
$"{nameof(ILibraryItemRepository)}:{nameof(LibraryItemRepository)}", typeof(CollectionRepository),
$"{nameof(ICollectionRepository)}:{nameof(CollectionRepository)}", typeof(ShowRepository),
$"{nameof(IShowRepository)}:{nameof(ShowRepository)}", typeof(SeasonRepository),
$"{nameof(ISeasonRepository)}:{nameof(SeasonRepository)}", typeof(EpisodeRepository),
$"{nameof(IEpisodeRepository)}:{nameof(EpisodeRepository)}", typeof(TrackRepository),
$"{nameof(ITrackRepository)}:{nameof(TrackRepository)}", typeof(PeopleRepository),
$"{nameof(IPeopleRepository)}:{nameof(PeopleRepository)}", typeof(StudioRepository),
$"{nameof(IStudioRepository)}:{nameof(StudioRepository)}", typeof(GenreRepository),
$"{nameof(IGenreRepository)}:{nameof(GenreRepository)}", typeof(ProviderRepository),
$"{nameof(IProviderRepository)}:{nameof(ProviderRepository)}"
}; };
/// <inheritdoc /> /// <inheritdoc />
public string[] Requires => new[] public Type[] Requires => new[]
{ {
"DatabaseContext:" typeof(DatabaseContext)
}; };
/// <inheritdoc /> /// <inheritdoc />
public void Configure(IUnityContainer container, IConfiguration config, IApplicationBuilder app, bool debugMode) public void Configure(IUnityContainer container)
{ {
container.RegisterType<IFileManager, FileManager>(new SingletonLifetimeManager()); container.RegisterType<IFileManager, FileManager>(new SingletonLifetimeManager());
container.RegisterType<ITranscoder, Transcoder>(new SingletonLifetimeManager()); container.RegisterType<ITranscoder, Transcoder>(new SingletonLifetimeManager());
container.RegisterType<IThumbnailsManager, ThumbnailsManager>(new SingletonLifetimeManager()); container.RegisterType<IThumbnailsManager, ThumbnailsManager>(new SingletonLifetimeManager());
container.RegisterType<IProviderManager, ProviderManager>(new SingletonLifetimeManager()); container.RegisterType<IProviderManager, ProviderManager>(new SingletonLifetimeManager());
container.RegisterType<IPluginManager, PluginManager>(new SingletonLifetimeManager());
container.RegisterType<ITaskManager, TaskManager>(new SingletonLifetimeManager()); container.RegisterType<ITaskManager, TaskManager>(new SingletonLifetimeManager());
container.RegisterType<ILibraryManager, LibraryManager>(new HierarchicalLifetimeManager()); container.RegisterType<ILibraryManager, LibraryManager>(new HierarchicalLifetimeManager());

View File

@ -5,9 +5,9 @@
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<SpaRoot>../Kyoo.WebApp/</SpaRoot> <SpaRoot>$(SolutionDir)/Kyoo.WebApp/</SpaRoot>
<LoginRoot>../Kyoo.WebLogin/</LoginRoot> <LoginRoot>$(SolutionDir)/Kyoo.WebLogin/</LoginRoot>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot> <TranscoderRoot>$(SolutionDir)/Kyoo.Transcoder/</TranscoderRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules/**</DefaultItemExcludes> <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules/**</DefaultItemExcludes>
<!-- Set this to true if you enable server-side prerendering --> <!-- Set this to true if you enable server-side prerendering -->

View File

@ -66,7 +66,7 @@ namespace Kyoo
} }
/// <summary> /// <summary>
/// Createa a web host /// Create a a web host
/// </summary> /// </summary>
/// <param name="args">Command line parameters that can be handled by kestrel</param> /// <param name="args">Command line parameters that can be handled by kestrel</param>
/// <returns>A new web host instance</returns> /// <returns>A new web host instance</returns>

View File

@ -61,16 +61,9 @@ namespace Kyoo
}); });
services.AddHttpClient(); services.AddHttpClient();
services.AddDbContext<DatabaseContext>(options =>
{
options.UseNpgsql(_configuration.GetDatabaseConnection());
// .EnableSensitiveDataLogging()
// .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
}, ServiceLifetime.Transient);
services.AddDbContext<IdentityDatabase>(options => services.AddDbContext<IdentityDatabase>(options =>
{ {
options.UseNpgsql(_configuration.GetDatabaseConnection()); options.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
}); });
string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
@ -94,13 +87,13 @@ namespace Kyoo
.AddConfigurationStore(options => .AddConfigurationStore(options =>
{ {
options.ConfigureDbContext = builder => options.ConfigureDbContext = builder =>
builder.UseNpgsql(_configuration.GetDatabaseConnection(), builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"),
sql => sql.MigrationsAssembly(assemblyName)); sql => sql.MigrationsAssembly(assemblyName));
}) })
.AddOperationalStore(options => .AddOperationalStore(options =>
{ {
options.ConfigureDbContext = builder => options.ConfigureDbContext = builder =>
builder.UseNpgsql(_configuration.GetDatabaseConnection(), builder.UseNpgsql(_configuration.GetDatabaseConnection("postgres"),
sql => sql.MigrationsAssembly(assemblyName)); sql => sql.MigrationsAssembly(assemblyName));
options.EnableTokenCleanup = true; options.EnableTokenCleanup = true;
}) })
@ -147,9 +140,6 @@ namespace Kyoo
{ {
AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) } AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) }
}); });
services.AddScoped<DbContext, DatabaseContext>();
} }
public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env)
@ -214,11 +204,14 @@ namespace Kyoo
if (env.IsDevelopment()) if (env.IsDevelopment())
spa.UseAngularCliServer("start"); spa.UseAngularCliServer("start");
}); });
container.RegisterType<IPluginManager, PluginManager>(new SingletonLifetimeManager());
IPluginManager pluginManager = new PluginManager(container, _configuration, new Logger<PluginManager>(_loggerFactory));
pluginManager.ReloadPlugins();
new CoreModule().Configure(container, _configuration, app, env.IsDevelopment());
container.RegisterFactory<IHostedService>(c => c.Resolve<ITaskManager>(), new SingletonLifetimeManager());
// TODO the reload should re inject components from the constructor. // TODO the reload should re inject components from the constructor.
// TODO fin a way to inject tasks without a IUnityContainer. // TODO fin a way to inject tasks without a IUnityContainer.
container.RegisterFactory<IHostedService>(c => c.Resolve<ITaskManager>(), new SingletonLifetimeManager());
} }
} }
} }

View File

@ -177,7 +177,7 @@ namespace Kyoo.Tasks
await libraryManager.Create(track); await libraryManager.Create(track);
Console.WriteLine($"Registering subtitle at: {path}."); Console.WriteLine($"Registering subtitle at: {path}.");
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}."); await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
} }
@ -317,7 +317,7 @@ namespace Kyoo.Tasks
season.Show = show; season.Show = show;
return season; return season;
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
Season season = await MetadataProvider.GetSeason(show, seasonNumber, library); Season season = await MetadataProvider.GetSeason(show, seasonNumber, library);
await libraryManager.CreateIfNotExists(season); await libraryManager.CreateIfNotExists(season);

View File

@ -68,7 +68,7 @@ namespace Kyoo.Api
{ {
return await _libraryManager.Get(showSlug, seasonNumber); return await _libraryManager.Get(showSlug, seasonNumber);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -82,7 +82,7 @@ namespace Kyoo.Api
{ {
return await _libraryManager.Get(showID, seasonNumber); return await _libraryManager.Get(showID, seasonNumber);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -183,7 +183,7 @@ namespace Kyoo.Api
Episode episode = await _libraryManager.Get<Episode>(id); Episode episode = await _libraryManager.Get<Episode>(id);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -198,7 +198,7 @@ namespace Kyoo.Api
Episode episode = await _libraryManager.Get<Episode>(slug); Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -47,7 +47,7 @@ namespace Kyoo.Api
Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase),
limit); limit);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -48,7 +48,7 @@ namespace Kyoo.Api
return Page(resources, limit); return Page(resources, limit);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -76,7 +76,7 @@ namespace Kyoo.Api
return Page(resources, limit); return Page(resources, limit);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -247,7 +247,7 @@ namespace Kyoo.Api
{ {
return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.ID == showID)); return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.ID == showID));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -261,7 +261,7 @@ namespace Kyoo.Api
{ {
return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.Slug == slug)); return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.Slug == slug));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -384,7 +384,7 @@ namespace Kyoo.Api
.ToDictionary(Path.GetFileNameWithoutExtension, .ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}");
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -401,7 +401,7 @@ namespace Kyoo.Api
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug);
return _files.FileResult(path); return _files.FileResult(path);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -416,7 +416,7 @@ namespace Kyoo.Api
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowPoster(show)); return _files.FileResult(await _thumbs.GetShowPoster(show));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -431,7 +431,7 @@ namespace Kyoo.Api
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowLogo(show)); return _files.FileResult(await _thumbs.GetShowLogo(show));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -446,7 +446,7 @@ namespace Kyoo.Api
Show show = await _libraryManager.Get<Show>(slug); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowBackdrop(show)); return _files.FileResult(await _thumbs.GetShowBackdrop(show));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -36,7 +36,7 @@ namespace Kyoo.Api
_taskManager.StartTask(taskSlug, args); _taskManager.StartTask(taskSlug, args);
return Ok(); return Ok();
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -31,7 +31,7 @@ namespace Kyoo.Api
{ {
return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.ID == id)); return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.ID == id));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -47,7 +47,7 @@ namespace Kyoo.Api
// TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor // TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor
return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.Slug == slug)); return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.Slug == slug));
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -52,7 +52,7 @@ namespace Kyoo.Api
Episode episode = await _libraryManager.Get<Episode>(slug); Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(episode.Path, true); return _files.FileResult(episode.Path, true);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -71,7 +71,7 @@ namespace Kyoo.Api
return StatusCode(500); return StatusCode(500);
return _files.FileResult(path, true); return _files.FileResult(path, true);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }
@ -90,7 +90,7 @@ namespace Kyoo.Api
return StatusCode(500); return StatusCode(500);
return _files.FileResult(path, true); return _files.FileResult(path, true);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -27,7 +27,7 @@ namespace Kyoo.Api
Episode item = await _libraryManager.Get<Episode>(slug); Episode item = await _libraryManager.Get<Episode>(slug);
return await WatchItem.FromEpisode(item, _libraryManager); return await WatchItem.FromEpisode(item, _libraryManager);
} }
catch (ItemNotFound) catch (ItemNotFoundException)
{ {
return NotFound(); return NotFound();
} }

View File

@ -3,14 +3,16 @@
"public_url": "http://localhost:5000/", "public_url": "http://localhost:5000/",
"database": { "database": {
"server": "127.0.0.1", "postgres": {
"port": "5432", "server": "127.0.0.1",
"database": "kyooDB", "port": "5432",
"user ID": "kyoo", "database": "kyooDB",
"password": "kyooPassword", "user ID": "kyoo",
"pooling": "true", "password": "kyooPassword",
"maxPoolSize": "95", "pooling": "true",
"timeout": "30" "maxPoolSize": "95",
"timeout": "30"
}
}, },
"logging": { "logging": {