mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Merge pull request #31 from AnonymusRaccoon/tvdb
Adding a tvdb provider
This commit is contained in:
commit
5fd1917ab4
@ -9,6 +9,7 @@ using IdentityServer4.Services;
|
|||||||
using Kyoo.Authentication.Models;
|
using Kyoo.Authentication.Models;
|
||||||
using Kyoo.Authentication.Views;
|
using Kyoo.Authentication.Views;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
using Kyoo.Models.Permissions;
|
using Kyoo.Models.Permissions;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
@ -65,6 +66,11 @@ namespace Kyoo.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration manager used to register typed/untyped implementations.
|
||||||
|
/// </summary>
|
||||||
|
[Injected] public IConfigurationManager ConfigurationManager { private get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new authentication module instance and use the given configuration and environment.
|
/// Create a new authentication module instance and use the given configuration and environment.
|
||||||
@ -98,8 +104,6 @@ namespace Kyoo.Authentication
|
|||||||
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
|
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
|
||||||
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
|
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
|
||||||
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
|
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
|
||||||
services.AddConfiguration<AuthenticationOption>(AuthenticationOption.Path);
|
|
||||||
|
|
||||||
|
|
||||||
List<Client> clients = new();
|
List<Client> clients = new();
|
||||||
_configuration.GetSection("authentication:clients").Bind(clients);
|
_configuration.GetSection("authentication:clients").Bind(clients);
|
||||||
@ -139,6 +143,8 @@ namespace Kyoo.Authentication
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ConfigureAspNet(IApplicationBuilder app)
|
public void ConfigureAspNet(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
|
ConfigurationManager.AddTyped<AuthenticationOption>(AuthenticationOption.Path);
|
||||||
|
|
||||||
app.UseCookiePolicy(new CookiePolicyOptions
|
app.UseCookiePolicy(new CookiePolicyOptions
|
||||||
{
|
{
|
||||||
MinimumSameSitePolicy = SameSiteMode.Strict
|
MinimumSameSitePolicy = SameSiteMode.Strict
|
||||||
|
@ -36,7 +36,7 @@ namespace Kyoo.Authentication.Views
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A file manager to send profile pictures
|
/// A file manager to send profile pictures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IFileManager _files;
|
private readonly IFileSystem _files;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options about authentication. Those options are monitored and reloads are supported.
|
/// Options about authentication. Those options are monitored and reloads are supported.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -50,7 +50,7 @@ namespace Kyoo.Authentication.Views
|
|||||||
/// <param name="files">A file manager to send profile pictures</param>
|
/// <param name="files">A file manager to send profile pictures</param>
|
||||||
/// <param name="options">Authentication options (this may be hot reloaded)</param>
|
/// <param name="options">Authentication options (this may be hot reloaded)</param>
|
||||||
public AccountApi(IUserRepository users,
|
public AccountApi(IUserRepository users,
|
||||||
IFileManager files,
|
IFileSystem files,
|
||||||
IOptions<AuthenticationOption> options)
|
IOptions<AuthenticationOption> options)
|
||||||
{
|
{
|
||||||
_users = users;
|
_users = users;
|
||||||
@ -205,8 +205,8 @@ namespace Kyoo.Authentication.Views
|
|||||||
user.Username = data.Username;
|
user.Username = data.Username;
|
||||||
if (data.Picture?.Length > 0)
|
if (data.Picture?.Length > 0)
|
||||||
{
|
{
|
||||||
string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
||||||
await using Stream file = _files.NewFile(path);
|
await using Stream file = await _files.NewFile(path);
|
||||||
await data.Picture.CopyToAsync(file);
|
await data.Picture.CopyToAsync(file);
|
||||||
}
|
}
|
||||||
return await _users.Edit(user, false);
|
return await _users.Edit(user, false);
|
||||||
|
@ -12,6 +12,21 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IConfigurationManager
|
public interface IConfigurationManager
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add an editable configuration to the editable configuration list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The root path of the editable configuration. It should not be a nested type.</param>
|
||||||
|
/// <typeparam name="T">The type of the configuration</typeparam>
|
||||||
|
void AddTyped<T>(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The root path of the editable configuration. It should not be a nested type.</param>
|
||||||
|
void AddUntyped(string path);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the value of a setting using it's path.
|
/// Get the value of a setting using it's path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -10,7 +10,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
|
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFileManager
|
public interface IFileSystem
|
||||||
{
|
{
|
||||||
// TODO find a way to handle Transmux/Transcode with this system.
|
// TODO find a way to handle Transmux/Transcode with this system.
|
||||||
|
|
||||||
@ -41,21 +41,37 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="path">The path of the file</param>
|
/// <param name="path">The path of the file</param>
|
||||||
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
|
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
|
||||||
/// <returns>A reader to read the file.</returns>
|
/// <returns>A reader to read the file.</returns>
|
||||||
public Stream GetReader([NotNull] string path);
|
public Task<Stream> GetReader([NotNull] string path);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new file at <paramref name="path"></paramref>.
|
/// Create a new file at <paramref name="path"></paramref>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path of the new file.</param>
|
/// <param name="path">The path of the new file.</param>
|
||||||
/// <returns>A writer to write to the new file.</returns>
|
/// <returns>A writer to write to the new file.</returns>
|
||||||
public Stream NewFile([NotNull] string path);
|
public Task<Stream> NewFile([NotNull] string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new directory at the given path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path of the directory</param>
|
||||||
|
/// <returns>The path of the newly created directory is returned.</returns>
|
||||||
|
public Task<string> CreateDirectory([NotNull] string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combine multiple paths.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paths">The paths to combine</param>
|
||||||
|
/// <returns>The combined path.</returns>
|
||||||
|
public string Combine(params string[] paths);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List files in a directory.
|
/// List files in a directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path of the directory</param>
|
/// <param name="path">The path of the directory</param>
|
||||||
|
/// <param name="options">Should the search be recursive or not.</param>
|
||||||
/// <returns>A list of files's path.</returns>
|
/// <returns>A list of files's path.</returns>
|
||||||
public Task<ICollection<string>> ListFiles([NotNull] string path);
|
public Task<ICollection<string>> ListFiles([NotNull] string path,
|
||||||
|
SearchOption options = SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if a file exists at the given path.
|
/// Check if a file exists at the given path.
|
||||||
@ -71,24 +87,6 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="show">The show to proceed</param>
|
/// <param name="show">The show to proceed</param>
|
||||||
/// <returns>The extra directory of the show</returns>
|
/// <returns>The extra directory of the show</returns>
|
||||||
public string GetExtraDirectory(Show show);
|
public string GetExtraDirectory([NotNull] Show show);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="season">The season to proceed</param>
|
|
||||||
/// <returns>The extra directory of the season</returns>
|
|
||||||
public string GetExtraDirectory(Season season);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="episode">The episode to proceed</param>
|
|
||||||
/// <returns>The extra directory of the episode</returns>
|
|
||||||
public string GetExtraDirectory(Episode episode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
37
Kyoo.Common/Controllers/IIdentifier.cs
Normal file
37
Kyoo.Common/Controllers/IIdentifier.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Kyoo.Models.Exceptions;
|
||||||
|
|
||||||
|
namespace Kyoo.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An interface to identify episodes, shows and metadata based on the episode file.
|
||||||
|
/// </summary>
|
||||||
|
public interface IIdentifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identify a path and return the parsed metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path of the episode file to parse.</param>
|
||||||
|
/// <exception cref="IdentificationFailedException">
|
||||||
|
/// The identifier could not work for the given path.
|
||||||
|
/// </exception>
|
||||||
|
/// <returns>
|
||||||
|
/// A tuple of models representing parsed metadata.
|
||||||
|
/// If no metadata could be parsed for a type, null can be returned.
|
||||||
|
/// </returns>
|
||||||
|
Task<(Collection, Show, Season, Episode)> Identify(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identify an external subtitle or track file from it's path and return the parsed metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path of the external track file to parse.</param>
|
||||||
|
/// <exception cref="IdentificationFailedException">
|
||||||
|
/// The identifier could not work for the given path.
|
||||||
|
/// </exception>
|
||||||
|
/// <returns>
|
||||||
|
/// The metadata of the track identified.
|
||||||
|
/// </returns>
|
||||||
|
Task<Track> IdentifyTrack(string path);
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,76 @@
|
|||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An interface to automatically retrieve metadata from external providers.
|
||||||
|
/// </summary>
|
||||||
public interface IMetadataProvider
|
public interface IMetadataProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Provider"/> corresponding to this provider.
|
||||||
|
/// This allow to map metadata to a provider, keep metadata links and
|
||||||
|
/// know witch <see cref="IMetadataProvider"/> is used for a specific <see cref="Library"/>.
|
||||||
|
/// </summary>
|
||||||
Provider Provider { get; }
|
Provider Provider { get; }
|
||||||
|
|
||||||
Task<Collection> GetCollectionFromName(string name);
|
/// <summary>
|
||||||
|
/// Return a new item with metadata from your provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">
|
||||||
|
/// 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.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// You must not use metadata from the given <paramref name="item"/>.
|
||||||
|
/// Merging metadata is the job of Kyoo, a complex <typeparamref name="T"/> is given
|
||||||
|
/// to make a precise search and give you every available properties, not to discard properties.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>A new <typeparamref name="T"/> containing metadata from your provider or null</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
|
Task<T> Get<T>([NotNull] T item)
|
||||||
|
where T : class, IResource;
|
||||||
|
|
||||||
Task<Show> GetShowByID(Show show);
|
/// <summary>
|
||||||
Task<ICollection<Show>> SearchShows(string showName, bool isMovie);
|
/// Search for a specific type of items with a given query.
|
||||||
Task<ICollection<PeopleRole>> GetPeople(Show show);
|
/// </summary>
|
||||||
|
/// <param name="query">The search query to use.</param>
|
||||||
|
/// <returns>The list of items that could be found on this specific provider.</returns>
|
||||||
|
[ItemNotNull]
|
||||||
|
Task<ICollection<T>> Search<T>(string query)
|
||||||
|
where T : class, IResource;
|
||||||
|
}
|
||||||
|
|
||||||
Task<Season> GetSeason(Show show, int seasonNumber);
|
/// <summary>
|
||||||
|
/// A special <see cref="IMetadataProvider"/> that merge results.
|
||||||
|
/// This interface exists to specify witch provider to use but it can be used like any other metadata provider.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AProviderComposite : IMetadataProvider
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[ItemNotNull]
|
||||||
|
public abstract Task<T> Get<T>(T item)
|
||||||
|
where T : class, IResource;
|
||||||
|
|
||||||
Task<Episode> GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber);
|
/// <inheritdoc />
|
||||||
|
public abstract Task<ICollection<T>> Search<T>(string query)
|
||||||
|
where T : class, IResource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public Provider Provider => null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Select witch providers to use.
|
||||||
|
/// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providers">The list of providers to use</param>
|
||||||
|
public abstract void UseProviders(IEnumerable<Provider> providers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Autofac;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -10,8 +11,10 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A common interface used to discord plugins
|
/// A common interface used to discord plugins
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>You can inject services in the IPlugin constructor.
|
/// <remarks>
|
||||||
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.</remarks>
|
/// You can inject services in the IPlugin constructor.
|
||||||
|
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.
|
||||||
|
/// </remarks>
|
||||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||||
public interface IPlugin
|
public interface IPlugin
|
||||||
{
|
{
|
||||||
@ -55,6 +58,15 @@ namespace Kyoo.Controllers
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
ICollection<Type> Requires { get; }
|
ICollection<Type> Requires { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A configure method that will be run on plugin's startup.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The autofac service container to register services.</param>
|
||||||
|
void Configure(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
// Skipped
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A configure method that will be run on plugin's startup.
|
/// A configure method that will be run on plugin's startup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -64,21 +76,34 @@ namespace Kyoo.Controllers
|
|||||||
/// or <see cref="ProviderCondition.Has(System.Collections.Generic.ICollection{System.Type},System.Collections.Generic.ICollection{System.Type})"/>>
|
/// or <see cref="ProviderCondition.Has(System.Collections.Generic.ICollection{System.Type},System.Collections.Generic.ICollection{System.Type})"/>>
|
||||||
/// You can't simply check on the service collection because some dependencies might be registered after your plugin.
|
/// You can't simply check on the service collection because some dependencies might be registered after your plugin.
|
||||||
/// </param>
|
/// </param>
|
||||||
void Configure(IServiceCollection services, ICollection<Type> availableTypes);
|
void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||||
|
{
|
||||||
|
// Skipped
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An optional configuration step to allow a plugin to change asp net configurations.
|
/// 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.
|
/// WARNING: This is only called on Kyoo's startup so you must restart the app to apply this changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param>
|
/// <param name="app">
|
||||||
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.
|
||||||
|
/// </param>
|
||||||
|
void ConfigureAspNet(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
// Skipped
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An optional function to execute and initialize your plugin.
|
/// An optional function to execute and initialize your plugin.
|
||||||
/// It can be used to initialize a database connection, fill initial data or anything.
|
/// It can be used to initialize a database connection, fill initial data or anything.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">A service provider to request services</param>
|
/// <param name="provider">A service provider to request services</param>
|
||||||
void Initialize(IServiceProvider provider) {}
|
void Initialize(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
// Skipped
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Autofac;
|
||||||
using Kyoo.Models.Exceptions;
|
using Kyoo.Models.Exceptions;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -42,7 +43,13 @@ namespace Kyoo.Controllers
|
|||||||
public void LoadPlugins(ICollection<IPlugin> plugins);
|
public void LoadPlugins(ICollection<IPlugin> plugins);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configure services adding or removing services as the plugins wants.
|
/// Configure container adding or removing services as the plugins wants.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The container to populate</param>
|
||||||
|
void ConfigureContainer(ContainerBuilder builder);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configure services via the microsoft way. This allow libraries to add their services.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service collection to populate</param>
|
/// <param name="services">The service collection to populate</param>
|
||||||
public void ConfigureServices(IServiceCollection services);
|
public void ConfigureServices(IServiceCollection services);
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Kyoo.Models;
|
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
|
||||||
{
|
|
||||||
public interface IProviderManager
|
|
||||||
{
|
|
||||||
Task<Collection> GetCollectionFromName(string name, Library library);
|
|
||||||
Task<Show> CompleteShow(Show show, Library library);
|
|
||||||
Task<Show> SearchShow(string showName, bool isMovie, Library library);
|
|
||||||
Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library);
|
|
||||||
Task<Season> GetSeason(Show show, int seasonNumber, Library library);
|
|
||||||
Task<Episode> GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, Library library);
|
|
||||||
Task<ICollection<PeopleRole>> GetPeople(Show show, Library library);
|
|
||||||
}
|
|
||||||
}
|
|
@ -128,6 +128,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="id">The id of the resource</param>
|
/// <param name="id">The id of the resource</param>
|
||||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> Get(int id);
|
Task<T> Get(int id);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a resource from it's slug.
|
/// Get a resource from it's slug.
|
||||||
@ -135,6 +136,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="slug">The slug of the resource</param>
|
/// <param name="slug">The slug of the resource</param>
|
||||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> Get(string slug);
|
Task<T> Get(string slug);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the first resource that match the predicate.
|
/// Get the first resource that match the predicate.
|
||||||
@ -142,6 +144,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="where">A predicate to filter the resource.</param>
|
/// <param name="where">A predicate to filter the resource.</param>
|
||||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> Get(Expression<Func<T, bool>> where);
|
Task<T> Get(Expression<Func<T, bool>> where);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -149,18 +152,21 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The id of the resource</param>
|
/// <param name="id">The id of the resource</param>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
Task<T> GetOrDefault(int id);
|
Task<T> GetOrDefault(int id);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a resource from it's slug or null if it is not found.
|
/// Get a resource from it's slug or null if it is not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="slug">The slug of the resource</param>
|
/// <param name="slug">The slug of the resource</param>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
Task<T> GetOrDefault(string slug);
|
Task<T> GetOrDefault(string slug);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the first resource that match the predicate or null if it is not found.
|
/// Get the first resource that match the predicate or null if it is not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="where">A predicate to filter the resource.</param>
|
/// <param name="where">A predicate to filter the resource.</param>
|
||||||
/// <returns>The resource found</returns>
|
/// <returns>The resource found</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
|
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -168,6 +174,7 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query string.</param>
|
/// <param name="query">The query string.</param>
|
||||||
/// <returns>A list of resources found</returns>
|
/// <returns>A list of resources found</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<ICollection<T>> Search(string query);
|
Task<ICollection<T>> Search(string query);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -177,6 +184,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
|
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
|
||||||
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
||||||
/// <returns>A list of resources that match every filters</returns>
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
|
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
|
||||||
Sort<T> sort = default,
|
Sort<T> sort = default,
|
||||||
Pagination limit = default);
|
Pagination limit = default);
|
||||||
@ -187,6 +195,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="sort">A sort by predicate. The order is ascending.</param>
|
/// <param name="sort">A sort by predicate. The order is ascending.</param>
|
||||||
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
||||||
/// <returns>A list of resources that match every filters</returns>
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<ICollection<T>> GetAll([Optional] Expression<Func<T, bool>> where,
|
Task<ICollection<T>> GetAll([Optional] Expression<Func<T, bool>> where,
|
||||||
Expression<Func<T, object>> sort,
|
Expression<Func<T, object>> sort,
|
||||||
Pagination limit = default
|
Pagination limit = default
|
||||||
@ -205,6 +214,7 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="obj">The item to register</param>
|
/// <param name="obj">The item to register</param>
|
||||||
/// <returns>The resource registers and completed by database's information (related items & so on)</returns>
|
/// <returns>The resource registers and completed by database's information (related items & so on)</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> Create([NotNull] T obj);
|
Task<T> Create([NotNull] T obj);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -212,6 +222,7 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="obj">The object to create</param>
|
/// <param name="obj">The object to create</param>
|
||||||
/// <returns>The newly created item or the existing value if it existed.</returns>
|
/// <returns>The newly created item or the existing value if it existed.</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> CreateIfNotExists([NotNull] T obj);
|
Task<T> CreateIfNotExists([NotNull] T obj);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -221,6 +232,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
|
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
|
||||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
/// <returns>The resource edited and completed by database's information (related items & so on)</returns>
|
/// <returns>The resource edited and completed by database's information (related items & so on)</returns>
|
||||||
|
[ItemNotNull]
|
||||||
Task<T> Edit([NotNull] T edited, bool resetOld);
|
Task<T> Edit([NotNull] T edited, bool resetOld);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -3,12 +3,15 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Models;
|
||||||
using Kyoo.Models.Attributes;
|
using Kyoo.Models.Attributes;
|
||||||
|
using Kyoo.Models.Exceptions;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This struct will be used to generate the swagger documentation of the task.</remarks>
|
/// <remarks>This struct will be used to generate the swagger documentation of the task.</remarks>
|
||||||
public record TaskParameter
|
public record TaskParameter
|
||||||
@ -60,6 +63,24 @@ namespace Kyoo.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new required task parameter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter</param>
|
||||||
|
/// <param name="description">The description of the parameter</param>
|
||||||
|
/// <typeparam name="T">The type of the parameter.</typeparam>
|
||||||
|
/// <returns>A new task parameter.</returns>
|
||||||
|
public static TaskParameter CreateRequired<T>(string name, string description)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
Type = typeof(T),
|
||||||
|
IsRequired = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a parameter's value to give to a task.
|
/// Create a parameter's value to give to a task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -94,6 +115,17 @@ namespace Kyoo.Controllers
|
|||||||
/// <returns>The value of this parameter.</returns>
|
/// <returns>The value of this parameter.</returns>
|
||||||
public T As<T>()
|
public T As<T>()
|
||||||
{
|
{
|
||||||
|
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));
|
return (T)Convert.ChangeType(Value, typeof(T));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,58 +163,38 @@ namespace Kyoo.Controllers
|
|||||||
public interface ITask
|
public interface ITask
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The slug of the task, used to start it.
|
/// The list of parameters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Slug { get; }
|
/// <returns>
|
||||||
|
/// All parameters that this task as. Every one of them will be given to the run function with a value.
|
||||||
/// <summary>
|
/// </returns>
|
||||||
/// The name of the task that will be displayed to the user.
|
public TaskParameters GetParameters();
|
||||||
/// </summary>
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A quick description of what this task will do.
|
|
||||||
/// </summary>
|
|
||||||
public string Description { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An optional message to display to help the user.
|
|
||||||
/// </summary>
|
|
||||||
public string HelpMessage { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Should this task be automatically run at app startup?
|
|
||||||
/// </summary>
|
|
||||||
public bool RunOnStartup { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
|
|
||||||
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
|
|
||||||
/// </summary>
|
|
||||||
public int Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start this task.
|
/// Start this task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="arguments">The list of parameters.</param>
|
/// <param name="arguments">
|
||||||
|
/// The list of parameters.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="progress">
|
||||||
|
/// The progress reporter. Used to inform the sender the percentage of completion of this task
|
||||||
|
/// .</param>
|
||||||
/// <param name="cancellationToken">A token to request the task's cancellation.
|
/// <param name="cancellationToken">A token to request the task's cancellation.
|
||||||
/// If this task is not cancelled quickly, it might be killed by the runner.</param>
|
/// If this task is not cancelled quickly, it might be killed by the runner.
|
||||||
|
/// </param>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Your task can have any service as a public field and use the <see cref="InjectedAttribute"/>,
|
/// Your task can have any service as a public field and use the <see cref="InjectedAttribute"/>,
|
||||||
/// they will be set to an available service from the service container before calling this method.
|
/// 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.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public Task Run(TaskParameters arguments, CancellationToken cancellationToken);
|
/// <exception cref="TaskFailedException">
|
||||||
|
/// An exception meaning that the task has failed for handled reasons like invalid arguments,
|
||||||
/// <summary>
|
/// invalid environment, missing plugins or failures not related to a default in the code.
|
||||||
/// The list of parameters
|
/// This exception allow the task to display a failure message to the end user while others exceptions
|
||||||
/// </summary>
|
/// will be displayed as unhandled exceptions and display a stack trace.
|
||||||
/// <returns>All parameters that this task as. Every one of them will be given to the run function with a value.</returns>
|
/// </exception>
|
||||||
public TaskParameters GetParameters();
|
public Task Run([NotNull] TaskParameters arguments,
|
||||||
|
[NotNull] IProgress<float> progress,
|
||||||
/// <summary>
|
CancellationToken cancellationToken);
|
||||||
/// If this task is running, return the percentage of completion of this task or null if no percentage can be given.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The percentage of completion of the task.</returns>
|
|
||||||
public int? Progress();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading;
|
||||||
|
using Kyoo.Common.Models.Attributes;
|
||||||
using Kyoo.Models.Exceptions;
|
using Kyoo.Models.Exceptions;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
@ -13,22 +16,67 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start a new task (or queue it).
|
/// Start a new task (or queue it).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="taskSlug">The slug of the task to run</param>
|
/// <param name="taskSlug">
|
||||||
/// <param name="arguments">A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.</param>
|
/// The slug of the task to run.
|
||||||
/// <exception cref="ArgumentException">If the number of arguments is invalid or if an argument can't be converted.</exception>
|
/// </param>
|
||||||
/// <exception cref="ItemNotFoundException">The task could not be found.</exception>
|
/// <param name="progress">
|
||||||
void StartTask(string taskSlug, Dictionary<string, object> arguments = null);
|
/// A progress reporter to know the percentage of completion of the task.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="arguments">
|
||||||
|
/// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A custom cancellation token for the task.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
|
||||||
|
/// invalid.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ItemNotFoundException">
|
||||||
|
/// The task could not be found.
|
||||||
|
/// </exception>
|
||||||
|
void StartTask(string taskSlug,
|
||||||
|
[NotNull] IProgress<float> progress,
|
||||||
|
Dictionary<string, object> arguments = null,
|
||||||
|
CancellationToken? cancellationToken = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a new task (or queue it).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progress">
|
||||||
|
/// A progress reporter to know the percentage of completion of the task.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="arguments">
|
||||||
|
/// A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.
|
||||||
|
/// </param>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// The type of the task to start.
|
||||||
|
/// </typeparam>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A custom cancellation token for the task.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
|
||||||
|
/// invalid.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ItemNotFoundException">
|
||||||
|
/// The task could not be found.
|
||||||
|
/// </exception>
|
||||||
|
void StartTask<T>([NotNull] IProgress<float> progress,
|
||||||
|
Dictionary<string, object> arguments = null,
|
||||||
|
CancellationToken? cancellationToken = null)
|
||||||
|
where T : ITask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all currently running tasks
|
/// Get all currently running tasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of currently running tasks.</returns>
|
/// <returns>A list of currently running tasks.</returns>
|
||||||
ICollection<ITask> GetRunningTasks();
|
ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all available tasks
|
/// Get all available tasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of every tasks that this instance know.</returns>
|
/// <returns>A list of every tasks that this instance know.</returns>
|
||||||
ICollection<ITask> GetAllTasks();
|
ICollection<TaskMetadataAttribute> GetAllTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,23 +1,59 @@
|
|||||||
using Kyoo.Models;
|
using System;
|
||||||
|
using Kyoo.Models;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Download images and retrieve the path of those images for a resource.
|
||||||
|
/// </summary>
|
||||||
public interface IThumbnailsManager
|
public interface IThumbnailsManager
|
||||||
{
|
{
|
||||||
Task Validate(Show show, bool alwaysDownload = false);
|
/// <summary>
|
||||||
Task Validate(Season season, bool alwaysDownload = false);
|
/// Download images of a specified item.
|
||||||
Task Validate(Episode episode, bool alwaysDownload = false);
|
/// If no images is available to download, do nothing and silently return.
|
||||||
Task Validate(People actors, bool alwaysDownload = false);
|
/// </summary>
|
||||||
Task Validate(Provider actors, bool alwaysDownload = false);
|
/// <param name="item">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <typeparam name="T">The type of the item</typeparam>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
Task<bool> DownloadImages<T>([NotNull] T item, bool alwaysDownload = false)
|
||||||
|
where T : IResource;
|
||||||
|
|
||||||
Task<string> GetShowPoster([NotNull] Show show);
|
|
||||||
Task<string> GetShowLogo([NotNull] Show show);
|
/// <summary>
|
||||||
Task<string> GetShowBackdrop([NotNull] Show show);
|
/// Retrieve the local path of the poster of the given item.
|
||||||
Task<string> GetSeasonPoster([NotNull] Season season);
|
/// </summary>
|
||||||
Task<string> GetEpisodeThumb([NotNull] Episode episode);
|
/// <param name="item">The item to retrieve the poster from.</param>
|
||||||
Task<string> GetPeoplePoster([NotNull] People people);
|
/// <typeparam name="T">The type of the item</typeparam>
|
||||||
Task<string> GetProviderLogo([NotNull] Provider provider);
|
/// <exception cref="NotSupportedException">If the type does not have a poster</exception>
|
||||||
|
/// <returns>The path of the poster for the given resource (it might or might not exists).</returns>
|
||||||
|
Task<string> GetPoster<T>([NotNull] T item)
|
||||||
|
where T : IResource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the local path of the logo of the given item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to retrieve the logo from.</param>
|
||||||
|
/// <typeparam name="T">The type of the item</typeparam>
|
||||||
|
/// <exception cref="NotSupportedException">If the type does not have a logo</exception>
|
||||||
|
/// <returns>The path of the logo for the given resource (it might or might not exists).</returns>
|
||||||
|
Task<string> GetLogo<T>([NotNull] T item)
|
||||||
|
where T : IResource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the local path of the thumbnail of the given item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to retrieve the thumbnail from.</param>
|
||||||
|
/// <typeparam name="T">The type of the item</typeparam>
|
||||||
|
/// <exception cref="NotSupportedException">If the type does not have a thumbnail</exception>
|
||||||
|
/// <returns>The path of the thumbnail for the given resource (it might or might not exists).</returns>
|
||||||
|
Task<string> GetThumbnail<T>([NotNull] T item)
|
||||||
|
where T : IResource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Autofac" Version="6.2.0" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
53
Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs
Normal file
53
Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.Composition;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Common.Models.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to inform how a <see cref="IFileSystem"/> works.
|
||||||
|
/// </summary>
|
||||||
|
[MetadataAttribute]
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class FileSystemMetadataAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The scheme(s) used to identify this path.
|
||||||
|
/// It can be something like http, https, ftp, file and so on.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If multiples files with the same schemes exists, an exception will be thrown.
|
||||||
|
/// </remarks>
|
||||||
|
public string[] Scheme { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>true</c> if the scheme should be removed from the path before calling
|
||||||
|
/// methods of this <see cref="IFileSystem"/>, <c>false</c> otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public bool StripScheme { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="FileSystemMetadataAttribute"/> using the specified schemes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schemes">The schemes to use.</param>
|
||||||
|
public FileSystemMetadataAttribute(string[] schemes)
|
||||||
|
{
|
||||||
|
Scheme = schemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="FileSystemMetadataAttribute"/> using a dictionary of metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">
|
||||||
|
/// The dictionary of metadata. This method expect the dictionary to contain a field
|
||||||
|
/// per property in this attribute, with the same types as the properties of this attribute.
|
||||||
|
/// </param>
|
||||||
|
public FileSystemMetadataAttribute(IDictionary<string, object> metadata)
|
||||||
|
{
|
||||||
|
Scheme = (string[])metadata[nameof(Scheme)];
|
||||||
|
StripScheme = (bool)metadata[nameof(StripScheme)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,8 @@ namespace Kyoo.Models.Attributes
|
|||||||
/// An attribute to inform that the service will be injected automatically by a service provider.
|
/// An attribute to inform that the service will be injected automatically by a service provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>.
|
/// It should only be used on <see cref="IPlugin"/> and it will be injected before
|
||||||
/// It can also be used on <see cref="IPlugin"/> and it will be injected before calling <see cref="IPlugin.ConfigureAspNet"/>.
|
/// calling <see cref="IPlugin.ConfigureAspNet"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
|
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
|
||||||
|
77
Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs
Normal file
77
Kyoo.Common/Models/Attributes/TaskMetadataAttribute.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.Composition;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Common.Models.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to inform how a <see cref="IFileSystem"/> works.
|
||||||
|
/// </summary>
|
||||||
|
[MetadataAttribute]
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class TaskMetadataAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The slug of the task, used to start it.
|
||||||
|
/// </summary>
|
||||||
|
public string Slug { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the task that will be displayed to the user.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A quick description of what this task will do.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should this task be automatically run at app startup?
|
||||||
|
/// </summary>
|
||||||
|
public bool RunOnStartup { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
|
||||||
|
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>true</c> if this task should not be displayed to the user, <c>false</c> otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHidden { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="TaskMetadataAttribute"/> with the given slug, name and description.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slug">The slug of the task, used to start it.</param>
|
||||||
|
/// <param name="name">The name of the task that will be displayed to the user.</param>
|
||||||
|
/// <param name="description">A quick description of what this task will do.</param>
|
||||||
|
public TaskMetadataAttribute(string slug, string name, string description)
|
||||||
|
{
|
||||||
|
Slug = slug;
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="TaskMetadataAttribute"/> using a dictionary of metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">
|
||||||
|
/// The dictionary of metadata. This method expect the dictionary to contain a field
|
||||||
|
/// per property in this attribute, with the same types as the properties of this attribute.
|
||||||
|
/// </param>
|
||||||
|
public TaskMetadataAttribute(IDictionary<string, object> metadata)
|
||||||
|
{
|
||||||
|
Slug = (string)metadata[nameof(Slug)];
|
||||||
|
Name = (string)metadata[nameof(Name)];
|
||||||
|
Description = (string)metadata[nameof(Description)];
|
||||||
|
RunOnStartup = (bool)metadata[nameof(RunOnStartup)];
|
||||||
|
Priority = (int)metadata[nameof(Priority)];
|
||||||
|
IsHidden = (bool)metadata[nameof(IsHidden)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Models.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An exception raised when an <see cref="IIdentifier"/> failed.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class IdentificationFailedException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="IdentificationFailedException"/> with a default message.
|
||||||
|
/// </summary>
|
||||||
|
public IdentificationFailedException()
|
||||||
|
: base("An identification failed.")
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="IdentificationFailedException"/> with a custom message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to use.</param>
|
||||||
|
public IdentificationFailedException(string message)
|
||||||
|
: base(message)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serialization constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">Serialization infos</param>
|
||||||
|
/// <param name="context">The serialization context</param>
|
||||||
|
protected IdentificationFailedException(SerializationInfo info, StreamingContext context)
|
||||||
|
: base(info, context)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
45
Kyoo.Common/Models/Exceptions/TaskFailedException.cs
Normal file
45
Kyoo.Common/Models/Exceptions/TaskFailedException.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Models.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An exception raised when an <see cref="ITask"/> failed.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class TaskFailedException : AggregateException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="TaskFailedException"/> with a default message.
|
||||||
|
/// </summary>
|
||||||
|
public TaskFailedException()
|
||||||
|
: base("A task failed.")
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="TaskFailedException"/> with a custom message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to use.</param>
|
||||||
|
public TaskFailedException(string message)
|
||||||
|
: base(message)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="TaskFailedException"/> wrapping another exception.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">The exception to wrap.</param>
|
||||||
|
public TaskFailedException(Exception exception)
|
||||||
|
: base(exception)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serialization constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">Serialization infos</param>
|
||||||
|
/// <param name="context">The serialization context</param>
|
||||||
|
protected TaskFailedException(SerializationInfo info, StreamingContext context)
|
||||||
|
: base(info, context)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
@ -58,7 +58,7 @@ namespace Kyoo.Models
|
|||||||
/// By default, the http path for this poster is returned from the public API.
|
/// By default, the http path for this poster is returned from the public API.
|
||||||
/// This can be disabled using the internal query flag.
|
/// This can be disabled using the internal query flag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; }
|
[SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")] public string Poster { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of this item (ether a collection, a show or a movie).
|
/// The type of this item (ether a collection, a show or a movie).
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
|
||||||
namespace Kyoo.Models
|
namespace Kyoo.Models
|
||||||
{
|
{
|
||||||
@ -107,12 +108,12 @@ namespace Kyoo.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A reference of the first resource.
|
/// A reference of the first resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T1 First { get; set; }
|
[SerializeIgnore] public T1 First { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A reference to the second resource.
|
/// A reference to the second resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T2 Second { get; set; }
|
[SerializeIgnore] public T2 Second { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -20,6 +20,12 @@ namespace Kyoo.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A shortcut to access the provider of this metadata.
|
||||||
|
/// Unlike the <see cref="Link{T, T2}.Second"/> property, this is serializable.
|
||||||
|
/// </summary>
|
||||||
|
public Provider Provider => Second;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
|
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -20,9 +20,11 @@ namespace Kyoo.Models
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (ShowSlug == null && Show == null)
|
if (ShowSlug != null || Show != null)
|
||||||
return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
|
||||||
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||||
|
return ShowID != 0
|
||||||
|
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
[UsedImplicitly] [NotNull] private set
|
[UsedImplicitly] [NotNull] private set
|
||||||
{
|
{
|
||||||
@ -93,7 +95,7 @@ namespace Kyoo.Models
|
|||||||
public int? AbsoluteNumber { get; set; }
|
public int? AbsoluteNumber { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
|
/// The path of the video file for this episode. Any format supported by a <see cref="IFileSystem"/> is allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializeIgnore] public string Path { get; set; }
|
[SerializeIgnore] public string Path { get; set; }
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Kyoo.Common.Models.Attributes;
|
using Kyoo.Common.Models.Attributes;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models.Attributes;
|
using Kyoo.Models.Attributes;
|
||||||
@ -30,7 +31,7 @@ namespace Kyoo.Models
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The path of the root directory of this show.
|
/// The path of the root directory of this show.
|
||||||
/// This can be any kind of path supported by <see cref="IFileManager"/>
|
/// This can be any kind of path supported by <see cref="IFileSystem"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializeIgnore] public string Path { get; set; }
|
[SerializeIgnore] public string Path { get; set; }
|
||||||
|
|
||||||
@ -42,10 +43,10 @@ namespace Kyoo.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is this show airing, not aired yet or finished?
|
/// Is this show airing, not aired yet or finished?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Status? Status { get; set; }
|
public Status Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An URL to a trailer. This could be any path supported by the <see cref="IFileManager"/>.
|
/// An URL to a trailer. This could be any path supported by the <see cref="IFileSystem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
|
/// 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; }
|
public string TrailerUrl { get; set; }
|
||||||
@ -157,6 +158,7 @@ namespace Kyoo.Models
|
|||||||
/// <remarks>This method will never return anything if the <see cref="ExternalIDs"/> are not loaded.</remarks>
|
/// <remarks>This method will never return anything if the <see cref="ExternalIDs"/> are not loaded.</remarks>
|
||||||
/// <param name="provider">The slug of the provider</param>
|
/// <param name="provider">The slug of the provider</param>
|
||||||
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
|
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
|
||||||
|
[CanBeNull]
|
||||||
public string GetID(string provider)
|
public string GetID(string provider)
|
||||||
{
|
{
|
||||||
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
|
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
|
||||||
@ -183,5 +185,5 @@ namespace Kyoo.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The enum containing show's status.
|
/// The enum containing show's status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum Status { Finished, Airing, Planned, Unknown }
|
public enum Status { Unknown, Finished, Airing, Planned }
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,8 @@ namespace Kyoo.Models
|
|||||||
{
|
{
|
||||||
string type = Type.ToString().ToLower();
|
string type = Type.ToString().ToLower();
|
||||||
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
||||||
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
|
string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
|
||||||
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
|
return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||||
}
|
}
|
||||||
[UsedImplicitly] private set
|
[UsedImplicitly] private set
|
||||||
{
|
{
|
||||||
@ -47,11 +47,13 @@ namespace Kyoo.Models
|
|||||||
|
|
||||||
if (!match.Success)
|
if (!match.Success)
|
||||||
throw new ArgumentException("Invalid track slug. " +
|
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;
|
EpisodeSlug = match.Groups["ep"].Value;
|
||||||
Language = match.Groups["lang"].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;
|
IsForced = match.Groups["forced"].Success;
|
||||||
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
||||||
}
|
}
|
||||||
@ -154,30 +156,17 @@ namespace Kyoo.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="baseSlug">The slug to edit</param>
|
/// <param name="baseSlug">The slug to edit</param>
|
||||||
/// <param name="type">The new type of this </param>
|
/// <param name="type">The new type of this </param>
|
||||||
/// <param name="language"></param>
|
|
||||||
/// <param name="index"></param>
|
|
||||||
/// <param name="forced"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string EditSlug(string baseSlug,
|
public static string BuildSlug(string baseSlug,
|
||||||
StreamType type = StreamType.Unknown,
|
StreamType type)
|
||||||
string language = null,
|
|
||||||
int? index = null,
|
|
||||||
bool? forced = null)
|
|
||||||
{
|
{
|
||||||
Track track = new() {Slug = baseSlug};
|
return baseSlug.EndsWith($".{type}", StringComparison.InvariantCultureIgnoreCase)
|
||||||
if (type != StreamType.Unknown)
|
? baseSlug
|
||||||
track.Type = type;
|
: $"{baseSlug}.{type.ToString().ToLowerInvariant()}";
|
||||||
if (language != null)
|
|
||||||
track.Language = language;
|
|
||||||
if (index != null)
|
|
||||||
track.TrackIndex = index.Value;
|
|
||||||
if (forced != null)
|
|
||||||
track.IsForced = forced.Value;
|
|
||||||
return track.Slug;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace Kyoo.Models
|
|||||||
public DateTime? ReleaseDate { get; set; }
|
public DateTime? ReleaseDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
|
/// The path of the video file for this episode. Any format supported by a <see cref="IFileSystem"/> is allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializeIgnore] public string Path { get; set; }
|
[SerializeIgnore] public string Path { get; set; }
|
||||||
|
|
||||||
@ -176,13 +176,16 @@ namespace Kyoo.Models
|
|||||||
return new WatchItem
|
return new WatchItem
|
||||||
{
|
{
|
||||||
EpisodeID = ep.ID,
|
EpisodeID = ep.ID,
|
||||||
|
Slug = ep.Slug,
|
||||||
ShowSlug = ep.Show.Slug,
|
ShowSlug = ep.Show.Slug,
|
||||||
|
ShowTitle = ep.Show.Title,
|
||||||
SeasonNumber = ep.SeasonNumber,
|
SeasonNumber = ep.SeasonNumber,
|
||||||
EpisodeNumber = ep.EpisodeNumber,
|
EpisodeNumber = ep.EpisodeNumber,
|
||||||
AbsoluteNumber = ep.AbsoluteNumber,
|
AbsoluteNumber = ep.AbsoluteNumber,
|
||||||
Title = ep.Title,
|
Title = ep.Title,
|
||||||
ReleaseDate = ep.ReleaseDate,
|
ReleaseDate = ep.ReleaseDate,
|
||||||
Path = ep.Path,
|
Path = ep.Path,
|
||||||
|
Container = PathIO.GetExtension(ep.Path)![1..],
|
||||||
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
|
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
|
||||||
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
|
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
|
||||||
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
|
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
using System;
|
using Autofac;
|
||||||
using System.Linq;
|
using Autofac.Builder;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Kyoo
|
namespace Kyoo
|
||||||
{
|
{
|
||||||
@ -15,86 +13,63 @@ namespace Kyoo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new task to the container.
|
/// Register a new task to the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The container</param>
|
/// <param name="builder">The container</param>
|
||||||
/// <typeparam name="T">The type of the task</typeparam>
|
/// <typeparam name="T">The type of the task</typeparam>
|
||||||
/// <returns>The initial container.</returns>
|
/// <returns>The registration builder of this new task. That can be used to edit the registration.</returns>
|
||||||
public static IServiceCollection AddTask<T>(this IServiceCollection services)
|
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||||
|
RegisterTask<T>(this ContainerBuilder builder)
|
||||||
where T : class, ITask
|
where T : class, ITask
|
||||||
{
|
{
|
||||||
services.AddSingleton<ITask, T>();
|
return builder.RegisterType<T>().As<ITask>();
|
||||||
return services;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new metadata provider to the container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The container</param>
|
||||||
|
/// <typeparam name="T">The type of the task</typeparam>
|
||||||
|
/// <returns>The registration builder of this new provider. That can be used to edit the registration.</returns>
|
||||||
|
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||||
|
RegisterProvider<T>(this ContainerBuilder builder)
|
||||||
|
where T : class, IMetadataProvider
|
||||||
|
{
|
||||||
|
return builder.RegisterType<T>().As<IMetadataProvider>().InstancePerLifetimeScope();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new repository to the container.
|
/// Register a new repository to the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The container</param>
|
/// <param name="builder">The container</param>
|
||||||
/// <param name="lifetime">The lifetime of the repository. The default is scoped.</param>
|
|
||||||
/// <typeparam name="T">The type of the repository.</typeparam>
|
/// <typeparam name="T">The type of the repository.</typeparam>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If your repository implements a special interface, please use <see cref="AddRepository{T,T2}"/>
|
/// If your repository implements a special interface, please use <see cref="RegisterRepository{T,T2}"/>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>The initial container.</returns>
|
/// <returns>The initial container.</returns>
|
||||||
public static IServiceCollection AddRepository<T>(this IServiceCollection services,
|
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||||
ServiceLifetime lifetime = ServiceLifetime.Scoped)
|
RegisterRepository<T>(this ContainerBuilder builder)
|
||||||
where T : IBaseRepository
|
where T : IBaseRepository
|
||||||
{
|
{
|
||||||
Type repository = Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>));
|
return builder.RegisterType<T>()
|
||||||
|
.As<IBaseRepository>()
|
||||||
if (repository != null)
|
.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)))
|
||||||
services.Add(ServiceDescriptor.Describe(repository, typeof(T), lifetime));
|
.InstancePerLifetimeScope();
|
||||||
services.Add(ServiceDescriptor.Describe(typeof(IBaseRepository), typeof(T), lifetime));
|
|
||||||
return services;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new repository with a custom mapping to the container.
|
/// Register a new repository with a custom mapping to the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services"></param>
|
/// <param name="builder">The container</param>
|
||||||
/// <param name="lifetime">The lifetime of the repository. The default is scoped.</param>
|
|
||||||
/// <typeparam name="T">The custom mapping you have for your repository.</typeparam>
|
/// <typeparam name="T">The custom mapping you have for your repository.</typeparam>
|
||||||
/// <typeparam name="T2">The type of the repository.</typeparam>
|
/// <typeparam name="T2">The type of the repository.</typeparam>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If your repository does not implements a special interface, please use <see cref="AddRepository{T}"/>
|
/// If your repository does not implements a special interface, please use <see cref="RegisterRepository{T}"/>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>The initial container.</returns>
|
/// <returns>The initial container.</returns>
|
||||||
public static IServiceCollection AddRepository<T, T2>(this IServiceCollection services,
|
public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||||
ServiceLifetime lifetime = ServiceLifetime.Scoped)
|
RegisterRepository<T, T2>(this ContainerBuilder builder)
|
||||||
where T2 : IBaseRepository, T
|
where T2 : IBaseRepository, T
|
||||||
{
|
{
|
||||||
services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime));
|
return builder.RegisterRepository<T2>().As<T>();
|
||||||
return services.AddRepository<T2>(lifetime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add an editable configuration to the editable configuration list
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services">The service collection to edit</param>
|
|
||||||
/// <param name="path">The root path of the editable configuration. It should not be a nested type.</param>
|
|
||||||
/// <typeparam name="T">The type of the configuration</typeparam>
|
|
||||||
/// <returns>The given service collection is returned.</returns>
|
|
||||||
public static IServiceCollection AddConfiguration<T>(this IServiceCollection services, string path)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
if (services.Any(x => x.ServiceType == typeof(T)))
|
|
||||||
return services;
|
|
||||||
foreach (ConfigurationReference confRef in ConfigurationReference.CreateReference<T>(path))
|
|
||||||
services.AddSingleton(confRef);
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services">The service collection to edit</param>
|
|
||||||
/// <param name="path">The root path of the editable configuration. It should not be a nested type.</param>
|
|
||||||
/// <returns>The given service collection is returned.</returns>
|
|
||||||
public static IServiceCollection AddUntypedConfiguration(this IServiceCollection services, string path)
|
|
||||||
{
|
|
||||||
services.AddSingleton(ConfigurationReference.CreateUntyped(path));
|
|
||||||
return services;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -5,6 +5,7 @@ using System.ComponentModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Models;
|
||||||
using Kyoo.Models.Attributes;
|
using Kyoo.Models.Attributes;
|
||||||
|
|
||||||
namespace Kyoo
|
namespace Kyoo
|
||||||
@ -111,7 +112,8 @@ namespace Kyoo
|
|||||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||||
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
||||||
/// <returns><see cref="first"/></returns>
|
/// <returns><see cref="first"/></returns>
|
||||||
public static T Merge<T>(T first, T second)
|
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
|
||||||
|
public static T Merge<T>([CanBeNull] T first, [CanBeNull] T second)
|
||||||
{
|
{
|
||||||
if (first == null)
|
if (first == null)
|
||||||
return second;
|
return second;
|
||||||
@ -127,9 +129,7 @@ namespace Kyoo
|
|||||||
{
|
{
|
||||||
object oldValue = property.GetValue(first);
|
object oldValue = property.GetValue(first);
|
||||||
object newValue = property.GetValue(second);
|
object newValue = property.GetValue(second);
|
||||||
object defaultValue = property.PropertyType.IsValueType
|
object defaultValue = property.PropertyType.GetClrDefault();
|
||||||
? Activator.CreateInstance(property.PropertyType)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (oldValue?.Equals(defaultValue) != false)
|
if (oldValue?.Equals(defaultValue) != false)
|
||||||
property.SetValue(first, newValue);
|
property.SetValue(first, newValue);
|
||||||
@ -139,11 +139,14 @@ namespace Kyoo
|
|||||||
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
|
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
|
||||||
.GenericTypeArguments
|
.GenericTypeArguments
|
||||||
.First();
|
.First();
|
||||||
|
Func<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
|
||||||
|
? (x, y) => x.Slug == y.Slug
|
||||||
|
: null;
|
||||||
property.SetValue(first, Utility.RunGenericMethod<object>(
|
property.SetValue(first, Utility.RunGenericMethod<object>(
|
||||||
typeof(Utility),
|
typeof(Merger),
|
||||||
nameof(MergeLists),
|
nameof(MergeLists),
|
||||||
enumerableType,
|
enumerableType,
|
||||||
oldValue, newValue, null));
|
oldValue, newValue, equalityComparer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ namespace Kyoo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="str">The string to slugify</param>
|
/// <param name="str">The string to slugify</param>
|
||||||
/// <returns>The slug version of the given string</returns>
|
/// <returns>The slug version of the given string</returns>
|
||||||
public static string ToSlug(string str)
|
public static string ToSlug([CanBeNull] string str)
|
||||||
{
|
{
|
||||||
if (str == null)
|
if (str == null)
|
||||||
return null;
|
return null;
|
||||||
@ -182,13 +182,47 @@ namespace Kyoo
|
|||||||
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
|
/// <summary>
|
||||||
|
/// Retrieve a method from an <see cref="Type"/> with the given name and respect the
|
||||||
|
/// amount of parameters and generic parameters. This works for polymorphic methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">
|
||||||
|
/// The type owning the method. For non static methods, this is the <c>this</c>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="flag">
|
||||||
|
/// The binding flags of the method. This allow you to specify public/private and so on.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="name">
|
||||||
|
/// The name of the method.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="generics">
|
||||||
|
/// The list of generic parameters.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="args">
|
||||||
|
/// The list of parameters.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||||
|
/// <returns>The method handle of the matching method.</returns>
|
||||||
|
[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)
|
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
|
||||||
.Where(x => x.Name == name)
|
.Where(x => x.Name == name)
|
||||||
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
||||||
.Where(x => x.GetParameters().Length == args.Length)
|
.Where(x => x.GetParameters().Length == args.Length)
|
||||||
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
|
.IfEmpty(() => throw new ArgumentException($"A method named {name} with " +
|
||||||
$"{args.Length} arguments and {generics.Length} generic " +
|
$"{args.Length} arguments and {generics.Length} generic " +
|
||||||
$"types could not be found on {type.Name}."))
|
$"types could not be found on {type.Name}."))
|
||||||
// TODO this won't work but I don't know why.
|
// TODO this won't work but I don't know why.
|
||||||
@ -211,9 +245,34 @@ namespace Kyoo
|
|||||||
|
|
||||||
if (methods.Length == 1)
|
if (methods.Length == 1)
|
||||||
return methods[0];
|
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run a generic static method for a runtime <see cref="Type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
|
||||||
|
/// you could do:
|
||||||
|
/// <code lang="C#">
|
||||||
|
/// Utility.RunGenericMethod<object>(
|
||||||
|
/// typeof(Utility),
|
||||||
|
/// nameof(MergeLists),
|
||||||
|
/// enumerableType,
|
||||||
|
/// oldValue, newValue, equalityComparer)
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||||
|
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||||
|
/// <param name="type">The generic type to run the method with.</param>
|
||||||
|
/// <param name="args">The list of arguments of the method</param>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||||
|
/// </typeparam>
|
||||||
|
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||||
|
/// <returns>The return of the method you wanted to run.</returns>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
|
||||||
public static T RunGenericMethod<T>(
|
public static T RunGenericMethod<T>(
|
||||||
[NotNull] Type owner,
|
[NotNull] Type owner,
|
||||||
[NotNull] string methodName,
|
[NotNull] string methodName,
|
||||||
@ -223,6 +282,34 @@ namespace Kyoo
|
|||||||
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run a generic static method for a multiple runtime <see cref="Type"/>.
|
||||||
|
/// If your generic method only needs one type, see
|
||||||
|
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
|
||||||
|
/// you could do:
|
||||||
|
/// <code>
|
||||||
|
/// Utility.RunGenericMethod<object>(
|
||||||
|
/// typeof(Utility),
|
||||||
|
/// nameof(MergeLists),
|
||||||
|
/// enumerableType,
|
||||||
|
/// oldValue, newValue, equalityComparer)
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||||
|
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||||
|
/// <param name="types">The list of generic types to run the method with.</param>
|
||||||
|
/// <param name="args">The list of arguments of the method</param>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||||
|
/// </typeparam>
|
||||||
|
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||||
|
/// <returns>The return of the method you wanted to run.</returns>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||||
|
[PublicAPI]
|
||||||
public static T RunGenericMethod<T>(
|
public static T RunGenericMethod<T>(
|
||||||
[NotNull] Type owner,
|
[NotNull] Type owner,
|
||||||
[NotNull] string methodName,
|
[NotNull] string methodName,
|
||||||
@ -238,9 +325,34 @@ namespace Kyoo
|
|||||||
if (types.Length < 1)
|
if (types.Length < 1)
|
||||||
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
||||||
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run a generic method for a runtime <see cref="Type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
|
||||||
|
/// you could do:
|
||||||
|
/// <code>
|
||||||
|
/// Utility.RunGenericMethod<object>(
|
||||||
|
/// typeof(Utility),
|
||||||
|
/// nameof(MergeLists),
|
||||||
|
/// enumerableType,
|
||||||
|
/// oldValue, newValue, equalityComparer)
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <param name="instance">The <c>this</c> of the method to run.</param>
|
||||||
|
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||||
|
/// <param name="type">The generic type to run the method with.</param>
|
||||||
|
/// <param name="args">The list of arguments of the method</param>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||||
|
/// </typeparam>
|
||||||
|
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||||
|
/// <returns>The return of the method you wanted to run.</returns>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
|
||||||
public static T RunGenericMethod<T>(
|
public static T RunGenericMethod<T>(
|
||||||
[NotNull] object instance,
|
[NotNull] object instance,
|
||||||
[NotNull] string methodName,
|
[NotNull] string methodName,
|
||||||
@ -250,6 +362,33 @@ namespace Kyoo
|
|||||||
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run a generic method for a multiple runtime <see cref="Type"/>.
|
||||||
|
/// If your generic method only needs one type, see
|
||||||
|
/// <see cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
|
||||||
|
/// you could do:
|
||||||
|
/// <code>
|
||||||
|
/// Utility.RunGenericMethod<object>(
|
||||||
|
/// typeof(Utility),
|
||||||
|
/// nameof(MergeLists),
|
||||||
|
/// enumerableType,
|
||||||
|
/// oldValue, newValue, equalityComparer)
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <param name="instance">The <c>this</c> of the method to run.</param>
|
||||||
|
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||||
|
/// <param name="types">The list of generic types to run the method with.</param>
|
||||||
|
/// <param name="args">The list of arguments of the method</param>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||||
|
/// </typeparam>
|
||||||
|
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||||
|
/// <returns>The return of the method you wanted to run.</returns>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
|
||||||
|
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||||
public static T RunGenericMethod<T>(
|
public static T RunGenericMethod<T>(
|
||||||
[NotNull] object instance,
|
[NotNull] object instance,
|
||||||
[NotNull] string methodName,
|
[NotNull] string methodName,
|
||||||
@ -263,7 +402,7 @@ namespace Kyoo
|
|||||||
if (types == null || types.Length == 0)
|
if (types == null || types.Length == 0)
|
||||||
throw new ArgumentNullException(nameof(types));
|
throw new ArgumentNullException(nameof(types));
|
||||||
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
|
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<string, string> query)
|
public static string ToQueryString(this Dictionary<string, string> query)
|
||||||
|
@ -38,7 +38,7 @@ namespace Kyoo.CommonApi
|
|||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
public virtual async Task<ActionResult<T>> Get(string slug)
|
public virtual async Task<ActionResult<T>> Get(string slug)
|
||||||
{
|
{
|
||||||
T ret = await _repository.Get(slug);
|
T ret = await _repository.GetOrDefault(slug);
|
||||||
if (ret == null)
|
if (ret == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -115,9 +115,10 @@ namespace Kyoo.Controllers
|
|||||||
|
|
||||||
public object GetValue(object target)
|
public object GetValue(object target)
|
||||||
{
|
{
|
||||||
return Regex.Replace(_format, @"(?<!{){(\w+)}", x =>
|
return Regex.Replace(_format, @"(?<!{){(\w+)(:(\w+))?}", x =>
|
||||||
{
|
{
|
||||||
string value = x.Groups[1].Value;
|
string value = x.Groups[1].Value;
|
||||||
|
string modifier = x.Groups[3].Value;
|
||||||
|
|
||||||
if (value == "HOST")
|
if (value == "HOST")
|
||||||
return _host;
|
return _host;
|
||||||
@ -127,9 +128,22 @@ namespace Kyoo.Controllers
|
|||||||
.FirstOrDefault(y => y.Name == value);
|
.FirstOrDefault(y => y.Name == value);
|
||||||
if (properties == null)
|
if (properties == null)
|
||||||
return null;
|
return null;
|
||||||
if (properties.GetValue(target) is string ret)
|
object objValue = properties.GetValue(target);
|
||||||
return ret;
|
if (objValue is not string ret)
|
||||||
|
ret = objValue?.ToString();
|
||||||
|
if (ret == null)
|
||||||
throw new ArgumentException($"Invalid serializer replacement {value}");
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ namespace Kyoo.Controllers
|
|||||||
}
|
}
|
||||||
catch (DuplicatedItemException)
|
catch (DuplicatedItemException)
|
||||||
{
|
{
|
||||||
return await GetOrDefault(obj.Slug);
|
return await Get(obj.Slug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,10 +43,18 @@ namespace Kyoo.CommonApi
|
|||||||
PropertyInfo[] properties = type.GetProperties()
|
PropertyInfo[] properties = type.GetProperties()
|
||||||
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
|
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
fields = fields.Select(x =>
|
if (fields.Count == 1 && fields.Contains("all"))
|
||||||
|
{
|
||||||
|
fields = properties.Select(x => x.Name).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fields = fields
|
||||||
|
.Select(x =>
|
||||||
{
|
{
|
||||||
string property = properties
|
string property = properties
|
||||||
.FirstOrDefault(y => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
|
.FirstOrDefault(y
|
||||||
|
=> string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||||
?.Name;
|
?.Name;
|
||||||
if (property != null)
|
if (property != null)
|
||||||
return property;
|
return property;
|
||||||
@ -60,6 +68,7 @@ namespace Kyoo.CommonApi
|
|||||||
if (context.Result != null)
|
if (context.Result != null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
context.HttpContext.Items["fields"] = fields;
|
context.HttpContext.Items["fields"] = fields;
|
||||||
base.OnActionExecuting(context);
|
base.OnActionExecuting(context);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace Kyoo.Postgresql.Migrations
|
namespace Kyoo.Postgresql.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PostgresContext))]
|
[DbContext(typeof(PostgresContext))]
|
||||||
[Migration("20210627141933_Initial")]
|
[Migration("20210723224326_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
.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" })
|
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||||
.HasAnnotation("ProductVersion", "5.0.7")
|
.HasAnnotation("ProductVersion", "5.0.8")
|
||||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.ToTable("libraries");
|
b.ToTable("libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("end_air");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("overview");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("poster");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
|
b.Property<Status?>("Status")
|
||||||
|
.HasColumnType("status")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<ItemType>("Type")
|
||||||
|
.HasColumnType("item_type")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("ID")
|
||||||
|
.HasName("pk_library_items");
|
||||||
|
|
||||||
|
b.ToView("library_items");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("start_air");
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
b.Property<Status?>("Status")
|
b.Property<Status>("Status")
|
||||||
.HasColumnType("status")
|
.HasColumnType("status")
|
||||||
.HasColumnName("status");
|
.HasColumnName("status");
|
||||||
|
|
||||||
@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID")
|
.HasForeignKey("StudioID")
|
||||||
.HasConstraintName("fk_shows_studios_studio_id");
|
.HasConstraintName("fk_shows_studios_studio_id")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
@ -12,7 +12,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
{
|
{
|
||||||
migrationBuilder.AlterDatabase()
|
migrationBuilder.AlterDatabase()
|
||||||
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
|
.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");
|
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment");
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@ -208,7 +208,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
||||||
path = table.Column<string>(type: "text", nullable: true),
|
path = table.Column<string>(type: "text", nullable: true),
|
||||||
overview = table.Column<string>(type: "text", nullable: true),
|
overview = table.Column<string>(type: "text", nullable: true),
|
||||||
status = table.Column<Status>(type: "status", nullable: true),
|
status = table.Column<Status>(type: "status", nullable: false),
|
||||||
trailer_url = table.Column<string>(type: "text", nullable: true),
|
trailer_url = table.Column<string>(type: "text", nullable: true),
|
||||||
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||||
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace Kyoo.Postgresql.Migrations
|
namespace Kyoo.Postgresql.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PostgresContext))]
|
[DbContext(typeof(PostgresContext))]
|
||||||
[Migration("20210627141941_Triggers")]
|
[Migration("20210723224335_Triggers")]
|
||||||
partial class Triggers
|
partial class Triggers
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
.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" })
|
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||||
.HasAnnotation("ProductVersion", "5.0.7")
|
.HasAnnotation("ProductVersion", "5.0.8")
|
||||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.ToTable("libraries");
|
b.ToTable("libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("end_air");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("overview");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("poster");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
|
b.Property<Status?>("Status")
|
||||||
|
.HasColumnType("status")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<ItemType>("Type")
|
||||||
|
.HasColumnType("item_type")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("ID")
|
||||||
|
.HasName("pk_library_items");
|
||||||
|
|
||||||
|
b.ToView("library_items");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("start_air");
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
b.Property<Status?>("Status")
|
b.Property<Status>("Status")
|
||||||
.HasColumnType("status")
|
.HasColumnType("status")
|
||||||
.HasColumnName("status");
|
.HasColumnName("status");
|
||||||
|
|
||||||
@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID")
|
.HasForeignKey("StudioID")
|
||||||
.HasConstraintName("fk_shows_studios_studio_id");
|
.HasConstraintName("fk_shows_studios_studio_id")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
@ -91,7 +91,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
END,
|
END,
|
||||||
CASE (is_forced)
|
CASE (is_forced)
|
||||||
WHEN false THEN ''
|
WHEN false THEN ''
|
||||||
ELSE '-forced'
|
ELSE '.forced'
|
||||||
END,
|
END,
|
||||||
'.', type
|
'.', type
|
||||||
) WHERE episode_id = NEW.id;
|
) WHERE episode_id = NEW.id;
|
||||||
@ -117,14 +117,14 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
END IF;
|
END IF;
|
||||||
NEW.slug := CONCAT(
|
NEW.slug := CONCAT(
|
||||||
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
|
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
|
||||||
'.', NEW.language,
|
'.', COALESCE(NEW.language, 'und'),
|
||||||
CASE (NEW.track_index)
|
CASE (NEW.track_index)
|
||||||
WHEN 0 THEN ''
|
WHEN 0 THEN ''
|
||||||
ELSE CONCAT('-', NEW.track_index)
|
ELSE CONCAT('-', NEW.track_index)
|
||||||
END,
|
END,
|
||||||
CASE (NEW.is_forced)
|
CASE (NEW.is_forced)
|
||||||
WHEN false THEN ''
|
WHEN false THEN ''
|
||||||
ELSE '-forced'
|
ELSE '.forced'
|
||||||
END,
|
END,
|
||||||
'.', NEW.type
|
'.', NEW.type
|
||||||
);
|
);
|
@ -18,10 +18,10 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
.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" })
|
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||||
.HasAnnotation("ProductVersion", "5.0.7")
|
.HasAnnotation("ProductVersion", "5.0.8")
|
||||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
@ -187,6 +187,51 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.ToTable("libraries");
|
b.ToTable("libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("end_air");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("overview");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("poster");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
|
b.Property<Status?>("Status")
|
||||||
|
.HasColumnType("status")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<ItemType>("Type")
|
||||||
|
.HasColumnType("item_type")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("ID")
|
||||||
|
.HasName("pk_library_items");
|
||||||
|
|
||||||
|
b.ToView("library_items");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -619,7 +664,7 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("start_air");
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
b.Property<Status?>("Status")
|
b.Property<Status>("Status")
|
||||||
.HasColumnType("status")
|
.HasColumnType("status")
|
||||||
.HasColumnName("status");
|
.HasColumnName("status");
|
||||||
|
|
||||||
@ -1076,7 +1121,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID")
|
.HasForeignKey("StudioID")
|
||||||
.HasConstraintName("fk_shows_studios_studio_id");
|
.HasConstraintName("fk_shows_studios_studio_id")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
namespace Kyoo.Postgresql
|
namespace Kyoo.Postgresql
|
||||||
{
|
{
|
||||||
@ -73,6 +74,10 @@ namespace Kyoo.Postgresql
|
|||||||
{
|
{
|
||||||
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
|
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
|
||||||
context.Database.Migrate();
|
context.Database.Migrate();
|
||||||
|
|
||||||
|
using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
|
||||||
|
conn.Open();
|
||||||
|
conn.ReloadTypes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
namespace Kyoo.SqLite.Migrations
|
namespace Kyoo.SqLite.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(SqLiteContext))]
|
[DbContext(typeof(SqLiteContext))]
|
||||||
[Migration("20210626141337_Initial")]
|
[Migration("20210723224542_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.7");
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.ToTable("Libraries");
|
b.ToTable("Libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToView("LibraryItems");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.Property<DateTime?>("StartAir")
|
b.Property<DateTime?>("StartAir")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("StudioID")
|
b.Property<int?>("StudioID")
|
||||||
@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
{
|
{
|
||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID");
|
.HasForeignKey("StudioID")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
@ -200,7 +200,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
Aliases = table.Column<string>(type: "TEXT", nullable: true),
|
Aliases = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
Path = table.Column<string>(type: "TEXT", nullable: true),
|
Path = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
Status = table.Column<int>(type: "INTEGER", nullable: true),
|
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
namespace Kyoo.SqLite.Migrations
|
namespace Kyoo.SqLite.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(SqLiteContext))]
|
[DbContext(typeof(SqLiteContext))]
|
||||||
[Migration("20210626141347_Triggers")]
|
[Migration("20210723224550_Triggers")]
|
||||||
partial class Triggers
|
partial class Triggers
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.7");
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.ToTable("Libraries");
|
b.ToTable("Libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToView("LibraryItems");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.Property<DateTime?>("StartAir")
|
b.Property<DateTime?>("StartAir")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("StudioID")
|
b.Property<int?>("StudioID")
|
||||||
@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
{
|
{
|
||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID");
|
.HasForeignKey("StudioID")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
@ -61,14 +61,14 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
AND Language = new.Language AND IsForced = new.IsForced
|
AND Language = new.Language AND IsForced = new.IsForced
|
||||||
) WHERE ID = new.ID AND TrackIndex = 0;
|
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||||
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||||
'.' || Language ||
|
'.' || COALESCE(Language, 'und') ||
|
||||||
CASE (TrackIndex)
|
CASE (TrackIndex)
|
||||||
WHEN 0 THEN ''
|
WHEN 0 THEN ''
|
||||||
ELSE '-' || (TrackIndex)
|
ELSE '-' || (TrackIndex)
|
||||||
END ||
|
END ||
|
||||||
CASE (IsForced)
|
CASE (IsForced)
|
||||||
WHEN false THEN ''
|
WHEN false THEN ''
|
||||||
ELSE '-forced'
|
ELSE '.forced'
|
||||||
END ||
|
END ||
|
||||||
CASE (Type)
|
CASE (Type)
|
||||||
WHEN 1 THEN '.video'
|
WHEN 1 THEN '.video'
|
||||||
@ -98,7 +98,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
END ||
|
END ||
|
||||||
CASE (IsForced)
|
CASE (IsForced)
|
||||||
WHEN false THEN ''
|
WHEN false THEN ''
|
||||||
ELSE '-forced'
|
ELSE '.forced'
|
||||||
END ||
|
END ||
|
||||||
CASE (Type)
|
CASE (Type)
|
||||||
WHEN 1 THEN '.video'
|
WHEN 1 THEN '.video'
|
||||||
@ -123,7 +123,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
END ||
|
END ||
|
||||||
CASE (IsForced)
|
CASE (IsForced)
|
||||||
WHEN false THEN ''
|
WHEN false THEN ''
|
||||||
ELSE '-forced'
|
ELSE '.forced'
|
||||||
END ||
|
END ||
|
||||||
CASE (Type)
|
CASE (Type)
|
||||||
WHEN 1 THEN '.video'
|
WHEN 1 THEN '.video'
|
@ -14,7 +14,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.7");
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
@ -141,6 +141,41 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.ToTable("Libraries");
|
b.ToTable("Libraries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToView("LibraryItems");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("FirstID")
|
b.Property<int>("FirstID")
|
||||||
@ -475,7 +510,7 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
b.Property<DateTime?>("StartAir")
|
b.Property<DateTime?>("StartAir")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("StudioID")
|
b.Property<int?>("StudioID")
|
||||||
@ -862,7 +897,8 @@ namespace Kyoo.SqLite.Migrations
|
|||||||
{
|
{
|
||||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
.WithMany("Shows")
|
.WithMany("Shows")
|
||||||
.HasForeignKey("StudioID");
|
.HasForeignKey("StudioID")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.Navigation("Studio");
|
b.Navigation("Studio");
|
||||||
});
|
});
|
||||||
|
@ -25,6 +25,7 @@ namespace Kyoo.Tests
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Repositories.Dispose();
|
Repositories.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -5,7 +5,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -1,8 +1,11 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
||||||
@ -23,7 +26,7 @@ namespace Kyoo.Tests.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class ALibraryTests : RepositoryTests<Models.Library>
|
public abstract class ALibraryTests : RepositoryTests<Library>
|
||||||
{
|
{
|
||||||
private readonly ILibraryRepository _repository;
|
private readonly ILibraryRepository _repository;
|
||||||
|
|
||||||
@ -32,5 +35,17 @@ namespace Kyoo.Tests.Library
|
|||||||
{
|
{
|
||||||
_repository = Repositories.LibraryManager.LibraryRepository;
|
_repository = Repositories.LibraryManager.LibraryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateWithProvider()
|
||||||
|
{
|
||||||
|
Library library = TestSample.GetNew<Library>();
|
||||||
|
library.Providers = new[] { TestSample.Get<Provider>() };
|
||||||
|
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<Provider>().Slug, retrieved.Providers.First().Slug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -5,7 +5,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
public class GlobalTests : IDisposable, IAsyncDisposable
|
public class GlobalTests : IDisposable, IAsyncDisposable
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
||||||
@ -47,5 +47,20 @@ namespace Kyoo.Tests.Library
|
|||||||
Track track = await _repository.Get(1);
|
Track track = await _repository.Get(1);
|
||||||
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
|
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<Episode>().ID
|
||||||
|
});
|
||||||
|
Track track = await _repository.Get(5);
|
||||||
|
Assert.Equal("anohana-s1e1.und.video", track.Slug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Kyoo.Tests.Library
|
namespace Kyoo.Tests.Database
|
||||||
{
|
{
|
||||||
namespace SqLite
|
namespace SqLite
|
||||||
{
|
{
|
@ -9,8 +9,14 @@ namespace Kyoo.Tests
|
|||||||
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
|
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
typeof(Show),
|
typeof(Library),
|
||||||
() => new Show()
|
() => 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<Type, Func<object>> Samples = new()
|
private static readonly Dictionary<Type, Func<object>> Samples = new()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
typeof(Models.Library),
|
typeof(Library),
|
||||||
() => new Models.Library
|
() => new Library
|
||||||
{
|
{
|
||||||
ID = 1,
|
ID = 1,
|
||||||
Slug = "deck",
|
Slug = "deck",
|
||||||
@ -227,7 +233,7 @@ namespace Kyoo.Tests
|
|||||||
provider.ID = 0;
|
provider.ID = 0;
|
||||||
context.Providers.Add(provider);
|
context.Providers.Add(provider);
|
||||||
|
|
||||||
Models.Library library = Get<Models.Library>();
|
Library library = Get<Library>();
|
||||||
library.ID = 0;
|
library.ID = 0;
|
||||||
library.Collections = new List<Collection> {collection};
|
library.Collections = new List<Collection> {collection};
|
||||||
library.Providers = new List<Provider> {provider};
|
library.Providers = new List<Provider> {provider};
|
192
Kyoo.Tests/Identifier/IdentifierTests.cs
Normal file
192
Kyoo.Tests/Identifier/IdentifierTests.cs
Normal file
@ -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<ILibraryManager> _manager;
|
||||||
|
private readonly IIdentifier _identifier;
|
||||||
|
|
||||||
|
public Identifier()
|
||||||
|
{
|
||||||
|
Mock<IOptionsMonitor<MediaOptions>> options = new();
|
||||||
|
options.Setup(x => x.CurrentValue).Returns(new MediaOptions
|
||||||
|
{
|
||||||
|
Regex = new []
|
||||||
|
{
|
||||||
|
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? S(?<Season>\\d+)E(?<Episode>\\d+)\\..*$",
|
||||||
|
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? (?<Absolute>\\d+)\\..*$",
|
||||||
|
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))?\\..*$"
|
||||||
|
},
|
||||||
|
SubtitleRegex = new[]
|
||||||
|
{
|
||||||
|
"^(?<Episode>.+)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_manager = new Mock<ILibraryManager>();
|
||||||
|
_identifier = new RegexIdentifier(options.Object, _manager.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EpisodeIdentification()
|
||||||
|
{
|
||||||
|
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), 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<Library>(), 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<Library>(), 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<Library>(), 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<Library>(), 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<Library>(), default)).ReturnsAsync(new[]
|
||||||
|
{
|
||||||
|
new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<IdentificationFailedException>(() => _identifier.Identify("/invalid/path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubtitleIdentification()
|
||||||
|
{
|
||||||
|
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), 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<Library>(), 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<Library>(), default)).ReturnsAsync(new[]
|
||||||
|
{
|
||||||
|
new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<IdentificationFailedException>(() => _identifier.IdentifyTrack("/invalid/path"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
Kyoo.Tests/Identifier/ProviderTests.cs
Normal file
124
Kyoo.Tests/Identifier/ProviderTests.cs
Normal file
@ -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<IMetadataProvider>(),
|
||||||
|
_factory.CreateLogger<ProviderComposite>());
|
||||||
|
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<IMetadataProvider>(),
|
||||||
|
_factory.CreateLogger<ProviderComposite>());
|
||||||
|
ICollection<Show> ret = await provider.Search<Show>("show");
|
||||||
|
Assert.Empty(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OneProviderGetTest()
|
||||||
|
{
|
||||||
|
Show show = new()
|
||||||
|
{
|
||||||
|
ID = 4,
|
||||||
|
Genres = new[] { new Genre("genre") }
|
||||||
|
};
|
||||||
|
Mock<IMetadataProvider> 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<ProviderComposite>());
|
||||||
|
|
||||||
|
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<IMetadataProvider> 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<IMetadataProvider> 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<IMetadataProvider> mockFailing = new();
|
||||||
|
mockFailing.Setup(x => x.Provider).Returns(new Provider("mockFail", ""));
|
||||||
|
mockFailing.Setup(x => x.Get(show)).Throws<ArgumentException>();
|
||||||
|
|
||||||
|
AProviderComposite provider = new ProviderComposite(new []
|
||||||
|
{
|
||||||
|
mock.Object,
|
||||||
|
mockTwo.Object,
|
||||||
|
mockFailing.Object
|
||||||
|
},
|
||||||
|
_factory.CreateLogger<ProviderComposite>());
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
Kyoo.Tests/Identifier/Tvdb/ConvertorTests.cs
Normal file
160
Kyoo.Tests/Identifier/Tvdb/ConvertorTests.cs
Normal file
@ -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<Provider>();
|
||||||
|
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<Provider>();
|
||||||
|
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<Provider>();
|
||||||
|
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<Provider>();
|
||||||
|
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<Provider>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@
|
|||||||
<PackageReference Include="Divergic.Logging.Xunit" Version="3.6.0" />
|
<PackageReference Include="Divergic.Logging.Xunit" Version="3.6.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
|
<PackageReference Include="TvDbSharper" Version="3.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@ -32,6 +34,7 @@
|
|||||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
||||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
||||||
<ProjectReference Include="../Kyoo/Kyoo.csproj" />
|
<ProjectReference Include="../Kyoo/Kyoo.csproj" />
|
||||||
|
<ProjectReference Include="..\Kyoo.TheTvdb\Kyoo.TheTvdb.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Kyoo.Tests
|
namespace Kyoo.Tests.Utility
|
||||||
{
|
{
|
||||||
public class EnumerableTests
|
public class EnumerableTests
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Kyoo.Tests
|
namespace Kyoo.Tests.Utility
|
||||||
{
|
{
|
||||||
public class MergerTests
|
public class MergerTests
|
||||||
{
|
{
|
||||||
@ -17,5 +21,192 @@ namespace Kyoo.Tests
|
|||||||
Assert.Null(genre.Name);
|
Assert.Null(genre.Name);
|
||||||
Assert.Null(genre.Slug);
|
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<Genre>(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<Exception>(() => 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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Kyoo.Tests
|
namespace Kyoo.Tests.Utility
|
||||||
{
|
{
|
||||||
public class TaskTests
|
public class TaskTests
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Kyoo.Tests
|
using Utils = Kyoo.Utility;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Utility
|
||||||
{
|
{
|
||||||
public class UtilityTests
|
public class UtilityTests
|
||||||
{
|
{
|
||||||
@ -13,12 +16,12 @@ namespace Kyoo.Tests
|
|||||||
Expression<Func<Show, int>> member = x => x.ID;
|
Expression<Func<Show, int>> member = x => x.ID;
|
||||||
Expression<Func<Show, object>> memberCast = x => x.ID;
|
Expression<Func<Show, object>> memberCast = x => x.ID;
|
||||||
|
|
||||||
Assert.False(Utility.IsPropertyExpression(null));
|
Assert.False(Utils.IsPropertyExpression(null));
|
||||||
Assert.True(Utility.IsPropertyExpression(member));
|
Assert.True(Utils.IsPropertyExpression(member));
|
||||||
Assert.True(Utility.IsPropertyExpression(memberCast));
|
Assert.True(Utils.IsPropertyExpression(memberCast));
|
||||||
|
|
||||||
Expression<Func<Show, object>> call = x => x.GetID("test");
|
Expression<Func<Show, object>> call = x => x.GetID("test");
|
||||||
Assert.False(Utility.IsPropertyExpression(call));
|
Assert.False(Utils.IsPropertyExpression(call));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -27,9 +30,51 @@ namespace Kyoo.Tests
|
|||||||
Expression<Func<Show, int>> member = x => x.ID;
|
Expression<Func<Show, int>> member = x => x.ID;
|
||||||
Expression<Func<Show, object>> memberCast = x => x.ID;
|
Expression<Func<Show, object>> memberCast = x => x.ID;
|
||||||
|
|
||||||
Assert.Equal("ID", Utility.GetPropertyName(member));
|
Assert.Equal("ID", Utils.GetPropertyName(member));
|
||||||
Assert.Equal("ID", Utility.GetPropertyName(memberCast));
|
Assert.Equal("ID", Utils.GetPropertyName(memberCast));
|
||||||
Assert.Throws<ArgumentException>(() => Utility.GetPropertyName(null));
|
Assert.Throws<ArgumentException>(() => Utils.GetPropertyName(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMethodTest()
|
||||||
|
{
|
||||||
|
MethodInfo method = Utils.GetMethod(typeof(UtilityTests),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
nameof(GetMethodTest),
|
||||||
|
Array.Empty<Type>(),
|
||||||
|
Array.Empty<object>());
|
||||||
|
Assert.Equal(MethodBase.GetCurrentMethod(), method);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMethodInvalidGenericsTest()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => Utils.GetMethod(typeof(UtilityTests),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
nameof(GetMethodTest),
|
||||||
|
new [] { typeof(Utils) },
|
||||||
|
Array.Empty<object>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMethodInvalidParamsTest()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => Utils.GetMethod(typeof(UtilityTests),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
nameof(GetMethodTest),
|
||||||
|
Array.Empty<Type>(),
|
||||||
|
new object[] { this }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMethodTest2()
|
||||||
|
{
|
||||||
|
MethodInfo method = Utils.GetMethod(typeof(Merger),
|
||||||
|
BindingFlags.Static | BindingFlags.Public,
|
||||||
|
nameof(Merger.MergeLists),
|
||||||
|
new [] { typeof(string) },
|
||||||
|
new object[] { "string", "string2", null });
|
||||||
|
Assert.Equal(nameof(Merger.MergeLists), method.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
160
Kyoo.TheTvdb/Convertors.cs
Normal file
160
Kyoo.TheTvdb/Convertors.cs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using TvDbSharper.Dto;
|
||||||
|
|
||||||
|
namespace Kyoo.TheTvdb
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A set of extensions methods used to convert tvdb models to Kyoo models.
|
||||||
|
/// </summary>
|
||||||
|
public static class Convertors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the string representation of the status in the tvdb API to a Kyoo's <see cref="Status"/> enum.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">The string representing the status.</param>
|
||||||
|
/// <returns>A kyoo <see cref="Status"/> value or null.</returns>
|
||||||
|
private static Status _GetStatus(string status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
"Ended" => Status.Finished,
|
||||||
|
"Continuing" => Status.Airing,
|
||||||
|
_ => Status.Unknown
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a TVDB date and return a <see cref="DateTime"/> or null if the string is invalid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="date">The date string to parse</param>
|
||||||
|
/// <returns>The parsed <see cref="DateTime"/> or null.</returns>
|
||||||
|
private static DateTime? _ParseDate(string date)
|
||||||
|
{
|
||||||
|
return DateTime.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None, out DateTime parsed)
|
||||||
|
? parsed
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a series search to a show.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The search result</param>
|
||||||
|
/// <param name="provider">The provider representing the tvdb inside kyoo</param>
|
||||||
|
/// <returns>A show representing the given search result.</returns>
|
||||||
|
public static Show ToShow(this SeriesSearchResult result, Provider provider)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Slug = result.Slug,
|
||||||
|
Title = result.SeriesName,
|
||||||
|
Aliases = result.Aliases,
|
||||||
|
Overview = result.Overview,
|
||||||
|
Status = _GetStatus(result.Status),
|
||||||
|
StartAir = _ParseDate(result.FirstAired),
|
||||||
|
Poster = result.Poster != null ? $"https://www.thetvdb.com{result.Poster}" : null,
|
||||||
|
ExternalIDs = new[]
|
||||||
|
{
|
||||||
|
new MetadataID<Show>
|
||||||
|
{
|
||||||
|
DataID = result.Id.ToString(),
|
||||||
|
Link = $"https://www.thetvdb.com/series/{result.Slug}",
|
||||||
|
Second = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a tvdb series to a kyoo show.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="series">The series to convert</param>
|
||||||
|
/// <param name="provider">The provider representing the tvdb inside kyoo</param>
|
||||||
|
/// <returns>A show representing the given series.</returns>
|
||||||
|
public static Show ToShow(this Series series, Provider provider)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Slug = series.Slug,
|
||||||
|
Title = series.SeriesName,
|
||||||
|
Aliases = series.Aliases,
|
||||||
|
Overview = series.Overview,
|
||||||
|
Status = _GetStatus(series.Status),
|
||||||
|
StartAir = _ParseDate(series.FirstAired),
|
||||||
|
Poster = series.Poster != null ? $"https://www.thetvdb.com/banners/{series.Poster}" : null,
|
||||||
|
Backdrop = series.FanArt != null ? $"https://www.thetvdb.com/banners/{series.FanArt}" : null,
|
||||||
|
Genres = series.Genre.Select(y => new Genre(y)).ToList(),
|
||||||
|
ExternalIDs = new[]
|
||||||
|
{
|
||||||
|
new MetadataID<Show>
|
||||||
|
{
|
||||||
|
DataID = series.Id.ToString(),
|
||||||
|
Link = $"https://www.thetvdb.com/series/{series.Slug}",
|
||||||
|
Second = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a tvdb actor to a kyoo <see cref="PeopleRole"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actor">The actor to convert</param>
|
||||||
|
/// <param name="provider">The provider representing the tvdb inside kyoo</param>
|
||||||
|
/// <returns>A people role representing the given actor in the role they played.</returns>
|
||||||
|
public static PeopleRole ToPeopleRole(this Actor actor, Provider provider)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
People = new People
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(actor.Name),
|
||||||
|
Name = actor.Name,
|
||||||
|
Poster = actor.Image != null ? $"https://www.thetvdb.com/banners/{actor.Image}" : null,
|
||||||
|
ExternalIDs = new []
|
||||||
|
{
|
||||||
|
new MetadataID<People>()
|
||||||
|
{
|
||||||
|
DataID = actor.Id.ToString(),
|
||||||
|
Link = $"https://www.thetvdb.com/people/{actor.Id}",
|
||||||
|
Second = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Role = actor.Role,
|
||||||
|
Type = "Actor"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a tvdb episode to a kyoo <see cref="Episode"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">The episode to convert</param>
|
||||||
|
/// <param name="provider">The provider representing the tvdb inside kyoo</param>
|
||||||
|
/// <returns>A episode representing the given tvdb episode.</returns>
|
||||||
|
public static Episode ToEpisode(this EpisodeRecord episode, Provider provider)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
SeasonNumber = episode.AiredSeason,
|
||||||
|
EpisodeNumber = episode.AiredEpisodeNumber,
|
||||||
|
AbsoluteNumber = episode.AbsoluteNumber,
|
||||||
|
Title = episode.EpisodeName,
|
||||||
|
Overview = episode.Overview,
|
||||||
|
Thumb = episode.Filename != null ? $"https://www.thetvdb.com/banners/{episode.Filename}" : null,
|
||||||
|
ExternalIDs = new[]
|
||||||
|
{
|
||||||
|
new MetadataID<Episode>
|
||||||
|
{
|
||||||
|
DataID = episode.Id.ToString(),
|
||||||
|
Link = $"https://www.thetvdb.com/series/{episode.SeriesId}/episodes/{episode.Id}",
|
||||||
|
Second = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
Kyoo.TheTvdb/Kyoo.TheTvdb.csproj
Normal file
34
Kyoo.TheTvdb/Kyoo.TheTvdb.csproj
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
|
||||||
|
<Company>SDG</Company>
|
||||||
|
<Authors>Zoe Roux</Authors>
|
||||||
|
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
|
||||||
|
<LangVersion>default</LangVersion>
|
||||||
|
<RootNamespace>Kyoo.TheTvdb</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/the-tvdb</OutputPath>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||||
|
<GenerateDependencyFile>false</GenerateDependencyFile>
|
||||||
|
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="TvDbSharper" Version="3.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
81
Kyoo.TheTvdb/PluginTvdb.cs
Normal file
81
Kyoo.TheTvdb/PluginTvdb.cs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Autofac;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
using Kyoo.TheTvdb.Models;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using TvDbSharper;
|
||||||
|
|
||||||
|
namespace Kyoo.TheTvdb
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A plugin that add a <see cref="IMetadataProvider"/> for The TVDB.
|
||||||
|
/// </summary>
|
||||||
|
public class PluginTvdb : IPlugin
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Slug => "the-tvdb";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "The TVDB Provider";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => "A metadata provider for The TVDB.";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<Type> Provides => new []
|
||||||
|
{
|
||||||
|
typeof(IMetadataProvider)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration to use.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration manager used to register typed/untyped implementations.
|
||||||
|
/// </summary>
|
||||||
|
[Injected] public IConfigurationManager ConfigurationManager { private get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new tvdb module instance and use the given configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The configuration to use</param>
|
||||||
|
public PluginTvdb(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
builder.RegisterType<TvDbClient>().As<ITvDbClient>();
|
||||||
|
builder.RegisterProvider<ProviderTvdb>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||||
|
{
|
||||||
|
services.Configure<TvdbOption>(_configuration.GetSection(TvdbOption.Path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ConfigureAspNet(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
ConfigurationManager.AddTyped<TvdbOption>(TvdbOption.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
Kyoo.TheTvdb/ProviderTvdb.cs
Normal file
146
Kyoo.TheTvdb/ProviderTvdb.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Kyoo.TheTvdb.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using TvDbSharper;
|
||||||
|
using TvDbSharper.Dto;
|
||||||
|
|
||||||
|
namespace Kyoo.TheTvdb
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A metadata provider for The TVDB.
|
||||||
|
/// </summary>
|
||||||
|
public class ProviderTvdb : IMetadataProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The internal tvdb client used to make requests.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ITvDbClient _client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The API key used to authenticate with the tvdb API.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IOptions<TvdbOption> _apiKey;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Provider Provider => new()
|
||||||
|
{
|
||||||
|
Slug = "the-tvdb",
|
||||||
|
Name = "TheTVDB",
|
||||||
|
LogoExtension = "png",
|
||||||
|
Logo = "https://www.thetvdb.com/images/logo.png"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ProviderTvdb"/> using a tvdb client and an api key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The tvdb client to use</param>
|
||||||
|
/// <param name="apiKey">The api key</param>
|
||||||
|
public ProviderTvdb(ITvDbClient client, IOptions<TvdbOption> apiKey)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate and refresh the token of the tvdb client.
|
||||||
|
/// </summary>
|
||||||
|
private Task _Authenticate()
|
||||||
|
{
|
||||||
|
if (_client.Authentication.Token == null)
|
||||||
|
return _client.Authentication.AuthenticateAsync(_apiKey.Value.ApiKey);
|
||||||
|
return _client.Authentication.RefreshTokenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<T> Get<T>(T item)
|
||||||
|
where T : class, IResource
|
||||||
|
{
|
||||||
|
await _Authenticate();
|
||||||
|
return item switch
|
||||||
|
{
|
||||||
|
Show show => await _GetShow(show) as T,
|
||||||
|
Episode episode => await _GetEpisode(episode) as T,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve metadata about a show.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="show">The base show to retrieve metadata for.</param>
|
||||||
|
/// <returns>A new show filled with metadata from the tvdb.</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
|
private async Task<Show> _GetShow([NotNull] Show show)
|
||||||
|
{
|
||||||
|
if (show.IsMovie)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!int.TryParse(show.GetID(Provider.Slug), out int id))
|
||||||
|
{
|
||||||
|
Show found = (await _SearchShow(show.Title)).FirstOrDefault();
|
||||||
|
if (found == null)
|
||||||
|
return null;
|
||||||
|
return await Get(found);
|
||||||
|
}
|
||||||
|
TvDbResponse<Series> series = await _client.Series.GetAsync(id);
|
||||||
|
Show ret = series.Data.ToShow(Provider);
|
||||||
|
|
||||||
|
TvDbResponse<Actor[]> people = await _client.Series.GetActorsAsync(id);
|
||||||
|
ret.People = people.Data.Select(x => x.ToPeopleRole(Provider)).ToArray();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve metadata about an episode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">The base episode to retrieve metadata for.</param>
|
||||||
|
/// <returns>A new episode filled with metadata from the tvdb.</returns>
|
||||||
|
[ItemCanBeNull]
|
||||||
|
private async Task<Episode> _GetEpisode([NotNull] Episode episode)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(episode.Show?.GetID(Provider.Slug), out int id))
|
||||||
|
return null;
|
||||||
|
EpisodeQuery query = episode.AbsoluteNumber != null
|
||||||
|
? new EpisodeQuery {AbsoluteNumber = episode.AbsoluteNumber}
|
||||||
|
: new EpisodeQuery {AiredSeason = episode.SeasonNumber, AiredEpisode = episode.EpisodeNumber};
|
||||||
|
TvDbResponse<EpisodeRecord[]> episodes = await _client.Series.GetEpisodesAsync(id, 0, query);
|
||||||
|
return episodes.Data.FirstOrDefault()?.ToEpisode(Provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ICollection<T>> Search<T>(string query)
|
||||||
|
where T : class, IResource
|
||||||
|
{
|
||||||
|
await _Authenticate();
|
||||||
|
if (typeof(T) == typeof(Show))
|
||||||
|
return (await _SearchShow(query) as ICollection<T>)!;
|
||||||
|
return ArraySegment<T>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for shows in the tvdb.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query to ask the tvdb about.</param>
|
||||||
|
/// <returns>A list of shows that could be found on the tvdb.</returns>
|
||||||
|
[ItemNotNull]
|
||||||
|
private async Task<ICollection<Show>> _SearchShow(string query)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TvDbResponse<SeriesSearchResult[]> shows = await _client.Search.SearchSeriesByNameAsync(query);
|
||||||
|
return shows.Data.Select(x => x.ToShow(Provider)).ToArray();
|
||||||
|
}
|
||||||
|
catch (TvDbServerException)
|
||||||
|
{
|
||||||
|
return ArraySegment<Show>.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
Kyoo.TheTvdb/TvdbOption.cs
Normal file
18
Kyoo.TheTvdb/TvdbOption.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Kyoo.TheTvdb.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The option containing the api key for the tvdb.
|
||||||
|
/// </summary>
|
||||||
|
public class TvdbOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The path to get this option from the root configuration.
|
||||||
|
/// </summary>
|
||||||
|
public const string Path = "tvdb";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api key of the tvdb.
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit 22a02671918201d6d9d4e80a76f01b59b216a82d
|
Subproject commit c037270d3339fcf0075984a089f353c5c332a751
|
6
Kyoo.sln
6
Kyoo.sln
@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -47,5 +49,9 @@ Global
|
|||||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -36,7 +36,29 @@ namespace Kyoo.Controllers
|
|||||||
_references = references.ToDictionary(x => x.Path, x => x.Type, StringComparer.OrdinalIgnoreCase);
|
_references = references.ToDictionary(x => x.Path, x => x.Type, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type GetType(string path)
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void AddTyped<T>(string path)
|
||||||
|
{
|
||||||
|
foreach (ConfigurationReference confRef in ConfigurationReference.CreateReference<T>(path))
|
||||||
|
_references.Add(confRef.Path, confRef.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void AddUntyped(string path)
|
||||||
|
{
|
||||||
|
ConfigurationReference config = ConfigurationReference.CreateUntyped(path);
|
||||||
|
_references.Add(config.Path, config.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the type of the resource at the given path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path of the resource</param>
|
||||||
|
/// <exception cref="ArgumentException">The path is not editable or readable</exception>
|
||||||
|
/// <exception cref="ItemNotFoundException">No configuration exists for the given path</exception>
|
||||||
|
/// <returns>The type of the resource at the given path</returns>
|
||||||
|
private Type _GetType(string path)
|
||||||
{
|
{
|
||||||
path = path.Replace("__", ":");
|
path = path.Replace("__", ":");
|
||||||
|
|
||||||
@ -59,7 +81,7 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
path = path.Replace("__", ":");
|
path = path.Replace("__", ":");
|
||||||
// TODO handle lists and dictionaries.
|
// TODO handle lists and dictionaries.
|
||||||
Type type = GetType(path);
|
Type type = _GetType(path);
|
||||||
object ret = _configuration.GetValue(type, path);
|
object ret = _configuration.GetValue(type, path);
|
||||||
if (ret != null)
|
if (ret != null)
|
||||||
return ret;
|
return ret;
|
||||||
@ -73,7 +95,7 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
path = path.Replace("__", ":");
|
path = path.Replace("__", ":");
|
||||||
// TODO handle lists and dictionaries.
|
// TODO handle lists and dictionaries.
|
||||||
Type type = GetType(path);
|
Type type = _GetType(path);
|
||||||
if (typeof(T).IsAssignableFrom(type))
|
if (typeof(T).IsAssignableFrom(type))
|
||||||
throw new InvalidCastException($"The type {typeof(T).Name} is not valid for " +
|
throw new InvalidCastException($"The type {typeof(T).Name} is not valid for " +
|
||||||
$"a resource of type {type.Name}.");
|
$"a resource of type {type.Name}.");
|
||||||
@ -84,12 +106,12 @@ namespace Kyoo.Controllers
|
|||||||
public async Task EditValue(string path, object value)
|
public async Task EditValue(string path, object value)
|
||||||
{
|
{
|
||||||
path = path.Replace("__", ":");
|
path = path.Replace("__", ":");
|
||||||
Type type = GetType(path);
|
Type type = _GetType(path);
|
||||||
value = JObject.FromObject(value).ToObject(type);
|
value = JObject.FromObject(value).ToObject(type);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
throw new ArgumentException("Invalid value format.");
|
throw new ArgumentException("Invalid value format.");
|
||||||
|
|
||||||
ExpandoObject config = ToObject(_configuration);
|
ExpandoObject config = _ToObject(_configuration);
|
||||||
IDictionary<string, object> configDic = config;
|
IDictionary<string, object> configDic = config;
|
||||||
configDic[path] = value;
|
configDic[path] = value;
|
||||||
JObject obj = JObject.FromObject(config);
|
JObject obj = JObject.FromObject(config);
|
||||||
@ -104,7 +126,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="config">The configuration to transform</param>
|
/// <param name="config">The configuration to transform</param>
|
||||||
/// <returns>A strongly typed representation of the configuration.</returns>
|
/// <returns>A strongly typed representation of the configuration.</returns>
|
||||||
[SuppressMessage("ReSharper", "RedundantJumpStatement")]
|
[SuppressMessage("ReSharper", "RedundantJumpStatement")]
|
||||||
private ExpandoObject ToObject(IConfiguration config)
|
private ExpandoObject _ToObject(IConfiguration config)
|
||||||
{
|
{
|
||||||
ExpandoObject obj = new();
|
ExpandoObject obj = new();
|
||||||
|
|
||||||
@ -112,12 +134,12 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Type type = GetType(section.Path);
|
Type type = _GetType(section.Path);
|
||||||
obj.TryAdd(section.Key, section.Get(type));
|
obj.TryAdd(section.Key, section.Get(type));
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
obj.TryAdd(section.Key, ToUntyped(section));
|
obj.TryAdd(section.Key, _ToUntyped(section));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -133,13 +155,13 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="config">The section to convert</param>
|
/// <param name="config">The section to convert</param>
|
||||||
/// <returns>The converted section</returns>
|
/// <returns>The converted section</returns>
|
||||||
private static object ToUntyped(IConfigurationSection config)
|
private static object _ToUntyped(IConfigurationSection config)
|
||||||
{
|
{
|
||||||
ExpandoObject obj = new();
|
ExpandoObject obj = new();
|
||||||
|
|
||||||
foreach (IConfigurationSection section in config.GetChildren())
|
foreach (IConfigurationSection section in config.GetChildren())
|
||||||
{
|
{
|
||||||
obj.TryAdd(section.Key, ToUntyped(section));
|
obj.TryAdd(section.Key, _ToUntyped(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!obj.Any())
|
if (!obj.Any())
|
||||||
|
143
Kyoo/Controllers/FileSystems/FileSystemComposite.cs
Normal file
143
Kyoo/Controllers/FileSystems/FileSystemComposite.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Autofac.Features.Metadata;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Common.Models.Attributes;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Kyoo.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A composite that merge every <see cref="IFileSystem"/> available
|
||||||
|
/// using <see cref="FileSystemMetadataAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class FileSystemComposite : IFileSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of <see cref="IFileSystem"/> mapped to their metadata.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> _fileSystems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="FileSystemComposite"/> from a list of <see cref="IFileSystem"/> mapped to their
|
||||||
|
/// metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSystems">The list of filesystem mapped to their metadata.</param>
|
||||||
|
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems)
|
||||||
|
{
|
||||||
|
_fileSystems = fileSystems;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the file system that should be used for a given path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">
|
||||||
|
/// The path that was requested.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="usablePath">
|
||||||
|
/// The path that the returned file system wants
|
||||||
|
/// (respecting <see cref="FileSystemMetadataAttribute.StripScheme"/>).
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentException">No file system was registered for the given path.</exception>
|
||||||
|
/// <returns>The file system that should be used for a given path</returns>
|
||||||
|
[NotNull]
|
||||||
|
private IFileSystem _GetFileSystemForPath([NotNull] string path, [NotNull] out string usablePath)
|
||||||
|
{
|
||||||
|
Regex schemeMatcher = new(@"(.+)://(.*)", RegexOptions.Compiled);
|
||||||
|
Match match = schemeMatcher.Match(path);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
usablePath = path;
|
||||||
|
Meta<Func<IFileSystem>, FileSystemMetadataAttribute> defaultFs = _fileSystems
|
||||||
|
.SingleOrDefault(x => x.Metadata.Scheme.Contains(""));
|
||||||
|
if (defaultFs == null)
|
||||||
|
throw new ArgumentException($"No file system registered for the default scheme.");
|
||||||
|
return defaultFs.Value.Invoke();
|
||||||
|
}
|
||||||
|
string scheme = match.Groups[1].Value;
|
||||||
|
Meta<Func<IFileSystem>, FileSystemMetadataAttribute> ret = _fileSystems
|
||||||
|
.SingleOrDefault(x => x.Metadata.Scheme.Contains(scheme));
|
||||||
|
if (ret == null)
|
||||||
|
throw new ArgumentException($"No file system registered for the scheme: {scheme}.");
|
||||||
|
usablePath = ret.Metadata.StripScheme ? match.Groups[2].Value : path;
|
||||||
|
return ret.Value.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
return new NotFoundResult();
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.FileResult(relativePath, rangeSupport, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Stream> GetReader(string path)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.GetReader(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Stream> NewFile(string path)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.NewFile(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<string> CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.CreateDirectory(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Combine(params string[] paths)
|
||||||
|
{
|
||||||
|
return _GetFileSystemForPath(paths[0], out string relativePath)
|
||||||
|
.Combine(paths[1..].Prepend(relativePath).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.ListFiles(relativePath, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<bool> Exists(string path)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
return _GetFileSystemForPath(path, out string relativePath)
|
||||||
|
.Exists(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetExtraDirectory(Show show)
|
||||||
|
{
|
||||||
|
if (show == null)
|
||||||
|
throw new ArgumentNullException(nameof(show));
|
||||||
|
return _GetFileSystemForPath(show.Path, out string _)
|
||||||
|
.GetExtraDirectory(show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
Kyoo/Controllers/FileSystems/HttpFileSystem.cs
Normal file
123
Kyoo/Controllers/FileSystems/HttpFileSystem.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Common.Models.Attributes;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Kyoo.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="IFileSystem"/> for http/https links.
|
||||||
|
/// </summary>
|
||||||
|
[FileSystemMetadata(new [] {"http", "https"})]
|
||||||
|
public class HttpFileSystem : IFileSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The http client factory used to create clients.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a <see cref="HttpFileSystem"/> using the given client factory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="factory">The http client factory used to create clients.</param>
|
||||||
|
public HttpFileSystem(IHttpClientFactory factory)
|
||||||
|
{
|
||||||
|
_clientFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
return new NotFoundResult();
|
||||||
|
return new HttpForwardResult(new Uri(path), rangeSupport, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Stream> GetReader(string path)
|
||||||
|
{
|
||||||
|
HttpClient client = _clientFactory.CreateClient();
|
||||||
|
return client.GetStreamAsync(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Stream> NewFile(string path)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("An http filesystem is readonly, a new file can't be created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<string> CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("An http filesystem is readonly, a directory can't be created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Combine(params string[] paths)
|
||||||
|
{
|
||||||
|
return Path.Combine(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Listing files is not supported on an http filesystem.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<bool> Exists(string path)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Checking if a file exists is not supported on an http filesystem.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetExtraDirectory(Show show)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IActionResult"/> to proxy an http request.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpForwardResult : IActionResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The path of the request to forward.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Uri _path;
|
||||||
|
/// <summary>
|
||||||
|
/// Should the proxied result support ranges requests?
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool _rangeSupport;
|
||||||
|
/// <summary>
|
||||||
|
/// If not null, override the content type of the resulting request.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string _type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="HttpForwardResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path of the request to forward.</param>
|
||||||
|
/// <param name="rangeSupport">Should the proxied result support ranges requests?</param>
|
||||||
|
/// <param name="type">If not null, override the content type of the resulting request.</param>
|
||||||
|
public HttpForwardResult(Uri path, bool rangeSupport, string type = null)
|
||||||
|
{
|
||||||
|
_path = path;
|
||||||
|
_rangeSupport = rangeSupport;
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task ExecuteResultAsync(ActionContext context)
|
||||||
|
{
|
||||||
|
// TODO implement that, example: https://github.com/twitchax/AspNetCore.Proxy/blob/14dd0f212d7abb43ca1bf8c890d5efb95db66acb/src/Core/Extensions/Http.cs#L15
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Common.Models.Attributes;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
@ -9,9 +10,10 @@ using Microsoft.AspNetCore.StaticFiles;
|
|||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="IFileManager"/> for the local filesystem (using System.IO).
|
/// A <see cref="IFileSystem"/> for the local filesystem (using System.IO).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FileManager : IFileManager
|
[FileSystemMetadata(new [] {"", "file"}, StripScheme = true)]
|
||||||
|
public class LocalFileSystem : IFileSystem
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An extension provider to get content types from files extensions.
|
/// An extension provider to get content types from files extensions.
|
||||||
@ -41,7 +43,7 @@ namespace Kyoo.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IActionResult FileResult(string path, bool range = false, string type = null)
|
public IActionResult FileResult(string path, bool rangeSupport = false, string type = null)
|
||||||
{
|
{
|
||||||
if (path == null)
|
if (path == null)
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
@ -49,40 +51,56 @@ namespace Kyoo.Controllers
|
|||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
|
return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
|
||||||
{
|
{
|
||||||
EnableRangeProcessing = range
|
EnableRangeProcessing = rangeSupport
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Stream GetReader(string path)
|
public Task<Stream> GetReader(string path)
|
||||||
{
|
{
|
||||||
if (path == null)
|
if (path == null)
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
return File.OpenRead(path);
|
return Task.FromResult<Stream>(File.OpenRead(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Stream NewFile(string path)
|
public Task<Stream> NewFile(string path)
|
||||||
{
|
{
|
||||||
if (path == null)
|
if (path == null)
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
return File.Create(path);
|
return Task.FromResult<Stream>(File.Create(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<ICollection<string>> ListFiles(string path)
|
public Task<string> CreateDirectory(string path)
|
||||||
{
|
{
|
||||||
if (path == null)
|
if (path == null)
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
return Task.FromResult<ICollection<string>>(Directory.Exists(path)
|
Directory.CreateDirectory(path);
|
||||||
? Directory.GetFiles(path)
|
return Task.FromResult(path);
|
||||||
: Array.Empty<string>());
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Combine(params string[] paths)
|
||||||
|
{
|
||||||
|
return Path.Combine(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ICollection<string>> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
if (path == null)
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
string[] ret = Directory.Exists(path)
|
||||||
|
? Directory.GetFiles(path, "*", options)
|
||||||
|
: Array.Empty<string>();
|
||||||
|
return Task.FromResult<ICollection<string>>(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<bool> Exists(string path)
|
public Task<bool> Exists(string path)
|
||||||
{
|
{
|
||||||
return Task.FromResult(File.Exists(path));
|
return Task.FromResult(File.Exists(path) || Directory.Exists(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -92,24 +110,5 @@ namespace Kyoo.Controllers
|
|||||||
Directory.CreateDirectory(path);
|
Directory.CreateDirectory(path);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetExtraDirectory(Season season)
|
|
||||||
{
|
|
||||||
if (season.Show == null)
|
|
||||||
throw new NotImplementedException("Can't get season's extra directory when season.Show == null.");
|
|
||||||
// TODO use a season.Path here.
|
|
||||||
string path = Path.Combine(season.Show.Path, "Extra");
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetExtraDirectory(Episode episode)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra");
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Loader;
|
using System.Runtime.Loader;
|
||||||
|
using Autofac;
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -21,7 +22,7 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The service provider. It allow plugin's activation.
|
/// The service provider. It allow plugin's activation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IServiceProvider _provider;
|
private IServiceProvider _provider;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The configuration to get the plugin's directory.
|
/// The configuration to get the plugin's directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -51,6 +52,13 @@ namespace Kyoo.Controllers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetProvider(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
// TODO temporary bullshit to inject services before the configure asp net.
|
||||||
|
// TODO should rework this when the host will be reworked, as well as the asp net configure.
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public T GetPlugin<T>(string name)
|
public T GetPlugin<T>(string name)
|
||||||
@ -127,6 +135,13 @@ namespace Kyoo.Controllers
|
|||||||
_logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
|
_logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ConfigureContainer(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
foreach (IPlugin plugin in _plugins)
|
||||||
|
plugin.Configure(builder);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
@ -139,7 +154,12 @@ namespace Kyoo.Controllers
|
|||||||
public void ConfigureAspnet(IApplicationBuilder app)
|
public void ConfigureAspnet(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
foreach (IPlugin plugin in _plugins)
|
foreach (IPlugin plugin in _plugins)
|
||||||
|
{
|
||||||
|
using IServiceScope scope = _provider.CreateScope();
|
||||||
|
Helper.InjectServices(plugin, x => scope.ServiceProvider.GetRequiredService(x));
|
||||||
plugin.ConfigureAspNet(app);
|
plugin.ConfigureAspNet(app);
|
||||||
|
Helper.InjectServices(plugin, _ => null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
103
Kyoo/Controllers/ProviderComposite.cs
Normal file
103
Kyoo/Controllers/ProviderComposite.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Kyoo.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A metadata provider composite that merge results from all available providers.
|
||||||
|
/// </summary>
|
||||||
|
public class ProviderComposite : AProviderComposite
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of metadata providers
|
||||||
|
/// </summary>
|
||||||
|
private readonly ICollection<IMetadataProvider> _providers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of selected providers. If no provider has been selected, this is null.
|
||||||
|
/// </summary>
|
||||||
|
private ICollection<Provider> _selectedProviders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger used to print errors.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<ProviderComposite> _logger;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ProviderComposite"/> with a list of available providers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providers">The list of providers to merge.</param>
|
||||||
|
/// <param name="logger">The logger used to print errors.</param>
|
||||||
|
public ProviderComposite(IEnumerable<IMetadataProvider> providers, ILogger<ProviderComposite> logger)
|
||||||
|
{
|
||||||
|
_providers = providers.ToArray();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void UseProviders(IEnumerable<Provider> providers)
|
||||||
|
{
|
||||||
|
_selectedProviders = providers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the list of providers that should be used for queries.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The list of providers to use, respecting the <see cref="UseProviders"/>.</returns>
|
||||||
|
private IEnumerable<IMetadataProvider> _GetProviders()
|
||||||
|
{
|
||||||
|
return _selectedProviders?
|
||||||
|
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
|
||||||
|
.Where(x => x != null)
|
||||||
|
?? _providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<T> Get<T>(T item)
|
||||||
|
{
|
||||||
|
T ret = item;
|
||||||
|
|
||||||
|
foreach (IMetadataProvider provider in _GetProviders())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ret = Merger.Merge(ret, await provider.Get(ret));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "The provider {Provider} could not get a {Type}",
|
||||||
|
provider.Provider.Name, typeof(T).Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ICollection<T>> Search<T>(string query)
|
||||||
|
{
|
||||||
|
List<T> ret = new();
|
||||||
|
|
||||||
|
foreach (IMetadataProvider provider in _GetProviders())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ret.AddRange(await provider.Search<T>(query));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "The provider {Provider} could not search for {Type}",
|
||||||
|
provider.Provider.Name, typeof(T).Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,166 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Kyoo.Models;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
|
||||||
{
|
|
||||||
public class ProviderManager : IProviderManager
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<IMetadataProvider> _providers;
|
|
||||||
|
|
||||||
public ProviderManager(IPluginManager pluginManager)
|
|
||||||
{
|
|
||||||
_providers = pluginManager.GetPlugins<IMetadataProvider>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what)
|
|
||||||
where T : new()
|
|
||||||
{
|
|
||||||
T ret = new();
|
|
||||||
|
|
||||||
IEnumerable<IMetadataProvider> providers = library?.Providers
|
|
||||||
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
|
|
||||||
.Where(x => x != null)
|
|
||||||
?? _providers;
|
|
||||||
|
|
||||||
foreach (IMetadataProvider provider in providers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ret = Merger.Merge(ret, await providerCall(provider));
|
|
||||||
} catch (Exception ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync(
|
|
||||||
$"The provider {provider.Provider.Name} could not work for {what}. Exception: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<T>> GetMetadata<T>(
|
|
||||||
Func<IMetadataProvider, Task<ICollection<T>>> providerCall,
|
|
||||||
Library library,
|
|
||||||
string what)
|
|
||||||
{
|
|
||||||
List<T> ret = new();
|
|
||||||
|
|
||||||
IEnumerable<IMetadataProvider> providers = library?.Providers
|
|
||||||
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
|
|
||||||
.Where(x => x != null)
|
|
||||||
?? _providers;
|
|
||||||
|
|
||||||
foreach (IMetadataProvider provider in providers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ret.AddRange(await providerCall(provider) ?? new List<T>());
|
|
||||||
} catch (Exception ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync(
|
|
||||||
$"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Collection> GetCollectionFromName(string name, Library library)
|
|
||||||
{
|
|
||||||
Collection collection = await GetMetadata(
|
|
||||||
provider => provider.GetCollectionFromName(name),
|
|
||||||
library,
|
|
||||||
$"the collection {name}");
|
|
||||||
collection.Name ??= name;
|
|
||||||
collection.Slug ??= Utility.ToSlug(name);
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Show> CompleteShow(Show show, Library library)
|
|
||||||
{
|
|
||||||
return await GetMetadata(provider => provider.GetShowByID(show), library, $"the show {show.Title}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Show> SearchShow(string showName, bool isMovie, Library library)
|
|
||||||
{
|
|
||||||
Show show = await GetMetadata(async provider =>
|
|
||||||
{
|
|
||||||
Show searchResult = (await provider.SearchShows(showName, isMovie))?.FirstOrDefault();
|
|
||||||
if (searchResult == null)
|
|
||||||
return null;
|
|
||||||
return await provider.GetShowByID(searchResult);
|
|
||||||
}, library, $"the show {showName}");
|
|
||||||
show.Slug = Utility.ToSlug(showName);
|
|
||||||
show.Title ??= showName;
|
|
||||||
show.IsMovie = isMovie;
|
|
||||||
show.Genres = show.Genres?.GroupBy(x => x.Slug).Select(x => x.First()).ToList();
|
|
||||||
show.People = show.People?.GroupBy(x => x.Slug).Select(x => x.First()).ToList();
|
|
||||||
return show;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library)
|
|
||||||
{
|
|
||||||
IEnumerable<Show> shows = await GetMetadata(
|
|
||||||
provider => provider.SearchShows(showName, isMovie),
|
|
||||||
library,
|
|
||||||
$"the show {showName}");
|
|
||||||
return shows.Select(show =>
|
|
||||||
{
|
|
||||||
show.Slug = Utility.ToSlug(showName);
|
|
||||||
show.Title ??= showName;
|
|
||||||
show.IsMovie = isMovie;
|
|
||||||
return show;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Season> GetSeason(Show show, int seasonNumber, Library library)
|
|
||||||
{
|
|
||||||
Season season = await GetMetadata(
|
|
||||||
provider => provider.GetSeason(show, seasonNumber),
|
|
||||||
library,
|
|
||||||
$"the season {seasonNumber} of {show.Title}");
|
|
||||||
season.Show = show;
|
|
||||||
season.ShowID = show.ID;
|
|
||||||
season.ShowSlug = show.Slug;
|
|
||||||
season.Title ??= $"Season {season.SeasonNumber}";
|
|
||||||
return season;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Episode> GetEpisode(Show show,
|
|
||||||
string episodePath,
|
|
||||||
int? seasonNumber,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? absoluteNumber,
|
|
||||||
Library library)
|
|
||||||
{
|
|
||||||
Episode episode = await GetMetadata(
|
|
||||||
provider => provider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber),
|
|
||||||
library,
|
|
||||||
"an episode");
|
|
||||||
episode.Show = show;
|
|
||||||
episode.ShowID = show.ID;
|
|
||||||
episode.ShowSlug = show.Slug;
|
|
||||||
episode.Path = episodePath;
|
|
||||||
episode.SeasonNumber ??= seasonNumber;
|
|
||||||
episode.EpisodeNumber ??= episodeNumber;
|
|
||||||
episode.AbsoluteNumber ??= absoluteNumber;
|
|
||||||
return episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ICollection<PeopleRole>> GetPeople(Show show, Library library)
|
|
||||||
{
|
|
||||||
List<PeopleRole> people = await GetMetadata(
|
|
||||||
provider => provider.GetPeople(show),
|
|
||||||
library,
|
|
||||||
$"a cast member of {show.Title}");
|
|
||||||
return people?.GroupBy(x => x.Slug)
|
|
||||||
.Select(x => x.First())
|
|
||||||
.Select(x =>
|
|
||||||
{
|
|
||||||
x.Show = show;
|
|
||||||
x.ShowID = show.ID;
|
|
||||||
return x;
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
141
Kyoo/Controllers/RegexIdentifier.cs
Normal file
141
Kyoo/Controllers/RegexIdentifier.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Kyoo.Models.Exceptions;
|
||||||
|
using Kyoo.Models.Options;
|
||||||
|
using Kyoo.Models.Watch;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Kyoo.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An identifier that use a regex to extract basics metadata.
|
||||||
|
/// </summary>
|
||||||
|
public class RegexIdentifier : IIdentifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration of kyoo to retrieve the identifier regex.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IOptionsMonitor<MediaOptions> _configuration;
|
||||||
|
/// <summary>
|
||||||
|
/// The library manager used to retrieve libraries paths.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="RegexIdentifier"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The regex patterns to use.</param>
|
||||||
|
/// <param name="libraryManager">The library manager used to retrieve libraries paths.</param>
|
||||||
|
public RegexIdentifier(IOptionsMonitor<MediaOptions> configuration, ILibraryManager libraryManager)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the relative path of an episode or subtitle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The full path of the episode</param>
|
||||||
|
/// <returns>The path relative to the library root.</returns>
|
||||||
|
private async Task<string> _GetRelativePath(string path)
|
||||||
|
{
|
||||||
|
string libraryPath = (await _libraryManager.GetAll<Library>())
|
||||||
|
.SelectMany(x => x.Paths)
|
||||||
|
.Where(path.StartsWith)
|
||||||
|
.OrderByDescending(x => x.Length)
|
||||||
|
.FirstOrDefault();
|
||||||
|
return path[(libraryPath?.Length ?? 0)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(Collection, Show, Season, Episode)> Identify(string path)
|
||||||
|
{
|
||||||
|
string relativePath = await _GetRelativePath(path);
|
||||||
|
Match match = _configuration.CurrentValue.Regex
|
||||||
|
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
|
||||||
|
.Select(x => x.Match(relativePath))
|
||||||
|
.FirstOrDefault(x => x.Success);
|
||||||
|
|
||||||
|
if (match == null)
|
||||||
|
throw new IdentificationFailedException($"The episode at {path} does not match the episode's regex.");
|
||||||
|
|
||||||
|
(Collection collection, Show show, Season season, Episode episode) ret = (
|
||||||
|
collection: new Collection
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(match.Groups["Collection"].Value),
|
||||||
|
Name = match.Groups["Collection"].Value
|
||||||
|
},
|
||||||
|
show: new Show
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(match.Groups["Show"].Value),
|
||||||
|
Title = match.Groups["Show"].Value,
|
||||||
|
Path = Path.GetDirectoryName(path),
|
||||||
|
StartAir = match.Groups["StartYear"].Success
|
||||||
|
? new DateTime(int.Parse(match.Groups["StartYear"].Value), 1, 1)
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
season: null,
|
||||||
|
episode: new Episode
|
||||||
|
{
|
||||||
|
SeasonNumber = match.Groups["Season"].Success
|
||||||
|
? int.Parse(match.Groups["Season"].Value)
|
||||||
|
: null,
|
||||||
|
EpisodeNumber = match.Groups["Episode"].Success
|
||||||
|
? int.Parse(match.Groups["Episode"].Value)
|
||||||
|
: null,
|
||||||
|
AbsoluteNumber = match.Groups["Absolute"].Success
|
||||||
|
? int.Parse(match.Groups["Absolute"].Value)
|
||||||
|
: null,
|
||||||
|
Path = path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ret.episode.SeasonNumber.HasValue)
|
||||||
|
ret.season = new Season { SeasonNumber = ret.episode.SeasonNumber.Value };
|
||||||
|
|
||||||
|
|
||||||
|
if (ret.episode.SeasonNumber == null && ret.episode.EpisodeNumber == null
|
||||||
|
&& ret.episode.AbsoluteNumber == null)
|
||||||
|
{
|
||||||
|
ret.show.IsMovie = true;
|
||||||
|
ret.episode.Title = ret.show.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Track> IdentifyTrack(string path)
|
||||||
|
{
|
||||||
|
Match match = _configuration.CurrentValue.SubtitleRegex
|
||||||
|
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
|
||||||
|
.Select(x => x.Match(path))
|
||||||
|
.FirstOrDefault(x => x.Success);
|
||||||
|
|
||||||
|
if (match == null)
|
||||||
|
throw new IdentificationFailedException($"The subtitle at {path} does not match the subtitle's regex.");
|
||||||
|
|
||||||
|
string episodePath = match.Groups["Episode"].Value;
|
||||||
|
string extension = Path.GetExtension(path);
|
||||||
|
return Task.FromResult(new Track
|
||||||
|
{
|
||||||
|
Type = StreamType.Subtitle,
|
||||||
|
Language = match.Groups["Language"].Value,
|
||||||
|
IsDefault = match.Groups["Default"].Success,
|
||||||
|
IsForced = match.Groups["Forced"].Success,
|
||||||
|
Codec = FileExtensions.SubtitleExtensions.GetValueOrDefault(extension, extension[1..]),
|
||||||
|
IsExternal = true,
|
||||||
|
Path = path,
|
||||||
|
Episode = new Episode
|
||||||
|
{
|
||||||
|
Path = episodePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -114,9 +114,6 @@ namespace Kyoo.Controllers
|
|||||||
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
|
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
|
||||||
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
|
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
|
||||||
return await ValidateTracks(obj);
|
return await ValidateTracks(obj);
|
||||||
// TODO check if this is needed
|
|
||||||
// obj.Slug = await _database.Entry(obj).Property(x => x.Slug).
|
|
||||||
// return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -81,17 +81,28 @@ namespace Kyoo.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
|
public override Task<LibraryItem> Create(LibraryItem obj)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
|
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException();
|
public override Task<LibraryItem> Edit(LibraryItem obj, bool resetOld)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task Delete(int id) => throw new InvalidOperationException();
|
public override Task Delete(int id)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task Delete(string slug) => throw new InvalidOperationException();
|
public override Task Delete(string slug)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task Delete(LibraryItem obj) => throw new InvalidOperationException();
|
public override Task Delete(LibraryItem obj)
|
||||||
|
=> throw new InvalidOperationException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a basic queryable for a library with the right mapping from shows & collections.
|
/// Get a basic queryable for a library with the right mapping from shows & collections.
|
||||||
|
@ -30,7 +30,7 @@ namespace Kyoo.Controllers
|
|||||||
/// Create a new <see cref="LibraryRepository"/> instance.
|
/// Create a new <see cref="LibraryRepository"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="database">The database handle</param>
|
/// <param name="database">The database handle</param>
|
||||||
/// <param name="providers">The providere repository</param>
|
/// <param name="providers">The provider repository</param>
|
||||||
public LibraryRepository(DatabaseContext database, IProviderRepository providers)
|
public LibraryRepository(DatabaseContext database, IProviderRepository providers)
|
||||||
: base(database)
|
: base(database)
|
||||||
{
|
{
|
||||||
@ -53,8 +53,8 @@ namespace Kyoo.Controllers
|
|||||||
public override async Task<Library> Create(Library obj)
|
public override async Task<Library> Create(Library obj)
|
||||||
{
|
{
|
||||||
await base.Create(obj);
|
await base.Create(obj);
|
||||||
obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToList();
|
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
_database.Entry(obj).State = EntityState.Added;
|
||||||
|
obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
|
||||||
await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists).");
|
await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists).");
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@ -63,6 +63,9 @@ namespace Kyoo.Controllers
|
|||||||
protected override async Task Validate(Library resource)
|
protected override async Task Validate(Library resource)
|
||||||
{
|
{
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
|
resource.ProviderLinks = resource.Providers?
|
||||||
|
.Select(x => Link.Create(resource, x))
|
||||||
|
.ToList();
|
||||||
await resource.ProviderLinks.ForEachAsync(async id =>
|
await resource.ProviderLinks.ForEachAsync(async id =>
|
||||||
{
|
{
|
||||||
id.Second = await _providers.CreateIfNotExists(id.Second);
|
id.Second = await _providers.CreateIfNotExists(id.Second);
|
||||||
|
@ -96,7 +96,12 @@ namespace Kyoo.Controllers
|
|||||||
protected override async Task Validate(Season resource)
|
protected override async Task Validate(Season resource)
|
||||||
{
|
{
|
||||||
if (resource.ShowID <= 0)
|
if (resource.ShowID <= 0)
|
||||||
throw new InvalidOperationException($"Can't store a season not related to any show (showID: {resource.ShowID}).");
|
{
|
||||||
|
if (resource.Show == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Can't store a season not related to any show (showID: {resource.ShowID}).");
|
||||||
|
resource.ShowID = resource.Show.ID;
|
||||||
|
}
|
||||||
|
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
await resource.ExternalIDs.ForEachAsync(async id =>
|
||||||
|
@ -4,11 +4,12 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Autofac.Features.Metadata;
|
||||||
|
using Autofac.Features.OwnedInstances;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Kyoo.Models.Attributes;
|
using Kyoo.Common.Models.Attributes;
|
||||||
using Kyoo.Models.Exceptions;
|
using Kyoo.Models.Exceptions;
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@ -22,9 +23,52 @@ namespace Kyoo.Controllers
|
|||||||
public class TaskManager : BackgroundService, ITaskManager
|
public class TaskManager : BackgroundService, ITaskManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The service provider used to activate
|
/// The class representing task under this <see cref="TaskManager"/> jurisdiction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IServiceProvider _provider;
|
private class ManagedTask
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The metadata for this task (the slug, and other useful information).
|
||||||
|
/// </summary>
|
||||||
|
public TaskMetadataAttribute Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The function used to create the task object.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Owned<ITask>> Factory { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next scheduled date for this task
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ScheduledDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing a task inside the <see cref="TaskManager._queuedTasks"/> list.
|
||||||
|
/// </summary>
|
||||||
|
private class QueuedTask
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The task currently queued.
|
||||||
|
/// </summary>
|
||||||
|
public ManagedTask Task { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The progress reporter that this task should use.
|
||||||
|
/// </summary>
|
||||||
|
public IProgress<float> ProgressReporter { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The arguments to give to run the task with.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Arguments { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A token informing the task that it should be cancelled or not.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken? CancellationToken { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The configuration instance used to get schedule information
|
/// The configuration instance used to get schedule information
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -37,15 +81,15 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of tasks and their next scheduled run.
|
/// The list of tasks and their next scheduled run.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<(ITask task, DateTime scheduledDate)> _tasks;
|
private readonly List<ManagedTask> _tasks;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The queue of tasks that should be run as soon as possible.
|
/// The queue of tasks that should be run as soon as possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Queue<(ITask, Dictionary<string, object>)> _queuedTasks = new();
|
private readonly Queue<QueuedTask> _queuedTasks = new();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently running task.
|
/// The currently running task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private ITask _runningTask;
|
private (TaskMetadataAttribute, ITask)? _runningTask;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cancellation token used to cancel the running task when the runner should shutdown.
|
/// The cancellation token used to cancel the running task when the runner should shutdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -55,22 +99,24 @@ namespace Kyoo.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="TaskManager"/>.
|
/// Create a new <see cref="TaskManager"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tasks">The list of tasks to manage</param>
|
/// <param name="tasks">The list of tasks to manage with their metadata</param>
|
||||||
/// <param name="provider">The service provider to request services for tasks</param>
|
|
||||||
/// <param name="options">The configuration to load schedule information.</param>
|
/// <param name="options">The configuration to load schedule information.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
public TaskManager(IEnumerable<ITask> tasks,
|
public TaskManager(IEnumerable<Meta<Func<Owned<ITask>>, TaskMetadataAttribute>> tasks,
|
||||||
IServiceProvider provider,
|
|
||||||
IOptionsMonitor<TaskOptions> options,
|
IOptionsMonitor<TaskOptions> options,
|
||||||
ILogger<TaskManager> logger)
|
ILogger<TaskManager> logger)
|
||||||
{
|
{
|
||||||
_provider = provider;
|
|
||||||
_options = options;
|
_options = options;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_tasks = tasks.Select(x => (x, GetNextTaskDate(x.Slug))).ToList();
|
_tasks = tasks.Select(x => new ManagedTask
|
||||||
|
{
|
||||||
|
Factory = x.Value,
|
||||||
|
Metadata = x.Metadata,
|
||||||
|
ScheduledDate = GetNextTaskDate(x.Metadata.Slug)
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
if (_tasks.Any())
|
if (_tasks.Any())
|
||||||
_logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.task.Name));
|
_logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.Metadata.Name));
|
||||||
else
|
else
|
||||||
_logger.LogInformation("Task manager initiated without any tasks");
|
_logger.LogInformation("Task manager initiated without any tasks");
|
||||||
}
|
}
|
||||||
@ -100,21 +146,26 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="cancellationToken">A token to stop the runner</param>
|
/// <param name="cancellationToken">A token to stop the runner</param>
|
||||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
EnqueueStartupTasks();
|
_EnqueueStartupTasks();
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (_queuedTasks.Any())
|
if (_queuedTasks.Any())
|
||||||
{
|
{
|
||||||
(ITask task, Dictionary<string, object> arguments) = _queuedTasks.Dequeue();
|
QueuedTask task = _queuedTasks.Dequeue();
|
||||||
_runningTask = task;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await RunTask(task, arguments);
|
await _RunTask(task.Task, task.ProgressReporter, task.Arguments, task.CancellationToken);
|
||||||
|
}
|
||||||
|
catch (TaskFailedException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("The task \"{Task}\" failed: {Message}",
|
||||||
|
task.Task.Metadata.Name, ex.Message);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name);
|
_logger.LogError(e, "An unhandled exception occured while running the task {Task}",
|
||||||
|
task.Task.Metadata.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -129,21 +180,31 @@ namespace Kyoo.Controllers
|
|||||||
/// Parse parameters, inject a task and run it.
|
/// Parse parameters, inject a task and run it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="task">The task to run</param>
|
/// <param name="task">The task to run</param>
|
||||||
|
/// <param name="progress">A progress reporter to know the percentage of completion of the task.</param>
|
||||||
/// <param name="arguments">The arguments to pass to the function</param>
|
/// <param name="arguments">The arguments to pass to the function</param>
|
||||||
/// <exception cref="ArgumentException">There was an invalid argument or a required argument was not found.</exception>
|
/// <param name="cancellationToken">An optional cancellation token that will be passed to the task.</param>
|
||||||
private async Task RunTask(ITask task, Dictionary<string, object> arguments)
|
/// <exception cref="ArgumentException">
|
||||||
|
/// If the number of arguments is invalid, if an argument can't be converted or if the task finds the argument
|
||||||
|
/// invalid.
|
||||||
|
/// </exception>
|
||||||
|
private async Task _RunTask(ManagedTask task,
|
||||||
|
[NotNull] IProgress<float> progress,
|
||||||
|
Dictionary<string, object> arguments,
|
||||||
|
CancellationToken? cancellationToken = null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Task starting: {Task}", task.Name);
|
using (_logger.BeginScope("Task: {Task}", task.Metadata.Name))
|
||||||
|
{
|
||||||
ICollection<TaskParameter> all = task.GetParameters();
|
await using Owned<ITask> taskObj = task.Factory.Invoke();
|
||||||
|
ICollection<TaskParameter> all = taskObj.Value.GetParameters();
|
||||||
|
|
||||||
|
_runningTask = (task.Metadata, taskObj.Value);
|
||||||
ICollection<string> invalids = arguments.Keys
|
ICollection<string> invalids = arguments.Keys
|
||||||
.Where(x => all.Any(y => x != y.Name))
|
.Where(x => all.All(y => x != y.Name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (invalids.Any())
|
if (invalids.Any())
|
||||||
{
|
{
|
||||||
string invalidsStr = string.Join(", ", invalids);
|
throw new ArgumentException($"{string.Join(", ", invalids)} are " +
|
||||||
throw new ArgumentException($"{invalidsStr} are invalid arguments for the task {task.Name}");
|
$"invalid arguments for the task {task.Metadata.Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskParameters args = new(all
|
TaskParameters args = new(all
|
||||||
@ -153,31 +214,22 @@ namespace Kyoo.Controllers
|
|||||||
.FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase))
|
.FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
.Value;
|
.Value;
|
||||||
if (value == null && x.IsRequired)
|
if (value == null && x.IsRequired)
|
||||||
throw new ArgumentException($"The argument {x.Name} is required to run {task.Name}" +
|
throw new ArgumentException($"The argument {x.Name} is required to run " +
|
||||||
" but it was not specified.");
|
$"{task.Metadata.Name} but it was not specified.");
|
||||||
return x.CreateValue(value ?? x.DefaultValue);
|
return x.CreateValue(value ?? x.DefaultValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
using IServiceScope scope = _provider.CreateScope();
|
_logger.LogInformation("Task starting: {Task} ({Parameters})",
|
||||||
InjectServices(task, x => scope.ServiceProvider.GetRequiredService(x));
|
task.Metadata.Name, args.ToDictionary(x => x.Name, x => x.As<object>()));
|
||||||
await task.Run(args, _taskToken.Token);
|
|
||||||
InjectServices(task, _ => null);
|
CancellationToken token = cancellationToken != null
|
||||||
_logger.LogInformation("Task finished: {Task}", task.Name);
|
? CancellationTokenSource.CreateLinkedTokenSource(_taskToken.Token, cancellationToken.Value).Token
|
||||||
|
: _taskToken.Token;
|
||||||
|
await taskObj.Value.Run(args, progress, token);
|
||||||
|
|
||||||
|
_logger.LogInformation("Task finished: {Task}", task.Metadata.Name);
|
||||||
|
_runningTask = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Inject services into the <see cref="InjectedAttribute"/> marked properties of the given object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">The object to inject</param>
|
|
||||||
/// <param name="retrieve">The function used to retrieve services. (The function is called immediately)</param>
|
|
||||||
private static void InjectServices(ITask obj, [InstantHandle] Func<Type, object> retrieve)
|
|
||||||
{
|
|
||||||
IEnumerable<PropertyInfo> properties = obj.GetType().GetProperties()
|
|
||||||
.Where(x => x.GetCustomAttribute<InjectedAttribute>() != null)
|
|
||||||
.Where(x => x.CanWrite);
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in properties)
|
|
||||||
property.SetValue(obj, retrieve(property.PropertyType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -185,37 +237,59 @@ namespace Kyoo.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void QueueScheduledTasks()
|
private void QueueScheduledTasks()
|
||||||
{
|
{
|
||||||
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.scheduledDate <= DateTime.Now)
|
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.ScheduledDate <= DateTime.Now)
|
||||||
.Select(x => x.task.Slug);
|
.Select(x => x.Metadata.Slug);
|
||||||
foreach (string task in tasksToQueue)
|
foreach (string task in tasksToQueue)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Queuing task scheduled for running: {Task}", task);
|
_logger.LogDebug("Queuing task scheduled for running: {Task}", task);
|
||||||
StartTask(task, new Dictionary<string, object>());
|
StartTask(task, new Progress<float>(), new Dictionary<string, object>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queue startup tasks with respect to the priority rules.
|
/// Queue startup tasks with respect to the priority rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void EnqueueStartupTasks()
|
private void _EnqueueStartupTasks()
|
||||||
{
|
{
|
||||||
IEnumerable<ITask> startupTasks = _tasks.Select(x => x.task)
|
IEnumerable<string> startupTasks = _tasks
|
||||||
.Where(x => x.RunOnStartup)
|
.Where(x => x.Metadata.RunOnStartup)
|
||||||
.OrderByDescending(x => x.Priority);
|
.OrderByDescending(x => x.Metadata.Priority)
|
||||||
foreach (ITask task in startupTasks)
|
.Select(x => x.Metadata.Slug);
|
||||||
_queuedTasks.Enqueue((task, new Dictionary<string, object>()));
|
foreach (string task in startupTasks)
|
||||||
|
StartTask(task, new Progress<float>(), new Dictionary<string, object>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void StartTask(string taskSlug, Dictionary<string, object> arguments = null)
|
public void StartTask(string taskSlug,
|
||||||
|
IProgress<float> progress,
|
||||||
|
Dictionary<string, object> arguments = null,
|
||||||
|
CancellationToken? cancellationToken = null)
|
||||||
{
|
{
|
||||||
arguments ??= new Dictionary<string, object>();
|
arguments ??= new Dictionary<string, object>();
|
||||||
|
|
||||||
int index = _tasks.FindIndex(x => x.task.Slug == taskSlug);
|
int index = _tasks.FindIndex(x => x.Metadata.Slug == taskSlug);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
|
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
|
||||||
_queuedTasks.Enqueue((_tasks[index].task, arguments));
|
_queuedTasks.Enqueue(new QueuedTask
|
||||||
_tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug));
|
{
|
||||||
|
Task = _tasks[index],
|
||||||
|
ProgressReporter = progress,
|
||||||
|
Arguments = arguments,
|
||||||
|
CancellationToken = cancellationToken
|
||||||
|
});
|
||||||
|
_tasks[index].ScheduledDate = GetNextTaskDate(taskSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void StartTask<T>(IProgress<float> progress,
|
||||||
|
Dictionary<string, object> arguments = null,
|
||||||
|
CancellationToken? cancellationToken = null)
|
||||||
|
where T : ITask
|
||||||
|
{
|
||||||
|
TaskMetadataAttribute metadata = typeof(T).GetCustomAttribute<TaskMetadataAttribute>();
|
||||||
|
if (metadata == null)
|
||||||
|
throw new ArgumentException($"No metadata found on the given task (type: {typeof(T).Name}).");
|
||||||
|
StartTask(metadata.Slug, progress, arguments, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -231,15 +305,17 @@ namespace Kyoo.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ICollection<ITask> GetRunningTasks()
|
public ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks()
|
||||||
{
|
{
|
||||||
return new[] {_runningTask};
|
return _runningTask == null
|
||||||
|
? ArraySegment<(TaskMetadataAttribute, ITask)>.Empty
|
||||||
|
: new[] { _runningTask.Value };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ICollection<ITask> GetAllTasks()
|
public ICollection<TaskMetadataAttribute> GetAllTasks()
|
||||||
{
|
{
|
||||||
return _tasks.Select(x => x.task).ToArray();
|
return _tasks.Select(x => x.Metadata).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,157 +1,292 @@
|
|||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Kyoo.Controllers
|
namespace Kyoo.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Download images and retrieve the path of those images for a resource.
|
||||||
|
/// </summary>
|
||||||
public class ThumbnailsManager : IThumbnailsManager
|
public class ThumbnailsManager : IThumbnailsManager
|
||||||
{
|
{
|
||||||
private readonly IFileManager _files;
|
/// <summary>
|
||||||
|
/// The file manager used to download the image if the file is distant
|
||||||
|
/// </summary>
|
||||||
|
private readonly IFileSystem _files;
|
||||||
|
/// <summary>
|
||||||
|
/// A logger to report errors.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<ThumbnailsManager> _logger;
|
||||||
|
/// <summary>
|
||||||
|
/// The options containing the base path of people images and provider logos.
|
||||||
|
/// </summary>
|
||||||
private readonly IOptionsMonitor<BasicOptions> _options;
|
private readonly IOptionsMonitor<BasicOptions> _options;
|
||||||
|
/// <summary>
|
||||||
|
/// A library manager used to load episode and seasons shows if they are not loaded.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Lazy<ILibraryManager> _library;
|
||||||
|
|
||||||
public ThumbnailsManager(IFileManager files, IOptionsMonitor<BasicOptions> options)
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ThumbnailsManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files">The file manager to use.</param>
|
||||||
|
/// <param name="logger">A logger to report errors</param>
|
||||||
|
/// <param name="options">The options to use.</param>
|
||||||
|
/// <param name="library">A library manager used to load shows if they are not loaded.</param>
|
||||||
|
public ThumbnailsManager(IFileSystem files,
|
||||||
|
ILogger<ThumbnailsManager> logger,
|
||||||
|
IOptionsMonitor<BasicOptions> options,
|
||||||
|
Lazy<ILibraryManager> library)
|
||||||
{
|
{
|
||||||
_files = files;
|
_files = files;
|
||||||
|
_logger = logger;
|
||||||
_options = options;
|
_options = options;
|
||||||
Directory.CreateDirectory(_options.CurrentValue.PeoplePath);
|
_library = library;
|
||||||
Directory.CreateDirectory(_options.CurrentValue.ProviderPath);
|
|
||||||
|
options.OnChange(x =>
|
||||||
|
{
|
||||||
|
_files.CreateDirectory(x.PeoplePath);
|
||||||
|
_files.CreateDirectory(x.ProviderPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task DownloadImage(string url, string localPath, string what)
|
/// <inheritdoc />
|
||||||
|
public Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false)
|
||||||
|
where T : IResource
|
||||||
{
|
{
|
||||||
|
if (item == null)
|
||||||
|
throw new ArgumentNullException(nameof(item));
|
||||||
|
return item switch
|
||||||
|
{
|
||||||
|
Show show => _Validate(show, alwaysDownload),
|
||||||
|
Season season => _Validate(season, alwaysDownload),
|
||||||
|
Episode episode => _Validate(episode, alwaysDownload),
|
||||||
|
People people => _Validate(people, alwaysDownload),
|
||||||
|
Provider provider => _Validate(provider, alwaysDownload),
|
||||||
|
_ => Task.FromResult(false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An helper function to download an image using a <see cref="LocalFileSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The distant url of the image</param>
|
||||||
|
/// <param name="localPath">The local path of the image</param>
|
||||||
|
/// <param name="what">What is currently downloaded (used for errors)</param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _DownloadImage(string url, string localPath, string what)
|
||||||
|
{
|
||||||
|
if (url == localPath)
|
||||||
|
return false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using WebClient client = new();
|
await using Stream reader = await _files.GetReader(url);
|
||||||
await client.DownloadFileTaskAsync(new Uri(url), localPath);
|
await using Stream local = await _files.NewFile(localPath);
|
||||||
|
await reader.CopyToAsync(local);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (WebException exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"{what} could not be downloaded. Error: {exception.Message}.");
|
_logger.LogError(ex, "{What} could not be downloaded", what);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Validate(Show show, bool alwaysDownload)
|
/// <summary>
|
||||||
|
/// Download images of a specified show.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="show">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _Validate([NotNull] Show show, bool alwaysDownload)
|
||||||
{
|
{
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
if (show.Poster != null)
|
if (show.Poster != null)
|
||||||
{
|
{
|
||||||
string posterPath = await GetShowPoster(show);
|
string posterPath = await GetPoster(show);
|
||||||
if (alwaysDownload || !File.Exists(posterPath))
|
if (alwaysDownload || !await _files.Exists(posterPath))
|
||||||
await DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
|
ret |= await _DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
|
||||||
}
|
}
|
||||||
if (show.Logo != null)
|
if (show.Logo != null)
|
||||||
{
|
{
|
||||||
string logoPath = await GetShowLogo(show);
|
string logoPath = await GetLogo(show);
|
||||||
if (alwaysDownload || !File.Exists(logoPath))
|
if (alwaysDownload || !await _files.Exists(logoPath))
|
||||||
await DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
|
ret |= await _DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
|
||||||
}
|
}
|
||||||
if (show.Backdrop != null)
|
if (show.Backdrop != null)
|
||||||
{
|
{
|
||||||
string backdropPath = await GetShowBackdrop(show);
|
string backdropPath = await GetThumbnail(show);
|
||||||
if (alwaysDownload || !File.Exists(backdropPath))
|
if (alwaysDownload || !await _files.Exists(backdropPath))
|
||||||
await DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
|
ret |= await _DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (PeopleRole role in show.People)
|
return ret;
|
||||||
await Validate(role.People, alwaysDownload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Validate([NotNull] People people, bool alwaysDownload)
|
/// <summary>
|
||||||
|
/// Download images of a specified person.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="people">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _Validate([NotNull] People people, bool alwaysDownload)
|
||||||
{
|
{
|
||||||
if (people == null)
|
if (people == null)
|
||||||
throw new ArgumentNullException(nameof(people));
|
throw new ArgumentNullException(nameof(people));
|
||||||
if (people.Poster == null)
|
if (people.Poster == null)
|
||||||
return;
|
return false;
|
||||||
string localPath = await GetPeoplePoster(people);
|
string localPath = await GetPoster(people);
|
||||||
if (alwaysDownload || !File.Exists(localPath))
|
if (alwaysDownload || !await _files.Exists(localPath))
|
||||||
await DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}");
|
return await _DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Validate(Season season, bool alwaysDownload)
|
/// <summary>
|
||||||
|
/// Download images of a specified season.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="season">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _Validate([NotNull] Season season, bool alwaysDownload)
|
||||||
{
|
{
|
||||||
if (season?.Show?.Path == null || season.Poster == null)
|
if (season.Poster == null)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
string localPath = await GetSeasonPoster(season);
|
string localPath = await GetPoster(season);
|
||||||
if (alwaysDownload || !File.Exists(localPath))
|
if (alwaysDownload || !await _files.Exists(localPath))
|
||||||
await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}");
|
return await _DownloadImage(season.Poster, localPath, $"The poster of {season.Slug}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Validate(Episode episode, bool alwaysDownload)
|
/// <summary>
|
||||||
|
/// Download images of a specified episode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _Validate([NotNull] Episode episode, bool alwaysDownload)
|
||||||
{
|
{
|
||||||
if (episode?.Path == null || episode.Thumb == null)
|
if (episode.Thumb == null)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
string localPath = await GetEpisodeThumb(episode);
|
string localPath = await _GetEpisodeThumb(episode);
|
||||||
if (alwaysDownload || !File.Exists(localPath))
|
if (alwaysDownload || !await _files.Exists(localPath))
|
||||||
await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
|
return await _DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Validate(Provider provider, bool alwaysDownload)
|
/// <summary>
|
||||||
|
/// Download images of a specified provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">
|
||||||
|
/// The item to cache images.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="alwaysDownload">
|
||||||
|
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
|
||||||
|
private async Task<bool> _Validate([NotNull] Provider provider, bool alwaysDownload)
|
||||||
{
|
{
|
||||||
if (provider.Logo == null)
|
if (provider.Logo == null)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
string localPath = await GetProviderLogo(provider);
|
string localPath = await GetLogo(provider);
|
||||||
if (alwaysDownload || !File.Exists(localPath))
|
if (alwaysDownload || !await _files.Exists(localPath))
|
||||||
await DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}");
|
return await _DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetShowBackdrop(Show show)
|
/// <inheritdoc />
|
||||||
|
public Task<string> GetPoster<T>(T item)
|
||||||
|
where T : IResource
|
||||||
{
|
{
|
||||||
if (show?.Path == null)
|
if (item == null)
|
||||||
throw new ArgumentNullException(nameof(show));
|
throw new ArgumentNullException(nameof(item));
|
||||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "backdrop.jpg"));
|
return item switch
|
||||||
|
{
|
||||||
|
Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "poster.jpg")),
|
||||||
|
Season season => _GetSeasonPoster(season),
|
||||||
|
People actor => Task.FromResult(_files.Combine(_options.CurrentValue.PeoplePath, $"{actor.Slug}.jpg")),
|
||||||
|
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a poster.")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetShowLogo(Show show)
|
/// <summary>
|
||||||
|
/// Retrieve the path of a season's poster.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="season">The season to retrieve the poster from.</param>
|
||||||
|
/// <returns>The path of the season's poster.</returns>
|
||||||
|
private async Task<string> _GetSeasonPoster(Season season)
|
||||||
{
|
{
|
||||||
if (show?.Path == null)
|
if (season.Show == null)
|
||||||
throw new ArgumentNullException(nameof(show));
|
await _library.Value.Load(season, x => x.Show);
|
||||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "logo.png"));
|
return _files.Combine(_files.GetExtraDirectory(season.Show), $"season-{season.SeasonNumber}.jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetShowPoster(Show show)
|
/// <inheritdoc />
|
||||||
|
public Task<string> GetThumbnail<T>(T item)
|
||||||
|
where T : IResource
|
||||||
{
|
{
|
||||||
if (show?.Path == null)
|
if (item == null)
|
||||||
throw new ArgumentNullException(nameof(show));
|
throw new ArgumentNullException(nameof(item));
|
||||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "poster.jpg"));
|
return item switch
|
||||||
|
{
|
||||||
|
Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "backdrop.jpg")),
|
||||||
|
Episode episode => _GetEpisodeThumb(episode),
|
||||||
|
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetSeasonPoster(Season season)
|
/// <summary>
|
||||||
|
/// Get the path for an episode's thumbnail.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="episode">The episode to retrieve the thumbnail from</param>
|
||||||
|
/// <returns>The path of the given episode's thumbnail.</returns>
|
||||||
|
private async Task<string> _GetEpisodeThumb(Episode episode)
|
||||||
{
|
{
|
||||||
if (season == null)
|
if (episode.Show == null)
|
||||||
throw new ArgumentNullException(nameof(season));
|
await _library.Value.Load(episode, x => x.Show);
|
||||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(season), $"season-{season.SeasonNumber}.jpg"));
|
string dir = _files.Combine(_files.GetExtraDirectory(episode.Show), "Thumbnails");
|
||||||
|
await _files.CreateDirectory(dir);
|
||||||
|
return _files.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetEpisodeThumb(Episode episode)
|
/// <inheritdoc />
|
||||||
|
public Task<string> GetLogo<T>(T item)
|
||||||
|
where T : IResource
|
||||||
{
|
{
|
||||||
string dir = Path.Combine(_files.GetExtraDirectory(episode), "Thumbnails");
|
if (item == null)
|
||||||
Directory.CreateDirectory(dir);
|
throw new ArgumentNullException(nameof(item));
|
||||||
return Task.FromResult(Path.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg"));
|
return Task.FromResult(item switch
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> GetPeoplePoster(People people)
|
|
||||||
{
|
{
|
||||||
if (people == null)
|
Show show => _files.Combine(_files.GetExtraDirectory(show), "logo.png"),
|
||||||
throw new ArgumentNullException(nameof(people));
|
Provider provider => _files.Combine(_options.CurrentValue.ProviderPath,
|
||||||
string peoplePath = _options.CurrentValue.PeoplePath;
|
$"{provider.Slug}.{provider.LogoExtension}"),
|
||||||
string thumbPath = Path.GetFullPath(Path.Combine(peoplePath, $"{people.Slug}.jpg"));
|
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.")
|
||||||
return Task.FromResult(thumbPath.StartsWith(peoplePath) ? thumbPath : null);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> GetProviderLogo(Provider provider)
|
|
||||||
{
|
|
||||||
if (provider == null)
|
|
||||||
throw new ArgumentNullException(nameof(provider));
|
|
||||||
string providerPath = _options.CurrentValue.ProviderPath;
|
|
||||||
string thumbPath = Path.GetFullPath(Path.Combine(providerPath, $"{provider.Slug}.{provider.LogoExtension}"));
|
|
||||||
return Task.FromResult(thumbPath.StartsWith(providerPath) ? thumbPath : null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,24 +72,29 @@ namespace Kyoo.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IFileManager _files;
|
private readonly IFileSystem _files;
|
||||||
private readonly IOptions<BasicOptions> _options;
|
private readonly IOptions<BasicOptions> _options;
|
||||||
|
private readonly Lazy<ILibraryManager> _library;
|
||||||
|
|
||||||
public Transcoder(IFileManager files, IOptions<BasicOptions> options)
|
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, Lazy<ILibraryManager> library)
|
||||||
{
|
{
|
||||||
_files = files;
|
_files = files;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_library = library;
|
||||||
|
|
||||||
if (TranscoderAPI.init() != Marshal.SizeOf<Stream>())
|
if (TranscoderAPI.init() != Marshal.SizeOf<Stream>())
|
||||||
throw new BadTranscoderException();
|
throw new BadTranscoderException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Track[]> ExtractInfos(Episode episode, bool reextract)
|
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract)
|
||||||
{
|
{
|
||||||
string dir = _files.GetExtraDirectory(episode);
|
if (episode.Show == null)
|
||||||
|
await _library.Value.Load(episode, x => x.Show);
|
||||||
|
|
||||||
|
string dir = _files.GetExtraDirectory(episode.Show);
|
||||||
if (dir == null)
|
if (dir == null)
|
||||||
throw new ArgumentException("Invalid path.");
|
throw new ArgumentException("Invalid path.");
|
||||||
return Task.Factory.StartNew(
|
return await Task.Factory.StartNew(
|
||||||
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
|
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
|
||||||
TaskCreationOptions.LongRunning);
|
TaskCreationOptions.LongRunning);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using Autofac;
|
||||||
|
using Autofac.Core;
|
||||||
|
using Autofac.Core.Registration;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
using Kyoo.Models.Permissions;
|
using Kyoo.Models.Permissions;
|
||||||
using Kyoo.Tasks;
|
using Kyoo.Tasks;
|
||||||
@ -31,12 +34,14 @@ namespace Kyoo
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ICollection<Type> Provides => new[]
|
public ICollection<Type> Provides => new[]
|
||||||
{
|
{
|
||||||
typeof(IFileManager),
|
typeof(IFileSystem),
|
||||||
typeof(ITranscoder),
|
typeof(ITranscoder),
|
||||||
typeof(IThumbnailsManager),
|
typeof(IThumbnailsManager),
|
||||||
typeof(IProviderManager),
|
typeof(IMetadataProvider),
|
||||||
typeof(ITaskManager),
|
typeof(ITaskManager),
|
||||||
typeof(ILibraryManager)
|
typeof(ILibraryManager),
|
||||||
|
typeof(IIdentifier),
|
||||||
|
typeof(AProviderComposite)
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -78,6 +83,11 @@ namespace Kyoo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration manager used to register typed/untyped implementations.
|
||||||
|
/// </summary>
|
||||||
|
[Injected] public IConfigurationManager ConfigurationManager { private get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new core module instance and use the given configuration.
|
/// Create a new core module instance and use the given configuration.
|
||||||
@ -88,19 +98,57 @@ namespace Kyoo
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
builder.RegisterComposite<FileSystemComposite, IFileSystem>();
|
||||||
|
builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance();
|
||||||
|
builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance();
|
||||||
|
|
||||||
|
builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().SingleInstance();
|
||||||
|
builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance();
|
||||||
|
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().SingleInstance();
|
||||||
|
builder.RegisterType<TaskManager>().As<ITaskManager>().SingleInstance();
|
||||||
|
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
|
||||||
|
builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance();
|
||||||
|
|
||||||
|
builder.RegisterComposite<ProviderComposite, IMetadataProvider>();
|
||||||
|
builder.Register(x => (AProviderComposite)x.Resolve<IMetadataProvider>());
|
||||||
|
|
||||||
|
builder.RegisterTask<Crawler>();
|
||||||
|
builder.RegisterTask<Housekeeping>();
|
||||||
|
builder.RegisterTask<RegisterEpisode>();
|
||||||
|
builder.RegisterTask<RegisterSubtitle>();
|
||||||
|
builder.RegisterTask<MetadataProviderLoader>();
|
||||||
|
|
||||||
|
static bool DatabaseIsPresent(IComponentRegistryBuilder x)
|
||||||
|
=> x.IsRegistered(new TypedService(typeof(DatabaseContext)));
|
||||||
|
|
||||||
|
builder.RegisterRepository<ILibraryRepository, LibraryRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<ICollectionRepository, CollectionRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IShowRepository, ShowRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<ISeasonRepository, SeasonRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<ITrackRepository, TrackRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IPeopleRepository, PeopleRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IStudioRepository, StudioRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IGenreRepository, GenreRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IProviderRepository, ProviderRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
builder.RegisterRepository<IUserRepository, UserRepository>().OnlyIf(DatabaseIsPresent);
|
||||||
|
|
||||||
|
builder.RegisterType<PassthroughPermissionValidator>().As<IPermissionValidator>()
|
||||||
|
.IfNotRegistered(typeof(IPermissionValidator));
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||||
{
|
{
|
||||||
string publicUrl = _configuration.GetPublicUrl();
|
string publicUrl = _configuration.GetPublicUrl();
|
||||||
|
|
||||||
services.Configure<BasicOptions>(_configuration.GetSection(BasicOptions.Path));
|
services.Configure<BasicOptions>(_configuration.GetSection(BasicOptions.Path));
|
||||||
services.AddConfiguration<BasicOptions>(BasicOptions.Path);
|
|
||||||
services.Configure<TaskOptions>(_configuration.GetSection(TaskOptions.Path));
|
services.Configure<TaskOptions>(_configuration.GetSection(TaskOptions.Path));
|
||||||
services.AddConfiguration<TaskOptions>(TaskOptions.Path);
|
|
||||||
services.Configure<MediaOptions>(_configuration.GetSection(MediaOptions.Path));
|
services.Configure<MediaOptions>(_configuration.GetSection(MediaOptions.Path));
|
||||||
services.AddConfiguration<MediaOptions>(MediaOptions.Path);
|
|
||||||
services.AddUntypedConfiguration("database");
|
|
||||||
services.AddUntypedConfiguration("logging");
|
|
||||||
|
|
||||||
services.AddControllers()
|
services.AddControllers()
|
||||||
.AddNewtonsoftJson(x =>
|
.AddNewtonsoftJson(x =>
|
||||||
@ -109,41 +157,18 @@ namespace Kyoo
|
|||||||
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
|
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
|
|
||||||
services.AddSingleton<IFileManager, FileManager>();
|
|
||||||
services.AddSingleton<ITranscoder, Transcoder>();
|
|
||||||
services.AddSingleton<IThumbnailsManager, ThumbnailsManager>();
|
|
||||||
services.AddSingleton<IProviderManager, ProviderManager>();
|
|
||||||
services.AddSingleton<ITaskManager, TaskManager>();
|
|
||||||
services.AddHostedService(x => x.GetService<ITaskManager>() as TaskManager);
|
services.AddHostedService(x => x.GetService<ITaskManager>() as TaskManager);
|
||||||
|
|
||||||
services.AddScoped<ILibraryManager, LibraryManager>();
|
|
||||||
|
|
||||||
if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes))
|
|
||||||
{
|
|
||||||
services.AddRepository<ILibraryRepository, LibraryRepository>();
|
|
||||||
services.AddRepository<ILibraryItemRepository, LibraryItemRepository>();
|
|
||||||
services.AddRepository<ICollectionRepository, CollectionRepository>();
|
|
||||||
services.AddRepository<IShowRepository, ShowRepository>();
|
|
||||||
services.AddRepository<ISeasonRepository, SeasonRepository>();
|
|
||||||
services.AddRepository<IEpisodeRepository, EpisodeRepository>();
|
|
||||||
services.AddRepository<ITrackRepository, TrackRepository>();
|
|
||||||
services.AddRepository<IPeopleRepository, PeopleRepository>();
|
|
||||||
services.AddRepository<IStudioRepository, StudioRepository>();
|
|
||||||
services.AddRepository<IGenreRepository, GenreRepository>();
|
|
||||||
services.AddRepository<IProviderRepository, ProviderRepository>();
|
|
||||||
services.AddRepository<IUserRepository, UserRepository>();
|
|
||||||
}
|
|
||||||
|
|
||||||
services.AddTask<Crawler>();
|
|
||||||
|
|
||||||
if (services.All(x => x.ServiceType != typeof(IPermissionValidator)))
|
|
||||||
services.AddSingleton<IPermissionValidator, PassthroughPermissionValidator>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ConfigureAspNet(IApplicationBuilder app)
|
public void ConfigureAspNet(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
|
ConfigurationManager.AddTyped<BasicOptions>(BasicOptions.Path);
|
||||||
|
ConfigurationManager.AddTyped<TaskOptions>(TaskOptions.Path);
|
||||||
|
ConfigurationManager.AddTyped<MediaOptions>(MediaOptions.Path);
|
||||||
|
ConfigurationManager.AddUntyped("database");
|
||||||
|
ConfigurationManager.AddUntyped("logging");
|
||||||
|
|
||||||
FileExtensionContentTypeProvider contentTypeProvider = new();
|
FileExtensionContentTypeProvider contentTypeProvider = new();
|
||||||
contentTypeProvider.Mappings[".data"] = "application/octet-stream";
|
contentTypeProvider.Mappings[".data"] = "application/octet-stream";
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
47
Kyoo/Helper.cs
Normal file
47
Kyoo/Helper.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Kyoo
|
||||||
|
{
|
||||||
|
public static class Helper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Inject services into the <see cref="InjectedAttribute"/> marked properties of the given object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The object to inject</param>
|
||||||
|
/// <param name="retrieve">The function used to retrieve services. (The function is called immediately)</param>
|
||||||
|
public static void InjectServices(object obj, [InstantHandle] Func<Type, object> retrieve)
|
||||||
|
{
|
||||||
|
IEnumerable<PropertyInfo> properties = obj.GetType().GetProperties()
|
||||||
|
.Where(x => x.GetCustomAttribute<InjectedAttribute>() != null)
|
||||||
|
.Where(x => x.CanWrite);
|
||||||
|
|
||||||
|
foreach (PropertyInfo property in properties)
|
||||||
|
property.SetValue(obj, retrieve(property.PropertyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An helper method to get json content from an http server. This is a temporary thing and will probably be
|
||||||
|
/// replaced by a call to the function of the same name in the <c>System.Net.Http.Json</c> namespace when .net6
|
||||||
|
/// gets released.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The http server to use.</param>
|
||||||
|
/// <param name="url">The url to retrieve</param>
|
||||||
|
/// <typeparam name="T">The type of object to convert</typeparam>
|
||||||
|
/// <returns>A T representing the json contained at the given url.</returns>
|
||||||
|
public static async Task<T> GetFromJsonAsync<T>(this HttpClient client, string url)
|
||||||
|
{
|
||||||
|
HttpResponseMessage ret = await client.GetAsync(url);
|
||||||
|
ret.EnsureSuccessStatusCode();
|
||||||
|
string content = await ret.Content.ReadAsStringAsync();
|
||||||
|
return JsonConvert.DeserializeObject<T>(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,8 +32,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
||||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
||||||
|
<PackageReference Include="Autofac" Version="6.2.0" />
|
||||||
|
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" />
|
||||||
|
<PackageReference Include="Autofac.Extras.AttributeMetadata" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.17" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.17" />
|
||||||
|
71
Kyoo/Models/FileExtensions.cs
Normal file
71
Kyoo/Models/FileExtensions.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Kyoo.Models.Watch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A static class allowing one to identify files extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class FileExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of known video extensions
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ImmutableArray<string> VideoExtensions = ImmutableArray.Create(
|
||||||
|
".webm",
|
||||||
|
".mkv",
|
||||||
|
".flv",
|
||||||
|
".vob",
|
||||||
|
".ogg",
|
||||||
|
".ogv",
|
||||||
|
".avi",
|
||||||
|
".mts",
|
||||||
|
".m2ts",
|
||||||
|
".ts",
|
||||||
|
".mov",
|
||||||
|
".qt",
|
||||||
|
".asf",
|
||||||
|
".mp4",
|
||||||
|
".m4p",
|
||||||
|
".m4v",
|
||||||
|
".mpg",
|
||||||
|
".mp2",
|
||||||
|
".mpeg",
|
||||||
|
".mpe",
|
||||||
|
".mpv",
|
||||||
|
".m2v",
|
||||||
|
".3gp",
|
||||||
|
".3g2"
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a file represent a video file (only by checking the extension of the file)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">The path of the file to check</param>
|
||||||
|
/// <returns><c>true</c> if the file is a video file, <c>false</c> otherwise.</returns>
|
||||||
|
public static bool IsVideo(string filePath)
|
||||||
|
{
|
||||||
|
return VideoExtensions.Contains(Path.GetExtension(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The dictionary of known subtitles extensions and the name of the subtitle codec.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ImmutableDictionary<string, string> SubtitleExtensions = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{".ass", "ass"},
|
||||||
|
{".str", "subrip"}
|
||||||
|
}.ToImmutableDictionary();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a file represent a subtitle file (only by checking the extension of the file)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">The path of the file to check</param>
|
||||||
|
/// <returns><c>true</c> if the file is a subtitle file, <c>false</c> otherwise.</returns>
|
||||||
|
public static bool IsSubtitle(string filePath)
|
||||||
|
{
|
||||||
|
return SubtitleExtensions.ContainsKey(Path.GetExtension(filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Kyoo.Models
|
|
||||||
{
|
|
||||||
public class LazyDi<T> : Lazy<T>
|
|
||||||
{
|
|
||||||
public LazyDi(IServiceProvider provider)
|
|
||||||
: base(provider.GetRequiredService<T>)
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,11 +13,11 @@ namespace Kyoo.Models.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A regex for episodes
|
/// A regex for episodes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Regex { get; set; }
|
public string[] Regex { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A regex for subtitles
|
/// A regex for subtitles
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SubtitleRegex { get; set; }
|
public string[] SubtitleRegex { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,12 +2,14 @@ using System;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Autofac;
|
||||||
|
using Autofac.Extensions.DependencyInjection;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Kyoo
|
namespace Kyoo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -30,6 +32,8 @@ namespace Kyoo
|
|||||||
if (!File.Exists("./settings.json"))
|
if (!File.Exists("./settings.json"))
|
||||||
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json");
|
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json");
|
||||||
|
|
||||||
|
IWebHostBuilder builder = CreateWebHostBuilder(args);
|
||||||
|
|
||||||
bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch
|
bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"d" => true,
|
"d" => true,
|
||||||
@ -43,18 +47,21 @@ namespace Kyoo
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (debug == null && Environment.GetEnvironmentVariable("ENVIRONMENT") != null)
|
if (debug == null && Environment.GetEnvironmentVariable("ENVIRONMENT") != null)
|
||||||
Console.WriteLine($"Invalid ENVIRONMENT variable. Supported values are \"debug\" and \"prod\". Ignoring...");
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Invalid ENVIRONMENT variable. Supported values are \"debug\" and \"prod\". Ignoring...");
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
debug ??= true;
|
debug ??= true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Console.WriteLine($"Running as {Environment.UserName}.");
|
|
||||||
IWebHostBuilder builder = CreateWebHostBuilder(args);
|
|
||||||
if (debug != null)
|
if (debug != null)
|
||||||
builder = builder.UseEnvironment(debug == true ? "Development" : "Production");
|
builder = builder.UseEnvironment(debug == true ? "Development" : "Production");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"Running as {Environment.UserName}.");
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -86,6 +93,11 @@ namespace Kyoo
|
|||||||
IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build();
|
IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build();
|
||||||
|
|
||||||
return new WebHostBuilder()
|
return new WebHostBuilder()
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
AutofacServiceProviderFactory factory = new();
|
||||||
|
x.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<ContainerBuilder>>(factory));
|
||||||
|
})
|
||||||
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
|
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
|
||||||
.UseConfiguration(configuration)
|
.UseConfiguration(configuration)
|
||||||
.ConfigureAppConfiguration(x => SetupConfig(x, args))
|
.ConfigureAppConfiguration(x => SetupConfig(x, args))
|
||||||
@ -99,12 +111,6 @@ namespace Kyoo
|
|||||||
.AddDebug()
|
.AddDebug()
|
||||||
.AddEventSourceLogger();
|
.AddEventSourceLogger();
|
||||||
})
|
})
|
||||||
.UseDefaultServiceProvider((context, options) =>
|
|
||||||
{
|
|
||||||
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
|
|
||||||
if (context.HostingEnvironment.IsDevelopment())
|
|
||||||
StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration);
|
|
||||||
})
|
|
||||||
.ConfigureServices(x => x.AddRouting())
|
.ConfigureServices(x => x.AddRouting())
|
||||||
.UseKestrel(options => { options.AddServerHeader = false; })
|
.UseKestrel(options => { options.AddServerHeader = false; })
|
||||||
.UseIIS()
|
.UseIIS()
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Autofac;
|
||||||
|
using Autofac.Extras.AttributeMetadata;
|
||||||
using Kyoo.Authentication;
|
using Kyoo.Authentication;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models;
|
|
||||||
using Kyoo.Models.Options;
|
using Kyoo.Models.Options;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.Tasks;
|
using Kyoo.Tasks;
|
||||||
@ -71,19 +72,23 @@ namespace Kyoo
|
|||||||
|
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
|
|
||||||
services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>));
|
|
||||||
|
|
||||||
services.AddSingleton(_plugins);
|
|
||||||
services.AddTask<PluginInitializer>();
|
|
||||||
_plugins.ConfigureServices(services);
|
_plugins.ConfigureServices(services);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ConfigureContainer(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
builder.RegisterModule<AttributedMetadataModule>();
|
||||||
|
builder.RegisterInstance(_plugins).As<IPluginManager>().ExternallyOwned();
|
||||||
|
builder.RegisterTask<PluginInitializer>();
|
||||||
|
_plugins.ConfigureContainer(builder);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configure the asp net host.
|
/// Configure the asp net host.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="app">The asp net host to configure</param>
|
/// <param name="app">The asp net host to configure</param>
|
||||||
/// <param name="env">The host environment (is the app in development mode?)</param>
|
/// <param name="env">The host environment (is the app in development mode?)</param>
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
@ -111,6 +116,8 @@ namespace Kyoo
|
|||||||
});
|
});
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
if (_plugins is PluginManager manager)
|
||||||
|
manager.SetProvider(provider);
|
||||||
_plugins.ConfigureAspnet(app);
|
_plugins.ConfigureAspnet(app);
|
||||||
|
|
||||||
app.UseSpa(spa =>
|
app.UseSpa(spa =>
|
||||||
|
@ -1,36 +1,60 @@
|
|||||||
using System;
|
using System;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Common.Models.Attributes;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models.Attributes;
|
using Kyoo.Models.Watch;
|
||||||
using Kyoo.Models.Exceptions;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Kyoo.Tasks
|
namespace Kyoo.Tasks
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A task to add new video files.
|
||||||
|
/// </summary>
|
||||||
|
[TaskMetadata("scan", "Scan libraries", "Scan your libraries and load data for new shows.", RunOnStartup = true)]
|
||||||
public class Crawler : ITask
|
public class Crawler : ITask
|
||||||
{
|
{
|
||||||
public string Slug => "scan";
|
/// <summary>
|
||||||
public string Name => "Scan libraries";
|
/// The library manager used to get libraries and providers to use.
|
||||||
public string Description => "Scan your libraries, load data for new shows and remove shows that don't exist anymore.";
|
/// </summary>
|
||||||
public string HelpMessage => "Reloading all libraries is a long process and may take up to 24 hours if it is the first scan in a while.";
|
private readonly ILibraryManager _libraryManager;
|
||||||
public bool RunOnStartup => true;
|
/// <summary>
|
||||||
public int Priority => 0;
|
/// The file manager used walk inside directories and check they existences.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
/// <summary>
|
||||||
|
/// A task manager used to create sub tasks for each episode to add to the database.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ITaskManager _taskManager;
|
||||||
|
/// <summary>
|
||||||
|
/// The logger used to inform the current status to the console.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<Crawler> _logger;
|
||||||
|
|
||||||
[Injected] public IServiceProvider ServiceProvider { private get; set; }
|
/// <summary>
|
||||||
[Injected] public IThumbnailsManager ThumbnailsManager { private get; set; }
|
/// Create a new <see cref="Crawler"/>.
|
||||||
[Injected] public IProviderManager MetadataProvider { private get; set; }
|
/// </summary>
|
||||||
[Injected] public ITranscoder Transcoder { private get; set; }
|
/// <param name="libraryManager">The library manager to retrieve existing episodes/library/tracks</param>
|
||||||
[Injected] public IConfiguration Config { private get; set; }
|
/// <param name="fileSystem">The file system to glob files</param>
|
||||||
|
/// <param name="taskManager">The task manager used to start <see cref="RegisterEpisode"/>.</param>
|
||||||
|
/// <param name="logger">The logger used print messages.</param>
|
||||||
|
public Crawler(ILibraryManager libraryManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ITaskManager taskManager,
|
||||||
|
ILogger<Crawler> logger)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_taskManager = taskManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
private int _parallelTasks;
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TaskParameters GetParameters()
|
public TaskParameters GetParameters()
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
@ -39,397 +63,108 @@ namespace Kyoo.Tasks
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? Progress()
|
/// <inheritdoc />
|
||||||
|
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// TODO implement this later.
|
string argument = arguments["slug"].As<string>();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Run(TaskParameters parameters,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
string argument = parameters["slug"].As<string>();
|
|
||||||
|
|
||||||
_parallelTasks = Config.GetValue<int>("parallelTasks");
|
|
||||||
if (_parallelTasks <= 0)
|
|
||||||
_parallelTasks = 30;
|
|
||||||
|
|
||||||
using IServiceScope serviceScope = ServiceProvider.CreateScope();
|
|
||||||
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
|
|
||||||
|
|
||||||
foreach (Show show in await libraryManager!.GetAll<Show>())
|
|
||||||
if (!Directory.Exists(show.Path))
|
|
||||||
await libraryManager.Delete(show);
|
|
||||||
|
|
||||||
ICollection<Episode> episodes = await libraryManager.GetAll<Episode>();
|
|
||||||
foreach (Episode episode in episodes)
|
|
||||||
if (!File.Exists(episode.Path))
|
|
||||||
await libraryManager.Delete(episode);
|
|
||||||
|
|
||||||
ICollection<Track> tracks = await libraryManager.GetAll<Track>();
|
|
||||||
foreach (Track track in tracks)
|
|
||||||
if (!File.Exists(track.Path))
|
|
||||||
await libraryManager.Delete(track);
|
|
||||||
|
|
||||||
ICollection<Library> libraries = argument == null
|
ICollection<Library> libraries = argument == null
|
||||||
? await libraryManager.GetAll<Library>()
|
? await _libraryManager.GetAll<Library>()
|
||||||
: new [] { await libraryManager.GetOrDefault<Library>(argument)};
|
: new [] { await _libraryManager.GetOrDefault<Library>(argument)};
|
||||||
|
|
||||||
if (argument != null && libraries.First() == null)
|
if (argument != null && libraries.First() == null)
|
||||||
throw new ArgumentException($"No library found with the name {argument}");
|
throw new ArgumentException($"No library found with the name {argument}");
|
||||||
|
|
||||||
foreach (Library library in libraries)
|
foreach (Library library in libraries)
|
||||||
await libraryManager.Load(library, x => x.Providers);
|
await _libraryManager.Load(library, x => x.Providers);
|
||||||
|
|
||||||
|
progress.Report(0);
|
||||||
|
float percent = 0;
|
||||||
|
|
||||||
|
ICollection<Episode> episodes = await _libraryManager.GetAll<Episode>();
|
||||||
|
ICollection<Track> tracks = await _libraryManager.GetAll<Track>();
|
||||||
foreach (Library library in libraries)
|
foreach (Library library in libraries)
|
||||||
await Scan(library, episodes, tracks, cancellationToken);
|
{
|
||||||
Console.WriteLine("Scan finished!");
|
IProgress<float> reporter = new Progress<float>(x =>
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
progress.Report(percent + x / libraries.Count);
|
||||||
|
});
|
||||||
|
await Scan(library, episodes, tracks, reporter, cancellationToken);
|
||||||
|
percent += 100f / libraries.Count;
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Scan(Library library, IEnumerable<Episode> episodes, IEnumerable<Track> tracks, CancellationToken cancellationToken)
|
progress.Report(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Scan(Library library,
|
||||||
|
IEnumerable<Episode> episodes,
|
||||||
|
IEnumerable<Track> tracks,
|
||||||
|
IProgress<float> progress,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Scanning library {library.Name} at {string.Join(", ", library.Paths)}.");
|
_logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths);
|
||||||
foreach (string path in library.Paths)
|
foreach (string path in library.Paths)
|
||||||
{
|
{
|
||||||
|
ICollection<string> files = await _fileSystem.ListFiles(path, SearchOption.AllDirectories);
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
string[] files;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
|
|
||||||
}
|
|
||||||
catch (DirectoryNotFoundException)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"The library's directory {path} could not be found (library slug: {library.Slug})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
catch (PathTooLongException)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"The library's directory {path} is too long for this system. (library slug: {library.Slug})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"The library's directory {path} is invalid. (library slug: {library.Slug})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"{ex.Message} (library slug: {library.Slug})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// We try to group episodes by shows to register one episode of each show first.
|
||||||
|
// This speeds up the scan process because further episodes of a show are registered when all metadata
|
||||||
|
// of the show has already been fetched.
|
||||||
List<IGrouping<string, string>> shows = files
|
List<IGrouping<string, string>> shows = files
|
||||||
.Where(x => IsVideo(x) && episodes.All(y => y.Path != x))
|
.Where(FileExtensions.IsVideo)
|
||||||
|
.Where(x => episodes.All(y => y.Path != x))
|
||||||
.GroupBy(Path.GetDirectoryName)
|
.GroupBy(Path.GetDirectoryName)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// TODO If the library's path end with a /, the regex is broken.
|
|
||||||
IEnumerable<string> tasks = shows.Select(x => x.First());
|
|
||||||
foreach (string[] showTasks in tasks.BatchBy(_parallelTasks))
|
|
||||||
await Task.WhenAll(showTasks
|
|
||||||
.Select(x => RegisterFile(x, x.Substring(path.Length), library, cancellationToken)));
|
|
||||||
|
|
||||||
tasks = shows.SelectMany(x => x.Skip(1));
|
string[] paths = shows.Select(x => x.First())
|
||||||
foreach (string[] episodeTasks in tasks.BatchBy(_parallelTasks * 2))
|
.Concat(shows.SelectMany(x => x.Skip(1)))
|
||||||
await Task.WhenAll(episodeTasks
|
|
||||||
.Select(x => RegisterFile(x, x.Substring(path.Length), library, cancellationToken)));
|
|
||||||
|
|
||||||
await Task.WhenAll(files.Where(x => IsSubtitle(x) && tracks.All(y => y.Path != x))
|
|
||||||
.Select(x => RegisterExternalSubtitle(x, cancellationToken)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RegisterExternalSubtitle(string path, CancellationToken token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles"))
|
|
||||||
return;
|
|
||||||
using IServiceScope serviceScope = ServiceProvider.CreateScope();
|
|
||||||
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
|
|
||||||
|
|
||||||
string patern = Config.GetValue<string>("subtitleRegex");
|
|
||||||
Regex regex = new(patern, RegexOptions.IgnoreCase);
|
|
||||||
Match match = regex.Match(path);
|
|
||||||
|
|
||||||
if (!match.Success)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string episodePath = match.Groups["Episode"].Value;
|
|
||||||
Episode episode = await libraryManager!.Get<Episode>(x => x.Path.StartsWith(episodePath));
|
|
||||||
Track track = new()
|
|
||||||
{
|
|
||||||
Type = StreamType.Subtitle,
|
|
||||||
Language = match.Groups["Language"].Value,
|
|
||||||
IsDefault = match.Groups["Default"].Value.Length > 0,
|
|
||||||
IsForced = match.Groups["Forced"].Value.Length > 0,
|
|
||||||
Codec = SubtitleExtensions[Path.GetExtension(path)],
|
|
||||||
IsExternal = true,
|
|
||||||
Path = path,
|
|
||||||
Episode = episode
|
|
||||||
};
|
|
||||||
|
|
||||||
await libraryManager.Create(track);
|
|
||||||
Console.WriteLine($"Registering subtitle at: {path}.");
|
|
||||||
}
|
|
||||||
catch (ItemNotFoundException)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"Unknown error while registering subtitle: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using IServiceScope serviceScope = ServiceProvider.CreateScope();
|
|
||||||
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
|
|
||||||
|
|
||||||
string patern = Config.GetValue<string>("regex");
|
|
||||||
Regex regex = new(patern, RegexOptions.IgnoreCase);
|
|
||||||
Match match = regex.Match(relativePath);
|
|
||||||
|
|
||||||
if (!match.Success)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"The episode at {path} does not match the episode's regex.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string showPath = Path.GetDirectoryName(path);
|
|
||||||
string collectionName = match.Groups["Collection"].Value;
|
|
||||||
string showName = match.Groups["Show"].Value;
|
|
||||||
int? seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : null;
|
|
||||||
int? episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : null;
|
|
||||||
int? absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : null;
|
|
||||||
|
|
||||||
Collection collection = await GetCollection(libraryManager, collectionName, library);
|
|
||||||
bool isMovie = seasonNumber == null && episodeNumber == null && absoluteNumber == null;
|
|
||||||
Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
|
|
||||||
if (isMovie)
|
|
||||||
await libraryManager!.Create(await GetMovie(show, path));
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Season season = seasonNumber != null
|
|
||||||
? await GetSeason(libraryManager, show, seasonNumber.Value, library)
|
|
||||||
: null;
|
|
||||||
Episode episode = await GetEpisode(libraryManager,
|
|
||||||
show,
|
|
||||||
season,
|
|
||||||
episodeNumber,
|
|
||||||
absoluteNumber,
|
|
||||||
path,
|
|
||||||
library);
|
|
||||||
await libraryManager!.Create(episode);
|
|
||||||
}
|
|
||||||
|
|
||||||
await libraryManager.AddShowLink(show, library, collection);
|
|
||||||
Console.WriteLine($"Episode at {path} registered.");
|
|
||||||
}
|
|
||||||
catch (DuplicatedItemException ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"{path}: {ex.Message}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"Unknown exception thrown while registering episode at {path}." +
|
|
||||||
$"\nException: {ex.Message}" +
|
|
||||||
$"\n{ex.StackTrace}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Collection> GetCollection(ILibraryManager libraryManager,
|
|
||||||
string collectionName,
|
|
||||||
Library library)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(collectionName))
|
|
||||||
return null;
|
|
||||||
Collection collection = await libraryManager.GetOrDefault<Collection>(Utility.ToSlug(collectionName));
|
|
||||||
if (collection != null)
|
|
||||||
return collection;
|
|
||||||
collection = await MetadataProvider.GetCollectionFromName(collectionName, library);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await libraryManager.Create(collection);
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
catch (DuplicatedItemException)
|
|
||||||
{
|
|
||||||
return await libraryManager.GetOrDefault<Collection>(collection.Slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Show> GetShow(ILibraryManager libraryManager,
|
|
||||||
string showTitle,
|
|
||||||
string showPath,
|
|
||||||
bool isMovie,
|
|
||||||
Library library)
|
|
||||||
{
|
|
||||||
Show old = await libraryManager.GetOrDefault<Show>(x => x.Path == showPath);
|
|
||||||
if (old != null)
|
|
||||||
{
|
|
||||||
await libraryManager.Load(old, x => x.ExternalIDs);
|
|
||||||
return old;
|
|
||||||
}
|
|
||||||
Show show = await MetadataProvider.SearchShow(showTitle, isMovie, library);
|
|
||||||
show.Path = showPath;
|
|
||||||
show.People = await MetadataProvider.GetPeople(show, library);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
show = await libraryManager.Create(show);
|
|
||||||
}
|
|
||||||
catch (DuplicatedItemException)
|
|
||||||
{
|
|
||||||
old = await libraryManager.GetOrDefault<Show>(show.Slug);
|
|
||||||
if (old != null && old.Path == showPath)
|
|
||||||
{
|
|
||||||
await libraryManager.Load(old, x => x.ExternalIDs);
|
|
||||||
return old;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show.StartAir != null)
|
|
||||||
{
|
|
||||||
show.Slug += $"-{show.StartAir.Value.Year}";
|
|
||||||
await libraryManager.Create(show);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
await ThumbnailsManager.Validate(show);
|
|
||||||
return show;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Season> GetSeason(ILibraryManager libraryManager,
|
|
||||||
Show show,
|
|
||||||
int seasonNumber,
|
|
||||||
Library library)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Season season = await libraryManager.Get(show.Slug, seasonNumber);
|
|
||||||
season.Show = show;
|
|
||||||
return season;
|
|
||||||
}
|
|
||||||
catch (ItemNotFoundException)
|
|
||||||
{
|
|
||||||
Season season = await MetadataProvider.GetSeason(show, seasonNumber, library);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await libraryManager.Create(season);
|
|
||||||
await ThumbnailsManager.Validate(season);
|
|
||||||
}
|
|
||||||
catch (DuplicatedItemException)
|
|
||||||
{
|
|
||||||
season = await libraryManager.Get(show.Slug, seasonNumber);
|
|
||||||
}
|
|
||||||
season.Show = show;
|
|
||||||
return season;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Episode> GetEpisode(ILibraryManager libraryManager,
|
|
||||||
Show show,
|
|
||||||
Season season,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? absoluteNumber,
|
|
||||||
string episodePath,
|
|
||||||
Library library)
|
|
||||||
{
|
|
||||||
Episode episode = await MetadataProvider.GetEpisode(show,
|
|
||||||
episodePath,
|
|
||||||
season?.SeasonNumber,
|
|
||||||
episodeNumber,
|
|
||||||
absoluteNumber,
|
|
||||||
library);
|
|
||||||
|
|
||||||
if (episode.SeasonNumber != null)
|
|
||||||
{
|
|
||||||
season ??= await GetSeason(libraryManager, show, episode.SeasonNumber.Value, library);
|
|
||||||
episode.Season = season;
|
|
||||||
episode.SeasonID = season?.ID;
|
|
||||||
}
|
|
||||||
await ThumbnailsManager.Validate(episode);
|
|
||||||
await GetTracks(episode);
|
|
||||||
return episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Episode> GetMovie(Show show, string episodePath)
|
|
||||||
{
|
|
||||||
Episode episode = new()
|
|
||||||
{
|
|
||||||
Title = show.Title,
|
|
||||||
Path = episodePath,
|
|
||||||
Show = show,
|
|
||||||
ShowID = show.ID,
|
|
||||||
ShowSlug = show.Slug
|
|
||||||
};
|
|
||||||
episode.Tracks = await GetTracks(episode);
|
|
||||||
return episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ICollection<Track>> GetTracks(Episode episode)
|
|
||||||
{
|
|
||||||
episode.Tracks = (await Transcoder.ExtractInfos(episode, false))
|
|
||||||
.Where(x => x.Type != StreamType.Attachment)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
return episode.Tracks;
|
float percent = 0;
|
||||||
|
IProgress<float> reporter = new Progress<float>(x =>
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
progress.Report((percent + x / paths.Length - 10) / library.Paths.Length);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (string episodePath in paths)
|
||||||
|
{
|
||||||
|
_taskManager.StartTask<RegisterEpisode>(reporter, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["path"] = episodePath,
|
||||||
|
["library"] = library
|
||||||
|
}, cancellationToken);
|
||||||
|
percent += 100f / paths.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string[] VideoExtensions =
|
|
||||||
{
|
|
||||||
".webm",
|
|
||||||
".mkv",
|
|
||||||
".flv",
|
|
||||||
".vob",
|
|
||||||
".ogg",
|
|
||||||
".ogv",
|
|
||||||
".avi",
|
|
||||||
".mts",
|
|
||||||
".m2ts",
|
|
||||||
".ts",
|
|
||||||
".mov",
|
|
||||||
".qt",
|
|
||||||
".asf",
|
|
||||||
".mp4",
|
|
||||||
".m4p",
|
|
||||||
".m4v",
|
|
||||||
".mpg",
|
|
||||||
".mp2",
|
|
||||||
".mpeg",
|
|
||||||
".mpe",
|
|
||||||
".mpv",
|
|
||||||
".m2v",
|
|
||||||
".3gp",
|
|
||||||
".3g2"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool IsVideo(string filePath)
|
string[] subtitles = files
|
||||||
|
.Where(FileExtensions.IsSubtitle)
|
||||||
|
.Where(x => !x.Contains("/Extra/"))
|
||||||
|
.Where(x => tracks.All(y => y.Path != x))
|
||||||
|
.ToArray();
|
||||||
|
percent = 0;
|
||||||
|
reporter = new Progress<float>(x =>
|
||||||
{
|
{
|
||||||
return VideoExtensions.Contains(Path.GetExtension(filePath));
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
progress.Report((90 + (percent + x / subtitles.Length)) / library.Paths.Length);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (string trackPath in subtitles)
|
||||||
|
{
|
||||||
|
_taskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["path"] = trackPath
|
||||||
|
}, cancellationToken);
|
||||||
|
percent += 100f / subtitles.Length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> SubtitleExtensions = new()
|
|
||||||
{
|
|
||||||
{".ass", "ass"},
|
|
||||||
{".str", "subrip"}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool IsSubtitle(string filePath)
|
|
||||||
{
|
|
||||||
return SubtitleExtensions.ContainsKey(Path.GetExtension(filePath));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user