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.Views;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Models.Permissions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -64,6 +65,11 @@ namespace Kyoo.Authentication
|
||||
/// The environment information to check if the app runs in debug mode
|
||||
/// </summary>
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
/// <summary>
|
||||
/// The configuration manager used to register typed/untyped implementations.
|
||||
/// </summary>
|
||||
[Injected] public IConfigurationManager ConfigurationManager { private get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -98,9 +104,7 @@ namespace Kyoo.Authentication
|
||||
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
|
||||
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
|
||||
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
|
||||
services.AddConfiguration<AuthenticationOption>(AuthenticationOption.Path);
|
||||
|
||||
|
||||
|
||||
List<Client> clients = new();
|
||||
_configuration.GetSection("authentication:clients").Bind(clients);
|
||||
CertificateOption certificateOptions = new();
|
||||
@ -139,6 +143,8 @@ namespace Kyoo.Authentication
|
||||
/// <inheritdoc />
|
||||
public void ConfigureAspNet(IApplicationBuilder app)
|
||||
{
|
||||
ConfigurationManager.AddTyped<AuthenticationOption>(AuthenticationOption.Path);
|
||||
|
||||
app.UseCookiePolicy(new CookiePolicyOptions
|
||||
{
|
||||
MinimumSameSitePolicy = SameSiteMode.Strict
|
||||
|
@ -36,7 +36,7 @@ namespace Kyoo.Authentication.Views
|
||||
/// <summary>
|
||||
/// A file manager to send profile pictures
|
||||
/// </summary>
|
||||
private readonly IFileManager _files;
|
||||
private readonly IFileSystem _files;
|
||||
/// <summary>
|
||||
/// Options about authentication. Those options are monitored and reloads are supported.
|
||||
/// </summary>
|
||||
@ -50,7 +50,7 @@ namespace Kyoo.Authentication.Views
|
||||
/// <param name="files">A file manager to send profile pictures</param>
|
||||
/// <param name="options">Authentication options (this may be hot reloaded)</param>
|
||||
public AccountApi(IUserRepository users,
|
||||
IFileManager files,
|
||||
IFileSystem files,
|
||||
IOptions<AuthenticationOption> options)
|
||||
{
|
||||
_users = users;
|
||||
@ -205,8 +205,8 @@ namespace Kyoo.Authentication.Views
|
||||
user.Username = data.Username;
|
||||
if (data.Picture?.Length > 0)
|
||||
{
|
||||
string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
||||
await using Stream file = _files.NewFile(path);
|
||||
string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
|
||||
await using Stream file = await _files.NewFile(path);
|
||||
await data.Picture.CopyToAsync(file);
|
||||
}
|
||||
return await _users.Edit(user, false);
|
||||
|
@ -12,6 +12,21 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
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>
|
||||
/// Get the value of a setting using it's path.
|
||||
/// </summary>
|
||||
|
@ -10,7 +10,7 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// A service to abstract the file system to allow custom file systems (like distant file systems or external providers)
|
||||
/// </summary>
|
||||
public interface IFileManager
|
||||
public interface IFileSystem
|
||||
{
|
||||
// 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>
|
||||
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
|
||||
/// <returns>A reader to read the file.</returns>
|
||||
public Stream GetReader([NotNull] string path);
|
||||
public Task<Stream> GetReader([NotNull] string path);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new file at <paramref name="path"></paramref>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the new file.</param>
|
||||
/// <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>
|
||||
/// List files in a directory.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public Task<ICollection<string>> ListFiles([NotNull] string path);
|
||||
public Task<ICollection<string>> ListFiles([NotNull] string path,
|
||||
SearchOption options = SearchOption.TopDirectoryOnly);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a file exists at the given path.
|
||||
@ -71,24 +87,6 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="show">The show to proceed</param>
|
||||
/// <returns>The extra directory of the show</returns>
|
||||
public string GetExtraDirectory(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);
|
||||
public string GetExtraDirectory([NotNull] Show show);
|
||||
}
|
||||
}
|
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 System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface to automatically retrieve metadata from external providers.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
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);
|
||||
Task<ICollection<Show>> SearchShows(string showName, bool isMovie);
|
||||
Task<ICollection<PeopleRole>> GetPeople(Show show);
|
||||
/// <summary>
|
||||
/// Search for a specific type of items with a given query.
|
||||
/// </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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Autofac;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -10,8 +11,10 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// A common interface used to discord plugins
|
||||
/// </summary>
|
||||
/// <remarks>You can inject services in the IPlugin constructor.
|
||||
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.</remarks>
|
||||
/// <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)]
|
||||
public interface IPlugin
|
||||
{
|
||||
@ -55,6 +58,15 @@ namespace Kyoo.Controllers
|
||||
/// </remarks>
|
||||
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>
|
||||
/// A configure method that will be run on plugin's startup.
|
||||
/// </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})"/>>
|
||||
/// You can't simply check on the service collection because some dependencies might be registered after your plugin.
|
||||
/// </param>
|
||||
void Configure(IServiceCollection services, ICollection<Type> availableTypes);
|
||||
void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An optional configuration step to allow a plugin to change asp net configurations.
|
||||
/// WARNING: This is only called on Kyoo's startup so you must restart the app to apply this changes.
|
||||
/// </summary>
|
||||
/// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param>
|
||||
void ConfigureAspNet(IApplicationBuilder app) {}
|
||||
|
||||
/// <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>
|
||||
void ConfigureAspNet(IApplicationBuilder app)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An optional function to execute and initialize your plugin.
|
||||
/// It can be used to initialize a database connection, fill initial data or anything.
|
||||
/// </summary>
|
||||
/// <param name="provider">A service provider to request services</param>
|
||||
void Initialize(IServiceProvider provider) {}
|
||||
void Initialize(IServiceProvider provider)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Autofac;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -42,7 +43,13 @@ namespace Kyoo.Controllers
|
||||
public void LoadPlugins(ICollection<IPlugin> plugins);
|
||||
|
||||
/// <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>
|
||||
/// <param name="services">The service collection to populate</param>
|
||||
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>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemNotNull]
|
||||
Task<T> Get(int id);
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug.
|
||||
@ -135,6 +136,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemNotNull]
|
||||
Task<T> Get(string slug);
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemNotNull]
|
||||
Task<T> Get(Expression<Func<T, bool>> where);
|
||||
|
||||
/// <summary>
|
||||
@ -149,18 +152,21 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource</param>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemCanBeNull]
|
||||
Task<T> GetOrDefault(int id);
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemCanBeNull]
|
||||
Task<T> GetOrDefault(string slug);
|
||||
/// <summary>
|
||||
/// Get the first resource that match the predicate or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="where">A predicate to filter the resource.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
[ItemCanBeNull]
|
||||
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
|
||||
|
||||
/// <summary>
|
||||
@ -168,6 +174,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="query">The query string.</param>
|
||||
/// <returns>A list of resources found</returns>
|
||||
[ItemNotNull]
|
||||
Task<ICollection<T>> Search(string query);
|
||||
|
||||
/// <summary>
|
||||
@ -177,6 +184,7 @@ namespace Kyoo.Controllers
|
||||
/// <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>
|
||||
/// <returns>A list of resources that match every filters</returns>
|
||||
[ItemNotNull]
|
||||
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
|
||||
Sort<T> sort = 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="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>
|
||||
[ItemNotNull]
|
||||
Task<ICollection<T>> GetAll([Optional] Expression<Func<T, bool>> where,
|
||||
Expression<Func<T, object>> sort,
|
||||
Pagination limit = default
|
||||
@ -205,6 +214,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="obj">The item to register</param>
|
||||
/// <returns>The resource registers and completed by database's information (related items & so on)</returns>
|
||||
[ItemNotNull]
|
||||
Task<T> Create([NotNull] T obj);
|
||||
|
||||
/// <summary>
|
||||
@ -212,6 +222,7 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to create</param>
|
||||
/// <returns>The newly created item or the existing value if it existed.</returns>
|
||||
[ItemNotNull]
|
||||
Task<T> CreateIfNotExists([NotNull] T obj);
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
[ItemNotNull]
|
||||
Task<T> Edit([NotNull] T edited, bool resetOld);
|
||||
|
||||
/// <summary>
|
||||
|
@ -3,12 +3,15 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Models.Exceptions;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <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>
|
||||
/// <remarks>This struct will be used to generate the swagger documentation of the task.</remarks>
|
||||
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>
|
||||
/// Create a parameter's value to give to a task.
|
||||
/// </summary>
|
||||
@ -94,6 +115,17 @@ namespace Kyoo.Controllers
|
||||
/// <returns>The value of this parameter.</returns>
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -131,58 +163,38 @@ namespace Kyoo.Controllers
|
||||
public interface ITask
|
||||
{
|
||||
/// <summary>
|
||||
/// The slug of the task, used to start it.
|
||||
/// The list of parameters
|
||||
/// </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>
|
||||
/// 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; }
|
||||
/// <returns>
|
||||
/// All parameters that this task as. Every one of them will be given to the run function with a value.
|
||||
/// </returns>
|
||||
public TaskParameters GetParameters();
|
||||
|
||||
/// <summary>
|
||||
/// Start this task.
|
||||
/// </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.
|
||||
/// 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>
|
||||
/// 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 also will be removed after this method return (or throw) to prevent dangling services.
|
||||
/// </remarks>
|
||||
public Task Run(TaskParameters arguments, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// The list of parameters
|
||||
/// </summary>
|
||||
/// <returns>All parameters that this task as. Every one of them will be given to the run function with a value.</returns>
|
||||
public TaskParameters GetParameters();
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
/// <exception cref="TaskFailedException">
|
||||
/// An exception meaning that the task has failed for handled reasons like invalid arguments,
|
||||
/// invalid environment, missing plugins or failures not related to a default in the code.
|
||||
/// This exception allow the task to display a failure message to the end user while others exceptions
|
||||
/// will be displayed as unhandled exceptions and display a stack trace.
|
||||
/// </exception>
|
||||
public Task Run([NotNull] TaskParameters arguments,
|
||||
[NotNull] IProgress<float> progress,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Exceptions;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
@ -13,22 +16,67 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Start a new task (or queue it).
|
||||
/// </summary>
|
||||
/// <param name="taskSlug">The slug of the task to run</param>
|
||||
/// <param name="arguments">A list of arguments to pass to the task. An automatic conversion will be made if arguments to not fit.</param>
|
||||
/// <exception cref="ArgumentException">If the number of arguments is invalid or if an argument can't be converted.</exception>
|
||||
/// <exception cref="ItemNotFoundException">The task could not be found.</exception>
|
||||
void StartTask(string taskSlug, Dictionary<string, object> arguments = null);
|
||||
/// <param name="taskSlug">
|
||||
/// The slug of the task to run.
|
||||
/// </param>
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// Get all currently running tasks
|
||||
/// </summary>
|
||||
/// <returns>A list of currently running tasks.</returns>
|
||||
ICollection<ITask> GetRunningTasks();
|
||||
ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks();
|
||||
|
||||
/// <summary>
|
||||
/// Get all available tasks
|
||||
/// </summary>
|
||||
/// <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 JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Download images and retrieve the path of those images for a resource.
|
||||
/// </summary>
|
||||
public interface IThumbnailsManager
|
||||
{
|
||||
Task Validate(Show show, bool alwaysDownload = false);
|
||||
Task Validate(Season season, bool alwaysDownload = false);
|
||||
Task Validate(Episode episode, bool alwaysDownload = false);
|
||||
Task Validate(People actors, bool alwaysDownload = false);
|
||||
Task Validate(Provider actors, bool alwaysDownload = false);
|
||||
/// <summary>
|
||||
/// Download images of a specified item.
|
||||
/// If no images is available to download, do nothing and silently return.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
Task<string> GetShowBackdrop([NotNull] Show show);
|
||||
Task<string> GetSeasonPoster([NotNull] Season season);
|
||||
Task<string> GetEpisodeThumb([NotNull] Episode episode);
|
||||
Task<string> GetPeoplePoster([NotNull] People people);
|
||||
Task<string> GetProviderLogo([NotNull] Provider provider);
|
||||
/// <summary>
|
||||
/// Retrieve the local path of the poster of the given item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <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>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.2.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.1.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.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
|
||||
<PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>.
|
||||
/// It can also be used on <see cref="IPlugin"/> and it will be injected before calling <see cref="IPlugin.ConfigureAspNet"/>.
|
||||
/// It should only be used on <see cref="IPlugin"/> and it will be injected before
|
||||
/// calling <see cref="IPlugin.ConfigureAspNet"/>.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
[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.
|
||||
/// This can be disabled using the internal query flag.
|
||||
/// </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>
|
||||
/// The type of this item (ether a collection, a show or a movie).
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
@ -107,12 +108,12 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A reference of the first resource.
|
||||
/// </summary>
|
||||
public T1 First { get; set; }
|
||||
[SerializeIgnore] public T1 First { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the second resource.
|
||||
/// </summary>
|
||||
public T2 Second { get; set; }
|
||||
[SerializeIgnore] public T2 Second { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -19,6 +19,12 @@ namespace Kyoo.Models
|
||||
/// The URL of the resource on the external provider.
|
||||
/// </summary>
|
||||
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>
|
||||
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
|
||||
|
@ -20,9 +20,11 @@ namespace Kyoo.Models
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ShowSlug == null && Show == null)
|
||||
return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
if (ShowSlug != null || Show != null)
|
||||
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
return ShowID != 0
|
||||
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
|
||||
: null;
|
||||
}
|
||||
[UsedImplicitly] [NotNull] private set
|
||||
{
|
||||
@ -93,7 +95,7 @@ namespace Kyoo.Models
|
||||
public int? AbsoluteNumber { get; set; }
|
||||
|
||||
/// <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>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
@ -30,7 +31,7 @@ namespace Kyoo.Models
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
@ -42,10 +43,10 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status? Status { get; set; }
|
||||
public Status Status { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
|
||||
public string TrailerUrl { get; set; }
|
||||
@ -157,6 +158,7 @@ namespace Kyoo.Models
|
||||
/// <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>
|
||||
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
|
||||
[CanBeNull]
|
||||
public string GetID(string provider)
|
||||
{
|
||||
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
|
||||
@ -183,5 +185,5 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// The enum containing show's status.
|
||||
/// </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 index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
||||
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
|
||||
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||
string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
|
||||
return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||
}
|
||||
[UsedImplicitly] private set
|
||||
{
|
||||
@ -47,11 +47,13 @@ namespace Kyoo.Models
|
||||
|
||||
if (!match.Success)
|
||||
throw new ArgumentException("Invalid track slug. " +
|
||||
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
|
||||
"Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]");
|
||||
|
||||
EpisodeSlug = match.Groups["ep"].Value;
|
||||
Language = match.Groups["lang"].Value;
|
||||
TrackIndex = int.Parse(match.Groups["index"].Value);
|
||||
if (Language == "und")
|
||||
Language = null;
|
||||
TrackIndex = match.Groups["index"].Success ? int.Parse(match.Groups["index"].Value) : 0;
|
||||
IsForced = match.Groups["forced"].Success;
|
||||
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
||||
}
|
||||
@ -154,30 +156,17 @@ namespace Kyoo.Models
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="baseSlug">The slug to edit</param>
|
||||
/// <param name="type">The new type of this </param>
|
||||
/// <param name="language"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="forced"></param>
|
||||
/// <returns></returns>
|
||||
public static string EditSlug(string baseSlug,
|
||||
StreamType type = StreamType.Unknown,
|
||||
string language = null,
|
||||
int? index = null,
|
||||
bool? forced = null)
|
||||
public static string BuildSlug(string baseSlug,
|
||||
StreamType type)
|
||||
{
|
||||
Track track = new() {Slug = baseSlug};
|
||||
if (type != StreamType.Unknown)
|
||||
track.Type = type;
|
||||
if (language != null)
|
||||
track.Language = language;
|
||||
if (index != null)
|
||||
track.TrackIndex = index.Value;
|
||||
if (forced != null)
|
||||
track.IsForced = forced.Value;
|
||||
return track.Slug;
|
||||
return baseSlug.EndsWith($".{type}", StringComparison.InvariantCultureIgnoreCase)
|
||||
? baseSlug
|
||||
: $"{baseSlug}.{type.ToString().ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace Kyoo.Models
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
/// <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>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
@ -176,13 +176,16 @@ namespace Kyoo.Models
|
||||
return new WatchItem
|
||||
{
|
||||
EpisodeID = ep.ID,
|
||||
Slug = ep.Slug,
|
||||
ShowSlug = ep.Show.Slug,
|
||||
ShowTitle = ep.Show.Title,
|
||||
SeasonNumber = ep.SeasonNumber,
|
||||
EpisodeNumber = ep.EpisodeNumber,
|
||||
AbsoluteNumber = ep.AbsoluteNumber,
|
||||
Title = ep.Title,
|
||||
ReleaseDate = ep.ReleaseDate,
|
||||
Path = ep.Path,
|
||||
Container = PathIO.GetExtension(ep.Path)![1..],
|
||||
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
|
||||
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
|
||||
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
|
||||
|
@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Autofac;
|
||||
using Autofac.Builder;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo
|
||||
{
|
||||
@ -15,86 +13,63 @@ namespace Kyoo
|
||||
/// <summary>
|
||||
/// Register a new task to the container.
|
||||
/// </summary>
|
||||
/// <param name="services">The container</param>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The type of the task</typeparam>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IServiceCollection AddTask<T>(this IServiceCollection services)
|
||||
/// <returns>The registration builder of this new task. That can be used to edit the registration.</returns>
|
||||
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||
RegisterTask<T>(this ContainerBuilder builder)
|
||||
where T : class, ITask
|
||||
{
|
||||
services.AddSingleton<ITask, T>();
|
||||
return services;
|
||||
return builder.RegisterType<T>().As<ITask>();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Register a new repository to the container.
|
||||
/// </summary>
|
||||
/// <param name="services">The container</param>
|
||||
/// <param name="lifetime">The lifetime of the repository. The default is scoped.</param>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The type of the repository.</typeparam>
|
||||
/// <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>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IServiceCollection AddRepository<T>(this IServiceCollection services,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Scoped)
|
||||
public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||
RegisterRepository<T>(this ContainerBuilder builder)
|
||||
where T : IBaseRepository
|
||||
{
|
||||
Type repository = Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>));
|
||||
|
||||
if (repository != null)
|
||||
services.Add(ServiceDescriptor.Describe(repository, typeof(T), lifetime));
|
||||
services.Add(ServiceDescriptor.Describe(typeof(IBaseRepository), typeof(T), lifetime));
|
||||
return services;
|
||||
return builder.RegisterType<T>()
|
||||
.As<IBaseRepository>()
|
||||
.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>)))
|
||||
.InstancePerLifetimeScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new repository with a custom mapping to the container.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="lifetime">The lifetime of the repository. The default is scoped.</param>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The custom mapping you have for your repository.</typeparam>
|
||||
/// <typeparam name="T2">The type of the repository.</typeparam>
|
||||
/// <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>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IServiceCollection AddRepository<T, T2>(this IServiceCollection services,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Scoped)
|
||||
public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle>
|
||||
RegisterRepository<T, T2>(this ContainerBuilder builder)
|
||||
where T2 : IBaseRepository, T
|
||||
{
|
||||
services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime));
|
||||
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;
|
||||
return builder.RegisterRepository<T2>().As<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -5,6 +5,7 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo
|
||||
@ -111,7 +112,8 @@ namespace Kyoo
|
||||
/// <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>
|
||||
/// <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)
|
||||
return second;
|
||||
@ -127,9 +129,7 @@ namespace Kyoo
|
||||
{
|
||||
object oldValue = property.GetValue(first);
|
||||
object newValue = property.GetValue(second);
|
||||
object defaultValue = property.PropertyType.IsValueType
|
||||
? Activator.CreateInstance(property.PropertyType)
|
||||
: null;
|
||||
object defaultValue = property.PropertyType.GetClrDefault();
|
||||
|
||||
if (oldValue?.Equals(defaultValue) != false)
|
||||
property.SetValue(first, newValue);
|
||||
@ -139,11 +139,14 @@ namespace Kyoo
|
||||
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
|
||||
.GenericTypeArguments
|
||||
.First();
|
||||
Func<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
|
||||
? (x, y) => x.Slug == y.Slug
|
||||
: null;
|
||||
property.SetValue(first, Utility.RunGenericMethod<object>(
|
||||
typeof(Utility),
|
||||
typeof(Merger),
|
||||
nameof(MergeLists),
|
||||
enumerableType,
|
||||
oldValue, newValue, null));
|
||||
enumerableType,
|
||||
oldValue, newValue, equalityComparer));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ namespace Kyoo
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <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)
|
||||
return null;
|
||||
@ -182,15 +182,49 @@ namespace Kyoo
|
||||
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
|
||||
/// <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)
|
||||
.Where(x => x.Name == name)
|
||||
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
||||
.Where(x => x.GetParameters().Length == args.Length)
|
||||
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
|
||||
$"{args.Length} arguments and {generics.Length} generic " +
|
||||
$"types could not be found on {type.Name}."))
|
||||
.IfEmpty(() => throw new ArgumentException($"A method named {name} with " +
|
||||
$"{args.Length} arguments and {generics.Length} generic " +
|
||||
$"types could not be found on {type.Name}."))
|
||||
// TODO this won't work but I don't know why.
|
||||
// .Where(x =>
|
||||
// {
|
||||
@ -211,9 +245,34 @@ namespace Kyoo
|
||||
|
||||
if (methods.Length == 1)
|
||||
return methods[0];
|
||||
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
|
||||
throw new ArgumentException($"Multiple methods named {name} match the generics and parameters constraints.");
|
||||
}
|
||||
|
||||
/// <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>(
|
||||
[NotNull] Type owner,
|
||||
[NotNull] string methodName,
|
||||
@ -223,6 +282,34 @@ namespace Kyoo
|
||||
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>(
|
||||
[NotNull] Type owner,
|
||||
[NotNull] string methodName,
|
||||
@ -238,9 +325,34 @@ namespace Kyoo
|
||||
if (types.Length < 1)
|
||||
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
||||
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
||||
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
|
||||
return (T)method.MakeGenericMethod(types).Invoke(null, args.ToArray());
|
||||
}
|
||||
|
||||
/// <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>(
|
||||
[NotNull] object instance,
|
||||
[NotNull] string methodName,
|
||||
@ -250,6 +362,33 @@ namespace Kyoo
|
||||
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>(
|
||||
[NotNull] object instance,
|
||||
[NotNull] string methodName,
|
||||
@ -263,7 +402,7 @@ namespace Kyoo
|
||||
if (types == null || types.Length == 0)
|
||||
throw new ArgumentNullException(nameof(types));
|
||||
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
|
||||
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
|
||||
return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray());
|
||||
}
|
||||
|
||||
public static string ToQueryString(this Dictionary<string, string> query)
|
||||
|
@ -38,7 +38,7 @@ namespace Kyoo.CommonApi
|
||||
[PartialPermission(Kind.Read)]
|
||||
public virtual async Task<ActionResult<T>> Get(string slug)
|
||||
{
|
||||
T ret = await _repository.Get(slug);
|
||||
T ret = await _repository.GetOrDefault(slug);
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
return ret;
|
||||
|
@ -115,9 +115,10 @@ namespace Kyoo.Controllers
|
||||
|
||||
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 modifier = x.Groups[3].Value;
|
||||
|
||||
if (value == "HOST")
|
||||
return _host;
|
||||
@ -127,9 +128,22 @@ namespace Kyoo.Controllers
|
||||
.FirstOrDefault(y => y.Name == value);
|
||||
if (properties == null)
|
||||
return null;
|
||||
if (properties.GetValue(target) is string ret)
|
||||
return ret;
|
||||
throw new ArgumentException($"Invalid serializer replacement {value}");
|
||||
object objValue = properties.GetValue(target);
|
||||
if (objValue is not string ret)
|
||||
ret = objValue?.ToString();
|
||||
if (ret == null)
|
||||
throw new ArgumentException($"Invalid serializer replacement {value}");
|
||||
|
||||
foreach (char modification in modifier)
|
||||
{
|
||||
ret = modification switch
|
||||
{
|
||||
'l' => ret.ToLowerInvariant(),
|
||||
'u' => ret.ToUpperInvariant(),
|
||||
_ => throw new ArgumentException($"Invalid serializer modificator {modification}.")
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -208,7 +208,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
catch (DuplicatedItemException)
|
||||
{
|
||||
return await GetOrDefault(obj.Slug);
|
||||
return await Get(obj.Slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,22 +43,31 @@ namespace Kyoo.CommonApi
|
||||
PropertyInfo[] properties = type.GetProperties()
|
||||
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
|
||||
.ToArray();
|
||||
fields = fields.Select(x =>
|
||||
{
|
||||
string property = properties
|
||||
.FirstOrDefault(y => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
?.Name;
|
||||
if (property != null)
|
||||
return property;
|
||||
context.Result = new BadRequestObjectResult(new
|
||||
if (fields.Count == 1 && fields.Contains("all"))
|
||||
{
|
||||
fields = properties.Select(x => x.Name).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
fields = fields
|
||||
.Select(x =>
|
||||
{
|
||||
Error = $"{x} does not exist on {type.Name}."
|
||||
});
|
||||
return null;
|
||||
})
|
||||
.ToList();
|
||||
if (context.Result != null)
|
||||
return;
|
||||
string property = properties
|
||||
.FirstOrDefault(y
|
||||
=> string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
?.Name;
|
||||
if (property != null)
|
||||
return property;
|
||||
context.Result = new BadRequestObjectResult(new
|
||||
{
|
||||
Error = $"{x} does not exist on {type.Name}."
|
||||
});
|
||||
return null;
|
||||
})
|
||||
.ToList();
|
||||
if (context.Result != null)
|
||||
return;
|
||||
}
|
||||
}
|
||||
context.HttpContext.Items["fields"] = fields;
|
||||
base.OnActionExecuting(context);
|
||||
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
[DbContext(typeof(PostgresContext))]
|
||||
[Migration("20210627141933_Initial")]
|
||||
[Migration("20210723224326_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
||||
.HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
|
||||
.HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
|
||||
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.7")
|
||||
.HasAnnotation("ProductVersion", "5.0.8")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.ToTable("libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("start_air");
|
||||
|
||||
b.Property<Status?>("Status")
|
||||
b.Property<Status>("Status")
|
||||
.HasColumnType("status")
|
||||
.HasColumnName("status");
|
||||
|
||||
@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID")
|
||||
.HasConstraintName("fk_shows_studios_studio_id");
|
||||
.HasConstraintName("fk_shows_studios_studio_id")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
@ -12,7 +12,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
|
||||
.Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown")
|
||||
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
|
||||
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
@ -208,7 +208,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
path = 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),
|
||||
start_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
|
||||
{
|
||||
[DbContext(typeof(PostgresContext))]
|
||||
[Migration("20210627141941_Triggers")]
|
||||
[Migration("20210723224335_Triggers")]
|
||||
partial class Triggers
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -20,10 +20,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
||||
.HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
|
||||
.HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
|
||||
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.7")
|
||||
.HasAnnotation("ProductVersion", "5.0.8")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
@ -189,6 +189,51 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.ToTable("libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -621,7 +666,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("start_air");
|
||||
|
||||
b.Property<Status?>("Status")
|
||||
b.Property<Status>("Status")
|
||||
.HasColumnType("status")
|
||||
.HasColumnName("status");
|
||||
|
||||
@ -1078,7 +1123,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID")
|
||||
.HasConstraintName("fk_shows_studios_studio_id");
|
||||
.HasConstraintName("fk_shows_studios_studio_id")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
@ -91,7 +91,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
END,
|
||||
CASE (is_forced)
|
||||
WHEN false THEN ''
|
||||
ELSE '-forced'
|
||||
ELSE '.forced'
|
||||
END,
|
||||
'.', type
|
||||
) WHERE episode_id = NEW.id;
|
||||
@ -117,14 +117,14 @@ namespace Kyoo.Postgresql.Migrations
|
||||
END IF;
|
||||
NEW.slug := CONCAT(
|
||||
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
|
||||
'.', NEW.language,
|
||||
'.', COALESCE(NEW.language, 'und'),
|
||||
CASE (NEW.track_index)
|
||||
WHEN 0 THEN ''
|
||||
ELSE CONCAT('-', NEW.track_index)
|
||||
END,
|
||||
CASE (NEW.is_forced)
|
||||
WHEN false THEN ''
|
||||
ELSE '-forced'
|
||||
ELSE '.forced'
|
||||
END,
|
||||
'.', NEW.type
|
||||
);
|
@ -18,10 +18,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" })
|
||||
.HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
|
||||
.HasPostgresEnum(null, "status", new[] { "unknown", "finished", "airing", "planned" })
|
||||
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.7")
|
||||
.HasAnnotation("ProductVersion", "5.0.8")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
@ -187,6 +187,51 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.ToTable("libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -619,7 +664,7 @@ namespace Kyoo.Postgresql.Migrations
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("start_air");
|
||||
|
||||
b.Property<Status?>("Status")
|
||||
b.Property<Status>("Status")
|
||||
.HasColumnType("status")
|
||||
.HasColumnName("status");
|
||||
|
||||
@ -1076,7 +1121,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID")
|
||||
.HasConstraintName("fk_shows_studios_studio_id");
|
||||
.HasConstraintName("fk_shows_studios_studio_id")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Npgsql;
|
||||
|
||||
namespace Kyoo.Postgresql
|
||||
{
|
||||
@ -73,6 +74,10 @@ namespace Kyoo.Postgresql
|
||||
{
|
||||
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
|
||||
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
|
||||
{
|
||||
[DbContext(typeof(SqLiteContext))]
|
||||
[Migration("20210626141337_Initial")]
|
||||
[Migration("20210723224542_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.7");
|
||||
.HasAnnotation("ProductVersion", "5.0.8");
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
{
|
||||
@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.ToTable("Libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Status")
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
|
||||
{
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID");
|
||||
.HasForeignKey("StudioID")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
@ -200,7 +200,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
Aliases = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Path = 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),
|
||||
StartAir = 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
|
||||
{
|
||||
[DbContext(typeof(SqLiteContext))]
|
||||
[Migration("20210626141347_Triggers")]
|
||||
[Migration("20210723224550_Triggers")]
|
||||
partial class Triggers
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.7");
|
||||
.HasAnnotation("ProductVersion", "5.0.8");
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
{
|
||||
@ -143,6 +143,41 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.ToTable("Libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -477,7 +512,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Status")
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
@ -864,7 +899,8 @@ namespace Kyoo.SqLite.Migrations
|
||||
{
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID");
|
||||
.HasForeignKey("StudioID")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
@ -61,14 +61,14 @@ namespace Kyoo.SqLite.Migrations
|
||||
AND Language = new.Language AND IsForced = new.IsForced
|
||||
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||
'.' || Language ||
|
||||
'.' || COALESCE(Language, 'und') ||
|
||||
CASE (TrackIndex)
|
||||
WHEN 0 THEN ''
|
||||
ELSE '-' || (TrackIndex)
|
||||
END ||
|
||||
CASE (IsForced)
|
||||
WHEN false THEN ''
|
||||
ELSE '-forced'
|
||||
ELSE '.forced'
|
||||
END ||
|
||||
CASE (Type)
|
||||
WHEN 1 THEN '.video'
|
||||
@ -98,7 +98,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
END ||
|
||||
CASE (IsForced)
|
||||
WHEN false THEN ''
|
||||
ELSE '-forced'
|
||||
ELSE '.forced'
|
||||
END ||
|
||||
CASE (Type)
|
||||
WHEN 1 THEN '.video'
|
||||
@ -123,7 +123,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
END ||
|
||||
CASE (IsForced)
|
||||
WHEN false THEN ''
|
||||
ELSE '-forced'
|
||||
ELSE '.forced'
|
||||
END ||
|
||||
CASE (Type)
|
||||
WHEN 1 THEN '.video'
|
@ -14,7 +14,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.7");
|
||||
.HasAnnotation("ProductVersion", "5.0.8");
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||
{
|
||||
@ -141,6 +141,41 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.ToTable("Libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.LibraryItem", b =>
|
||||
{
|
||||
b.Property<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 =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
@ -475,7 +510,7 @@ namespace Kyoo.SqLite.Migrations
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Status")
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
@ -862,7 +897,8 @@ namespace Kyoo.SqLite.Migrations
|
||||
{
|
||||
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("StudioID");
|
||||
.HasForeignKey("StudioID")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Studio");
|
||||
});
|
||||
|
@ -25,6 +25,7 @@ namespace Kyoo.Tests
|
||||
public void Dispose()
|
||||
{
|
||||
Repositories.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -5,7 +5,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -1,8 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
||||
@ -23,7 +26,7 @@ namespace Kyoo.Tests.Library
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ALibraryTests : RepositoryTests<Models.Library>
|
||||
public abstract class ALibraryTests : RepositoryTests<Library>
|
||||
{
|
||||
private readonly ILibraryRepository _repository;
|
||||
|
||||
@ -32,5 +35,17 @@ namespace Kyoo.Tests.Library
|
||||
{
|
||||
_repository = Repositories.LibraryManager.LibraryRepository;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateWithProvider()
|
||||
{
|
||||
Library library = TestSample.GetNew<Library>();
|
||||
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.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -5,7 +5,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
public class GlobalTests : IDisposable, IAsyncDisposable
|
||||
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -3,7 +3,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -4,7 +4,7 @@ using Kyoo.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
||||
@ -47,5 +47,20 @@ namespace Kyoo.Tests.Library
|
||||
Track track = await _repository.Get(1);
|
||||
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UndefinedLanguageSlugTest()
|
||||
{
|
||||
await _repository.Create(new Track
|
||||
{
|
||||
ID = 5,
|
||||
TrackIndex = 0,
|
||||
Type = StreamType.Video,
|
||||
Language = null,
|
||||
EpisodeID = TestSample.Get<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.Abstractions;
|
||||
|
||||
namespace Kyoo.Tests.Library
|
||||
namespace Kyoo.Tests.Database
|
||||
{
|
||||
namespace SqLite
|
||||
{
|
@ -9,8 +9,14 @@ namespace Kyoo.Tests
|
||||
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
|
||||
{
|
||||
{
|
||||
typeof(Show),
|
||||
() => new Show()
|
||||
typeof(Library),
|
||||
() => new Library
|
||||
{
|
||||
ID = 2,
|
||||
Slug = "new-library",
|
||||
Name = "New Library",
|
||||
Paths = new [] {"/a/random/path"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -18,8 +24,8 @@ namespace Kyoo.Tests
|
||||
private static readonly Dictionary<Type, Func<object>> Samples = new()
|
||||
{
|
||||
{
|
||||
typeof(Models.Library),
|
||||
() => new Models.Library
|
||||
typeof(Library),
|
||||
() => new Library
|
||||
{
|
||||
ID = 1,
|
||||
Slug = "deck",
|
||||
@ -227,7 +233,7 @@ namespace Kyoo.Tests
|
||||
provider.ID = 0;
|
||||
context.Providers.Add(provider);
|
||||
|
||||
Models.Library library = Get<Models.Library>();
|
||||
Library library = Get<Library>();
|
||||
library.ID = 0;
|
||||
library.Collections = new List<Collection> {collection};
|
||||
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="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.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.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@ -32,6 +34,7 @@
|
||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
||||
<ProjectReference Include="../Kyoo/Kyoo.csproj" />
|
||||
<ProjectReference Include="..\Kyoo.TheTvdb\Kyoo.TheTvdb.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
namespace Kyoo.Tests.Utility
|
||||
{
|
||||
public class EnumerableTests
|
||||
{
|
||||
|
@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
namespace Kyoo.Tests.Utility
|
||||
{
|
||||
public class MergerTests
|
||||
{
|
||||
@ -17,5 +21,192 @@ namespace Kyoo.Tests
|
||||
Assert.Null(genre.Name);
|
||||
Assert.Null(genre.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeTest()
|
||||
{
|
||||
Genre genre = new()
|
||||
{
|
||||
ID = 5
|
||||
};
|
||||
Genre genre2 = new()
|
||||
{
|
||||
Name = "test"
|
||||
};
|
||||
Genre ret = Merger.Merge(genre, genre2);
|
||||
Assert.True(ReferenceEquals(genre, ret));
|
||||
Assert.Equal(5, ret.ID);
|
||||
Assert.Equal("test", genre.Name);
|
||||
Assert.Null(genre.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")]
|
||||
public void MergeNullTests()
|
||||
{
|
||||
Genre genre = new()
|
||||
{
|
||||
ID = 5
|
||||
};
|
||||
Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null)));
|
||||
Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre)));
|
||||
Assert.Null(Merger.Merge<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 Xunit;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
namespace Kyoo.Tests.Utility
|
||||
{
|
||||
public class TaskTests
|
||||
{
|
||||
|
@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Kyoo.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
using Utils = Kyoo.Utility;
|
||||
|
||||
namespace Kyoo.Tests.Utility
|
||||
{
|
||||
public class UtilityTests
|
||||
{
|
||||
@ -13,12 +16,12 @@ namespace Kyoo.Tests
|
||||
Expression<Func<Show, int>> member = x => x.ID;
|
||||
Expression<Func<Show, object>> memberCast = x => x.ID;
|
||||
|
||||
Assert.False(Utility.IsPropertyExpression(null));
|
||||
Assert.True(Utility.IsPropertyExpression(member));
|
||||
Assert.True(Utility.IsPropertyExpression(memberCast));
|
||||
Assert.False(Utils.IsPropertyExpression(null));
|
||||
Assert.True(Utils.IsPropertyExpression(member));
|
||||
Assert.True(Utils.IsPropertyExpression(memberCast));
|
||||
|
||||
Expression<Func<Show, object>> call = x => x.GetID("test");
|
||||
Assert.False(Utility.IsPropertyExpression(call));
|
||||
Assert.False(Utils.IsPropertyExpression(call));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -27,9 +30,51 @@ namespace Kyoo.Tests
|
||||
Expression<Func<Show, int>> member = x => x.ID;
|
||||
Expression<Func<Show, object>> memberCast = x => x.ID;
|
||||
|
||||
Assert.Equal("ID", Utility.GetPropertyName(member));
|
||||
Assert.Equal("ID", Utility.GetPropertyName(memberCast));
|
||||
Assert.Throws<ArgumentException>(() => Utility.GetPropertyName(null));
|
||||
Assert.Equal("ID", Utils.GetPropertyName(member));
|
||||
Assert.Equal("ID", Utils.GetPropertyName(memberCast));
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
EndGlobal
|
||||
|
@ -36,7 +36,29 @@ namespace Kyoo.Controllers
|
||||
_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("__", ":");
|
||||
|
||||
@ -59,7 +81,7 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
path = path.Replace("__", ":");
|
||||
// TODO handle lists and dictionaries.
|
||||
Type type = GetType(path);
|
||||
Type type = _GetType(path);
|
||||
object ret = _configuration.GetValue(type, path);
|
||||
if (ret != null)
|
||||
return ret;
|
||||
@ -73,7 +95,7 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
path = path.Replace("__", ":");
|
||||
// TODO handle lists and dictionaries.
|
||||
Type type = GetType(path);
|
||||
Type type = _GetType(path);
|
||||
if (typeof(T).IsAssignableFrom(type))
|
||||
throw new InvalidCastException($"The type {typeof(T).Name} is not valid for " +
|
||||
$"a resource of type {type.Name}.");
|
||||
@ -84,12 +106,12 @@ namespace Kyoo.Controllers
|
||||
public async Task EditValue(string path, object value)
|
||||
{
|
||||
path = path.Replace("__", ":");
|
||||
Type type = GetType(path);
|
||||
Type type = _GetType(path);
|
||||
value = JObject.FromObject(value).ToObject(type);
|
||||
if (value == null)
|
||||
throw new ArgumentException("Invalid value format.");
|
||||
|
||||
ExpandoObject config = ToObject(_configuration);
|
||||
ExpandoObject config = _ToObject(_configuration);
|
||||
IDictionary<string, object> configDic = config;
|
||||
configDic[path] = value;
|
||||
JObject obj = JObject.FromObject(config);
|
||||
@ -104,7 +126,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="config">The configuration to transform</param>
|
||||
/// <returns>A strongly typed representation of the configuration.</returns>
|
||||
[SuppressMessage("ReSharper", "RedundantJumpStatement")]
|
||||
private ExpandoObject ToObject(IConfiguration config)
|
||||
private ExpandoObject _ToObject(IConfiguration config)
|
||||
{
|
||||
ExpandoObject obj = new();
|
||||
|
||||
@ -112,12 +134,12 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
Type type = GetType(section.Path);
|
||||
Type type = _GetType(section.Path);
|
||||
obj.TryAdd(section.Key, section.Get(type));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
obj.TryAdd(section.Key, ToUntyped(section));
|
||||
obj.TryAdd(section.Key, _ToUntyped(section));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -133,13 +155,13 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="config">The section to convert</param>
|
||||
/// <returns>The converted section</returns>
|
||||
private static object ToUntyped(IConfigurationSection config)
|
||||
private static object _ToUntyped(IConfigurationSection config)
|
||||
{
|
||||
ExpandoObject obj = new();
|
||||
|
||||
foreach (IConfigurationSection section in config.GetChildren())
|
||||
{
|
||||
obj.TryAdd(section.Key, ToUntyped(section));
|
||||
obj.TryAdd(section.Key, _ToUntyped(section));
|
||||
}
|
||||
|
||||
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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
@ -9,9 +10,10 @@ using Microsoft.AspNetCore.StaticFiles;
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IFileManager"/> for the local filesystem (using System.IO).
|
||||
/// A <see cref="IFileSystem"/> for the local filesystem (using System.IO).
|
||||
/// </summary>
|
||||
public class FileManager : IFileManager
|
||||
[FileSystemMetadata(new [] {"", "file"}, StripScheme = true)]
|
||||
public class LocalFileSystem : IFileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// An extension provider to get content types from files extensions.
|
||||
@ -41,7 +43,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <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)
|
||||
return new NotFoundResult();
|
||||
@ -49,40 +51,56 @@ namespace Kyoo.Controllers
|
||||
return new NotFoundResult();
|
||||
return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
|
||||
{
|
||||
EnableRangeProcessing = range
|
||||
EnableRangeProcessing = rangeSupport
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream GetReader(string path)
|
||||
public Task<Stream> GetReader(string path)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
return File.OpenRead(path);
|
||||
return Task.FromResult<Stream>(File.OpenRead(path));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream NewFile(string path)
|
||||
public Task<Stream> NewFile(string path)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
return File.Create(path);
|
||||
return Task.FromResult<Stream>(File.Create(path));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> CreateDirectory(string path)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
Directory.CreateDirectory(path);
|
||||
return Task.FromResult(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ICollection<string>> ListFiles(string path)
|
||||
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));
|
||||
return Task.FromResult<ICollection<string>>(Directory.Exists(path)
|
||||
? Directory.GetFiles(path)
|
||||
: Array.Empty<string>());
|
||||
string[] ret = Directory.Exists(path)
|
||||
? Directory.GetFiles(path, "*", options)
|
||||
: Array.Empty<string>();
|
||||
return Task.FromResult<ICollection<string>>(ret);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> Exists(string path)
|
||||
{
|
||||
return Task.FromResult(File.Exists(path));
|
||||
return Task.FromResult(File.Exists(path) || Directory.Exists(path));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -92,24 +110,5 @@ namespace Kyoo.Controllers
|
||||
Directory.CreateDirectory(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.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Autofac;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -21,7 +22,7 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// The service provider. It allow plugin's activation.
|
||||
/// </summary>
|
||||
private readonly IServiceProvider _provider;
|
||||
private IServiceProvider _provider;
|
||||
/// <summary>
|
||||
/// The configuration to get the plugin's directory.
|
||||
/// </summary>
|
||||
@ -51,6 +52,13 @@ namespace Kyoo.Controllers
|
||||
_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 />
|
||||
public T GetPlugin<T>(string name)
|
||||
@ -126,6 +134,13 @@ namespace Kyoo.Controllers
|
||||
else
|
||||
_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 />
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
@ -139,7 +154,12 @@ namespace Kyoo.Controllers
|
||||
public void ConfigureAspnet(IApplicationBuilder app)
|
||||
{
|
||||
foreach (IPlugin plugin in _plugins)
|
||||
{
|
||||
using IServiceScope scope = _provider.CreateScope();
|
||||
Helper.InjectServices(plugin, x => scope.ServiceProvider.GetRequiredService(x));
|
||||
plugin.ConfigureAspNet(app);
|
||||
Helper.InjectServices(plugin, _ => null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
|
||||
return await ValidateTracks(obj);
|
||||
// TODO check if this is needed
|
||||
// obj.Slug = await _database.Entry(obj).Property(x => x.Slug).
|
||||
// return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -81,17 +81,28 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
|
||||
public override Task<LibraryItem> Create(LibraryItem obj)
|
||||
=> throw new InvalidOperationException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
|
||||
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj)
|
||||
=> throw new InvalidOperationException();
|
||||
|
||||
/// <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 />
|
||||
public override Task Delete(int id) => throw new InvalidOperationException();
|
||||
public override Task Delete(int id)
|
||||
=> throw new InvalidOperationException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Delete(string slug) => throw new InvalidOperationException();
|
||||
public override Task Delete(string slug)
|
||||
=> throw new InvalidOperationException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Delete(LibraryItem obj) => throw new InvalidOperationException();
|
||||
public override Task Delete(LibraryItem obj)
|
||||
=> throw new InvalidOperationException();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
: base(database)
|
||||
{
|
||||
@ -53,8 +53,8 @@ namespace Kyoo.Controllers
|
||||
public override async Task<Library> Create(Library obj)
|
||||
{
|
||||
await base.Create(obj);
|
||||
obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToList();
|
||||
_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).");
|
||||
return obj;
|
||||
}
|
||||
@ -63,6 +63,9 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(Library resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
resource.ProviderLinks = resource.Providers?
|
||||
.Select(x => Link.Create(resource, x))
|
||||
.ToList();
|
||||
await resource.ProviderLinks.ForEachAsync(async id =>
|
||||
{
|
||||
id.Second = await _providers.CreateIfNotExists(id.Second);
|
||||
|
@ -96,7 +96,12 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(Season resource)
|
||||
{
|
||||
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 resource.ExternalIDs.ForEachAsync(async id =>
|
||||
|
@ -4,11 +4,12 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Autofac.Features.Metadata;
|
||||
using Autofac.Features.OwnedInstances;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -22,9 +23,52 @@ namespace Kyoo.Controllers
|
||||
public class TaskManager : BackgroundService, ITaskManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The service provider used to activate
|
||||
/// The class representing task under this <see cref="TaskManager"/> jurisdiction.
|
||||
/// </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>
|
||||
/// The configuration instance used to get schedule information
|
||||
/// </summary>
|
||||
@ -37,15 +81,15 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// The list of tasks and their next scheduled run.
|
||||
/// </summary>
|
||||
private readonly List<(ITask task, DateTime scheduledDate)> _tasks;
|
||||
private readonly List<ManagedTask> _tasks;
|
||||
/// <summary>
|
||||
/// The queue of tasks that should be run as soon as possible.
|
||||
/// </summary>
|
||||
private readonly Queue<(ITask, Dictionary<string, object>)> _queuedTasks = new();
|
||||
private readonly Queue<QueuedTask> _queuedTasks = new();
|
||||
/// <summary>
|
||||
/// The currently running task.
|
||||
/// </summary>
|
||||
private ITask _runningTask;
|
||||
private (TaskMetadataAttribute, ITask)? _runningTask;
|
||||
/// <summary>
|
||||
/// The cancellation token used to cancel the running task when the runner should shutdown.
|
||||
/// </summary>
|
||||
@ -55,22 +99,24 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TaskManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="tasks">The list of tasks to manage</param>
|
||||
/// <param name="provider">The service provider to request services for tasks</param>
|
||||
/// <param name="tasks">The list of tasks to manage with their metadata</param>
|
||||
/// <param name="options">The configuration to load schedule information.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public TaskManager(IEnumerable<ITask> tasks,
|
||||
IServiceProvider provider,
|
||||
public TaskManager(IEnumerable<Meta<Func<Owned<ITask>>, TaskMetadataAttribute>> tasks,
|
||||
IOptionsMonitor<TaskOptions> options,
|
||||
ILogger<TaskManager> logger)
|
||||
{
|
||||
_provider = provider;
|
||||
_options = options;
|
||||
_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())
|
||||
_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
|
||||
_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>
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
EnqueueStartupTasks();
|
||||
_EnqueueStartupTasks();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (_queuedTasks.Any())
|
||||
{
|
||||
(ITask task, Dictionary<string, object> arguments) = _queuedTasks.Dequeue();
|
||||
_runningTask = task;
|
||||
QueuedTask task = _queuedTasks.Dequeue();
|
||||
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)
|
||||
{
|
||||
_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
|
||||
@ -129,93 +180,116 @@ namespace Kyoo.Controllers
|
||||
/// Parse parameters, inject a task and run it.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="ArgumentException">There was an invalid argument or a required argument was not found.</exception>
|
||||
private async Task RunTask(ITask task, Dictionary<string, object> arguments)
|
||||
/// <param name="cancellationToken">An optional cancellation token that will be passed to 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>
|
||||
private async Task _RunTask(ManagedTask task,
|
||||
[NotNull] IProgress<float> progress,
|
||||
Dictionary<string, object> arguments,
|
||||
CancellationToken? cancellationToken = null)
|
||||
{
|
||||
_logger.LogInformation("Task starting: {Task}", task.Name);
|
||||
|
||||
ICollection<TaskParameter> all = task.GetParameters();
|
||||
|
||||
ICollection<string> invalids = arguments.Keys
|
||||
.Where(x => all.Any(y => x != y.Name))
|
||||
.ToArray();
|
||||
if (invalids.Any())
|
||||
using (_logger.BeginScope("Task: {Task}", task.Metadata.Name))
|
||||
{
|
||||
string invalidsStr = string.Join(", ", invalids);
|
||||
throw new ArgumentException($"{invalidsStr} are invalid arguments for the task {task.Name}");
|
||||
}
|
||||
|
||||
TaskParameters args = new(all
|
||||
.Select(x =>
|
||||
await using Owned<ITask> taskObj = task.Factory.Invoke();
|
||||
ICollection<TaskParameter> all = taskObj.Value.GetParameters();
|
||||
|
||||
_runningTask = (task.Metadata, taskObj.Value);
|
||||
ICollection<string> invalids = arguments.Keys
|
||||
.Where(x => all.All(y => x != y.Name))
|
||||
.ToArray();
|
||||
if (invalids.Any())
|
||||
{
|
||||
object value = arguments
|
||||
.FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Value;
|
||||
if (value == null && x.IsRequired)
|
||||
throw new ArgumentException($"The argument {x.Name} is required to run {task.Name}" +
|
||||
" but it was not specified.");
|
||||
return x.CreateValue(value ?? x.DefaultValue);
|
||||
}));
|
||||
throw new ArgumentException($"{string.Join(", ", invalids)} are " +
|
||||
$"invalid arguments for the task {task.Metadata.Name}");
|
||||
}
|
||||
|
||||
using IServiceScope scope = _provider.CreateScope();
|
||||
InjectServices(task, x => scope.ServiceProvider.GetRequiredService(x));
|
||||
await task.Run(args, _taskToken.Token);
|
||||
InjectServices(task, _ => null);
|
||||
_logger.LogInformation("Task finished: {Task}", task.Name);
|
||||
TaskParameters args = new(all
|
||||
.Select(x =>
|
||||
{
|
||||
object value = arguments
|
||||
.FirstOrDefault(y => string.Equals(y.Key, x.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Value;
|
||||
if (value == null && x.IsRequired)
|
||||
throw new ArgumentException($"The argument {x.Name} is required to run " +
|
||||
$"{task.Metadata.Name} but it was not specified.");
|
||||
return x.CreateValue(value ?? x.DefaultValue);
|
||||
}));
|
||||
|
||||
_logger.LogInformation("Task starting: {Task} ({Parameters})",
|
||||
task.Metadata.Name, args.ToDictionary(x => x.Name, x => x.As<object>()));
|
||||
|
||||
CancellationToken token = cancellationToken != null
|
||||
? 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>
|
||||
/// Start tasks that are scheduled for start.
|
||||
/// </summary>
|
||||
private void QueueScheduledTasks()
|
||||
{
|
||||
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.scheduledDate <= DateTime.Now)
|
||||
.Select(x => x.task.Slug);
|
||||
IEnumerable<string> tasksToQueue = _tasks.Where(x => x.ScheduledDate <= DateTime.Now)
|
||||
.Select(x => x.Metadata.Slug);
|
||||
foreach (string task in tasksToQueue)
|
||||
{
|
||||
_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>
|
||||
/// Queue startup tasks with respect to the priority rules.
|
||||
/// </summary>
|
||||
private void EnqueueStartupTasks()
|
||||
private void _EnqueueStartupTasks()
|
||||
{
|
||||
IEnumerable<ITask> startupTasks = _tasks.Select(x => x.task)
|
||||
.Where(x => x.RunOnStartup)
|
||||
.OrderByDescending(x => x.Priority);
|
||||
foreach (ITask task in startupTasks)
|
||||
_queuedTasks.Enqueue((task, new Dictionary<string, object>()));
|
||||
IEnumerable<string> startupTasks = _tasks
|
||||
.Where(x => x.Metadata.RunOnStartup)
|
||||
.OrderByDescending(x => x.Metadata.Priority)
|
||||
.Select(x => x.Metadata.Slug);
|
||||
foreach (string task in startupTasks)
|
||||
StartTask(task, new Progress<float>(), new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
|
||||
int index = _tasks.FindIndex(x => x.task.Slug == taskSlug);
|
||||
int index = _tasks.FindIndex(x => x.Metadata.Slug == taskSlug);
|
||||
if (index == -1)
|
||||
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
|
||||
_queuedTasks.Enqueue((_tasks[index].task, arguments));
|
||||
_tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug));
|
||||
_queuedTasks.Enqueue(new QueuedTask
|
||||
{
|
||||
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>
|
||||
@ -231,15 +305,17 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<ITask> GetRunningTasks()
|
||||
public ICollection<(TaskMetadataAttribute, ITask)> GetRunningTasks()
|
||||
{
|
||||
return new[] {_runningTask};
|
||||
return _runningTask == null
|
||||
? ArraySegment<(TaskMetadataAttribute, ITask)>.Empty
|
||||
: new[] { _runningTask.Value };
|
||||
}
|
||||
|
||||
/// <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 System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Download images and retrieve the path of those images for a resource.
|
||||
/// </summary>
|
||||
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;
|
||||
/// <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;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
Directory.CreateDirectory(_options.CurrentValue.PeoplePath);
|
||||
Directory.CreateDirectory(_options.CurrentValue.ProviderPath);
|
||||
_library = library;
|
||||
|
||||
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
|
||||
{
|
||||
using WebClient client = new();
|
||||
await client.DownloadFileTaskAsync(new Uri(url), localPath);
|
||||
await using Stream reader = await _files.GetReader(url);
|
||||
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)
|
||||
{
|
||||
string posterPath = await GetShowPoster(show);
|
||||
if (alwaysDownload || !File.Exists(posterPath))
|
||||
await DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
|
||||
string posterPath = await GetPoster(show);
|
||||
if (alwaysDownload || !await _files.Exists(posterPath))
|
||||
ret |= await _DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}");
|
||||
}
|
||||
if (show.Logo != null)
|
||||
{
|
||||
string logoPath = await GetShowLogo(show);
|
||||
if (alwaysDownload || !File.Exists(logoPath))
|
||||
await DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
|
||||
string logoPath = await GetLogo(show);
|
||||
if (alwaysDownload || !await _files.Exists(logoPath))
|
||||
ret |= await _DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}");
|
||||
}
|
||||
if (show.Backdrop != null)
|
||||
{
|
||||
string backdropPath = await GetShowBackdrop(show);
|
||||
if (alwaysDownload || !File.Exists(backdropPath))
|
||||
await DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
|
||||
string backdropPath = await GetThumbnail(show);
|
||||
if (alwaysDownload || !await _files.Exists(backdropPath))
|
||||
ret |= await _DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}");
|
||||
}
|
||||
|
||||
foreach (PeopleRole role in show.People)
|
||||
await Validate(role.People, alwaysDownload);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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)
|
||||
throw new ArgumentNullException(nameof(people));
|
||||
if (people.Poster == null)
|
||||
return;
|
||||
string localPath = await GetPeoplePoster(people);
|
||||
if (alwaysDownload || !File.Exists(localPath))
|
||||
await DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}");
|
||||
return false;
|
||||
string localPath = await GetPoster(people);
|
||||
if (alwaysDownload || !await _files.Exists(localPath))
|
||||
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)
|
||||
return;
|
||||
if (season.Poster == null)
|
||||
return false;
|
||||
|
||||
string localPath = await GetSeasonPoster(season);
|
||||
if (alwaysDownload || !File.Exists(localPath))
|
||||
await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}");
|
||||
string localPath = await GetPoster(season);
|
||||
if (alwaysDownload || !await _files.Exists(localPath))
|
||||
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)
|
||||
return;
|
||||
if (episode.Thumb == null)
|
||||
return false;
|
||||
|
||||
string localPath = await GetEpisodeThumb(episode);
|
||||
if (alwaysDownload || !File.Exists(localPath))
|
||||
await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
|
||||
string localPath = await _GetEpisodeThumb(episode);
|
||||
if (alwaysDownload || !await _files.Exists(localPath))
|
||||
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)
|
||||
return;
|
||||
return false;
|
||||
|
||||
string localPath = await GetProviderLogo(provider);
|
||||
if (alwaysDownload || !File.Exists(localPath))
|
||||
await DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}");
|
||||
string localPath = await GetLogo(provider);
|
||||
if (alwaysDownload || !await _files.Exists(localPath))
|
||||
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)
|
||||
throw new ArgumentNullException(nameof(show));
|
||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "backdrop.jpg"));
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
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.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <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 (season.Show == null)
|
||||
await _library.Value.Load(season, x => x.Show);
|
||||
return _files.Combine(_files.GetExtraDirectory(season.Show), $"season-{season.SeasonNumber}.jpg");
|
||||
}
|
||||
|
||||
public Task<string> GetShowLogo(Show show)
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetThumbnail<T>(T item)
|
||||
where T : IResource
|
||||
{
|
||||
if (show?.Path == null)
|
||||
throw new ArgumentNullException(nameof(show));
|
||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "logo.png"));
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
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> GetShowPoster(Show show)
|
||||
/// <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 (show?.Path == null)
|
||||
throw new ArgumentNullException(nameof(show));
|
||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "poster.jpg"));
|
||||
if (episode.Show == null)
|
||||
await _library.Value.Load(episode, x => x.Show);
|
||||
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> GetSeasonPoster(Season season)
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetLogo<T>(T item)
|
||||
where T : IResource
|
||||
{
|
||||
if (season == null)
|
||||
throw new ArgumentNullException(nameof(season));
|
||||
return Task.FromResult(Path.Combine(_files.GetExtraDirectory(season), $"season-{season.SeasonNumber}.jpg"));
|
||||
}
|
||||
|
||||
public Task<string> GetEpisodeThumb(Episode episode)
|
||||
{
|
||||
string dir = Path.Combine(_files.GetExtraDirectory(episode), "Thumbnails");
|
||||
Directory.CreateDirectory(dir);
|
||||
return Task.FromResult(Path.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg"));
|
||||
}
|
||||
|
||||
public Task<string> GetPeoplePoster(People people)
|
||||
{
|
||||
if (people == null)
|
||||
throw new ArgumentNullException(nameof(people));
|
||||
string peoplePath = _options.CurrentValue.PeoplePath;
|
||||
string thumbPath = Path.GetFullPath(Path.Combine(peoplePath, $"{people.Slug}.jpg"));
|
||||
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);
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
return Task.FromResult(item switch
|
||||
{
|
||||
Show show => _files.Combine(_files.GetExtraDirectory(show), "logo.png"),
|
||||
Provider provider => _files.Combine(_options.CurrentValue.ProviderPath,
|
||||
$"{provider.Slug}.{provider.LogoExtension}"),
|
||||
_ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,24 +72,29 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IFileManager _files;
|
||||
private readonly IFileSystem _files;
|
||||
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;
|
||||
_options = options;
|
||||
_library = library;
|
||||
|
||||
if (TranscoderAPI.init() != Marshal.SizeOf<Stream>())
|
||||
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)
|
||||
throw new ArgumentException("Invalid path.");
|
||||
return Task.Factory.StartNew(
|
||||
return await Task.Factory.StartNew(
|
||||
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
|
||||
TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Autofac;
|
||||
using Autofac.Core;
|
||||
using Autofac.Core.Registration;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Models.Options;
|
||||
using Kyoo.Models.Permissions;
|
||||
using Kyoo.Tasks;
|
||||
@ -31,12 +34,14 @@ namespace Kyoo
|
||||
/// <inheritdoc />
|
||||
public ICollection<Type> Provides => new[]
|
||||
{
|
||||
typeof(IFileManager),
|
||||
typeof(IFileSystem),
|
||||
typeof(ITranscoder),
|
||||
typeof(IThumbnailsManager),
|
||||
typeof(IProviderManager),
|
||||
typeof(IMetadataProvider),
|
||||
typeof(ITaskManager),
|
||||
typeof(ILibraryManager)
|
||||
typeof(ILibraryManager),
|
||||
typeof(IIdentifier),
|
||||
typeof(AProviderComposite)
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -77,6 +82,11 @@ namespace Kyoo
|
||||
/// 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>
|
||||
@ -88,20 +98,58 @@ namespace Kyoo
|
||||
_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 />
|
||||
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||
{
|
||||
string publicUrl = _configuration.GetPublicUrl();
|
||||
|
||||
services.Configure<BasicOptions>(_configuration.GetSection(BasicOptions.Path));
|
||||
services.AddConfiguration<BasicOptions>(BasicOptions.Path);
|
||||
services.Configure<TaskOptions>(_configuration.GetSection(TaskOptions.Path));
|
||||
services.AddConfiguration<TaskOptions>(TaskOptions.Path);
|
||||
services.Configure<MediaOptions>(_configuration.GetSection(MediaOptions.Path));
|
||||
services.AddConfiguration<MediaOptions>(MediaOptions.Path);
|
||||
services.AddUntypedConfiguration("database");
|
||||
services.AddUntypedConfiguration("logging");
|
||||
|
||||
|
||||
services.AddControllers()
|
||||
.AddNewtonsoftJson(x =>
|
||||
{
|
||||
@ -109,41 +157,18 @@ namespace Kyoo
|
||||
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.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 />
|
||||
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();
|
||||
contentTypeProvider.Mappings[".data"] = "application/octet-stream";
|
||||
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>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.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.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
|
||||
<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>
|
||||
/// A regex for episodes
|
||||
/// </summary>
|
||||
public string Regex { get; set; }
|
||||
public string[] Regex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A regex for subtitles
|
||||
/// </summary>
|
||||
public string SubtitleRegex { get; set; }
|
||||
public string[] SubtitleRegex { get; set; }
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@ using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Kyoo
|
||||
{
|
||||
/// <summary>
|
||||
@ -30,6 +32,8 @@ namespace Kyoo
|
||||
if (!File.Exists("./settings.json"))
|
||||
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json");
|
||||
|
||||
IWebHostBuilder builder = CreateWebHostBuilder(args);
|
||||
|
||||
bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch
|
||||
{
|
||||
"d" => true,
|
||||
@ -43,18 +47,21 @@ namespace Kyoo
|
||||
};
|
||||
|
||||
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
|
||||
debug ??= true;
|
||||
#endif
|
||||
|
||||
Console.WriteLine($"Running as {Environment.UserName}.");
|
||||
IWebHostBuilder builder = CreateWebHostBuilder(args);
|
||||
if (debug != null)
|
||||
builder = builder.UseEnvironment(debug == true ? "Development" : "Production");
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Running as {Environment.UserName}.");
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -86,6 +93,11 @@ namespace Kyoo
|
||||
IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build();
|
||||
|
||||
return new WebHostBuilder()
|
||||
.ConfigureServices(x =>
|
||||
{
|
||||
AutofacServiceProviderFactory factory = new();
|
||||
x.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<ContainerBuilder>>(factory));
|
||||
})
|
||||
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.UseConfiguration(configuration)
|
||||
.ConfigureAppConfiguration(x => SetupConfig(x, args))
|
||||
@ -99,12 +111,6 @@ namespace Kyoo
|
||||
.AddDebug()
|
||||
.AddEventSourceLogger();
|
||||
})
|
||||
.UseDefaultServiceProvider((context, options) =>
|
||||
{
|
||||
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
|
||||
if (context.HostingEnvironment.IsDevelopment())
|
||||
StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration);
|
||||
})
|
||||
.ConfigureServices(x => x.AddRouting())
|
||||
.UseKestrel(options => { options.AddServerHeader = false; })
|
||||
.UseIIS()
|
||||
|
@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Autofac;
|
||||
using Autofac.Extras.AttributeMetadata;
|
||||
using Kyoo.Authentication;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Options;
|
||||
using Kyoo.Postgresql;
|
||||
using Kyoo.Tasks;
|
||||
@ -71,19 +72,23 @@ namespace Kyoo
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>));
|
||||
|
||||
services.AddSingleton(_plugins);
|
||||
services.AddTask<PluginInitializer>();
|
||||
_plugins.ConfigureServices(services);
|
||||
}
|
||||
|
||||
public void ConfigureContainer(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterModule<AttributedMetadataModule>();
|
||||
builder.RegisterInstance(_plugins).As<IPluginManager>().ExternallyOwned();
|
||||
builder.RegisterTask<PluginInitializer>();
|
||||
_plugins.ConfigureContainer(builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the asp net host.
|
||||
/// </summary>
|
||||
/// <param name="app">The asp net host to configure</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())
|
||||
app.UseDeveloperExceptionPage();
|
||||
@ -110,7 +115,9 @@ namespace Kyoo
|
||||
return next();
|
||||
});
|
||||
app.UseResponseCompression();
|
||||
|
||||
|
||||
if (_plugins is PluginManager manager)
|
||||
manager.SetProvider(provider);
|
||||
_plugins.ConfigureAspnet(app);
|
||||
|
||||
app.UseSpa(spa =>
|
||||
|
@ -1,36 +1,60 @@
|
||||
using System;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Kyoo.Models.Watch;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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 string Slug => "scan";
|
||||
public string Name => "Scan libraries";
|
||||
public string Description => "Scan your libraries, load data for new shows and remove shows that don't exist anymore.";
|
||||
public string HelpMessage => "Reloading all libraries is a long process and may take up to 24 hours if it is the first scan in a while.";
|
||||
public bool RunOnStartup => true;
|
||||
public int Priority => 0;
|
||||
|
||||
[Injected] public IServiceProvider ServiceProvider { private get; set; }
|
||||
[Injected] public IThumbnailsManager ThumbnailsManager { private get; set; }
|
||||
[Injected] public IProviderManager MetadataProvider { private get; set; }
|
||||
[Injected] public ITranscoder Transcoder { private get; set; }
|
||||
[Injected] public IConfiguration Config { private get; set; }
|
||||
/// <summary>
|
||||
/// The library manager used to get libraries and providers to use.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
private int _parallelTasks;
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Crawler"/>.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager to retrieve existing episodes/library/tracks</param>
|
||||
/// <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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TaskParameters GetParameters()
|
||||
{
|
||||
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.
|
||||
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);
|
||||
|
||||
string argument = arguments["slug"].As<string>();
|
||||
ICollection<Library> libraries = argument == null
|
||||
? await libraryManager.GetAll<Library>()
|
||||
: new [] { await libraryManager.GetOrDefault<Library>(argument)};
|
||||
|
||||
? await _libraryManager.GetAll<Library>()
|
||||
: new [] { await _libraryManager.GetOrDefault<Library>(argument)};
|
||||
|
||||
if (argument != null && libraries.First() == null)
|
||||
throw new ArgumentException($"No library found with the name {argument}");
|
||||
|
||||
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
private async Task Scan(Library library, IEnumerable<Episode> episodes, IEnumerable<Track> tracks, CancellationToken cancellationToken)
|
||||
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)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
continue;
|
||||
ICollection<string> files = await _fileSystem.ListFiles(path, SearchOption.AllDirectories);
|
||||
|
||||
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;
|
||||
}
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
List<IGrouping<string, string>> shows = files
|
||||
.Where(x => IsVideo(x) && episodes.All(y => y.Path != x))
|
||||
// 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
|
||||
.Where(FileExtensions.IsVideo)
|
||||
.Where(x => episodes.All(y => y.Path != x))
|
||||
.GroupBy(Path.GetDirectoryName)
|
||||
.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));
|
||||
foreach (string[] episodeTasks in tasks.BatchBy(_parallelTasks * 2))
|
||||
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
|
||||
string[] paths = shows.Select(x => x.First())
|
||||
.Concat(shows.SelectMany(x => x.Skip(1)))
|
||||
.ToArray();
|
||||
float percent = 0;
|
||||
IProgress<float> reporter = new Progress<float>(x =>
|
||||
{
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 =>
|
||||
{
|
||||
await libraryManager.Load(old, x => x.ExternalIDs);
|
||||
return old;
|
||||
}
|
||||
|
||||
if (show.StartAir != null)
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
progress.Report((90 + (percent + x / subtitles.Length)) / library.Paths.Length);
|
||||
});
|
||||
|
||||
foreach (string trackPath in subtitles)
|
||||
{
|
||||
show.Slug += $"-{show.StartAir.Value.Year}";
|
||||
await libraryManager.Create(show);
|
||||
_taskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
|
||||
{
|
||||
["path"] = trackPath
|
||||
}, cancellationToken);
|
||||
percent += 100f / subtitles.Length;
|
||||
}
|
||||
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();
|
||||
return episode.Tracks;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return VideoExtensions.Contains(Path.GetExtension(filePath));
|
||||
}
|
||||
|
||||
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