diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs
index 8e2c78c4..1e9dcd01 100644
--- a/Kyoo.Authentication/AuthenticationModule.cs
+++ b/Kyoo.Authentication/AuthenticationModule.cs
@@ -9,6 +9,7 @@ using IdentityServer4.Services;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Views;
using Kyoo.Controllers;
+using Kyoo.Models.Attributes;
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -64,6 +65,11 @@ namespace Kyoo.Authentication
/// The environment information to check if the app runs in debug mode
///
private readonly IWebHostEnvironment _environment;
+
+ ///
+ /// The configuration manager used to register typed/untyped implementations.
+ ///
+ [Injected] public IConfigurationManager ConfigurationManager { private get; set; }
///
@@ -98,9 +104,7 @@ namespace Kyoo.Authentication
services.Configure(_configuration.GetSection(PermissionOption.Path));
services.Configure(_configuration.GetSection(CertificateOption.Path));
services.Configure(_configuration.GetSection(AuthenticationOption.Path));
- services.AddConfiguration(AuthenticationOption.Path);
-
-
+
List clients = new();
_configuration.GetSection("authentication:clients").Bind(clients);
CertificateOption certificateOptions = new();
@@ -139,6 +143,8 @@ namespace Kyoo.Authentication
///
public void ConfigureAspNet(IApplicationBuilder app)
{
+ ConfigurationManager.AddTyped(AuthenticationOption.Path);
+
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict
diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs
index 59d964f0..4288250e 100644
--- a/Kyoo.Authentication/Views/AccountApi.cs
+++ b/Kyoo.Authentication/Views/AccountApi.cs
@@ -36,7 +36,7 @@ namespace Kyoo.Authentication.Views
///
/// A file manager to send profile pictures
///
- private readonly IFileManager _files;
+ private readonly IFileSystem _files;
///
/// Options about authentication. Those options are monitored and reloads are supported.
///
@@ -50,7 +50,7 @@ namespace Kyoo.Authentication.Views
/// A file manager to send profile pictures
/// Authentication options (this may be hot reloaded)
public AccountApi(IUserRepository users,
- IFileManager files,
+ IFileSystem files,
IOptions options)
{
_users = users;
@@ -205,8 +205,8 @@ namespace Kyoo.Authentication.Views
user.Username = data.Username;
if (data.Picture?.Length > 0)
{
- string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
- await using Stream file = _files.NewFile(path);
+ string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
+ await using Stream file = await _files.NewFile(path);
await data.Picture.CopyToAsync(file);
}
return await _users.Edit(user, false);
diff --git a/Kyoo.Common/Controllers/IConfigurationManager.cs b/Kyoo.Common/Controllers/IConfigurationManager.cs
index 9159d92c..02430b10 100644
--- a/Kyoo.Common/Controllers/IConfigurationManager.cs
+++ b/Kyoo.Common/Controllers/IConfigurationManager.cs
@@ -12,6 +12,21 @@ namespace Kyoo.Controllers
///
public interface IConfigurationManager
{
+ ///
+ /// Add an editable configuration to the editable configuration list
+ ///
+ /// The root path of the editable configuration. It should not be a nested type.
+ /// The type of the configuration
+ void AddTyped(string path);
+
+ ///
+ /// Add an editable configuration to the editable configuration list.
+ /// WARNING: this method allow you to add an unmanaged type. This type won't be editable. This can be used
+ /// for external libraries or variable arguments.
+ ///
+ /// The root path of the editable configuration. It should not be a nested type.
+ void AddUntyped(string path);
+
///
/// Get the value of a setting using it's path.
///
diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileSystem.cs
similarity index 76%
rename from Kyoo.Common/Controllers/IFileManager.cs
rename to Kyoo.Common/Controllers/IFileSystem.cs
index 03f22e79..2fb1c6d7 100644
--- a/Kyoo.Common/Controllers/IFileManager.cs
+++ b/Kyoo.Common/Controllers/IFileSystem.cs
@@ -10,7 +10,7 @@ namespace Kyoo.Controllers
///
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
///
- public interface IFileManager
+ public interface IFileSystem
{
// TODO find a way to handle Transmux/Transcode with this system.
@@ -41,21 +41,37 @@ namespace Kyoo.Controllers
/// The path of the file
/// If the file could not be found.
/// A reader to read the file.
- public Stream GetReader([NotNull] string path);
+ public Task GetReader([NotNull] string path);
///
/// Create a new file at .
///
/// The path of the new file.
/// A writer to write to the new file.
- public Stream NewFile([NotNull] string path);
+ public Task NewFile([NotNull] string path);
+
+ ///
+ /// Create a new directory at the given path
+ ///
+ /// The path of the directory
+ /// The path of the newly created directory is returned.
+ public Task CreateDirectory([NotNull] string path);
+
+ ///
+ /// Combine multiple paths.
+ ///
+ /// The paths to combine
+ /// The combined path.
+ public string Combine(params string[] paths);
///
/// 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.
@@ -71,24 +87,6 @@ namespace Kyoo.Controllers
///
/// The show to proceed
/// The extra directory of the show
- public string GetExtraDirectory(Show show);
-
- ///
- /// Get the extra directory of a season.
- /// This method is in this system to allow a filesystem to use a different metadata policy for one.
- /// It can be useful if the filesystem is readonly.
- ///
- /// The season to proceed
- /// The extra directory of the season
- public string GetExtraDirectory(Season season);
-
- ///
- /// Get the extra directory of an episode.
- /// This method is in this system to allow a filesystem to use a different metadata policy for one.
- /// It can be useful if the filesystem is readonly.
- ///
- /// The episode to proceed
- /// The extra directory of the episode
- public string GetExtraDirectory(Episode episode);
+ public string GetExtraDirectory([NotNull] Show show);
}
}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/IIdentifier.cs b/Kyoo.Common/Controllers/IIdentifier.cs
new file mode 100644
index 00000000..5d11c53b
--- /dev/null
+++ b/Kyoo.Common/Controllers/IIdentifier.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using Kyoo.Models;
+using Kyoo.Models.Exceptions;
+
+namespace Kyoo.Controllers
+{
+ ///
+ /// An interface to identify episodes, shows and metadata based on the episode file.
+ ///
+ public interface IIdentifier
+ {
+ ///
+ /// Identify a path and return the parsed metadata.
+ ///
+ /// The path of the episode file to parse.
+ ///
+ /// The identifier could not work for the given path.
+ ///
+ ///
+ /// A tuple of models representing parsed metadata.
+ /// If no metadata could be parsed for a type, null can be returned.
+ ///
+ Task<(Collection, Show, Season, Episode)> Identify(string path);
+
+ ///
+ /// Identify an external subtitle or track file from it's path and return the parsed metadata.
+ ///
+ /// The path of the external track file to parse.
+ ///
+ /// The identifier could not work for the given path.
+ ///
+ ///
+ /// The metadata of the track identified.
+ ///
+ Task IdentifyTrack(string path);
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs
index a0c30cbb..ed6ab40d 100644
--- a/Kyoo.Common/Controllers/IMetadataProvider.cs
+++ b/Kyoo.Common/Controllers/IMetadataProvider.cs
@@ -1,21 +1,76 @@
using Kyoo.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
+using JetBrains.Annotations;
namespace Kyoo.Controllers
{
+ ///
+ /// An interface to automatically retrieve metadata from external providers.
+ ///
public interface IMetadataProvider
{
+ ///
+ /// The corresponding to this provider.
+ /// This allow to map metadata to a provider, keep metadata links and
+ /// know witch is used for a specific .
+ ///
Provider Provider { get; }
- Task GetCollectionFromName(string name);
+ ///
+ /// Return a new item with metadata from your provider.
+ ///
+ ///
+ /// The item to retrieve metadata from. Most of the time, only the name will be available but other
+ /// properties may be filed by other providers before a call to this method. This can allow you to identify
+ /// the collection on your provider.
+ ///
+ ///
+ /// You must not use metadata from the given .
+ /// 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.
+ ///
+ /// A new containing metadata from your provider or null
+ [ItemCanBeNull]
+ Task Get([NotNull] T item)
+ where T : class, IResource;
- Task GetShowByID(Show show);
- Task> SearchShows(string showName, bool isMovie);
- Task> GetPeople(Show show);
+ ///
+ /// Search for a specific type of items with a given query.
+ ///
+ /// The search query to use.
+ /// The list of items that could be found on this specific provider.
+ [ItemNotNull]
+ Task> Search(string query)
+ where T : class, IResource;
+ }
- Task GetSeason(Show show, int seasonNumber);
+ ///
+ /// A special that merge results.
+ /// This interface exists to specify witch provider to use but it can be used like any other metadata provider.
+ ///
+ public abstract class AProviderComposite : IMetadataProvider
+ {
+ ///
+ [ItemNotNull]
+ public abstract Task Get(T item)
+ where T : class, IResource;
- Task GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber);
+ ///
+ public abstract Task> Search(string query)
+ where T : class, IResource;
+
+ ///
+ /// Since this is a composite and not a real provider, no metadata is available.
+ /// It is not meant to be stored or selected. This class will handle merge based on what is required.
+ ///
+ public Provider Provider => null;
+
+ ///
+ /// Select witch providers to use.
+ /// The associated with the given will be used.
+ ///
+ /// The list of providers to use
+ public abstract void UseProviders(IEnumerable providers);
}
}
diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs
index 3201df83..ea072c39 100644
--- a/Kyoo.Common/Controllers/IPlugin.cs
+++ b/Kyoo.Common/Controllers/IPlugin.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Autofac;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -10,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
{
@@ -55,6 +58,15 @@ namespace Kyoo.Controllers
///
ICollection Requires { get; }
+ ///
+ /// A configure method that will be run on plugin's startup.
+ ///
+ /// The autofac service container to register services.
+ void Configure(ContainerBuilder builder)
+ {
+ // Skipped
+ }
+
///
/// A configure method that will be run on plugin's startup.
///
@@ -64,21 +76,34 @@ namespace Kyoo.Controllers
/// or >
/// You can't simply check on the service collection because some dependencies might be registered after your plugin.
///
- void Configure(IServiceCollection services, ICollection availableTypes);
+ void Configure(IServiceCollection services, ICollection availableTypes)
+ {
+ // Skipped
+ }
+
///
/// 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.
- void ConfigureAspNet(IApplicationBuilder app) {}
-
+ ///
+ /// 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
+ }
+
///
/// An optional function to execute and initialize your plugin.
/// It can be used to initialize a database connection, fill initial data or anything.
///
/// A service provider to request services
- void Initialize(IServiceProvider provider) {}
+ void Initialize(IServiceProvider provider)
+ {
+ // Skipped
+ }
}
///
diff --git a/Kyoo.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs
index bd4ef513..04d308f3 100644
--- a/Kyoo.Common/Controllers/IPluginManager.cs
+++ b/Kyoo.Common/Controllers/IPluginManager.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Autofac;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -42,7 +43,13 @@ namespace Kyoo.Controllers
public void LoadPlugins(ICollection plugins);
///
- /// Configure services adding or removing services as the plugins wants.
+ /// Configure container adding or removing services as the plugins wants.
+ ///
+ /// The container to populate
+ void ConfigureContainer(ContainerBuilder builder);
+
+ ///
+ /// Configure services via the microsoft way. This allow libraries to add their services.
///
/// The service collection to populate
public void ConfigureServices(IServiceCollection services);
diff --git a/Kyoo.Common/Controllers/IProviderManager.cs b/Kyoo.Common/Controllers/IProviderManager.cs
deleted file mode 100644
index dd83a283..00000000
--- a/Kyoo.Common/Controllers/IProviderManager.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Kyoo.Models;
-
-namespace Kyoo.Controllers
-{
- public interface IProviderManager
- {
- Task GetCollectionFromName(string name, Library library);
- Task CompleteShow(Show show, Library library);
- Task SearchShow(string showName, bool isMovie, Library library);
- Task> SearchShows(string showName, bool isMovie, Library library);
- Task GetSeason(Show show, int seasonNumber, Library library);
- Task GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, Library library);
- Task> GetPeople(Show show, Library library);
- }
-}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs
index f7be69ad..a83d17fa 100644
--- a/Kyoo.Common/Controllers/IRepository.cs
+++ b/Kyoo.Common/Controllers/IRepository.cs
@@ -128,6 +128,7 @@ namespace Kyoo.Controllers
/// The id of the resource
/// If the item could not be found.
/// The resource found
+ [ItemNotNull]
Task Get(int id);
///
/// Get a resource from it's slug.
@@ -135,6 +136,7 @@ namespace Kyoo.Controllers
/// The slug of the resource
/// If the item could not be found.
/// The resource found
+ [ItemNotNull]
Task Get(string slug);
///
/// Get the first resource that match the predicate.
@@ -142,6 +144,7 @@ namespace Kyoo.Controllers
/// A predicate to filter the resource.
/// If the item could not be found.
/// The resource found
+ [ItemNotNull]
Task Get(Expression> where);
///
@@ -149,18 +152,21 @@ namespace Kyoo.Controllers
///
/// The id of the resource
/// The resource found
+ [ItemCanBeNull]
Task GetOrDefault(int id);
///
/// Get a resource from it's slug or null if it is not found.
///
/// The slug of the resource
/// The resource found
+ [ItemCanBeNull]
Task GetOrDefault(string slug);
///
/// Get the first resource that match the predicate or null if it is not found.
///
/// A predicate to filter the resource.
/// The resource found
+ [ItemCanBeNull]
Task GetOrDefault(Expression> where);
///
@@ -168,6 +174,7 @@ namespace Kyoo.Controllers
///
/// The query string.
/// A list of resources found
+ [ItemNotNull]
Task> Search(string query);
///
@@ -177,6 +184,7 @@ namespace Kyoo.Controllers
/// Sort information about the query (sort by, sort order)
/// How pagination should be done (where to start and how many to return)
/// A list of resources that match every filters
+ [ItemNotNull]
Task> GetAll(Expression> where = null,
Sort sort = default,
Pagination limit = default);
@@ -187,6 +195,7 @@ namespace Kyoo.Controllers
/// A sort by predicate. The order is ascending.
/// How pagination should be done (where to start and how many to return)
/// A list of resources that match every filters
+ [ItemNotNull]
Task> GetAll([Optional] Expression> where,
Expression> sort,
Pagination limit = default
@@ -205,6 +214,7 @@ namespace Kyoo.Controllers
///
/// The item to register
/// The resource registers and completed by database's information (related items & so on)
+ [ItemNotNull]
Task Create([NotNull] T obj);
///
@@ -212,6 +222,7 @@ namespace Kyoo.Controllers
///
/// The object to create
/// The newly created item or the existing value if it existed.
+ [ItemNotNull]
Task CreateIfNotExists([NotNull] T obj);
///
@@ -221,6 +232,7 @@ namespace Kyoo.Controllers
/// Should old properties of the resource be discarded or should null values considered as not changed?
/// If the item is not found
/// The resource edited and completed by database's information (related items & so on)
+ [ItemNotNull]
Task Edit([NotNull] T edited, bool resetOld);
///
diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs
index 75277dd2..e8d07000 100644
--- a/Kyoo.Common/Controllers/ITask.cs
+++ b/Kyoo.Common/Controllers/ITask.cs
@@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Kyoo.Models;
using Kyoo.Models.Attributes;
+using Kyoo.Models.Exceptions;
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 +63,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.
///
@@ -94,6 +115,17 @@ namespace Kyoo.Controllers
/// The value of this parameter.
public T As()
{
+ if (typeof(T) == typeof(object))
+ return (T)Value;
+
+ if (Value is IResource resource)
+ {
+ if (typeof(T) == typeof(string))
+ return (T)(object)resource.Slug;
+ if (typeof(T) == typeof(int))
+ return (T)(object)resource.ID;
+ }
+
return (T)Convert.ChangeType(Value, typeof(T));
}
}
@@ -131,58 +163,38 @@ namespace Kyoo.Controllers
public interface ITask
{
///
- /// The slug of the task, used to start it.
+ /// The list of parameters
///
- public string Slug { get; }
-
- ///
- /// The name of the task that will be displayed to the user.
- ///
- public string Name { get; }
-
- ///
- /// A quick description of what this task will do.
- ///
- public string Description { get; }
-
- ///
- /// An optional message to display to help the user.
- ///
- public string HelpMessage { get; }
-
- ///
- /// Should this task be automatically run at app startup?
- ///
- public bool RunOnStartup { get; }
-
- ///
- /// The priority of this task. Only used if is true.
- /// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
- ///
- public int Priority { get; }
+ ///
+ /// All parameters that this task as. Every one of them will be given to the run function with a value.
+ ///
+ public TaskParameters GetParameters();
///
/// Start this task.
///
- /// The list of parameters.
+ ///
+ /// 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.
+ /// 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(TaskParameters arguments, CancellationToken cancellationToken);
-
- ///
- /// The list of parameters
- ///
- /// 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.
- ///
- /// The percentage of completion of the task.
- public int? Progress();
+ ///
+ /// An exception meaning that the task has failed for handled reasons like invalid arguments,
+ /// invalid environment, missing plugins or failures not related to a default in the code.
+ /// This exception allow the task to display a failure message to the end user while others exceptions
+ /// will be displayed as unhandled exceptions and display a stack trace.
+ ///
+ 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..37b40f12 100644
--- a/Kyoo.Common/Controllers/ITaskManager.cs
+++ b/Kyoo.Common/Controllers/ITaskManager.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
@@ -13,22 +16,67 @@ 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;
///
/// Get all currently running tasks
///
/// A list of currently running tasks.
- ICollection GetRunningTasks();
+ ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks();
///
/// Get all available tasks
///
/// A list of every tasks that this instance know.
- ICollection GetAllTasks();
+ ICollection GetAllTasks();
}
}
\ No newline at end of file
diff --git a/Kyoo.Common/Controllers/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs
index ee31498a..465d4c62 100644
--- a/Kyoo.Common/Controllers/IThumbnailsManager.cs
+++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs
@@ -1,23 +1,59 @@
-using Kyoo.Models;
+using System;
+using Kyoo.Models;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Controllers
{
+ ///
+ /// Download images and retrieve the path of those images for a resource.
+ ///
public interface IThumbnailsManager
{
- Task Validate(Show show, bool alwaysDownload = false);
- Task Validate(Season season, bool alwaysDownload = false);
- Task Validate(Episode episode, bool alwaysDownload = false);
- Task Validate(People actors, bool alwaysDownload = false);
- Task Validate(Provider actors, bool alwaysDownload = false);
+ ///
+ /// Download images of a specified item.
+ /// If no images is available to download, do nothing and silently return.
+ ///
+ ///
+ /// The item to cache images.
+ ///
+ ///
+ /// true if images should be downloaded even if they already exists locally, false otherwise.
+ ///
+ /// The type of the item
+ /// true if an image has been downloaded, false otherwise.
+ Task DownloadImages([NotNull] T item, bool alwaysDownload = false)
+ where T : IResource;
+
- Task GetShowPoster([NotNull] Show show);
- Task GetShowLogo([NotNull] Show show);
- Task GetShowBackdrop([NotNull] Show show);
- Task GetSeasonPoster([NotNull] Season season);
- Task GetEpisodeThumb([NotNull] Episode episode);
- Task GetPeoplePoster([NotNull] People people);
- Task GetProviderLogo([NotNull] Provider provider);
+ ///
+ /// Retrieve the local path of the poster of the given item.
+ ///
+ /// The item to retrieve the poster from.
+ /// The type of the item
+ /// If the type does not have a poster
+ /// The path of the poster for the given resource (it might or might not exists).
+ Task GetPoster([NotNull] T item)
+ where T : IResource;
+
+ ///
+ /// Retrieve the local path of the logo of the given item.
+ ///
+ /// The item to retrieve the logo from.
+ /// The type of the item
+ /// If the type does not have a logo
+ /// The path of the logo for the given resource (it might or might not exists).
+ Task GetLogo([NotNull] T item)
+ where T : IResource;
+
+ ///
+ /// Retrieve the local path of the thumbnail of the given item.
+ ///
+ /// The item to retrieve the thumbnail from.
+ /// The type of the item
+ /// If the type does not have a thumbnail
+ /// The path of the thumbnail for the given resource (it might or might not exists).
+ Task GetThumbnail([NotNull] T item)
+ where T : IResource;
}
}
diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj
index 349ef6a0..844be997 100644
--- a/Kyoo.Common/Kyoo.Common.csproj
+++ b/Kyoo.Common/Kyoo.Common.csproj
@@ -21,11 +21,13 @@
+
+
diff --git a/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs b/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs
new file mode 100644
index 00000000..d2b997a5
--- /dev/null
+++ b/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using Kyoo.Controllers;
+
+namespace Kyoo.Common.Models.Attributes
+{
+ ///
+ /// An attribute to inform how a works.
+ ///
+ [MetadataAttribute]
+ [AttributeUsage(AttributeTargets.Class)]
+ public class FileSystemMetadataAttribute : Attribute
+ {
+ ///
+ /// The scheme(s) used to identify this path.
+ /// It can be something like http, https, ftp, file and so on.
+ ///
+ ///
+ /// If multiples files with the same schemes exists, an exception will be thrown.
+ ///
+ public string[] Scheme { get; }
+
+ ///
+ /// true if the scheme should be removed from the path before calling
+ /// methods of this , false otherwise.
+ ///
+ public bool StripScheme { get; set; }
+
+
+ ///
+ /// Create a new using the specified schemes.
+ ///
+ /// The schemes to use.
+ public FileSystemMetadataAttribute(string[] schemes)
+ {
+ Scheme = schemes;
+ }
+
+ ///
+ /// Create a new using a dictionary of metadata.
+ ///
+ ///
+ /// The dictionary of metadata. This method expect the dictionary to contain a field
+ /// per property in this attribute, with the same types as the properties of this attribute.
+ ///
+ public FileSystemMetadataAttribute(IDictionary metadata)
+ {
+ Scheme = (string[])metadata[nameof(Scheme)];
+ StripScheme = (bool)metadata[nameof(StripScheme)];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs
index b036acdf..d517300f 100644
--- a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs
+++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs
@@ -8,8 +8,8 @@ namespace Kyoo.Models.Attributes
/// An attribute to inform that the service will be injected automatically by a service provider.
///
///
- /// It should only be used on and will be injected before calling .
- /// It can also be used on and it will be injected before calling .
+ /// It should only be used on and it will be injected before
+ /// calling .
///
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
diff --git a/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs b/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs
new file mode 100644
index 00000000..0ebe7a34
--- /dev/null
+++ b/Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using Kyoo.Controllers;
+
+namespace Kyoo.Common.Models.Attributes
+{
+ ///
+ /// An attribute to inform how a works.
+ ///
+ [MetadataAttribute]
+ [AttributeUsage(AttributeTargets.Class)]
+ public class TaskMetadataAttribute : Attribute
+ {
+ ///
+ /// The slug of the task, used to start it.
+ ///
+ public string Slug { get; }
+
+ ///
+ /// The name of the task that will be displayed to the user.
+ ///
+ public string Name { get; }
+
+ ///
+ /// A quick description of what this task will do.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Should this task be automatically run at app startup?
+ ///
+ public bool RunOnStartup { get; set; }
+
+ ///
+ /// The priority of this task. Only used if is true.
+ /// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
+ ///
+ public int Priority { get; set; }
+
+ ///
+ /// true if this task should not be displayed to the user, false otherwise.
+ ///
+ public bool IsHidden { get; set; }
+
+
+ ///
+ /// Create a new with the given slug, name and description.
+ ///
+ /// The slug of the task, used to start it.
+ /// The name of the task that will be displayed to the user.
+ /// A quick description of what this task will do.
+ public TaskMetadataAttribute(string slug, string name, string description)
+ {
+ Slug = slug;
+ Name = name;
+ Description = description;
+ }
+
+ ///
+ /// Create a new using a dictionary of metadata.
+ ///
+ ///
+ /// The dictionary of metadata. This method expect the dictionary to contain a field
+ /// per property in this attribute, with the same types as the properties of this attribute.
+ ///
+ public TaskMetadataAttribute(IDictionary metadata)
+ {
+ Slug = (string)metadata[nameof(Slug)];
+ Name = (string)metadata[nameof(Name)];
+ Description = (string)metadata[nameof(Description)];
+ RunOnStartup = (bool)metadata[nameof(RunOnStartup)];
+ Priority = (int)metadata[nameof(Priority)];
+ IsHidden = (bool)metadata[nameof(IsHidden)];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs b/Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs
new file mode 100644
index 00000000..7c820f5a
--- /dev/null
+++ b/Kyoo.Common/Models/Exceptions/IdentificationFailedException.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Runtime.Serialization;
+using Kyoo.Controllers;
+
+namespace Kyoo.Models.Exceptions
+{
+ ///
+ /// An exception raised when an failed.
+ ///
+ [Serializable]
+ public class IdentificationFailedException : Exception
+ {
+ ///
+ /// Create a new with a default message.
+ ///
+ public IdentificationFailedException()
+ : base("An identification failed.")
+ {}
+
+ ///
+ /// Create a new with a custom message.
+ ///
+ /// The message to use.
+ public IdentificationFailedException(string message)
+ : base(message)
+ {}
+
+ ///
+ /// The serialization constructor
+ ///
+ /// Serialization infos
+ /// The serialization context
+ protected IdentificationFailedException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/Exceptions/TaskFailedException.cs b/Kyoo.Common/Models/Exceptions/TaskFailedException.cs
new file mode 100644
index 00000000..5fe65f7c
--- /dev/null
+++ b/Kyoo.Common/Models/Exceptions/TaskFailedException.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Runtime.Serialization;
+using Kyoo.Controllers;
+
+namespace Kyoo.Models.Exceptions
+{
+ ///
+ /// An exception raised when an failed.
+ ///
+ [Serializable]
+ public class TaskFailedException : AggregateException
+ {
+ ///
+ /// Create a new with a default message.
+ ///
+ public TaskFailedException()
+ : base("A task failed.")
+ {}
+
+ ///
+ /// Create a new with a custom message.
+ ///
+ /// The message to use.
+ public TaskFailedException(string message)
+ : base(message)
+ {}
+
+ ///
+ /// Create a new wrapping another exception.
+ ///
+ /// The exception to wrap.
+ public TaskFailedException(Exception exception)
+ : base(exception)
+ {}
+
+ ///
+ /// The serialization constructor
+ ///
+ /// Serialization infos
+ /// The serialization context
+ protected TaskFailedException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs
index 4df07770..6bc61c2e 100644
--- a/Kyoo.Common/Models/LibraryItem.cs
+++ b/Kyoo.Common/Models/LibraryItem.cs
@@ -58,7 +58,7 @@ namespace Kyoo.Models
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
///
- [SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; }
+ [SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")] public string Poster { get; set; }
///
/// The type of this item (ether a collection, a show or a movie).
diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs
index 6d815af9..09c519da 100644
--- a/Kyoo.Common/Models/Link.cs
+++ b/Kyoo.Common/Models/Link.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq.Expressions;
+using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
@@ -107,12 +108,12 @@ namespace Kyoo.Models
///
/// A reference of the first resource.
///
- public T1 First { get; set; }
+ [SerializeIgnore] public T1 First { get; set; }
///
/// A reference to the second resource.
///
- public T2 Second { get; set; }
+ [SerializeIgnore] public T2 Second { get; set; }
///
diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs
index 34872314..de6ac817 100644
--- a/Kyoo.Common/Models/MetadataID.cs
+++ b/Kyoo.Common/Models/MetadataID.cs
@@ -19,6 +19,12 @@ namespace Kyoo.Models
/// The URL of the resource on the external provider.
///
public string Link { get; set; }
+
+ ///
+ /// A shortcut to access the provider of this metadata.
+ /// Unlike the property, this is serializable.
+ ///
+ public Provider Provider => Second;
///
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs
index cdb7fb8f..7f76a8a4 100644
--- a/Kyoo.Common/Models/Resources/Episode.cs
+++ b/Kyoo.Common/Models/Resources/Episode.cs
@@ -20,9 +20,11 @@ namespace Kyoo.Models
{
get
{
- if (ShowSlug == null && Show == null)
- return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
- return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
+ if (ShowSlug != null || Show != null)
+ return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
+ return ShowID != 0
+ ? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
+ : null;
}
[UsedImplicitly] [NotNull] private set
{
@@ -93,7 +95,7 @@ namespace Kyoo.Models
public int? AbsoluteNumber { get; set; }
///
- /// The path of the video file for this episode. Any format supported by a is allowed.
+ /// The path of the video file for this episode. Any format supported by a is allowed.
///
[SerializeIgnore] public string Path { get; set; }
diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs
index ffb2ae49..57c9fcef 100644
--- a/Kyoo.Common/Models/Resources/Show.cs
+++ b/Kyoo.Common/Models/Resources/Show.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
@@ -30,7 +31,7 @@ namespace Kyoo.Models
///
/// The path of the root directory of this show.
- /// This can be any kind of path supported by
+ /// This can be any kind of path supported by
///
[SerializeIgnore] public string Path { get; set; }
@@ -42,10 +43,10 @@ namespace Kyoo.Models
///
/// Is this show airing, not aired yet or finished?
///
- public Status? Status { get; set; }
+ public Status Status { get; set; }
///
- /// An URL to a trailer. This could be any path supported by the .
+ /// An URL to a trailer. This could be any path supported by the .
///
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
public string TrailerUrl { get; set; }
@@ -157,6 +158,7 @@ namespace Kyoo.Models
/// This method will never return anything if the are not loaded.
/// The slug of the provider
/// The field of the asked provider.
+ [CanBeNull]
public string GetID(string provider)
{
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
@@ -183,5 +185,5 @@ namespace Kyoo.Models
///
/// The enum containing show's status.
///
- public enum Status { Finished, Airing, Planned, Unknown }
+ public enum Status { Unknown, Finished, Airing, Planned }
}
diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs
index f093699b..d82cdbe1 100644
--- a/Kyoo.Common/Models/Resources/Track.cs
+++ b/Kyoo.Common/Models/Resources/Track.cs
@@ -35,8 +35,8 @@ namespace Kyoo.Models
{
string type = Type.ToString().ToLower();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
- string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
- return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
+ string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
+ return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : "")}.{type}";
}
[UsedImplicitly] private set
{
@@ -47,11 +47,13 @@ namespace Kyoo.Models
if (!match.Success)
throw new ArgumentException("Invalid track slug. " +
- "Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
+ "Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]");
EpisodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value;
- TrackIndex = int.Parse(match.Groups["index"].Value);
+ if (Language == "und")
+ Language = null;
+ TrackIndex = match.Groups["index"].Success ? int.Parse(match.Groups["index"].Value) : 0;
IsForced = match.Groups["forced"].Success;
Type = Enum.Parse(match.Groups["type"].Value, true);
}
@@ -154,30 +156,17 @@ namespace Kyoo.Models
}
///
- /// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored)
+ /// Utility method to create a track slug from a incomplete slug (only add the type of the track).
///
/// The slug to edit
/// The new type of this
- ///
- ///
- ///
///
- public static string EditSlug(string baseSlug,
- StreamType type = StreamType.Unknown,
- string language = null,
- int? index = null,
- bool? forced = null)
+ public static string BuildSlug(string baseSlug,
+ StreamType type)
{
- Track track = new() {Slug = baseSlug};
- if (type != StreamType.Unknown)
- track.Type = type;
- if (language != null)
- track.Language = language;
- if (index != null)
- track.TrackIndex = index.Value;
- if (forced != null)
- track.IsForced = forced.Value;
- return track.Slug;
+ return baseSlug.EndsWith($".{type}", StringComparison.InvariantCultureIgnoreCase)
+ ? baseSlug
+ : $"{baseSlug}.{type.ToString().ToLowerInvariant()}";
}
}
}
diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs
index 3b64d54e..158563a8 100644
--- a/Kyoo.Common/Models/WatchItem.cs
+++ b/Kyoo.Common/Models/WatchItem.cs
@@ -62,7 +62,7 @@ namespace Kyoo.Models
public DateTime? ReleaseDate { get; set; }
///
- /// The path of the video file for this episode. Any format supported by a is allowed.
+ /// The path of the video file for this episode. Any format supported by a is allowed.
///
[SerializeIgnore] public string Path { get; set; }
@@ -176,13 +176,16 @@ namespace Kyoo.Models
return new WatchItem
{
EpisodeID = ep.ID,
+ Slug = ep.Slug,
ShowSlug = ep.Show.Slug,
+ ShowTitle = ep.Show.Title,
SeasonNumber = ep.SeasonNumber,
EpisodeNumber = ep.EpisodeNumber,
AbsoluteNumber = ep.AbsoluteNumber,
Title = ep.Title,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
+ Container = PathIO.GetExtension(ep.Path)![1..],
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
diff --git a/Kyoo.Common/Module.cs b/Kyoo.Common/Module.cs
index a8a81b88..7fb9bdbc 100644
--- a/Kyoo.Common/Module.cs
+++ b/Kyoo.Common/Module.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Linq;
+using Autofac;
+using Autofac.Builder;
using Kyoo.Controllers;
-using Kyoo.Models;
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
namespace Kyoo
{
@@ -15,86 +13,63 @@ namespace Kyoo
///
/// Register a new task to the container.
///
- /// The container
+ /// The container
/// The type of the task
- /// The initial container.
- public static IServiceCollection AddTask(this IServiceCollection services)
+ /// The registration builder of this new task. That can be used to edit the registration.
+ public static IRegistrationBuilder
+ RegisterTask(this ContainerBuilder builder)
where T : class, ITask
{
- services.AddSingleton();
- return services;
+ return builder.RegisterType().As();
+ }
+
+ ///
+ /// Register a new metadata provider to the container.
+ ///
+ /// The container
+ /// The type of the task
+ /// The registration builder of this new provider. That can be used to edit the registration.
+ public static IRegistrationBuilder
+ RegisterProvider(this ContainerBuilder builder)
+ where T : class, IMetadataProvider
+ {
+ return builder.RegisterType().As().InstancePerLifetimeScope();
}
///
/// Register a new repository to the container.
///
- /// The container
- /// The lifetime of the repository. The default is scoped.
+ /// The container
/// The type of the repository.
///
- /// If your repository implements a special interface, please use
+ /// If your repository implements a special interface, please use
///
/// The initial container.
- public static IServiceCollection AddRepository(this IServiceCollection services,
- ServiceLifetime lifetime = ServiceLifetime.Scoped)
+ public static IRegistrationBuilder
+ RegisterRepository(this ContainerBuilder builder)
where T : IBaseRepository
{
- Type repository = Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>));
-
- if (repository != null)
- services.Add(ServiceDescriptor.Describe(repository, typeof(T), lifetime));
- services.Add(ServiceDescriptor.Describe(typeof(IBaseRepository), typeof(T), lifetime));
- return services;
+ return builder.RegisterType()
+ .As()
+ .As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)))
+ .InstancePerLifetimeScope();
}
///
/// Register a new repository with a custom mapping to the container.
///
- ///
- /// The lifetime of the repository. The default is scoped.
+ /// The container
/// The custom mapping you have for your repository.
/// The type of the repository.
///
- /// If your repository does not implements a special interface, please use
+ /// If your repository does not implements a special interface, please use
///
/// The initial container.
- public static IServiceCollection AddRepository(this IServiceCollection services,
- ServiceLifetime lifetime = ServiceLifetime.Scoped)
+ public static IRegistrationBuilder
+ RegisterRepository(this ContainerBuilder builder)
where T2 : IBaseRepository, T
{
- services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime));
- return services.AddRepository(lifetime);
- }
-
- ///
- /// Add an editable configuration to the editable configuration list
- ///
- /// The service collection to edit
- /// The root path of the editable configuration. It should not be a nested type.
- /// The type of the configuration
- /// The given service collection is returned.
- public static IServiceCollection AddConfiguration(this IServiceCollection services, string path)
- where T : class
- {
- if (services.Any(x => x.ServiceType == typeof(T)))
- return services;
- foreach (ConfigurationReference confRef in ConfigurationReference.CreateReference(path))
- services.AddSingleton(confRef);
- return services;
- }
-
- ///
- /// Add an editable configuration to the editable configuration list.
- /// WARNING: this method allow you to add an unmanaged type. This type won't be editable. This can be used
- /// for external libraries or variable arguments.
- ///
- /// The service collection to edit
- /// The root path of the editable configuration. It should not be a nested type.
- /// The given service collection is returned.
- public static IServiceCollection AddUntypedConfiguration(this IServiceCollection services, string path)
- {
- services.AddSingleton(ConfigurationReference.CreateUntyped(path));
- return services;
+ return builder.RegisterRepository().As();
}
///
diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs
index 55cc17e3..cd860ea3 100644
--- a/Kyoo.Common/Utility/Merger.cs
+++ b/Kyoo.Common/Utility/Merger.cs
@@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
+using Kyoo.Models;
using Kyoo.Models.Attributes;
namespace Kyoo
@@ -111,7 +112,8 @@ 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
///
- public static T Merge(T first, T second)
+ [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
+ public static T Merge([CanBeNull] T first, [CanBeNull] T second)
{
if (first == null)
return second;
@@ -127,9 +129,7 @@ namespace Kyoo
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
- object defaultValue = property.PropertyType.IsValueType
- ? Activator.CreateInstance(property.PropertyType)
- : null;
+ object defaultValue = property.PropertyType.GetClrDefault();
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
@@ -139,11 +139,14 @@ namespace Kyoo
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
.GenericTypeArguments
.First();
+ Func equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
+ ? (x, y) => x.Slug == y.Slug
+ : null;
property.SetValue(first, Utility.RunGenericMethod(
- typeof(Utility),
+ typeof(Merger),
nameof(MergeLists),
- enumerableType,
- oldValue, newValue, null));
+ enumerableType,
+ oldValue, newValue, equalityComparer));
}
}
diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs
index f7041c51..2f7e14ad 100644
--- a/Kyoo.Common/Utility/Utility.cs
+++ b/Kyoo.Common/Utility/Utility.cs
@@ -72,7 +72,7 @@ namespace Kyoo
///
/// The string to slugify
/// The slug version of the given string
- public static string ToSlug(string str)
+ public static string ToSlug([CanBeNull] string str)
{
if (str == null)
return null;
@@ -182,15 +182,49 @@ namespace Kyoo
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
- public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
+ ///
+ /// Retrieve a method from an with the given name and respect the
+ /// amount of parameters and generic parameters. This works for polymorphic methods.
+ ///
+ ///
+ /// The type owning the method. For non static methods, this is the this .
+ ///
+ ///
+ /// The binding flags of the method. This allow you to specify public/private and so on.
+ ///
+ ///
+ /// The name of the method.
+ ///
+ ///
+ /// The list of generic parameters.
+ ///
+ ///
+ /// The list of parameters.
+ ///
+ /// No method match the given constraints.
+ /// The method handle of the matching method.
+ [PublicAPI]
+ [NotNull]
+ public static MethodInfo GetMethod([NotNull] Type type,
+ BindingFlags flag,
+ string name,
+ [NotNull] Type[] generics,
+ [NotNull] object[] args)
{
+ if (type == null)
+ throw new ArgumentNullException(nameof(type));
+ if (generics == null)
+ throw new ArgumentNullException(nameof(generics));
+ if (args == null)
+ throw new ArgumentNullException(nameof(args));
+
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
.Where(x => x.GetParameters().Length == args.Length)
- .IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
- $"{args.Length} arguments and {generics.Length} generic " +
- $"types could not be found on {type.Name}."))
+ .IfEmpty(() => throw new ArgumentException($"A method named {name} with " +
+ $"{args.Length} arguments and {generics.Length} generic " +
+ $"types could not be found on {type.Name}."))
// TODO this won't work but I don't know why.
// .Where(x =>
// {
@@ -211,9 +245,34 @@ namespace Kyoo
if (methods.Length == 1)
return methods[0];
- throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
+ throw new ArgumentException($"Multiple methods named {name} match the generics and parameters constraints.");
}
+ ///
+ /// Run a generic static method for a runtime .
+ ///
+ ///
+ /// To run for a List where you don't know the type at compile type,
+ /// you could do:
+ ///
+ /// Utility.RunGenericMethod<object>(
+ /// typeof(Utility),
+ /// nameof(MergeLists),
+ /// enumerableType,
+ /// oldValue, newValue, equalityComparer)
+ ///
+ ///
+ /// The type that owns the method. For non static methods, the type of this .
+ /// The name of the method. You should use the nameof keyword.
+ /// The generic type to run the method with.
+ /// The list of arguments of the method
+ ///
+ /// The return type of the method. You can put for an unknown one.
+ ///
+ /// No method match the given constraints.
+ /// The return of the method you wanted to run.
+ ///
+ ///
public static T RunGenericMethod(
[NotNull] Type owner,
[NotNull] string methodName,
@@ -223,6 +282,34 @@ namespace Kyoo
return RunGenericMethod(owner, methodName, new[] {type}, args);
}
+ ///
+ /// Run a generic static method for a multiple runtime .
+ /// If your generic method only needs one type, see
+ ///
+ ///
+ ///
+ /// To run for a List where you don't know the type at compile type,
+ /// you could do:
+ ///
+ /// Utility.RunGenericMethod<object>(
+ /// typeof(Utility),
+ /// nameof(MergeLists),
+ /// enumerableType,
+ /// oldValue, newValue, equalityComparer)
+ ///
+ ///
+ /// The type that owns the method. For non static methods, the type of this .
+ /// The name of the method. You should use the nameof keyword.
+ /// The list of generic types to run the method with.
+ /// The list of arguments of the method
+ ///
+ /// The return type of the method. You can put for an unknown one.
+ ///
+ /// No method match the given constraints.
+ /// The return of the method you wanted to run.
+ ///
+ ///
+ [PublicAPI]
public static T RunGenericMethod(
[NotNull] Type owner,
[NotNull] string methodName,
@@ -238,9 +325,34 @@ namespace Kyoo
if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
- return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
+ return (T)method.MakeGenericMethod(types).Invoke(null, args.ToArray());
}
+ ///
+ /// Run a generic method for a runtime .
+ ///
+ ///
+ /// To run for a List where you don't know the type at compile type,
+ /// you could do:
+ ///
+ /// Utility.RunGenericMethod<object>(
+ /// typeof(Utility),
+ /// nameof(MergeLists),
+ /// enumerableType,
+ /// oldValue, newValue, equalityComparer)
+ ///
+ ///
+ /// The this of the method to run.
+ /// The name of the method. You should use the nameof keyword.
+ /// The generic type to run the method with.
+ /// The list of arguments of the method
+ ///
+ /// The return type of the method. You can put for an unknown one.
+ ///
+ /// No method match the given constraints.
+ /// The return of the method you wanted to run.
+ ///
+ ///
public static T RunGenericMethod(
[NotNull] object instance,
[NotNull] string methodName,
@@ -250,6 +362,33 @@ namespace Kyoo
return RunGenericMethod(instance, methodName, new[] {type}, args);
}
+ ///
+ /// Run a generic method for a multiple runtime .
+ /// If your generic method only needs one type, see
+ ///
+ ///
+ ///
+ /// To run for a List where you don't know the type at compile type,
+ /// you could do:
+ ///
+ /// Utility.RunGenericMethod<object>(
+ /// typeof(Utility),
+ /// nameof(MergeLists),
+ /// enumerableType,
+ /// oldValue, newValue, equalityComparer)
+ ///
+ ///
+ /// The this of the method to run.
+ /// The name of the method. You should use the nameof keyword.
+ /// The list of generic types to run the method with.
+ /// The list of arguments of the method
+ ///
+ /// The return type of the method. You can put for an unknown one.
+ ///
+ /// No method match the given constraints.
+ /// The return of the method you wanted to run.
+ ///
+ ///
public static T RunGenericMethod(
[NotNull] object instance,
[NotNull] string methodName,
@@ -263,7 +402,7 @@ namespace Kyoo
if (types == null || types.Length == 0)
throw new ArgumentNullException(nameof(types));
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
- return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
+ return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray());
}
public static string ToQueryString(this Dictionary query)
diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs
index 336d3226..a6834c31 100644
--- a/Kyoo.CommonAPI/CrudApi.cs
+++ b/Kyoo.CommonAPI/CrudApi.cs
@@ -38,7 +38,7 @@ namespace Kyoo.CommonApi
[PartialPermission(Kind.Read)]
public virtual async Task> Get(string slug)
{
- T ret = await _repository.Get(slug);
+ T ret = await _repository.GetOrDefault(slug);
if (ret == null)
return NotFound();
return ret;
diff --git a/Kyoo.CommonAPI/JsonSerializer.cs b/Kyoo.CommonAPI/JsonSerializer.cs
index bf65a32a..3a2caac7 100644
--- a/Kyoo.CommonAPI/JsonSerializer.cs
+++ b/Kyoo.CommonAPI/JsonSerializer.cs
@@ -115,9 +115,10 @@ namespace Kyoo.Controllers
public object GetValue(object target)
{
- return Regex.Replace(_format, @"(?
+ return Regex.Replace(_format, @"(?
{
string value = x.Groups[1].Value;
+ string modifier = x.Groups[3].Value;
if (value == "HOST")
return _host;
@@ -127,9 +128,22 @@ namespace Kyoo.Controllers
.FirstOrDefault(y => y.Name == value);
if (properties == null)
return null;
- if (properties.GetValue(target) is string ret)
- return ret;
- throw new ArgumentException($"Invalid serializer replacement {value}");
+ object objValue = properties.GetValue(target);
+ if (objValue is not string ret)
+ ret = objValue?.ToString();
+ if (ret == null)
+ throw new ArgumentException($"Invalid serializer replacement {value}");
+
+ foreach (char modification in modifier)
+ {
+ ret = modification switch
+ {
+ 'l' => ret.ToLowerInvariant(),
+ 'u' => ret.ToUpperInvariant(),
+ _ => throw new ArgumentException($"Invalid serializer modificator {modification}.")
+ };
+ }
+ return ret;
});
}
diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs
index 8ab23443..0187e0dd 100644
--- a/Kyoo.CommonAPI/LocalRepository.cs
+++ b/Kyoo.CommonAPI/LocalRepository.cs
@@ -208,7 +208,7 @@ namespace Kyoo.Controllers
}
catch (DuplicatedItemException)
{
- return await GetOrDefault(obj.Slug);
+ return await Get(obj.Slug);
}
}
diff --git a/Kyoo.CommonAPI/ResourceViewAttribute.cs b/Kyoo.CommonAPI/ResourceViewAttribute.cs
index 06198663..487373c4 100644
--- a/Kyoo.CommonAPI/ResourceViewAttribute.cs
+++ b/Kyoo.CommonAPI/ResourceViewAttribute.cs
@@ -43,22 +43,31 @@ namespace Kyoo.CommonApi
PropertyInfo[] properties = type.GetProperties()
.Where(x => x.GetCustomAttribute() != null)
.ToArray();
- fields = fields.Select(x =>
- {
- string property = properties
- .FirstOrDefault(y => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
- ?.Name;
- if (property != null)
- return property;
- context.Result = new BadRequestObjectResult(new
+ if (fields.Count == 1 && fields.Contains("all"))
+ {
+ fields = properties.Select(x => x.Name).ToList();
+ }
+ else
+ {
+ fields = fields
+ .Select(x =>
{
- Error = $"{x} does not exist on {type.Name}."
- });
- return null;
- })
- .ToList();
- if (context.Result != null)
- return;
+ string property = properties
+ .FirstOrDefault(y
+ => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
+ ?.Name;
+ if (property != null)
+ return property;
+ context.Result = new BadRequestObjectResult(new
+ {
+ Error = $"{x} does not exist on {type.Name}."
+ });
+ return null;
+ })
+ .ToList();
+ if (context.Result != null)
+ return;
+ }
}
context.HttpContext.Items["fields"] = fields;
base.OnActionExecuting(context);
diff --git a/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210723224326_Initial.Designer.cs
similarity index 95%
rename from Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
rename to Kyoo.Postgresql/Migrations/20210723224326_Initial.Designer.cs
index f1733a77..3016a040 100644
--- a/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
+++ b/Kyoo.Postgresql/Migrations/20210723224326_Initial.Designer.cs
@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(PostgresContext))]
- [Migration("20210627141933_Initial")]
+ [Migration("20210723224326_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
- .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
+ .HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
.HasAnnotation("Relational:MaxIdentifierLength", 63)
- .HasAnnotation("ProductVersion", "5.0.7")
+ .HasAnnotation("ProductVersion", "5.0.8")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.Collection", b =>
@@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .HasColumnType("integer")
+ .HasColumnName("id")
+ .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ b.Property("EndAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("end_air");
+
+ b.Property("Overview")
+ .HasColumnType("text")
+ .HasColumnName("overview");
+
+ b.Property("Poster")
+ .HasColumnType("text")
+ .HasColumnName("poster");
+
+ b.Property("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property("StartAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("start_air");
+
+ b.Property("Status")
+ .HasColumnType("status")
+ .HasColumnName("status");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property("Type")
+ .HasColumnType("item_type")
+ .HasColumnName("type");
+
+ b.HasKey("ID")
+ .HasName("pk_library_items");
+
+ b.ToView("library_items");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("timestamp without time zone")
.HasColumnName("start_air");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("status")
.HasColumnName("status");
@@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
.HasForeignKey("StudioID")
- .HasConstraintName("fk_shows_studios_studio_id");
+ .HasConstraintName("fk_shows_studios_studio_id")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs b/Kyoo.Postgresql/Migrations/20210723224326_Initial.cs
similarity index 99%
rename from Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
rename to Kyoo.Postgresql/Migrations/20210723224326_Initial.cs
index 29b51490..2ba22c6a 100644
--- a/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
+++ b/Kyoo.Postgresql/Migrations/20210723224326_Initial.cs
@@ -12,7 +12,7 @@ namespace Kyoo.Postgresql.Migrations
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
- .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown")
+ .Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment");
migrationBuilder.CreateTable(
@@ -208,7 +208,7 @@ namespace Kyoo.Postgresql.Migrations
aliases = table.Column(type: "text[]", nullable: true),
path = table.Column(type: "text", nullable: true),
overview = table.Column(type: "text", nullable: true),
- status = table.Column(type: "status", nullable: true),
+ status = table.Column(type: "status", nullable: false),
trailer_url = table.Column(type: "text", nullable: true),
start_air = table.Column(type: "timestamp without time zone", nullable: true),
end_air = table.Column(type: "timestamp without time zone", nullable: true),
diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210723224335_Triggers.Designer.cs
similarity index 95%
rename from Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
rename to Kyoo.Postgresql/Migrations/20210723224335_Triggers.Designer.cs
index fc019baf..d377a09f 100644
--- a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
+++ b/Kyoo.Postgresql/Migrations/20210723224335_Triggers.Designer.cs
@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(PostgresContext))]
- [Migration("20210627141941_Triggers")]
+ [Migration("20210723224335_Triggers")]
partial class Triggers
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
- .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
+ .HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
.HasAnnotation("Relational:MaxIdentifierLength", 63)
- .HasAnnotation("ProductVersion", "5.0.7")
+ .HasAnnotation("ProductVersion", "5.0.8")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.Collection", b =>
@@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .HasColumnType("integer")
+ .HasColumnName("id")
+ .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ b.Property("EndAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("end_air");
+
+ b.Property("Overview")
+ .HasColumnType("text")
+ .HasColumnName("overview");
+
+ b.Property("Poster")
+ .HasColumnType("text")
+ .HasColumnName("poster");
+
+ b.Property("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property("StartAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("start_air");
+
+ b.Property("Status")
+ .HasColumnType("status")
+ .HasColumnName("status");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property("Type")
+ .HasColumnType("item_type")
+ .HasColumnName("type");
+
+ b.HasKey("ID")
+ .HasName("pk_library_items");
+
+ b.ToView("library_items");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("timestamp without time zone")
.HasColumnName("start_air");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("status")
.HasColumnName("status");
@@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
.HasForeignKey("StudioID")
- .HasConstraintName("fk_shows_studios_studio_id");
+ .HasConstraintName("fk_shows_studios_studio_id")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs b/Kyoo.Postgresql/Migrations/20210723224335_Triggers.cs
similarity index 98%
rename from Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
rename to Kyoo.Postgresql/Migrations/20210723224335_Triggers.cs
index 16569748..a773e02b 100644
--- a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
+++ b/Kyoo.Postgresql/Migrations/20210723224335_Triggers.cs
@@ -91,7 +91,7 @@ namespace Kyoo.Postgresql.Migrations
END,
CASE (is_forced)
WHEN false THEN ''
- ELSE '-forced'
+ ELSE '.forced'
END,
'.', type
) WHERE episode_id = NEW.id;
@@ -117,14 +117,14 @@ namespace Kyoo.Postgresql.Migrations
END IF;
NEW.slug := CONCAT(
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
- '.', NEW.language,
+ '.', COALESCE(NEW.language, 'und'),
CASE (NEW.track_index)
WHEN 0 THEN ''
ELSE CONCAT('-', NEW.track_index)
END,
CASE (NEW.is_forced)
WHEN false THEN ''
- ELSE '-forced'
+ ELSE '.forced'
END,
'.', NEW.type
);
diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
index e5044c60..f2d55f24 100644
--- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
+++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs
@@ -18,10 +18,10 @@ namespace Kyoo.Postgresql.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
- .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
+ .HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
.HasAnnotation("Relational:MaxIdentifierLength", 63)
- .HasAnnotation("ProductVersion", "5.0.7")
+ .HasAnnotation("ProductVersion", "5.0.8")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.Collection", b =>
@@ -187,6 +187,51 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .HasColumnType("integer")
+ .HasColumnName("id")
+ .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ b.Property("EndAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("end_air");
+
+ b.Property("Overview")
+ .HasColumnType("text")
+ .HasColumnName("overview");
+
+ b.Property("Poster")
+ .HasColumnType("text")
+ .HasColumnName("poster");
+
+ b.Property("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property("StartAir")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("start_air");
+
+ b.Property("Status")
+ .HasColumnType("status")
+ .HasColumnName("status");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property("Type")
+ .HasColumnType("item_type")
+ .HasColumnName("type");
+
+ b.HasKey("ID")
+ .HasName("pk_library_items");
+
+ b.ToView("library_items");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -619,7 +664,7 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("timestamp without time zone")
.HasColumnName("start_air");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("status")
.HasColumnName("status");
@@ -1076,7 +1121,8 @@ namespace Kyoo.Postgresql.Migrations
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
.HasForeignKey("StudioID")
- .HasConstraintName("fk_shows_studios_studio_id");
+ .HasConstraintName("fk_shows_studios_studio_id")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs
index f0c8f23c..124df770 100644
--- a/Kyoo.Postgresql/PostgresModule.cs
+++ b/Kyoo.Postgresql/PostgresModule.cs
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Npgsql;
namespace Kyoo.Postgresql
{
@@ -73,6 +74,10 @@ namespace Kyoo.Postgresql
{
DatabaseContext context = provider.GetRequiredService();
context.Database.Migrate();
+
+ using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
+ conn.Open();
+ conn.ReloadTypes();
}
}
}
\ No newline at end of file
diff --git a/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210723224542_Initial.Designer.cs
similarity index 96%
rename from Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs
rename to Kyoo.SqLite/Migrations/20210723224542_Initial.Designer.cs
index 1337ce6b..eca501eb 100644
--- a/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs
+++ b/Kyoo.SqLite/Migrations/20210723224542_Initial.Designer.cs
@@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kyoo.SqLite.Migrations
{
[DbContext(typeof(SqLiteContext))]
- [Migration("20210626141337_Initial")]
+ [Migration("20210723224542_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "5.0.7");
+ .HasAnnotation("ProductVersion", "5.0.8");
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
@@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
b.ToTable("Libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EndAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property("Poster")
+ .HasColumnType("TEXT");
+
+ b.Property("Slug")
+ .HasColumnType("TEXT");
+
+ b.Property("StartAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ID");
+
+ b.ToView("LibraryItems");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
b.Property("StartAir")
.HasColumnType("TEXT");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("INTEGER");
b.Property("StudioID")
@@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
{
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
- .HasForeignKey("StudioID");
+ .HasForeignKey("StudioID")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.SqLite/Migrations/20210626141337_Initial.cs b/Kyoo.SqLite/Migrations/20210723224542_Initial.cs
similarity index 99%
rename from Kyoo.SqLite/Migrations/20210626141337_Initial.cs
rename to Kyoo.SqLite/Migrations/20210723224542_Initial.cs
index 88823571..1fa93860 100644
--- a/Kyoo.SqLite/Migrations/20210626141337_Initial.cs
+++ b/Kyoo.SqLite/Migrations/20210723224542_Initial.cs
@@ -200,7 +200,7 @@ namespace Kyoo.SqLite.Migrations
Aliases = table.Column(type: "TEXT", nullable: true),
Path = table.Column(type: "TEXT", nullable: true),
Overview = table.Column(type: "TEXT", nullable: true),
- Status = table.Column(type: "INTEGER", nullable: true),
+ Status = table.Column(type: "INTEGER", nullable: false),
TrailerUrl = table.Column(type: "TEXT", nullable: true),
StartAir = table.Column(type: "TEXT", nullable: true),
EndAir = table.Column(type: "TEXT", nullable: true),
diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs b/Kyoo.SqLite/Migrations/20210723224550_Triggers.Designer.cs
similarity index 96%
rename from Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
rename to Kyoo.SqLite/Migrations/20210723224550_Triggers.Designer.cs
index 02045e3f..059a3aa4 100644
--- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
+++ b/Kyoo.SqLite/Migrations/20210723224550_Triggers.Designer.cs
@@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kyoo.SqLite.Migrations
{
[DbContext(typeof(SqLiteContext))]
- [Migration("20210626141347_Triggers")]
+ [Migration("20210723224550_Triggers")]
partial class Triggers
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "5.0.7");
+ .HasAnnotation("ProductVersion", "5.0.8");
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
@@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
b.ToTable("Libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EndAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property("Poster")
+ .HasColumnType("TEXT");
+
+ b.Property("Slug")
+ .HasColumnType("TEXT");
+
+ b.Property("StartAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ID");
+
+ b.ToView("LibraryItems");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
b.Property("StartAir")
.HasColumnType("TEXT");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("INTEGER");
b.Property("StudioID")
@@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
{
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
- .HasForeignKey("StudioID");
+ .HasForeignKey("StudioID")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210723224550_Triggers.cs
similarity index 98%
rename from Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
rename to Kyoo.SqLite/Migrations/20210723224550_Triggers.cs
index f3ae8325..370fdd37 100644
--- a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
+++ b/Kyoo.SqLite/Migrations/20210723224550_Triggers.cs
@@ -61,14 +61,14 @@ namespace Kyoo.SqLite.Migrations
AND Language = new.Language AND IsForced = new.IsForced
) WHERE ID = new.ID AND TrackIndex = 0;
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
- '.' || Language ||
+ '.' || COALESCE(Language, 'und') ||
CASE (TrackIndex)
WHEN 0 THEN ''
ELSE '-' || (TrackIndex)
END ||
CASE (IsForced)
WHEN false THEN ''
- ELSE '-forced'
+ ELSE '.forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
@@ -98,7 +98,7 @@ namespace Kyoo.SqLite.Migrations
END ||
CASE (IsForced)
WHEN false THEN ''
- ELSE '-forced'
+ ELSE '.forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
@@ -123,7 +123,7 @@ namespace Kyoo.SqLite.Migrations
END ||
CASE (IsForced)
WHEN false THEN ''
- ELSE '-forced'
+ ELSE '.forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
index 58ded130..12f5d94b 100644
--- a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
+++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
@@ -14,7 +14,7 @@ namespace Kyoo.SqLite.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "5.0.7");
+ .HasAnnotation("ProductVersion", "5.0.8");
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
@@ -141,6 +141,41 @@ namespace Kyoo.SqLite.Migrations
b.ToTable("Libraries");
});
+ modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EndAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property("Poster")
+ .HasColumnType("TEXT");
+
+ b.Property("Slug")
+ .HasColumnType("TEXT");
+
+ b.Property("StartAir")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ID");
+
+ b.ToView("LibraryItems");
+ });
+
modelBuilder.Entity("Kyoo.Models.Link", b =>
{
b.Property("FirstID")
@@ -475,7 +510,7 @@ namespace Kyoo.SqLite.Migrations
b.Property("StartAir")
.HasColumnType("TEXT");
- b.Property("Status")
+ b.Property("Status")
.HasColumnType("INTEGER");
b.Property("StudioID")
@@ -862,7 +897,8 @@ namespace Kyoo.SqLite.Migrations
{
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
- .HasForeignKey("StudioID");
+ .HasForeignKey("StudioID")
+ .OnDelete(DeleteBehavior.SetNull);
b.Navigation("Studio");
});
diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Database/RepositoryActivator.cs
similarity index 100%
rename from Kyoo.Tests/Library/RepositoryActivator.cs
rename to Kyoo.Tests/Database/RepositoryActivator.cs
diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Database/RepositoryTests.cs
similarity index 99%
rename from Kyoo.Tests/Library/RepositoryTests.cs
rename to Kyoo.Tests/Database/RepositoryTests.cs
index 51db3061..4cc72b4c 100644
--- a/Kyoo.Tests/Library/RepositoryTests.cs
+++ b/Kyoo.Tests/Database/RepositoryTests.cs
@@ -25,6 +25,7 @@ namespace Kyoo.Tests
public void Dispose()
{
Repositories.Dispose();
+ GC.SuppressFinalize(this);
}
public ValueTask DisposeAsync()
diff --git a/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs b/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
rename to Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
index 7a5976de..73691bf7 100644
--- a/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs b/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs
similarity index 99%
rename from Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
rename to Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs
index 6b1adf27..d9e0e9ff 100644
--- a/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs
@@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/GenreTests.cs b/Kyoo.Tests/Database/SpecificTests/GenreTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/GenreTests.cs
rename to Kyoo.Tests/Database/SpecificTests/GenreTests.cs
index d79dba5e..dc820187 100644
--- a/Kyoo.Tests/Library/SpecificTests/GenreTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/GenreTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
similarity index 98%
rename from Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
rename to Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
index b2db4f66..c5639dbb 100644
--- a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
+++ b/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs
@@ -5,7 +5,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs b/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
similarity index 53%
rename from Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
rename to Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
index fbed1793..079f50cf 100644
--- a/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs
@@ -1,8 +1,11 @@
+using System.Linq;
+using System.Threading.Tasks;
using Kyoo.Controllers;
+using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
@@ -23,7 +26,7 @@ namespace Kyoo.Tests.Library
}
}
- public abstract class ALibraryTests : RepositoryTests
+ public abstract class ALibraryTests : RepositoryTests
{
private readonly ILibraryRepository _repository;
@@ -32,5 +35,17 @@ namespace Kyoo.Tests.Library
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
+
+ [Fact]
+ public async Task CreateWithProvider()
+ {
+ Library library = TestSample.GetNew();
+ library.Providers = new[] { TestSample.Get() };
+ await _repository.Create(library);
+ Library retrieved = await _repository.Get(2);
+ await Repositories.LibraryManager.Load(retrieved, x => x.Providers);
+ Assert.Equal(1, retrieved.Providers.Count);
+ Assert.Equal(TestSample.Get().Slug, retrieved.Providers.First().Slug);
+ }
}
}
\ No newline at end of file
diff --git a/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs b/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
rename to Kyoo.Tests/Database/SpecificTests/PeopleTests.cs
index fc8b788d..23d40bfe 100644
--- a/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs b/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
rename to Kyoo.Tests/Database/SpecificTests/ProviderTests.cs
index 853e34a1..9c022875 100644
--- a/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs b/Kyoo.Tests/Database/SpecificTests/SanityTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/SanityTests.cs
rename to Kyoo.Tests/Database/SpecificTests/SanityTests.cs
index 78637d35..933bbf82 100644
--- a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/SanityTests.cs
@@ -5,7 +5,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
public class GlobalTests : IDisposable, IAsyncDisposable
{
diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs
similarity index 98%
rename from Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
rename to Kyoo.Tests/Database/SpecificTests/SeasonTests.cs
index 39be8b82..b1692747 100644
--- a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs
@@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Database/SpecificTests/ShowTests.cs
similarity index 99%
rename from Kyoo.Tests/Library/SpecificTests/ShowTests.cs
rename to Kyoo.Tests/Database/SpecificTests/ShowTests.cs
index 8940f0c3..63207710 100644
--- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/ShowTests.cs
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/StudioTests.cs b/Kyoo.Tests/Database/SpecificTests/StudioTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/StudioTests.cs
rename to Kyoo.Tests/Database/SpecificTests/StudioTests.cs
index f5093b19..c727f67a 100644
--- a/Kyoo.Tests/Library/SpecificTests/StudioTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/StudioTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs b/Kyoo.Tests/Database/SpecificTests/TrackTests.cs
similarity index 74%
rename from Kyoo.Tests/Library/SpecificTests/TrackTests.cs
rename to Kyoo.Tests/Database/SpecificTests/TrackTests.cs
index 3c2e2043..0ff0c156 100644
--- a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/TrackTests.cs
@@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
@@ -47,5 +47,20 @@ namespace Kyoo.Tests.Library
Track track = await _repository.Get(1);
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
}
+
+ [Fact]
+ public async Task UndefinedLanguageSlugTest()
+ {
+ await _repository.Create(new Track
+ {
+ ID = 5,
+ TrackIndex = 0,
+ Type = StreamType.Video,
+ Language = null,
+ EpisodeID = TestSample.Get().ID
+ });
+ Track track = await _repository.Get(5);
+ Assert.Equal("anohana-s1e1.und.video", track.Slug);
+ }
}
}
\ No newline at end of file
diff --git a/Kyoo.Tests/Library/SpecificTests/UserTests.cs b/Kyoo.Tests/Database/SpecificTests/UserTests.cs
similarity index 96%
rename from Kyoo.Tests/Library/SpecificTests/UserTests.cs
rename to Kyoo.Tests/Database/SpecificTests/UserTests.cs
index be67296d..24bfc789 100644
--- a/Kyoo.Tests/Library/SpecificTests/UserTests.cs
+++ b/Kyoo.Tests/Database/SpecificTests/UserTests.cs
@@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
-namespace Kyoo.Tests.Library
+namespace Kyoo.Tests.Database
{
namespace SqLite
{
diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Database/TestContext.cs
similarity index 100%
rename from Kyoo.Tests/Library/TestContext.cs
rename to Kyoo.Tests/Database/TestContext.cs
diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Database/TestSample.cs
similarity index 95%
rename from Kyoo.Tests/Library/TestSample.cs
rename to Kyoo.Tests/Database/TestSample.cs
index adbe7d84..e0ff955f 100644
--- a/Kyoo.Tests/Library/TestSample.cs
+++ b/Kyoo.Tests/Database/TestSample.cs
@@ -9,8 +9,14 @@ namespace Kyoo.Tests
private static readonly Dictionary> NewSamples = new()
{
{
- typeof(Show),
- () => new Show()
+ typeof(Library),
+ () => new Library
+ {
+ ID = 2,
+ Slug = "new-library",
+ Name = "New Library",
+ Paths = new [] {"/a/random/path"}
+ }
}
};
@@ -18,8 +24,8 @@ namespace Kyoo.Tests
private static readonly Dictionary> Samples = new()
{
{
- typeof(Models.Library),
- () => new Models.Library
+ typeof(Library),
+ () => new Library
{
ID = 1,
Slug = "deck",
@@ -227,7 +233,7 @@ namespace Kyoo.Tests
provider.ID = 0;
context.Providers.Add(provider);
- Models.Library library = Get();
+ Library library = Get();
library.ID = 0;
library.Collections = new List {collection};
library.Providers = new List {provider};
diff --git a/Kyoo.Tests/Identifier/IdentifierTests.cs b/Kyoo.Tests/Identifier/IdentifierTests.cs
new file mode 100644
index 00000000..36ad94b9
--- /dev/null
+++ b/Kyoo.Tests/Identifier/IdentifierTests.cs
@@ -0,0 +1,192 @@
+using System.Threading.Tasks;
+using Kyoo.Controllers;
+using Kyoo.Models;
+using Kyoo.Models.Exceptions;
+using Kyoo.Models.Options;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Kyoo.Tests.Identifier
+{
+ public class Identifier
+ {
+ private readonly Mock _manager;
+ private readonly IIdentifier _identifier;
+
+ public Identifier()
+ {
+ Mock> options = new();
+ options.Setup(x => x.CurrentValue).Returns(new MediaOptions
+ {
+ Regex = new []
+ {
+ "^\\/?(?.+)?\\/(?.+?)(?: \\((?\\d+)\\))?\\/\\k(?: \\(\\d+\\))? S(?\\d+)E(?\\d+)\\..*$",
+ "^\\/?(?.+)?\\/(?.+?)(?: \\((?\\d+)\\))?\\/\\k(?: \\(\\d+\\))? (?\\d+)\\..*$",
+ "^\\/?(?.+)?\\/(?.+?)(?: \\((?\\d+)\\))?\\/\\k(?: \\(\\d+\\))?\\..*$"
+ },
+ SubtitleRegex = new[]
+ {
+ "^(?.+)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$"
+ }
+ });
+
+ _manager = new Mock();
+ _identifier = new RegexIdentifier(options.Object, _manager.Object);
+ }
+
+
+ [Fact]
+ public async Task EpisodeIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo/Library/"}}
+ });
+ (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
+ "/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
+ Assert.Equal("Collection", collection.Name);
+ Assert.Equal("collection", collection.Slug);
+ Assert.Equal("Show", show.Title);
+ Assert.Equal("show", show.Slug);
+ Assert.Equal(2000, show.StartAir!.Value.Year);
+ Assert.Equal(1, season.SeasonNumber);
+ Assert.Equal(1, episode.SeasonNumber);
+ Assert.Equal(1, episode.EpisodeNumber);
+ Assert.Null(episode.AbsoluteNumber);
+ }
+
+ [Fact]
+ public async Task EpisodeIdentificationWithoutLibraryTrailingSlash()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo/Library"}}
+ });
+ (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
+ "/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
+ Assert.Equal("Collection", collection.Name);
+ Assert.Equal("collection", collection.Slug);
+ Assert.Equal("Show", show.Title);
+ Assert.Equal("show", show.Slug);
+ Assert.Equal(2000, show.StartAir!.Value.Year);
+ Assert.Equal(1, season.SeasonNumber);
+ Assert.Equal(1, episode.SeasonNumber);
+ Assert.Equal(1, episode.EpisodeNumber);
+ Assert.Null(episode.AbsoluteNumber);
+ }
+
+ [Fact]
+ public async Task EpisodeIdentificationMultiplePaths()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
+ "/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
+ Assert.Equal("Collection", collection.Name);
+ Assert.Equal("collection", collection.Slug);
+ Assert.Equal("Show", show.Title);
+ Assert.Equal("show", show.Slug);
+ Assert.Equal(2000, show.StartAir!.Value.Year);
+ Assert.Equal(1, season.SeasonNumber);
+ Assert.Equal(1, episode.SeasonNumber);
+ Assert.Equal(1, episode.EpisodeNumber);
+ Assert.Null(episode.AbsoluteNumber);
+ }
+
+ [Fact]
+ public async Task AbsoluteEpisodeIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
+ "/kyoo/Library/Collection/Show (2000)/Show 100.extension");
+ Assert.Equal("Collection", collection.Name);
+ Assert.Equal("collection", collection.Slug);
+ Assert.Equal("Show", show.Title);
+ Assert.Equal("show", show.Slug);
+ Assert.Equal(2000, show.StartAir!.Value.Year);
+ Assert.Null(season);
+ Assert.Null(episode.SeasonNumber);
+ Assert.Null(episode.EpisodeNumber);
+ Assert.Equal(100, episode.AbsoluteNumber);
+ }
+
+ [Fact]
+ public async Task MovieEpisodeIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ (Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
+ "/kyoo/Library/Collection/Show (2000)/Show.extension");
+ Assert.Equal("Collection", collection.Name);
+ Assert.Equal("collection", collection.Slug);
+ Assert.Equal("Show", show.Title);
+ Assert.Equal("show", show.Slug);
+ Assert.Equal(2000, show.StartAir!.Value.Year);
+ Assert.Null(season);
+ Assert.True(show.IsMovie);
+ Assert.Null(episode.SeasonNumber);
+ Assert.Null(episode.EpisodeNumber);
+ Assert.Null(episode.AbsoluteNumber);
+ }
+
+ [Fact]
+ public async Task InvalidEpisodeIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ await Assert.ThrowsAsync(() => _identifier.Identify("/invalid/path"));
+ }
+
+ [Fact]
+ public async Task SubtitleIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ Track track = await _identifier.IdentifyTrack("/kyoo/Library/Collection/Show (2000)/Show.eng.default.str");
+ Assert.True(track.IsExternal);
+ Assert.Equal("eng", track.Language);
+ Assert.Equal("subrip", track.Codec);
+ Assert.True(track.IsDefault);
+ Assert.False(track.IsForced);
+ Assert.StartsWith("/kyoo/Library/Collection/Show (2000)/Show", track.Episode.Path);
+ }
+
+ [Fact]
+ public async Task SubtitleIdentificationUnknownCodec()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ Track track = await _identifier.IdentifyTrack("/kyoo/Library/Collection/Show (2000)/Show.eng.default.extension");
+ Assert.True(track.IsExternal);
+ Assert.Equal("eng", track.Language);
+ Assert.Equal("extension", track.Codec);
+ Assert.True(track.IsDefault);
+ Assert.False(track.IsForced);
+ Assert.StartsWith("/kyoo/Library/Collection/Show (2000)/Show", track.Episode.Path);
+ }
+
+ [Fact]
+ public async Task InvalidSubtitleIdentification()
+ {
+ _manager.Setup(x => x.GetAll(null, new Sort(), default)).ReturnsAsync(new[]
+ {
+ new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
+ });
+ await Assert.ThrowsAsync(() => _identifier.IdentifyTrack("/invalid/path"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Tests/Identifier/ProviderTests.cs b/Kyoo.Tests/Identifier/ProviderTests.cs
new file mode 100644
index 00000000..c49ca3b2
--- /dev/null
+++ b/Kyoo.Tests/Identifier/ProviderTests.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Kyoo.Controllers;
+using Kyoo.Models;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Kyoo.Tests.Identifier
+{
+ public class ProviderTests
+ {
+ private readonly ILoggerFactory _factory;
+
+ public ProviderTests(ITestOutputHelper output)
+ {
+ _factory = LoggerFactory.Create(x =>
+ {
+ x.ClearProviders();
+ x.AddXunit(output);
+ });
+ }
+
+ [Fact]
+ public async Task NoProviderGetTest()
+ {
+ AProviderComposite provider = new ProviderComposite(Array.Empty(),
+ _factory.CreateLogger());
+ Show show = new()
+ {
+ ID = 4,
+ Genres = new[] { new Genre("genre") }
+ };
+ Show ret = await provider.Get(show);
+ KAssert.DeepEqual(show, ret);
+ }
+
+ [Fact]
+ public async Task NoProviderSearchTest()
+ {
+ AProviderComposite provider = new ProviderComposite(Array.Empty(),
+ _factory.CreateLogger());
+ ICollection ret = await provider.Search("show");
+ Assert.Empty(ret);
+ }
+
+ [Fact]
+ public async Task OneProviderGetTest()
+ {
+ Show show = new()
+ {
+ ID = 4,
+ Genres = new[] { new Genre("genre") }
+ };
+ Mock mock = new();
+ mock.Setup(x => x.Get(show)).ReturnsAsync(new Show
+ {
+ Title = "title",
+ Genres = new[] { new Genre("ToMerge")}
+ });
+ AProviderComposite provider = new ProviderComposite(new []
+ {
+ mock.Object
+ },
+ _factory.CreateLogger());
+
+ Show ret = await provider.Get(show);
+ Assert.Equal(4, ret.ID);
+ Assert.Equal("title", ret.Title);
+ Assert.Equal(2, ret.Genres.Count);
+ Assert.Contains("genre", ret.Genres.Select(x => x.Slug));
+ Assert.Contains("tomerge", ret.Genres.Select(x => x.Slug));
+ }
+
+ [Fact]
+ public async Task FailingProviderGetTest()
+ {
+ Show show = new()
+ {
+ ID = 4,
+ Genres = new[] { new Genre("genre") }
+ };
+ Mock mock = new();
+ mock.Setup(x => x.Provider).Returns(new Provider("mock", ""));
+ mock.Setup(x => x.Get(show)).ReturnsAsync(new Show
+ {
+ Title = "title",
+ Genres = new[] { new Genre("ToMerge")}
+ });
+
+ Mock mockTwo = new();
+ mockTwo.Setup(x => x.Provider).Returns(new Provider("mockTwo", ""));
+ mockTwo.Setup(x => x.Get(show)).ReturnsAsync(new Show
+ {
+ Title = "title2",
+ Status = Status.Finished,
+ Genres = new[] { new Genre("ToMerge")}
+ });
+
+ Mock mockFailing = new();
+ mockFailing.Setup(x => x.Provider).Returns(new Provider("mockFail", ""));
+ mockFailing.Setup(x => x.Get(show)).Throws();
+
+ AProviderComposite provider = new ProviderComposite(new []
+ {
+ mock.Object,
+ mockTwo.Object,
+ mockFailing.Object
+ },
+ _factory.CreateLogger());
+
+ Show ret = await provider.Get(show);
+ Assert.Equal(4, ret.ID);
+ Assert.Equal("title", ret.Title);
+ Assert.Equal(Status.Finished, ret.Status);
+ Assert.Equal(2, ret.Genres.Count);
+ Assert.Contains("genre", ret.Genres.Select(x => x.Slug));
+ Assert.Contains("tomerge", ret.Genres.Select(x => x.Slug));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Tests/Identifier/Tvdb/ConvertorTests.cs b/Kyoo.Tests/Identifier/Tvdb/ConvertorTests.cs
new file mode 100644
index 00000000..35e389b6
--- /dev/null
+++ b/Kyoo.Tests/Identifier/Tvdb/ConvertorTests.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Linq;
+using Kyoo.Models;
+using Kyoo.TheTvdb;
+using TvDbSharper.Dto;
+using Xunit;
+
+namespace Kyoo.Tests.Identifier.Tvdb
+{
+ public class ConvertorTests
+ {
+ [Fact]
+ public void SeriesSearchToShow()
+ {
+ SeriesSearchResult result = new()
+ {
+ Slug = "slug",
+ SeriesName = "name",
+ Aliases = new[] { "Aliases" },
+ Overview = "overview",
+ Status = "Ended",
+ FirstAired = "2021-07-23",
+ Poster = "/poster",
+ Id = 5
+ };
+ Provider provider = TestSample.Get();
+ Show show = result.ToShow(provider);
+
+ Assert.Equal("slug", show.Slug);
+ Assert.Equal("name", show.Title);
+ Assert.Single(show.Aliases);
+ Assert.Equal("Aliases", show.Aliases[0]);
+ Assert.Equal("overview", show.Overview);
+ Assert.Equal(new DateTime(2021, 7, 23), show.StartAir);
+ Assert.Equal("https://www.thetvdb.com/poster", show.Poster);
+ Assert.Single(show.ExternalIDs);
+ Assert.Equal("5", show.ExternalIDs.First().DataID);
+ Assert.Equal(provider, show.ExternalIDs.First().Provider);
+ Assert.Equal("https://www.thetvdb.com/series/slug", show.ExternalIDs.First().Link);
+ Assert.Equal(Status.Finished, show.Status);
+ }
+
+ [Fact]
+ public void SeriesSearchToShowInvalidDate()
+ {
+ SeriesSearchResult result = new()
+ {
+ Slug = "slug",
+ SeriesName = "name",
+ Aliases = new[] { "Aliases" },
+ Overview = "overview",
+ Status = "ad",
+ FirstAired = "2e021-07-23",
+ Poster = "/poster",
+ Id = 5
+ };
+ Provider provider = TestSample.Get();
+ Show show = result.ToShow(provider);
+
+ Assert.Equal("slug", show.Slug);
+ Assert.Equal("name", show.Title);
+ Assert.Single(show.Aliases);
+ Assert.Equal("Aliases", show.Aliases[0]);
+ Assert.Equal("overview", show.Overview);
+ Assert.Null(show.StartAir);
+ Assert.Equal("https://www.thetvdb.com/poster", show.Poster);
+ Assert.Single(show.ExternalIDs);
+ Assert.Equal("5", show.ExternalIDs.First().DataID);
+ Assert.Equal(provider, show.ExternalIDs.First().Provider);
+ Assert.Equal("https://www.thetvdb.com/series/slug", show.ExternalIDs.First().Link);
+ Assert.Equal(Status.Unknown, show.Status);
+ }
+
+ [Fact]
+ public void SeriesToShow()
+ {
+ Series result = new()
+ {
+ Slug = "slug",
+ SeriesName = "name",
+ Aliases = new[] { "Aliases" },
+ Overview = "overview",
+ Status = "Continuing",
+ FirstAired = "2021-07-23",
+ Poster = "poster",
+ FanArt = "fanart",
+ Id = 5,
+ Genre = new []
+ {
+ "Action",
+ "Test With Spéàacial characters"
+ }
+ };
+ Provider provider = TestSample.Get();
+ Show show = result.ToShow(provider);
+
+ Assert.Equal("slug", show.Slug);
+ Assert.Equal("name", show.Title);
+ Assert.Single(show.Aliases);
+ Assert.Equal("Aliases", show.Aliases[0]);
+ Assert.Equal("overview", show.Overview);
+ Assert.Equal(new DateTime(2021, 7, 23), show.StartAir);
+ Assert.Equal("https://www.thetvdb.com/banners/poster", show.Poster);
+ Assert.Equal("https://www.thetvdb.com/banners/fanart", show.Backdrop);
+ Assert.Single(show.ExternalIDs);
+ Assert.Equal("5", show.ExternalIDs.First().DataID);
+ Assert.Equal(provider, show.ExternalIDs.First().Provider);
+ Assert.Equal("https://www.thetvdb.com/series/slug", show.ExternalIDs.First().Link);
+ Assert.Equal(Status.Airing, show.Status);
+ Assert.Equal(2, show.Genres.Count);
+ Assert.Equal("action", show.Genres.ToArray()[0].Slug);
+ Assert.Equal("Action", show.Genres.ToArray()[0].Name);
+ Assert.Equal("Test With Spéàacial characters", show.Genres.ToArray()[1].Name);
+ Assert.Equal("test-with-speaacial-characters", show.Genres.ToArray()[1].Slug);
+ }
+
+ [Fact]
+ public void ActorToPeople()
+ {
+ Actor actor = new()
+ {
+ Id = 5,
+ Image = "image",
+ Name = "Name",
+ Role = "role"
+ };
+ Provider provider = TestSample.Get();
+ PeopleRole people = actor.ToPeopleRole(provider);
+
+ Assert.Equal("name", people.Slug);
+ Assert.Equal("Name", people.People.Name);
+ Assert.Equal("role", people.Role);
+ Assert.Equal("https://www.thetvdb.com/banners/image", people.People.Poster);
+ }
+
+ [Fact]
+ public void EpisodeRecordToEpisode()
+ {
+ EpisodeRecord record = new()
+ {
+ Id = 5,
+ AiredSeason = 2,
+ AiredEpisodeNumber = 3,
+ AbsoluteNumber = 23,
+ EpisodeName = "title",
+ Overview = "overview",
+ Filename = "thumb"
+ };
+ Provider provider = TestSample.Get();
+ Episode episode = record.ToEpisode(provider);
+
+ Assert.Equal("title", episode.Title);
+ Assert.Equal(2, episode.SeasonNumber);
+ Assert.Equal(3, episode.EpisodeNumber);
+ Assert.Equal(23, episode.AbsoluteNumber);
+ Assert.Equal("overview", episode.Overview);
+ Assert.Equal("https://www.thetvdb.com/banners/thumb", episode.Thumb);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj
index d198dae6..120f8ba7 100644
--- a/Kyoo.Tests/Kyoo.Tests.csproj
+++ b/Kyoo.Tests/Kyoo.Tests.csproj
@@ -17,6 +17,8 @@
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -32,6 +34,7 @@
+
diff --git a/Kyoo.Tests/Utility/EnumerableTests.cs b/Kyoo.Tests/Utility/EnumerableTests.cs
index 9cdd8a00..c03efa06 100644
--- a/Kyoo.Tests/Utility/EnumerableTests.cs
+++ b/Kyoo.Tests/Utility/EnumerableTests.cs
@@ -4,7 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Xunit;
-namespace Kyoo.Tests
+namespace Kyoo.Tests.Utility
{
public class EnumerableTests
{
diff --git a/Kyoo.Tests/Utility/MergerTests.cs b/Kyoo.Tests/Utility/MergerTests.cs
index 614d328f..285532c2 100644
--- a/Kyoo.Tests/Utility/MergerTests.cs
+++ b/Kyoo.Tests/Utility/MergerTests.cs
@@ -1,7 +1,11 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Kyoo.Models;
+using Kyoo.Models.Attributes;
using Xunit;
-namespace Kyoo.Tests
+namespace Kyoo.Tests.Utility
{
public class MergerTests
{
@@ -17,5 +21,192 @@ namespace Kyoo.Tests
Assert.Null(genre.Name);
Assert.Null(genre.Slug);
}
+
+ [Fact]
+ public void MergeTest()
+ {
+ Genre genre = new()
+ {
+ ID = 5
+ };
+ Genre genre2 = new()
+ {
+ Name = "test"
+ };
+ Genre ret = Merger.Merge(genre, genre2);
+ Assert.True(ReferenceEquals(genre, ret));
+ Assert.Equal(5, ret.ID);
+ Assert.Equal("test", genre.Name);
+ Assert.Null(genre.Slug);
+ }
+
+ [Fact]
+ [SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")]
+ public void MergeNullTests()
+ {
+ Genre genre = new()
+ {
+ ID = 5
+ };
+ Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null)));
+ Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre)));
+ Assert.Null(Merger.Merge(null, null));
+ }
+
+ private class TestIOnMerge : IOnMerge
+ {
+ public void OnMerge(object other)
+ {
+ Exception exception = new();
+ exception.Data[0] = other;
+ throw exception;
+ }
+ }
+
+ [Fact]
+ public void OnMergeTest()
+ {
+ TestIOnMerge test = new();
+ TestIOnMerge test2 = new();
+ Assert.Throws(() => Merger.Merge(test, test2));
+ try
+ {
+ Merger.Merge(test, test2);
+ }
+ catch (Exception ex)
+ {
+ Assert.True(ReferenceEquals(test2, ex.Data[0]));
+ }
+ }
+
+ private class Test
+ {
+ public int ID { get; set; }
+
+ public int[] Numbers { get; set; }
+ }
+
+ [Fact]
+ public void GlobalMergeListTest()
+ {
+ Test test = new()
+ {
+ ID = 5,
+ Numbers = new [] { 1 }
+ };
+ Test test2 = new()
+ {
+ Numbers = new [] { 3 }
+ };
+ Test ret = Merger.Merge(test, test2);
+ Assert.True(ReferenceEquals(test, ret));
+ Assert.Equal(5, ret.ID);
+
+ Assert.Equal(2, ret.Numbers.Length);
+ Assert.Equal(1, ret.Numbers[0]);
+ Assert.Equal(3, ret.Numbers[1]);
+ }
+
+ [Fact]
+ public void GlobalMergeListDuplicatesTest()
+ {
+ Test test = new()
+ {
+ ID = 5,
+ Numbers = new [] { 1 }
+ };
+ Test test2 = new()
+ {
+ Numbers = new []
+ {
+ 1,
+ 3,
+ 3
+ }
+ };
+ Test ret = Merger.Merge(test, test2);
+ Assert.True(ReferenceEquals(test, ret));
+ Assert.Equal(5, ret.ID);
+
+ Assert.Equal(4, ret.Numbers.Length);
+ Assert.Equal(1, ret.Numbers[0]);
+ Assert.Equal(1, ret.Numbers[1]);
+ Assert.Equal(3, ret.Numbers[2]);
+ Assert.Equal(3, ret.Numbers[3]);
+ }
+
+ [Fact]
+ public void GlobalMergeListDuplicatesResourcesTest()
+ {
+ Show test = new()
+ {
+ ID = 5,
+ Genres = new [] { new Genre("test") }
+ };
+ Show test2 = new()
+ {
+ Genres = new []
+ {
+ new Genre("test"),
+ new Genre("test2")
+ }
+ };
+ Show ret = Merger.Merge(test, test2);
+ Assert.True(ReferenceEquals(test, ret));
+ Assert.Equal(5, ret.ID);
+
+ Assert.Equal(2, ret.Genres.Count);
+ Assert.Equal("test", ret.Genres.ToArray()[0].Slug);
+ Assert.Equal("test2", ret.Genres.ToArray()[1].Slug);
+ }
+
+ [Fact]
+ public void MergeListTest()
+ {
+ int[] first = { 1 };
+ int[] second = {
+ 3,
+ 3
+ };
+ int[] ret = Merger.MergeLists(first, second);
+
+ Assert.Equal(3, ret.Length);
+ Assert.Equal(1, ret[0]);
+ Assert.Equal(3, ret[1]);
+ Assert.Equal(3, ret[2]);
+ }
+
+ [Fact]
+ public void MergeListDuplicateTest()
+ {
+ int[] first = { 1 };
+ int[] second = {
+ 1,
+ 3,
+ 3
+ };
+ int[] ret = Merger.MergeLists(first, second);
+
+ Assert.Equal(4, ret.Length);
+ Assert.Equal(1, ret[0]);
+ Assert.Equal(1, ret[1]);
+ Assert.Equal(3, ret[2]);
+ Assert.Equal(3, ret[3]);
+ }
+
+ [Fact]
+ public void MergeListDuplicateCustomEqualityTest()
+ {
+ int[] first = { 1 };
+ int[] second = {
+ 3,
+ 2
+ };
+ int[] ret = Merger.MergeLists(first, second, (x, y) => x % 2 == y % 2);
+
+ Assert.Equal(2, ret.Length);
+ Assert.Equal(1, ret[0]);
+ Assert.Equal(2, ret[1]);
+ }
}
}
\ No newline at end of file
diff --git a/Kyoo.Tests/Utility/TaskTests.cs b/Kyoo.Tests/Utility/TaskTests.cs
index 3a7baa48..3bcd723f 100644
--- a/Kyoo.Tests/Utility/TaskTests.cs
+++ b/Kyoo.Tests/Utility/TaskTests.cs
@@ -3,7 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using Xunit;
-namespace Kyoo.Tests
+namespace Kyoo.Tests.Utility
{
public class TaskTests
{
diff --git a/Kyoo.Tests/Utility/UtilityTests.cs b/Kyoo.Tests/Utility/UtilityTests.cs
index 15469411..f6f279aa 100644
--- a/Kyoo.Tests/Utility/UtilityTests.cs
+++ b/Kyoo.Tests/Utility/UtilityTests.cs
@@ -1,9 +1,12 @@
using System;
using System.Linq.Expressions;
+using System.Reflection;
using Kyoo.Models;
using Xunit;
-namespace Kyoo.Tests
+using Utils = Kyoo.Utility;
+
+namespace Kyoo.Tests.Utility
{
public class UtilityTests
{
@@ -13,12 +16,12 @@ namespace Kyoo.Tests
Expression> member = x => x.ID;
Expression> memberCast = x => x.ID;
- Assert.False(Utility.IsPropertyExpression(null));
- Assert.True(Utility.IsPropertyExpression(member));
- Assert.True(Utility.IsPropertyExpression(memberCast));
+ Assert.False(Utils.IsPropertyExpression(null));
+ Assert.True(Utils.IsPropertyExpression(member));
+ Assert.True(Utils.IsPropertyExpression(memberCast));
Expression> call = x => x.GetID("test");
- Assert.False(Utility.IsPropertyExpression(call));
+ Assert.False(Utils.IsPropertyExpression(call));
}
[Fact]
@@ -27,9 +30,51 @@ namespace Kyoo.Tests
Expression> member = x => x.ID;
Expression> memberCast = x => x.ID;
- Assert.Equal("ID", Utility.GetPropertyName(member));
- Assert.Equal("ID", Utility.GetPropertyName(memberCast));
- Assert.Throws