Merge pull request #27 from AnonymusRaccoon/plugins

Plugins rework and module splitting
This commit is contained in:
Zoe Roux 2021-05-13 22:01:46 +02:00 committed by GitHub
commit 01d7c008a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 4423 additions and 5657 deletions

View File

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Views;
using Kyoo.Controllers;
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Logging;
using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
namespace Kyoo.Authentication
{
/// <summary>
/// A module that enable OpenID authentication for Kyoo.
/// </summary>
public class AuthenticationModule : IPlugin
{
/// <inheritdoc />
public string Slug => "auth";
/// <inheritdoc />
public string Name => "Authentication";
/// <inheritdoc />
public string Description => "Enable OpenID authentication for Kyoo.";
/// <inheritdoc />
public ICollection<Type> Provides => ArraySegment<Type>.Empty;
/// <inheritdoc />
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc />
public ICollection<Type> Requires => new []
{
typeof(IUserRepository)
};
/// <summary>
/// The configuration to use.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// A logger factory to allow IdentityServer to log things.
/// </summary>
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// The environment information to check if the app runs in debug mode
/// </summary>
private readonly IWebHostEnvironment _environment;
/// <summary>
/// Create a new authentication module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="loggerFactory">The logger factory to allow IdentityServer to log things</param>
/// <param name="environment">The environment information to check if the app runs in debug mode</param>
public AuthenticationModule(IConfiguration configuration,
ILoggerFactory loggerFactory,
IWebHostEnvironment environment)
{
_configuration = configuration;
_loggerFactory = loggerFactory;
_environment = environment;
}
/// <inheritdoc />
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
{
string publicUrl = _configuration.GetValue<string>("publicUrl").TrimEnd('/');
if (_environment.IsDevelopment())
IdentityModelEventSource.ShowPII = true;
services.AddControllers();
// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
// TODO Check if tokens should be stored.
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
List<Client> clients = new();
_configuration.GetSection("authentication:clients").Bind(clients);
CertificateOption certificateOptions = new();
_configuration.GetSection(CertificateOption.Path).Bind(certificateOptions);
services.AddIdentityServer(options =>
{
options.IssuerUri = publicUrl;
options.UserInteraction.LoginUrl = $"{publicUrl}/login";
options.UserInteraction.ErrorUrl = $"{publicUrl}/error";
options.UserInteraction.LogoutUrl = $"{publicUrl}/logout";
})
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
.AddInMemoryApiScopes(IdentityContext.GetScopes())
.AddInMemoryApiResources(IdentityContext.GetApis())
.AddInMemoryClients(IdentityContext.GetClients().Concat(clients))
.AddProfileService<AccountApi>()
.AddSigninKeys(certificateOptions);
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = publicUrl;
options.Audience = "kyoo";
options.RequireHttpsMetadata = false;
});
services.AddSingleton<IPermissionValidator, PermissionValidatorFactory>();
DefaultCorsPolicyService cors = new(_loggerFactory.CreateLogger<DefaultCorsPolicyService>())
{
AllowedOrigins = {new Uri(publicUrl).GetLeftPart(UriPartial.Authority)}
};
services.AddSingleton<ICorsPolicyService>(cors);
}
/// <inheritdoc />
public void ConfigureAspNet(IApplicationBuilder app)
{
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict
});
app.UseAuthentication();
app.Use((ctx, next) =>
{
ctx.SetIdentityServerOrigin(_configuration.GetValue<string>("publicUrl").TrimEnd('/'));
return next();
});
app.UseIdentityServer();
app.UseAuthorization();
PhysicalFileProvider provider = new(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
"login"));
app.UseDefaultFiles(new DefaultFilesOptions
{
RequestPath = new PathString("/login"),
FileProvider = provider,
RedirectToAppendTrailingSlash = true
});
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = new PathString("/login"),
FileProvider = provider
});
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Kyoo.Authentication.Models;
using Microsoft.Extensions.DependencyInjection;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace Kyoo.Authentication
{
/// <summary>
/// A class containing multiple extensions methods to manage certificates.
/// </summary>
public static class Certificates
{
/// <summary>
/// Add the certificate file to the identity server. If the certificate will expire soon, automatically renew it.
/// If no certificate exists, one is generated.
/// </summary>
/// <param name="builder">The identity server that will be modified.</param>
/// <param name="options">The certificate options</param>
/// <returns></returns>
public static IIdentityServerBuilder AddSigninKeys(this IIdentityServerBuilder builder,
CertificateOption options)
{
X509Certificate2 certificate = GetCertificate(options);
builder.AddSigningCredential(certificate);
if (certificate.NotAfter.AddDays(7) <= DateTime.UtcNow)
{
Console.WriteLine("Signin certificate will expire soon, renewing it.");
if (File.Exists(options.OldFile))
File.Delete(options.OldFile);
File.Move(options.File, options.OldFile);
builder.AddValidationKey(GenerateCertificate(options.File, options.Password));
}
else if (File.Exists(options.OldFile))
builder.AddValidationKey(GetExistingCredential(options.OldFile, options.Password));
return builder;
}
/// <summary>
/// Get or generate the sign-in certificate.
/// </summary>
/// <param name="options">The certificate options</param>
/// <returns>A valid certificate</returns>
private static X509Certificate2 GetCertificate(CertificateOption options)
{
return File.Exists(options.File)
? GetExistingCredential(options.File, options.Password)
: GenerateCertificate(options.File, options.Password);
}
/// <summary>
/// Load a certificate from a file
/// </summary>
/// <param name="file">The path of the certificate</param>
/// <param name="password">The password of the certificate</param>
/// <returns>The loaded certificate</returns>
private static X509Certificate2 GetExistingCredential(string file, string password)
{
return new(file, password,
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable
);
}
/// <summary>
/// Generate a new certificate key and put it in the file at <see cref="file"/>.
/// </summary>
/// <param name="file">The path of the output file</param>
/// <param name="password">The password of the new certificate</param>
/// <returns>The generated certificate</returns>
private static X509Certificate2 GenerateCertificate(string file, string password)
{
SecureRandom random = new();
X509V3CertificateGenerator certificateGenerator = new();
certificateGenerator.SetSerialNumber(BigIntegers.CreateRandomInRange(BigInteger.One,
BigInteger.ValueOf(long.MaxValue), random));
certificateGenerator.SetIssuerDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetSubjectDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetNotBefore(DateTime.UtcNow.Date);
certificateGenerator.SetNotAfter(DateTime.UtcNow.Date.AddMonths(3));
KeyGenerationParameters keyGenerationParameters = new(random, 2048);
RsaKeyPairGenerator keyPairGenerator = new();
keyPairGenerator.Init(keyGenerationParameters);
AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
const string signatureAlgorithm = "MD5WithRSA";
Asn1SignatureFactory signatureFactory = new(signatureAlgorithm, subjectKeyPair.Private);
X509Certificate bouncyCert = certificateGenerator.Generate(signatureFactory);
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
store.SetKeyEntry("Kyoo_key", new AsymmetricKeyEntry(subjectKeyPair.Private), new []
{
new X509CertificateEntry(bouncyCert)
});
using MemoryStream pfxStream = new();
store.Save(pfxStream, password.ToCharArray(), random);
X509Certificate2 certificate = new(pfxStream.ToArray(), password, X509KeyStorageFlags.Exportable);
using FileStream fileStream = File.OpenWrite(file);
pfxStream.WriteTo(fileStream);
return certificate;
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using IdentityModel;
namespace Kyoo.Authentication
{
public static class PasswordUtils
{
/// <summary>
/// Generate an OneTimeAccessCode.
/// </summary>
/// <returns>A new otac.</returns>
public static string GenerateOTAC()
{
return CryptoRandom.CreateUniqueId();
}
/// <summary>
/// Hash a password to store it has a verification only.
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
public static string HashPassword(string password)
{
byte[] salt = new byte[16];
new RNGCryptoServiceProvider().GetBytes(salt);
Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
return Convert.ToBase64String(hashBytes);
}
/// <summary>
/// Check if a password is the same as a valid hashed password.
/// </summary>
/// <param name="password">The password to check</param>
/// <param name="validPassword">
/// The valid hashed password. This password must be hashed via <see cref="HashPassword"/>.
/// </param>
/// <returns>True if the password is valid, false otherwise.</returns>
public static bool CheckPassword(string password, string validPassword)
{
byte[] validHash = Convert.FromBase64String(validPassword);
byte[] salt = new byte[16];
Array.Copy(validHash, 0, salt, 0, 16);
Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
return hash.SequenceEqual(validHash.Skip(16));
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Authentication.Models;
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
namespace Kyoo.Authentication
{
/// <summary>
/// A permission validator to validate permission with user Permission array
/// or the default array from the configurations if the user is not logged.
/// </summary>
public class PermissionValidatorFactory : IPermissionValidator
{
/// <summary>
/// The permissions options to retrieve default permissions.
/// </summary>
private readonly IOptionsMonitor<PermissionOption> _options;
/// <summary>
/// Create a new factory with the given options
/// </summary>
/// <param name="options">The option containing default values.</param>
public PermissionValidatorFactory(IOptionsMonitor<PermissionOption> options)
{
_options = options;
}
/// <inheritdoc />
public IFilterMetadata Create(PermissionAttribute attribute)
{
return new PermissionValidator(attribute.Type, attribute.Kind, _options);
}
/// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute)
{
return new PermissionValidator((object)attribute.Type ?? attribute.Kind, _options);
}
/// <summary>
/// The authorization filter used by <see cref="PermissionValidatorFactory"/>
/// </summary>
private class PermissionValidator : IAsyncAuthorizationFilter
{
/// <summary>
/// The permission to validate
/// </summary>
private readonly string _permission;
/// <summary>
/// The kind of permission needed
/// </summary>
private readonly Kind? _kind;
/// <summary>
/// The permissions options to retrieve default permissions.
/// </summary>
private readonly IOptionsMonitor<PermissionOption> _options;
/// <summary>
/// Create a new permission validator with the given options
/// </summary>
/// <param name="permission">The permission to validate</param>
/// <param name="kind">The kind of permission needed</param>
/// <param name="options">The option containing default values.</param>
public PermissionValidator(string permission, Kind kind, IOptionsMonitor<PermissionOption> options)
{
_permission = permission;
_kind = kind;
_options = options;
}
/// <summary>
/// Create a new permission validator with the given options
/// </summary>
/// <param name="partialInfo">The partial permission to validate</param>
/// <param name="options">The option containing default values.</param>
public PermissionValidator(object partialInfo, IOptionsMonitor<PermissionOption> options)
{
if (partialInfo is Kind kind)
_kind = kind;
else if (partialInfo is string perm)
_permission = perm;
else
throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind.");
_options = options;
}
/// <inheritdoc />
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
string permission = _permission;
Kind? kind = _kind;
if (permission == null || kind == null)
{
switch (context.HttpContext.Items["PermissionType"])
{
case string perm:
permission = perm;
break;
case Kind kin:
kind = kin;
break;
case null when kind != null:
context.HttpContext.Items["PermissionType"] = kind;
return;
case null when permission != null:
context.HttpContext.Items["PermissionType"] = permission;
return;
default:
throw new ArgumentException("Multiple non-matching partial permission attribute " +
"are not supported.");
}
if (permission == null || kind == null)
throw new ArgumentException("The permission type or kind is still missing after two partial " +
"permission attributes, this is unsupported.");
}
string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}";
string overallStr = $"overall.{kind.ToString()!.ToLower()}";
AuthenticateResult res = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (res.Succeeded)
{
ICollection<string> permissions = res.Principal.GetPermissions();
if (permissions.All(x => x != permStr && x != overallStr))
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
}
else
{
ICollection<string> permissions = _options.CurrentValue.Default ?? Array.Empty<string>();
if (res.Failure != null || permissions.All(x => x != permStr && x != overallStr))
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
}
}
}
}
}

View File

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4;
using Kyoo.Models;
namespace Kyoo.Authentication
{
/// <summary>
/// Extension methods.
/// </summary>
public static class Extensions
{
/// <summary>
/// Get claims of an user.
/// </summary>
/// <param name="user">The user concerned</param>
/// <returns>The list of claims the user has</returns>
public static ICollection<Claim> GetClaims(this User user)
{
return new[]
{
new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
new Claim(JwtClaimTypes.Name, user.Username),
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
};
}
/// <summary>
/// Convert a user to an <see cref="IdentityServerUser"/>.
/// </summary>
/// <param name="user">The user to convert</param>
/// <returns>The corresponding identity server user.</returns>
public static IdentityServerUser ToIdentityUser(this User user)
{
return new(user.ID.ToString())
{
DisplayName = user.Username,
AdditionalClaims = new[] {new Claim("permissions", string.Join(',', user.Permissions))}
};
}
/// <summary>
/// Get the permissions of an user.
/// </summary>
/// <param name="user">The user</param>
/// <returns>The list of permissions</returns>
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
{
return user.Claims.FirstOrDefault(x => x.Type == "permissions")?.Value.Split(',');
}
}
}

View File

@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/authentication</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.Storage" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<LoginFiles Include="$(LoginRoot)**" Visible="false" />
</ItemGroup>
<Target Name="Publish login files" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Include="@(LoginFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
<Target Name="Prepare static files" AfterTargets="Build" Condition="$(Configuration) == 'Debug'">
<Copy SourceFiles="@(LoginFiles)" DestinationFolder="$(OutputPath)/login/%(RecursiveDir)" />
</Target>
</Project>

View File

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model only used on account update requests.
/// </summary>
public class AccountUpdateRequest
{
/// <summary>
/// The new email address of the user
/// </summary>
[EmailAddress(ErrorMessage = "The email is invalid.")]
public string Email { get; set; }
/// <summary>
/// The new username of the user.
/// </summary>
[MinLength(4, ErrorMessage = "The username must have at least 4 characters")]
public string Username { get; set; }
/// <summary>
/// The picture icon.
/// </summary>
public IFormFile Picture { get; set; }
}
}

View File

@ -0,0 +1,28 @@
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model only used on login requests.
/// </summary>
public class LoginRequest
{
/// <summary>
/// The user's username.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The user's password.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Should the user stay logged in? If true a cookie will be put.
/// </summary>
public bool StayLoggedIn { get; set; }
/// <summary>
/// The return url of the login flow.
/// </summary>
public string ReturnURL { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model to represent an otac request
/// </summary>
public class OtacRequest
{
/// <summary>
/// The One Time Access Code
/// </summary>
public string Otac { get; set; }
/// <summary>
/// Should the user stay logged
/// </summary>
public bool StayLoggedIn { get; set; }
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Models;
namespace Kyoo.Authentication.Models.DTO
{
/// <summary>
/// A model only used on register requests.
/// </summary>
public class RegisterRequest
{
/// <summary>
/// The user email address
/// </summary>
[EmailAddress(ErrorMessage = "The email must be a valid email address")]
public string Email { get; set; }
/// <summary>
/// The user's username.
/// </summary>
[MinLength(4, ErrorMessage = "The username must have at least {1} characters")]
public string Username { get; set; }
/// <summary>
/// The user's password.
/// </summary>
[MinLength(8, ErrorMessage = "The password must have at least {1} characters")]
public string Password { get; set; }
/// <summary>
/// Convert this register request to a new <see cref="User"/> class.
/// </summary>
/// <returns></returns>
public User ToUser()
{
return new()
{
Slug = Utility.ToSlug(Username),
Username = Username,
Password = Password,
Email = Email,
ExtraData = new Dictionary<string, string>()
};
}
}
}

View File

@ -2,10 +2,17 @@ using System.Collections.Generic;
using System.Linq;
using IdentityServer4.Models;
namespace Kyoo
namespace Kyoo.Authentication
{
/// <summary>
/// The hard coded context of the identity server.
/// </summary>
public static class IdentityContext
{
/// <summary>
/// The list of identity resources supported (email, profile and openid)
/// </summary>
/// <returns>The list of identity resources supported</returns>
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
@ -16,6 +23,13 @@ namespace Kyoo
};
}
/// <summary>
/// The list of officially supported clients.
/// </summary>
/// <remarks>
/// You can add custom clients in the settings.json file.
/// </remarks>
/// <returns>The list of officially supported clients.</returns>
public static IEnumerable<Client> GetClients()
{
return new List<Client>
@ -23,7 +37,7 @@ namespace Kyoo
new()
{
ClientId = "kyoo.webapp",
AccessTokenType = AccessTokenType.Jwt,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
@ -33,13 +47,17 @@ namespace Kyoo
AllowOfflineAccess = true,
RequireConsent = false,
AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.download", "kyoo.admin" },
AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.admin" },
RedirectUris = { "/", "/silent.html" },
PostLogoutRedirectUris = { "/logout" }
}
};
}
/// <summary>
/// The list of scopes supported by the API.
/// </summary>
/// <returns>The list of scopes</returns>
public static IEnumerable<ApiScope> GetScopes()
{
return new[]
@ -60,11 +78,6 @@ namespace Kyoo
DisplayName = "Allow playback of movies and episodes."
},
new ApiScope
{
Name = "kyoo.download",
DisplayName = "Allow downloading of episodes and movies from kyoo."
},
new ApiScope
{
Name = "kyoo.admin",
DisplayName = "Full access to the admin's API and the public API."
@ -72,13 +85,16 @@ namespace Kyoo
};
}
/// <summary>
/// The list of APIs (this is used to create Audiences)
/// </summary>
/// <returns>The list of apis</returns>
public static IEnumerable<ApiResource> GetApis()
{
return new[]
{
new ApiResource
new ApiResource("kyoo", "Kyoo")
{
Name = "Kyoo",
Scopes = GetScopes().Select(x => x.Name).ToArray()
}
};

View File

@ -0,0 +1,28 @@
namespace Kyoo.Authentication.Models
{
/// <summary>
/// The main authentication options.
/// </summary>
public class AuthenticationOption
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "authentication";
/// <summary>
/// The options for certificates
/// </summary>
public CertificateOption Certificate { get; set; }
/// <summary>
/// Options for permissions
/// </summary>
public PermissionOption Permissions { get; set; }
/// <summary>
/// Root path of user's profile pictures.
/// </summary>
public string ProfilePicturePath { get; set; }
}
}

View File

@ -0,0 +1,26 @@
namespace Kyoo.Authentication.Models
{
/// <summary>
/// A typed option model for the certificate
/// </summary>
public class CertificateOption
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "authentication:certificate";
/// <summary>
/// The path of the certificate file.
/// </summary>
public string File { get; set; }
/// <summary>
/// The path of the old certificate file.
/// </summary>
public string OldFile { get; set; }
/// <summary>
/// The password of the certificates.
/// </summary>
public string Password { get; set; }
}
}

View File

@ -0,0 +1,23 @@
namespace Kyoo.Authentication.Models
{
/// <summary>
/// Permission options.
/// </summary>
public class PermissionOption
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "authentication:permissions";
/// <summary>
/// The default permissions that will be given to a non-connected user.
/// </summary>
public string[] Default { get; set; }
/// <summary>
/// Permissions applied to a new user.
/// </summary>
public string[] NewUser { get; set; }
}
}

View File

@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Kyoo.Authentication.Views
{
/// <summary>
/// The class responsible for login, logout, permissions and claims of a user.
/// </summary>
[Route("api/account")]
[Route("api/accounts")]
[ApiController]
public class AccountApi : Controller, IProfileService
{
/// <summary>
/// The repository to handle users.
/// </summary>
private readonly IUserRepository _users;
/// <summary>
/// A file manager to send profile pictures
/// </summary>
private readonly IFileManager _files;
/// <summary>
/// Options about authentication. Those options are monitored and reloads are supported.
/// </summary>
private readonly IOptions<AuthenticationOption> _options;
/// <summary>
/// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
/// </summary>
/// <param name="users">The user repository to create and manage users</param>
/// <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,
IOptions<AuthenticationOption> options)
{
_users = users;
_files = files;
_options = options;
}
/// <summary>
/// Register a new user and return a OTAC to connect to it.
/// </summary>
/// <param name="request">The DTO register request</param>
/// <returns>A OTAC to connect to this new account</returns>
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
User user = request.ToUser();
user.Permissions = _options.Value.Permissions.NewUser;
user.Password = PasswordUtils.HashPassword(user.Password);
user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
try
{
await _users.Create(user);
}
catch (DuplicatedItemException)
{
return Conflict(new {Errors = new {Duplicate = new[] {"A user with this name already exists"}}});
}
return Ok(new {Otac = user.ExtraData["otac"]});
}
/// <summary>
/// Return an authentication properties based on a stay login property
/// </summary>
/// <param name="stayLogged">Should the user stay logged</param>
/// <returns>Authentication properties based on a stay login</returns>
private static AuthenticationProperties StayLogged(bool stayLogged)
{
if (!stayLogged)
return null;
return new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
};
}
/// <summary>
/// Login the user.
/// </summary>
/// <param name="login">The DTO login request</param>
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest login)
{
User user = await _users.GetOrDefault(x => x.Username == login.Username);
if (user == null)
return Unauthorized();
if (!PasswordUtils.CheckPassword(login.Password, user.Password))
return Unauthorized();
await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(login.StayLoggedIn));
return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
}
/// <summary>
/// Use a OTAC to login a user.
/// </summary>
/// <param name="otac">The OTAC request</param>
[HttpPost("otac-login")]
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
{
// TODO once hstore (Dictionary<string, string> accessor) are supported, use them.
// We retrieve all users, this is inefficient.
User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac);
if (user == null)
return Unauthorized();
if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <=
DateTime.UtcNow)
{
return BadRequest(new
{
code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
});
}
await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(otac.StayLoggedIn));
return Ok();
}
/// <summary>
/// Sign out an user
/// </summary>
[HttpGet("logout")]
[Authorize]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Ok();
}
/// <inheritdoc />
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
if (user == null)
return;
context.IssuedClaims.AddRange(user.GetClaims());
context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions)));
}
/// <inheritdoc />
public async Task IsActiveAsync(IsActiveContext context)
{
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
context.IsActive = user != null;
}
/// <summary>
/// Get the user's profile picture.
/// </summary>
/// <param name="slug">The user slug</param>
/// <returns>The profile picture of the user or 404 if not found</returns>
[HttpGet("picture/{slug}")]
public async Task<IActionResult> GetPicture(string slug)
{
User user = await _users.GetOrDefault(slug);
if (user == null)
return NotFound();
string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
return _files.FileResult(path);
}
/// <summary>
/// Update profile information (email, username, profile picture...)
/// </summary>
/// <param name="data">The new information</param>
/// <returns>The edited user</returns>
[HttpPut]
[Authorize]
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
{
User user = await _users.GetOrDefault(int.Parse(HttpContext.User.GetSubjectId()));
if (user == null)
return Unauthorized();
if (!string.IsNullOrEmpty(data.Email))
user.Email = data.Email;
if (!string.IsNullOrEmpty(data.Username))
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);
await data.Picture.CopyToAsync(file);
}
return await _users.Edit(user, false);
}
/// <summary>
/// Get permissions for a non connected user.
/// </summary>
/// <returns>The list of permissions of a default user.</returns>
[HttpGet("permissions")]
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
{
return _options.Value.Permissions.Default ?? Array.Empty<string>();
}
}
}

View File

@ -7,21 +7,88 @@ using Microsoft.AspNetCore.Mvc;
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 IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false);
public StreamReader GetReader([NotNull] string path);
public Task<ICollection<string>> ListFiles([NotNull] string path);
public Task<bool> Exists([NotNull] string path);
// TODO find a way to handle Transmux/Transcode with this system.
/// <summary>
/// Used for http queries returning a file. This should be used to return local files
/// or proxy them from a distant server
/// </summary>
/// <remarks>
/// If no file exists at the given path or if the path is null, a NotFoundResult is returned
/// to handle it gracefully.
/// </remarks>
/// <param name="path">The path of the file.</param>
/// <param name="rangeSupport">
/// Should the file be downloaded at once or is the client allowed to request only part of the file
/// </param>
/// <param name="type">
/// You can manually specify the content type of your file.
/// For example you can force a file to be returned as plain text using <c>text/plain</c>.
/// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
/// </param>
/// <returns>An <see cref="IActionResult"/> representing the file returned.</returns>
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
/// <summary>
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
/// To return files from an http endpoint, use <see cref="FileResult"/>.
/// </summary>
/// <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);
/// <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);
/// <summary>
/// List files in a directory.
/// </summary>
/// <param name="path">The path of the directory</param>
/// <returns>A list of files's path.</returns>
public Task<ICollection<string>> ListFiles([NotNull] string path);
/// <summary>
/// Check if a file exists at the given path.
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path exists, false otherwise</returns>
public Task<bool> Exists([NotNull] string path);
/// <summary>
/// Get the extra directory of a show.
/// 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="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);
}
}

View File

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

View File

@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
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>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IPlugin
{
/// <summary>
/// A slug to identify this plugin in queries.
/// </summary>
string Slug { get; }
/// <summary>
/// The name of the plugin
/// </summary>
string Name { get; }
/// <summary>
/// The description of this plugin. This will be displayed on the "installed plugins" page.
/// </summary>
string Description { get; }
/// <summary>
/// A list of services that are provided by this service. This allow other plugins to declare dependencies
/// </summary>
/// <remarks>
/// You should put the type's interface that will be register in configure.
/// </remarks>
ICollection<Type> Provides { get; }
/// <summary>
/// A list of types that will be provided only if a condition is met. The condition can be an arbitrary method or
/// a condition based on other type availability. For more information, see <see cref="ConditionalProvides"/>.
/// </summary>
ICollection<ConditionalProvide> ConditionalProvides { get; }
/// <summary>
/// A list of services that are required by this plugin.
/// You can put services that you provide conditionally here if you want.
/// Kyoo will warn the user that this plugin can't be loaded if a required service is not found.
/// </summary>
/// <remarks>
/// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager,
/// put typeof(ILibraryManager).
/// </remarks>
ICollection<Type> Requires { get; }
/// <summary>
/// A configure method that will be run on plugin's startup.
/// </summary>
/// <param name="services">A service container to register new services.</param>
/// <param name="availableTypes">The list of types that are available for this instance. This can be used
/// for conditional type. See <see cref="ProviderCondition.Has(System.Type,System.Collections.Generic.ICollection{System.Type})"/>
/// or <see cref="ProviderCondition.Has(System.Collections.Generic.ICollection{System.Type},System.Collections.Generic.ICollection{System.Type})"/>>
/// You can't simply check on the service collection because some dependencies might be registered after your plugin.
/// </param>
void Configure(IServiceCollection services, ICollection<Type> availableTypes);
/// <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) {}
/// <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) {}
}
/// <summary>
/// A type that will only be provided if a special condition is met. To check that your condition is met,
/// you can check the <see cref="ProviderCondition"/> class.
/// </summary>
public class ConditionalProvide : Tuple<Type, ProviderCondition>
{
/// <summary>
/// Get the type that may be provided
/// </summary>
public Type Type => Item1;
/// <summary>
/// Get the condition.
/// </summary>
public ProviderCondition Condition => Item2;
/// <summary>
/// Create a <see cref="ConditionalProvide"/> from a type and a condition.
/// </summary>
/// <param name="type">The type to provide</param>
/// <param name="condition">The condition</param>
public ConditionalProvide(Type type, ProviderCondition condition)
: base(type, condition)
{ }
/// <summary>
/// Create a <see cref="ConditionalProvide"/> from a tuple of (Type, ProviderCondition).
/// </summary>
/// <param name="tuple">The tuple to convert</param>
public ConditionalProvide((Type type, ProviderCondition condition) tuple)
: base(tuple.type, tuple.condition)
{ }
/// <summary>
/// Implicitly convert a tuple to a <see cref="ConditionalProvide"/>.
/// </summary>
/// <param name="tuple">The tuple to convert</param>
/// <returns>A new <see cref="ConditionalProvide"/> based on the given tuple.</returns>
public static implicit operator ConditionalProvide((Type, Type) tuple) => new (tuple);
}
/// <summary>
/// A condition for a conditional type.
/// </summary>
public class ProviderCondition
{
/// <summary>
/// The condition as a method. If true is returned, the type will be provided.
/// </summary>
public Func<bool> Condition { get; } = () => true;
/// <summary>
/// The list of types that this method needs.
/// </summary>
public ICollection<Type> Needed { get; } = ArraySegment<Type>.Empty;
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a raw function.
/// </summary>
/// <param name="condition">The predicate that will be used as condition</param>
public ProviderCondition(Func<bool> condition)
{
Condition = condition;
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a type. This allow you to inform that a type will
/// only be available if a dependency is met.
/// </summary>
/// <param name="needed">The type that you need</param>
public ProviderCondition(Type needed)
{
Needed = new[] {needed};
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a list of type. This allow you to inform that a type will
/// only be available if a list of dependencies are met.
/// </summary>
/// <param name="needed">The types that you need</param>
public ProviderCondition(ICollection<Type> needed)
{
Needed = needed;
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> with a list of types as dependencies and a predicate
/// for arbitrary conditions.
/// </summary>
/// <param name="needed">The list of dependencies</param>
/// <param name="condition">An arbitrary condition</param>
public ProviderCondition(ICollection<Type> needed, Func<bool> condition)
{
Needed = needed;
Condition = condition;
}
/// <summary>
/// Implicitly convert a type to a <see cref="ProviderCondition"/>.
/// </summary>
/// <param name="type">The type dependency</param>
/// <returns>A <see cref="ProviderCondition"/> that will return true if the given type is available.</returns>
public static implicit operator ProviderCondition(Type type) => new(type);
/// <summary>
/// Implicitly convert a list of type to a <see cref="ProviderCondition"/>.
/// </summary>
/// <param name="types">The list of type dependencies</param>
/// <returns>A <see cref="ProviderCondition"/> that will return true if the given types are available.</returns>
public static implicit operator ProviderCondition(Type[] types) => new(types);
/// <inheritdoc cref="op_Implicit(System.Type[])"/>
public static implicit operator ProviderCondition(List<Type> types) => new(types);
/// <summary>
/// Check if a type is available.
/// </summary>
/// <param name="needed">The type to check</param>
/// <param name="available">The list of types</param>
/// <returns>True if the dependency is met, false otherwise</returns>
public static bool Has(Type needed, ICollection<Type> available)
{
return available.Contains(needed);
}
/// <summary>
/// Check if a list of type are available.
/// </summary>
/// <param name="needed">The list of types to check</param>
/// <param name="available">The list of types</param>
/// <returns>True if the dependencies are met, false otherwise</returns>
public static bool Has(ICollection<Type> needed, ICollection<Type> available)
{
return needed.All(x => Has(x, available));
}
}
}

View File

@ -1,13 +1,56 @@
using System.Collections.Generic;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
/// <summary>
/// A manager to load plugins and retrieve information from them.
/// </summary>
public interface IPluginManager
{
/// <summary>
/// Get a single plugin that match the type and name given.
/// </summary>
/// <param name="name">The name of the plugin</param>
/// <typeparam name="T">The type of the plugin</typeparam>
/// <exception cref="ItemNotFoundException">If no plugins match the query</exception>
/// <returns>A plugin that match the queries</returns>
public T GetPlugin<T>(string name);
public IEnumerable<T> GetPlugins<T>();
public IEnumerable<IPlugin> GetAllPlugins();
public void ReloadPlugins();
/// <summary>
/// Get all plugins of the given type.
/// </summary>
/// <typeparam name="T">The type of plugins to get</typeparam>
/// <returns>A list of plugins matching the given type or an empty list of none match.</returns>
public ICollection<T> GetPlugins<T>();
/// <summary>
/// Get all plugins currently running on Kyoo. This also includes deleted plugins if the app as not been restarted.
/// </summary>
/// <returns>All plugins currently loaded.</returns>
public ICollection<IPlugin> GetAllPlugins();
/// <summary>
/// Load plugins and their dependencies from the plugin directory.
/// </summary>
/// <param name="plugins">
/// An initial plugin list to use.
/// You should not try to put plugins from the plugins directory here as they will get automatically loaded.
/// </param>
public void LoadPlugins(ICollection<IPlugin> plugins);
/// <summary>
/// Configure services adding or removing services as the plugins wants.
/// </summary>
/// <param name="services">The service collection to populate</param>
public void ConfigureServices(IServiceCollection services);
/// <summary>
/// Configure an asp net application applying plugins policies.
/// </summary>
/// <param name="app">The asp net application to configure</param>
public void ConfigureAspnet(IApplicationBuilder app);
}
}

View File

@ -11,7 +11,7 @@ using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
{
/// <summary>
/// Informations about the pagination. How many items should be displayed and where to start.
/// Information about the pagination. How many items should be displayed and where to start.
/// </summary>
public readonly struct Pagination
{
@ -44,7 +44,7 @@ namespace Kyoo.Controllers
}
/// <summary>
/// Informations about how a query should be sorted. What factor should decide the sort and in which order.
/// Information about how a query should be sorted. What factor should decide the sort and in which order.
/// </summary>
/// <typeparam name="T">For witch type this sort applies</typeparam>
public readonly struct Sort<T>
@ -54,7 +54,7 @@ namespace Kyoo.Controllers
/// </summary>
public Expression<Func<T, object>> Key { get; }
/// <summary>
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendent order.
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
/// </summary>
public bool Descendant { get; }
@ -127,21 +127,21 @@ namespace Kyoo.Controllers
/// Get a resource from it's ID.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(int id);
/// <summary>
/// Get a resource from it's slug.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(string slug);
/// <summary>
/// Get the first resource that match the predicate.
/// </summary>
/// <param name="where">A predicate to filter the resource.</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(Expression<Func<T, bool>> where);
@ -175,7 +175,7 @@ namespace Kyoo.Controllers
/// Get every resources that match all filters
/// </summary>
/// <param name="where">A filter predicate</param>
/// <param name="sort">Sort informations about the query (sort by, sort order)</param>
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
@ -205,86 +205,85 @@ namespace Kyoo.Controllers
/// Create a new resource.
/// </summary>
/// <param name="obj">The item to register</param>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
/// <returns>The resource registers and completed by database's information (related items & so on)</returns>
Task<T> Create([NotNull] T obj);
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="obj">The object to create</param>
/// <param name="silentFail">Allow issues to occurs in this method. Every issue is catched and ignored.</param>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false);
Task<T> CreateIfNotExists([NotNull] T obj);
/// <summary>
/// Edit a resource
/// </summary>
/// <param name="edited">The resourcce to edit, it's ID can't change.</param>
/// <param name="edited">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
/// <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>
Task<T> Edit([NotNull] T edited, bool resetOld);
/// <summary>
/// Delete a resource by it's ID
/// </summary>
/// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete(int id);
/// <summary>
/// Delete a resource by it's slug
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete(string slug);
/// <summary>
/// Delete a resource
/// </summary>
/// <param name="obj">The resource to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete([NotNull] T obj);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">One or multiple resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">An enumerable of resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<T> objs);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">One or multiple resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <param name="ids">One or multiple resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">An enumearble of resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <param name="ids">An enumerable of resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">One or multiple resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <param name="slugs">One or multiple resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">An enumerable of resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <param name="slugs">An enumerable of resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
}
@ -294,7 +293,7 @@ namespace Kyoo.Controllers
public interface IShowRepository : IRepository<Show>
{
/// <summary>
/// Link a show to a collection and/or a library. The given show is now part of thoses containers.
/// Link a show to a collection and/or a library. The given show is now part of those containers.
/// If both a library and a collection are given, the collection is added to the library too.
/// </summary>
/// <param name="showID">The ID of the show</param>
@ -306,7 +305,7 @@ namespace Kyoo.Controllers
/// Get a show's slug from it's ID.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <exception cref="ItemNotFound">If a show with the given ID is not found.</exception>
/// <exception cref="ItemNotFoundException">If a show with the given ID is not found.</exception>
/// <returns>The show's slug</returns>
Task<string> GetSlug(int showID);
}
@ -321,7 +320,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber);
@ -330,7 +329,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber);
@ -362,7 +361,7 @@ namespace Kyoo.Controllers
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary>
@ -371,7 +370,7 @@ namespace Kyoo.Controllers
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
@ -397,7 +396,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(int showID, int absoluteNumber);
/// <summary>
@ -405,7 +404,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
}
@ -420,8 +419,8 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The track found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
@ -429,7 +428,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
/// <returns>The track found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
}
@ -439,16 +438,16 @@ namespace Kyoo.Controllers
public interface ILibraryRepository : IRepository<Library> { }
/// <summary>
/// A repository to handle library items (A wrapper arround shows and collections).
/// A repository to handle library items (A wrapper around shows and collections).
/// </summary>
public interface ILibraryItemRepository : IRepository<LibraryItem>
{
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
@ -456,7 +455,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default,
Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
@ -470,11 +469,11 @@ namespace Kyoo.Controllers
) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
@ -482,7 +481,7 @@ namespace Kyoo.Controllers
Sort<LibraryItem> sort = default,
Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
@ -521,7 +520,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID,
@ -547,7 +546,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
@ -573,7 +572,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="id">The id of the person</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
@ -599,7 +598,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="slug">The slug of the person</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
@ -631,7 +630,7 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
@ -642,11 +641,16 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">A sort by expression</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID, object>> sort,
Pagination limit = default
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
}
/// <summary>
/// A repository to handle users.
/// </summary>
public interface IUserRepository : IRepository<User> {}
}

View File

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Models.Attributes;
namespace Kyoo.Controllers
{
/// <summary>
/// A single task parameter. This struct contains metadata to display and utility functions to get them in the taks.
/// </summary>
/// <remarks>This struct will be used to generate the swagger documentation of the task.</remarks>
public record TaskParameter
{
/// <summary>
/// The name of this parameter.
/// </summary>
public string Name { get; init; }
/// <summary>
/// The description of this parameter.
/// </summary>
public string Description { get; init; }
/// <summary>
/// The type of this parameter.
/// </summary>
public Type Type { get; init; }
/// <summary>
/// Is this parameter required or can it be ignored?
/// </summary>
public bool IsRequired { get; init; }
/// <summary>
/// The default value of this object.
/// </summary>
public object DefaultValue { get; init; }
/// <summary>
/// The value of the parameter.
/// </summary>
private object Value { get; init; }
/// <summary>
/// Create a new 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 Create<T>(string name, string description)
{
return new()
{
Name = name,
Description = description,
Type = typeof(T)
};
}
/// <summary>
/// Create a parameter's value to give to a task.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="value">The value of the parameter. It's type will be used as parameter's type.</param>
/// <typeparam name="T">The type of the parameter</typeparam>
/// <returns>A TaskParameter that can be used as value.</returns>
public static TaskParameter CreateValue<T>(string name, T value)
{
return new()
{
Name = name,
Type = typeof(T),
Value = value
};
}
/// <summary>
/// Create a parameter's value for the current parameter.
/// </summary>
/// <param name="value">The value to use</param>
/// <returns>A new parameter's value for this current parameter</returns>
public TaskParameter CreateValue(object value)
{
return this with {Value = value};
}
/// <summary>
/// Get the value of this parameter. If the value is of the wrong type, it will be converted.
/// </summary>
/// <typeparam name="T">The type of this parameter</typeparam>
/// <returns>The value of this parameter.</returns>
public T As<T>()
{
return (T)Convert.ChangeType(Value, typeof(T));
}
}
/// <summary>
/// A parameters container implementing an indexer to allow simple usage of parameters.
/// </summary>
public class TaskParameters : List<TaskParameter>
{
/// <summary>
/// An indexer that return the parameter with the specified name.
/// </summary>
/// <param name="name">The name of the task (case sensitive)</param>
public TaskParameter this[string name] => this.FirstOrDefault(x => x.Name == name);
/// <summary>
/// Create a new, empty, <see cref="TaskParameters"/>
/// </summary>
public TaskParameters() {}
/// <summary>
/// Create a <see cref="TaskParameters"/> with an initial parameters content
/// </summary>
/// <param name="parameters">The list of parameters</param>
public TaskParameters(IEnumerable<TaskParameter> parameters)
{
AddRange(parameters);
}
}
/// <summary>
/// A common interface that tasks should implement.
/// </summary>
public interface ITask
{
/// <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>
/// An optional message to display to help the user.
/// </summary>
public string HelpMessage { get; }
/// <summary>
/// Should this task be automatically run at app startup?
/// </summary>
public bool RunOnStartup { get; }
/// <summary>
/// The priority of this task. Only used if <see cref="RunOnStartup"/> is true.
/// It allow one to specify witch task will be started first as tasked are run on a Priority's descending order.
/// </summary>
public int Priority { get; }
/// <summary>
/// Start this task.
/// </summary>
/// <param name="arguments">The list of parameters.</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>
/// <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.
/// </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();
}
}

View File

@ -1,13 +1,34 @@
using System;
using System.Collections.Generic;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
{
/// <summary>
/// A service to handle long running tasks.
/// </summary>
/// <remarks>The concurrent number of running tasks is implementation dependent.</remarks>
public interface ITaskManager
{
bool StartTask(string taskSlug, string arguments = null);
ITask GetRunningTask();
void ReloadTask();
IEnumerable<ITask> GetAllTasks();
/// <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);
/// <summary>
/// Get all currently running tasks
/// </summary>
/// <returns>A list of currently running tasks.</returns>
ICollection<ITask> GetRunningTasks();
/// <summary>
/// Get all available tasks
/// </summary>
/// <returns>A list of every tasks that this instance know.</returns>
ICollection<ITask> GetAllTasks();
}
}

View File

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

View File

@ -40,7 +40,7 @@ namespace Kyoo.Controllers
/// <summary>
/// Create a new <see cref="LibraryManager"/> instancce with every repository available.
/// Create a new <see cref="LibraryManager"/> instance with every repository available.
/// </summary>
/// <param name="repositories">The list of repositories that this library manager should manage.
/// If a repository for every base type is not available, this instance won't be stable.</param>
@ -66,7 +66,7 @@ namespace Kyoo.Controllers
{
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
return ret;
throw new ItemNotFound();
throw new ItemNotFoundException($"No repository found for the type {typeof(T).Name}.");
}
/// <inheritdoc />

View File

@ -21,8 +21,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.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" />
</ItemGroup>

View File

@ -0,0 +1,91 @@
using System;
using System.Reflection;
namespace Kyoo
{
/// <summary>
/// Static class containing MethodOf calls.
/// </summary>
public static class MethodOfUtils
{
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf(Action action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T>(Action<T> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2>(Action<T, T2> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3>(Action<T, T2, T3> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T>(Func<T> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2>(Func<T, T2> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3>(Func<T, T2, T3> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3, T4>(Func<T, T2, T3, T4> action)
{
return action.Method;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using JetBrains.Annotations;
using Kyoo.Controllers;
namespace Kyoo.Models.Attributes
{
/// <summary>
/// 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"/>
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
public class InjectedAttribute : Attribute { }
}

View File

@ -2,10 +2,21 @@ using System;
namespace Kyoo.Models.Attributes
{
public class NotMergableAttribute : Attribute { }
/// <summary>
/// Specify that a property can't be merged.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotMergeableAttribute : Attribute { }
/// <summary>
/// An interface with a method called when this object is merged.
/// </summary>
public interface IOnMerge
{
/// <summary>
/// This function is called after the object has been merged.
/// </summary>
/// <param name="merged">The object that has been merged with this.</param>
void OnMerge(object merged);
}
}

View File

@ -0,0 +1,154 @@
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Models.Permissions
{
/// <summary>
/// The kind of permission needed.
/// </summary>
public enum Kind
{
Read,
Write,
Create,
Delete
}
/// <summary>
/// Specify permissions needed for the API.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission as string.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="permission">The kind of permission needed</param>
public PermissionAttribute(string type, Kind permission)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
Kind = permission;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
/// <summary>
/// Return this permission attribute as a string
/// </summary>
/// <returns>The string representation.</returns>
public string AsPermissionString()
{
return Type;
}
}
/// <summary>
/// Specify one part of a permissions needed for the API (the kind or the type).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PartialPermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission type.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
public PartialPermissionAttribute(string type)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
}
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="permission">The kind of permission needed</param>
public PartialPermissionAttribute(Kind permission)
{
Kind = permission;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
}
/// <summary>
/// A service to validate permissions
/// </summary>
public interface IPermissionValidator
{
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">The permission attribute to validate</param>
/// <returns>An authorization filter used to validate the permission</returns>
IFilterMetadata Create(PermissionAttribute attribute);
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
/// </param>
/// <returns>An authorization filter used to validate the permission</returns>
IFilterMetadata Create(PartialPermissionAttribute attribute);
}
}

View File

@ -1,19 +1,36 @@
using System;
using System.Runtime.Serialization;
namespace Kyoo.Models.Exceptions
{
/// <summary>
/// An exception raised when an item already exists in the database.
/// </summary>
[Serializable]
public class DuplicatedItemException : Exception
{
public override string Message { get; }
/// <summary>
/// Create a new <see cref="DuplicatedItemException"/> with the default message.
/// </summary>
public DuplicatedItemException()
{
Message = "Already exists in the databse.";
}
: base("Already exists in the database.")
{ }
/// <summary>
/// Create a new <see cref="DuplicatedItemException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use</param>
public DuplicatedItemException(string message)
{
Message = message;
}
: base(message)
{ }
/// <summary>
/// The serialization constructor
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected DuplicatedItemException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using System;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -22,7 +23,7 @@ namespace Kyoo.Models
public int? StartYear { get; set; }
public int? EndYear { get; set; }
[SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; }
private string _type => Type == ItemType.Collection ? "collection" : "show";
[UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show";
public ItemType Type { get; set; }
public LibraryItem() {}

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
namespace Kyoo.Models
@ -60,6 +61,7 @@ namespace Kyoo.Models
public Link() {}
[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
public Link(T1 first, T2 second, bool privateItems = false)
: base(first, second)
{

View File

@ -22,14 +22,5 @@ namespace Kyoo.Models
public string DataID { get; set; }
public string Link { get; set; }
public MetadataID() { }
public MetadataID(Provider provider, string dataID, string link)
{
Provider = provider;
DataID = dataID;
Link = link;
}
}
}

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -14,27 +13,5 @@ namespace Kyoo.Models
[SerializeIgnore] public virtual Show Show { get; set; }
public string Role { get; set; }
public string Type { get; set; }
public PeopleRole() {}
public PeopleRole(People people, Show show, string role, string type)
{
People = people;
Show = show;
Role = role;
Type = type;
}
public PeopleRole(string slug,
string name,
string role,
string type,
string poster,
IEnumerable<MetadataID> externalIDs)
{
People = new People(slug, name, poster, externalIDs);
Role = role;
Type = type;
}
}
}

View File

@ -1,10 +0,0 @@
using System.Collections.Generic;
namespace Kyoo.Models
{
public interface IPlugin
{
public string Name { get; }
public ICollection<ITask> Tasks { get; }
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -32,48 +31,6 @@ namespace Kyoo.Models
[EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
public Episode() { }
public Episode(int seasonNumber,
int episodeNumber,
int absoluteNumber,
string title,
string overview,
DateTime? releaseDate,
int runtime,
string thumb,
IEnumerable<MetadataID> externalIDs)
{
SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber;
AbsoluteNumber = absoluteNumber;
Title = title;
Overview = overview;
ReleaseDate = releaseDate;
Runtime = runtime;
Thumb = thumb;
ExternalIDs = externalIDs?.ToArray();
}
public Episode(int showID,
int seasonID,
int seasonNumber,
int episodeNumber,
int absoluteNumber,
string path,
string title,
string overview,
DateTime? releaseDate,
int runtime,
string poster,
IEnumerable<MetadataID> externalIDs)
: this(seasonNumber, episodeNumber, absoluteNumber, title, overview, releaseDate, runtime, poster, externalIDs)
{
ShowID = showID;
SeasonID = seasonID;
Path = path;
}
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber)
{
if (showSlug == null)

View File

@ -1,30 +1,23 @@
using System;
using System.Collections.Generic;
namespace Kyoo.Models
{
/// <summary>
/// An interface to represent a resource that can be retrieved from the database.
/// </summary>
public interface IResource
{
/// <summary>
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
/// </summary>
public int ID { get; set; }
/// <summary>
/// A human-readable identifier that can be used instead of an ID.
/// A slug must be unique for a type of resource but it can be changed.
/// </summary>
/// <remarks>
/// There is no setter for a slug since it can be computed from other fields.
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
/// </remarks>
public string Slug { get; }
}
public class ResourceComparer<T> : IEqualityComparer<T> where T : IResource
{
public bool Equals(T x, T y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null))
return false;
if (ReferenceEquals(y, null))
return false;
return x.ID == y.ID || x.Slug == y.Slug;
}
public int GetHashCode(T obj)
{
return HashCode.Combine(obj.ID, obj.Slug);
}
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -21,15 +20,5 @@ namespace Kyoo.Models
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif
public Library() { }
public Library(string slug, string name, IEnumerable<string> paths, IEnumerable<Provider> providers)
{
Slug = slug;
Name = name;
Paths = paths?.ToArray();
Providers = providers?.ToArray();
}
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -13,15 +12,5 @@ namespace Kyoo.Models
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<PeopleRole> Roles { get; set; }
public People() {}
public People(string slug, string name, string poster, IEnumerable<MetadataID> externalIDs)
{
Slug = slug;
Name = name;
Poster = poster;
ExternalIDs = externalIDs?.ToArray();
}
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
@ -22,24 +21,5 @@ namespace Kyoo.Models
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
public Season() { }
public Season(int showID,
int seasonNumber,
string title,
string overview,
int? year,
string poster,
IEnumerable<MetadataID> externalIDs)
{
ShowID = showID;
SeasonNumber = seasonNumber;
Title = title;
Overview = overview;
Year = year;
Poster = poster;
ExternalIDs = externalIDs?.ToArray();
}
}
}

View File

@ -42,62 +42,6 @@ namespace Kyoo.Models
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> GenreLinks { get; set; }
#endif
public Show() { }
public Show(string slug,
string title,
IEnumerable<string> aliases,
string path, string overview,
string trailerUrl,
IEnumerable<Genre> genres,
Status? status,
int? startYear,
int? endYear,
IEnumerable<MetadataID> externalIDs)
{
Slug = slug;
Title = title;
Aliases = aliases?.ToArray();
Path = path;
Overview = overview;
TrailerUrl = trailerUrl;
Genres = genres?.ToArray();
Status = status;
StartYear = startYear;
EndYear = endYear;
ExternalIDs = externalIDs?.ToArray();
}
public Show(string slug,
string title,
IEnumerable<string> aliases,
string path,
string overview,
string trailerUrl,
Status? status,
int? startYear,
int? endYear,
string poster,
string logo,
string backdrop,
IEnumerable<MetadataID> externalIDs)
{
Slug = slug;
Title = title;
Aliases = aliases?.ToArray();
Path = path;
Overview = overview;
TrailerUrl = trailerUrl;
Status = status;
StartYear = startYear;
EndYear = endYear;
Poster = poster;
Logo = logo;
Backdrop = backdrop;
ExternalIDs = externalIDs?.ToArray();
}
public string GetID(string provider)
{
return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID;

View File

@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace Kyoo.Models
{
/// <summary>
/// A single user of the app.
/// </summary>
public class User : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// A username displayed to the user.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The user email address.
/// </summary>
public string Email { get; set; }
/// <summary>
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
/// </summary>
public string Password { get; set; }
/// <summary>
/// The list of permissions of the user. The format of this is implementation dependent.
/// </summary>
public string[] Permissions { get; set; }
/// <summary>
/// Arbitrary extra data that can be used by specific authentication implementations.
/// </summary>
public Dictionary<string, string> ExtraData { get; set; }
/// <summary>
/// The list of shows the user has finished.
/// </summary>
public ICollection<Show> Watched { get; set; }
/// <summary>
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
/// </summary>
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
#if ENABLE_INTERNAL_LINKS
/// <summary>
/// Links between Users and Shows.
/// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; }
#endif
}
/// <summary>
/// Metadata of episode currently watching by an user
/// </summary>
public class WatchedEpisode : Link<User, Episode>
{
/// <summary>
/// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
/// </summary>
public int WatchedPercentage { get; set; }
}
}

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Kyoo.Models
{
public interface ITask
{
public string Slug { get; }
public string Name { get; }
public string Description { get; }
public string HelpMessage { get; }
public bool RunOnStartup { get; }
public int Priority { get; }
public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null);
public Task<IEnumerable<string>> GetPossibleParameters();
public int? Progress();
}
}

66
Kyoo.Common/Module.cs Normal file
View File

@ -0,0 +1,66 @@
using System;
using Kyoo.Controllers;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo
{
/// <summary>
/// A static class with helper functions to setup external modules
/// </summary>
public static class Module
{
/// <summary>
/// Register a new task to the container.
/// </summary>
/// <param name="services">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)
where T : class, ITask
{
services.AddSingleton<ITask, T>();
return services;
}
/// <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>
/// <typeparam name="T">The type of the repository.</typeparam>
/// <remarks>
/// If your repository implements a special interface, please use <see cref="AddRepository{T,T2}"/>
/// </remarks>
/// <returns>The initial container.</returns>
public static IServiceCollection AddRepository<T>(this IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Scoped)
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;
}
/// <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>
/// <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}"/>
/// </remarks>
/// <returns>The initial container.</returns>
public static IServiceCollection AddRepository<T, T2>(this IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Scoped)
where T2 : IBaseRepository, T
{
services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T2), lifetime));
return services.AddRepository<T2>(lifetime);
}
}
}

View File

@ -146,7 +146,7 @@ namespace Kyoo
}
/// <summary>
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergableAttribute"/> attribute
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <param name="first">The object to assign</param>
@ -158,7 +158,7 @@ namespace Kyoo
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null);
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
@ -191,7 +191,7 @@ namespace Kyoo
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null);
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
@ -215,7 +215,7 @@ namespace Kyoo
/// <summary>
/// An advanced <see cref="Complete{T}"/> function.
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
/// Enumerables will be merged (concatened).
/// Enumerable will be merged (concatenated).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary>
/// <param name="first">The object to complete</param>
@ -232,7 +232,7 @@ namespace Kyoo
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null);
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
@ -529,9 +529,9 @@ namespace Kyoo
await action(i);
}
private static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
{
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public | BindingFlags.NonPublic)
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)
@ -712,70 +712,18 @@ namespace Kyoo
}, TaskContinuationOptions.ExecuteSynchronously);
}
public static Expression<Func<T, bool>> ResourceEquals<T>(IResource obj)
where T : IResource
/// <summary>
/// Get a friendly type name (supporting generics)
/// For example a list of string will be displayed as List&lt;string&gt; and not as List`1.
/// </summary>
/// <param name="type">The type to use</param>
/// <returns>The friendly name of the type</returns>
public static string FriendlyName(this Type type)
{
if (obj.ID > 0)
return x => x.ID == obj.ID || x.Slug == obj.Slug;
return x => x.Slug == obj.Slug;
}
public static Func<T, bool> ResourceEqualsFunc<T>(IResource obj)
where T : IResource
{
if (obj.ID > 0)
return x => x.ID == obj.ID || x.Slug == obj.Slug;
return x => x.Slug == obj.Slug;
}
public static bool ResourceEquals([CanBeNull] object first, [CanBeNull] object second)
{
if (ReferenceEquals(first, second))
return true;
if (first is IResource f && second is IResource s)
return ResourceEquals(f, s);
IEnumerable eno = first as IEnumerable;
IEnumerable ens = second as IEnumerable;
if (eno == null || ens == null)
throw new ArgumentException("Arguments are not resources or lists of resources.");
Type type = GetEnumerableType(eno);
if (typeof(IResource).IsAssignableFrom(type))
return ResourceEquals(eno.Cast<IResource>(), ens.Cast<IResource>());
return RunGenericMethod<bool>(typeof(Enumerable), "SequenceEqual", type, first, second);
}
public static bool ResourceEquals<T>([CanBeNull] T first, [CanBeNull] T second)
where T : IResource
{
if (ReferenceEquals(first, second))
return true;
if (first == null || second == null)
return false;
return first.ID == second.ID || first.Slug == second.Slug;
}
public static bool ResourceEquals<T>([CanBeNull] IEnumerable<T> first, [CanBeNull] IEnumerable<T> second)
where T : IResource
{
if (ReferenceEquals(first, second))
return true;
if (first == null || second == null)
return false;
return first.SequenceEqual(second, new ResourceComparer<T>());
}
public static bool LinkEquals<T>([CanBeNull] T first, int? firstID, [CanBeNull] T second, int? secondID)
where T : IResource
{
if (ResourceEquals(first, second))
return true;
if (first == null && second != null
&& firstID == second.ID)
return true;
if (first != null && second == null
&& first.ID == secondID)
return true;
return firstID == secondID;
if (!type.IsGenericType)
return type.Name;
string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName()));
return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>";
}
}
}

View File

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
@ -21,40 +21,32 @@ namespace Kyoo.CommonApi
public CrudApi(IRepository<T> repository, IConfiguration configuration)
{
_repository = repository;
BaseURL = configuration.GetValue<string>("public_url").TrimEnd('/');
BaseURL = configuration.GetValue<string>("publicUrl").TrimEnd('/');
}
[HttpGet("{id:int}")]
[Authorize(Policy = "Read")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<T>> Get(int id)
{
try
{
return await _repository.Get(id);
}
catch (ItemNotFound)
{
T ret = await _repository.GetOrDefault(id);
if (ret == null)
return NotFound();
}
return ret;
}
[HttpGet("{slug}")]
[Authorize(Policy = "Read")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<T>> Get(string slug)
{
try
{
return await _repository.Get(slug);
}
catch (ItemNotFound)
{
T ret = await _repository.Get(slug);
if (ret == null)
return NotFound();
}
return ret;
}
[HttpGet("count")]
[Authorize(Policy = "Read")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<int>> GetCount([FromQuery] Dictionary<string, string> where)
{
try
@ -68,7 +60,7 @@ namespace Kyoo.CommonApi
}
[HttpGet]
[Authorize(Policy = "Read")]
[PartialPermission(Kind.Read)]
public virtual async Task<ActionResult<Page<T>>> GetAll([FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
@ -98,7 +90,7 @@ namespace Kyoo.CommonApi
}
[HttpPost]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Create)]
public virtual async Task<ActionResult<T>> Create([FromBody] T resource)
{
try
@ -111,13 +103,13 @@ namespace Kyoo.CommonApi
}
catch (DuplicatedItemException)
{
T existing = await _repository.Get(resource.Slug);
T existing = await _repository.GetOrDefault(resource.Slug);
return Conflict(existing);
}
}
[HttpPut]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource)
{
try
@ -129,14 +121,14 @@ namespace Kyoo.CommonApi
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpPut("{id:int}")]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit(int id, [FromQuery] bool resetOld, [FromBody] T resource)
{
resource.ID = id;
@ -144,14 +136,14 @@ namespace Kyoo.CommonApi
{
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpPut("{slug}")]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Write)]
public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource)
{
try
@ -160,21 +152,21 @@ namespace Kyoo.CommonApi
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(int id)
{
try
{
await _repository.Delete(id);
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}
@ -183,14 +175,14 @@ namespace Kyoo.CommonApi
}
[HttpDelete("{slug}")]
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(string slug)
{
try
{
await _repository.Delete(slug);
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}
@ -198,14 +190,14 @@ namespace Kyoo.CommonApi
return Ok();
}
[Authorize(Policy = "Write")]
[PartialPermission(Kind.Delete)]
public virtual async Task<IActionResult> Delete(Dictionary<string, string> where)
{
try
{
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where));
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
return NotFound();
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Controllers;
@ -7,17 +8,17 @@ using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Npgsql;
namespace Kyoo
{
/// <summary>
/// The database handle used for all local repositories.
/// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic.
/// </summary>
/// <remarks>
/// It should not be used directly, to access the database use a <see cref="ILibraryManager"/> or repositories.
/// </remarks>
public class DatabaseContext : DbContext
public abstract class DatabaseContext : DbContext
{
/// <summary>
/// All libraries of Kyoo. See <see cref="Library"/>.
@ -63,11 +64,20 @@ namespace Kyoo
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
/// </summary>
public DbSet<MetadataID> MetadataIds { get; set; }
/// <summary>
/// The list of registered users.
/// </summary>
public DbSet<User> Users { get; set; }
/// <summary>
/// All people's role. See <see cref="PeopleRole"/>.
/// </summary>
public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
/// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
/// <summary>
/// Get a generic link between two resource types.
@ -82,32 +92,31 @@ namespace Kyoo
{
return Set<Link<T1, T2>>();
}
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
public DatabaseContext()
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
}
/// <summary>
/// Create a new <see cref="DatabaseContext"/>.
/// The default constructor
/// </summary>
/// <param name="options">Connection options to use (witch databse provider to use, connection strings...)</param>
public DatabaseContext(DbContextOptions<DatabaseContext> options)
protected DatabaseContext() { }
/// <summary>
/// Create a new <see cref="DatabaseContext"/> using specific options
/// </summary>
/// <param name="options">The options to use.</param>
protected DatabaseContext(DbContextOptions options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
}
{ }
/// <summary>
/// Set basic configurations (like preventing query tracking)
/// </summary>
/// <param name="optionsBuilder">An option builder to fill.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
/// <summary>
/// Set database parameters to support every types of Kyoo.
/// </summary>
@ -116,10 +125,6 @@ namespace Kyoo
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasPostgresEnum<Status>();
modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<Track>()
.Property(t => t.IsDefault)
.ValueGeneratedNever();
@ -188,6 +193,17 @@ namespace Kyoo
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<Show, Genre>.PrimaryKey));
modelBuilder.Entity<User>()
.HasMany(x => x.Watched)
.WithMany("users")
.UsingEntity<Link<User, Show>>(
y => y
.HasOne(x => x.Second)
.WithMany(),
y => y
.HasOne(x => x.First)
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<User, Show>.PrimaryKey));
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Show)
@ -210,6 +226,9 @@ namespace Kyoo
.WithMany(x => x.MetadataLinks)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WatchedEpisode>()
.HasKey(x => new {First = x.FirstID, Second = x.SecondID});
modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
@ -217,6 +236,7 @@ namespace Kyoo
modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Collection>()
.HasIndex(x => x.Slug)
@ -248,13 +268,16 @@ namespace Kyoo
modelBuilder.Entity<Track>()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique();
modelBuilder.Entity<User>()
.HasIndex(x => x.Slug)
.IsUnique();
}
/// <summary>
/// Return a new or an in cache temporary object wih the same ID as the one given
/// </summary>
/// <param name="model">If a resource with the same ID is found in the database, it will be used.
/// <see cref="model"/> will be used overwise</param>
/// <see cref="model"/> will be used otherwise</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>A resource that is now tracked by this context.</returns>
public T GetTemporaryObject<T>(T model)
@ -467,13 +490,9 @@ namespace Kyoo
/// <summary>
/// Check if the exception is a duplicated exception.
/// </summary>
/// <remarks>WARNING: this only works for PostgreSQL</remarks>
/// <param name="ex">The exception to check</param>
/// <returns>True if the exception is a duplicate exception. False otherwise</returns>
private static bool IsDuplicateException(Exception ex)
{
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
}
protected abstract bool IsDuplicateException(Exception ex);
/// <summary>
/// Delete every changes that are on this context.
@ -486,5 +505,15 @@ namespace Kyoo
entry.State = EntityState.Detached;
}
}
/// <summary>
/// Perform a case insensitive like operation.
/// </summary>
/// <param name="query">An accessor to get the item that will be checked.</param>
/// <param name="format">The second operator of the like format.</param>
/// <typeparam name="T">The type of the item to query</typeparam>
/// <returns>An expression representing the like query. It can directly be passed to a where call.</returns>
public abstract Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format);
}
}

View File

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

View File

@ -12,10 +12,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -46,13 +46,13 @@ namespace Kyoo.Controllers
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
/// </summary>
/// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The tracked resource with the given ID</returns>
protected virtual async Task<T> GetWithTracking(int id)
{
T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}");
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret;
}
@ -61,7 +61,7 @@ namespace Kyoo.Controllers
{
T ret = await GetOrDefault(id);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}");
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret;
}
@ -70,7 +70,7 @@ namespace Kyoo.Controllers
{
T ret = await GetOrDefault(slug);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the slug {slug}");
throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}");
return ret;
}
@ -79,7 +79,7 @@ namespace Kyoo.Controllers
{
T ret = await GetOrDefault(where);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the given predicate.");
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret;
}
@ -118,14 +118,14 @@ namespace Kyoo.Controllers
/// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>The filtered query</returns>
protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default)
{
return ApplyFilters(query, Get, DefaultSort, where, sort, limit);
return ApplyFilters(query, GetOrDefault, DefaultSort, where, sort, limit);
}
/// <summary>
@ -137,7 +137,7 @@ namespace Kyoo.Controllers
/// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>The filtered query</returns>
protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query,
Func<int, Task<TValue>> get,
@ -193,14 +193,14 @@ namespace Kyoo.Controllers
}
/// <inheritdoc/>
public virtual async Task<T> CreateIfNotExists(T obj, bool silentFail = false)
public virtual async Task<T> CreateIfNotExists(T obj)
{
try
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
T old = await Get(obj.Slug);
T old = await GetOrDefault(obj.Slug);
if (old != null)
return old;
@ -208,13 +208,7 @@ namespace Kyoo.Controllers
}
catch (DuplicatedItemException)
{
return await Get(obj.Slug);
}
catch
{
if (silentFail)
return default;
throw;
return await GetOrDefault(obj.Slug);
}
}
@ -244,7 +238,7 @@ namespace Kyoo.Controllers
}
/// <summary>
/// An overridable method to edit relatiosn of a resource.
/// An overridable method to edit relation of a resource.
/// </summary>
/// <param name="resource">The non edited resource</param>
/// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param>

View File

@ -0,0 +1,41 @@
<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>
</PropertyGroup>
<PropertyGroup>
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -1,16 +1,18 @@
// <auto-generated />
using System;
using Kyoo;
using System.Collections.Generic;
using Kyoo.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20210420221509_Initial")]
[DbContext(typeof(PostgresContext))]
[Migration("20210507203809_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -21,7 +23,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("ProductVersion", "5.0.5")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.Collection", b =>
@ -224,6 +226,21 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Link<Show, Genre>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{
b.Property<int>("ID")
@ -419,8 +436,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int?>("StartYear")
.HasColumnType("integer");
b.Property<int?>("Status")
.HasColumnType("integer");
b.Property<Status?>("Status")
.HasColumnType("status");
b.Property<int?>("StudioID")
.HasColumnType("integer");
@ -497,8 +514,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<StreamType>("Type")
.HasColumnType("stream_type");
b.HasKey("ID");
@ -508,6 +525,58 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Tracks");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("ExtraData")
.HasColumnType("jsonb");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string[]>("Permissions")
.HasColumnType("text[]");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.Property<int>("WatchedPercentage")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.HasOne("Kyoo.Models.Season", "Season")
@ -620,6 +689,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{
b.HasOne("Kyoo.Models.Episode", "Episode")
@ -709,6 +797,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
@ -779,6 +886,13 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
{
b.Navigation("Shows");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618
}
}

View File

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal
namespace Kyoo.Postgresql.Migrations
{
public partial class Initial : Migration
{
@ -103,6 +105,24 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
table.PrimaryKey("PK_Studios", x => x.ID);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
ID = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slug = table.Column<string>(type: "text", nullable: false),
Username = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
Password = table.Column<string>(type: "text", nullable: true),
Permissions = table.Column<string[]>(type: "text[]", nullable: true),
ExtraData = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.ID);
});
migrationBuilder.CreateTable(
name: "Link<Library, Collection>",
columns: table => new
@ -162,7 +182,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
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<Status>(type: "status", nullable: true),
TrailerUrl = table.Column<string>(type: "text", nullable: true),
StartYear = table.Column<int>(type: "integer", nullable: true),
EndYear = table.Column<int>(type: "integer", nullable: true),
@ -255,6 +275,30 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link<User, Show>",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link<User, Show>", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_Link<User, Show>_Shows_SecondID",
column: x => x.SecondID,
principalTable: "Shows",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Link<User, Show>_Users_FirstID",
column: x => x.FirstID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PeopleRoles",
columns: table => new
@ -406,7 +450,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
Language = table.Column<string>(type: "text", nullable: true),
Codec = table.Column<string>(type: "text", nullable: true),
Path = table.Column<string>(type: "text", nullable: true),
Type = table.Column<int>(type: "integer", nullable: false)
Type = table.Column<StreamType>(type: "stream_type", nullable: false)
},
constraints: table =>
{
@ -419,6 +463,31 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "WatchedEpisodes",
columns: table => new
{
FirstID = table.Column<int>(type: "integer", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false),
WatchedPercentage = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WatchedEpisodes", x => new { x.FirstID, x.SecondID });
table.ForeignKey(
name: "FK_WatchedEpisodes_Episodes_SecondID",
column: x => x.SecondID,
principalTable: "Episodes",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WatchedEpisodes_Users_FirstID",
column: x => x.FirstID,
principalTable: "Users",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Collections_Slug",
table: "Collections",
@ -473,6 +542,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
table: "Link<Show, Genre>",
column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_Link<User, Show>_SecondID",
table: "Link<User, Show>",
column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_MetadataIds_EpisodeID",
table: "MetadataIds",
@ -548,6 +622,17 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
table: "Tracks",
columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Slug",
table: "Users",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WatchedEpisodes_SecondID",
table: "WatchedEpisodes",
column: "SecondID");
}
protected override void Down(MigrationBuilder migrationBuilder)
@ -567,6 +652,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
migrationBuilder.DropTable(
name: "Link<Show, Genre>");
migrationBuilder.DropTable(
name: "Link<User, Show>");
migrationBuilder.DropTable(
name: "MetadataIds");
@ -576,6 +664,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
migrationBuilder.DropTable(
name: "Tracks");
migrationBuilder.DropTable(
name: "WatchedEpisodes");
migrationBuilder.DropTable(
name: "Collections");
@ -594,6 +685,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
migrationBuilder.DropTable(
name: "Episodes");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Seasons");

View File

@ -1,15 +1,17 @@
// <auto-generated />
using System;
using Kyoo;
using System.Collections.Generic;
using Kyoo.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal
namespace Kyoo.Postgresql.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
[DbContext(typeof(PostgresContext))]
partial class PostgresContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
@ -19,7 +21,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" })
.HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" })
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("ProductVersion", "5.0.5")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Kyoo.Models.Collection", b =>
@ -222,6 +224,21 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Link<Show, Genre>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{
b.Property<int>("ID")
@ -417,8 +434,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int?>("StartYear")
.HasColumnType("integer");
b.Property<int?>("Status")
.HasColumnType("integer");
b.Property<Status?>("Status")
.HasColumnType("status");
b.Property<int?>("StudioID")
.HasColumnType("integer");
@ -495,8 +512,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Property<int>("TrackIndex")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<StreamType>("Type")
.HasColumnType("stream_type");
b.HasKey("ID");
@ -506,6 +523,58 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Tracks");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("ExtraData")
.HasColumnType("jsonb");
b.Property<string>("Password")
.HasColumnType("text");
b.Property<string[]>("Permissions")
.HasColumnType("text[]");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("integer");
b.Property<int>("SecondID")
.HasColumnType("integer");
b.Property<int>("WatchedPercentage")
.HasColumnType("integer");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.HasOne("Kyoo.Models.Season", "Season")
@ -618,6 +687,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
{
b.HasOne("Kyoo.Models.Episode", "Episode")
@ -707,6 +795,25 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
@ -777,6 +884,13 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
{
b.Navigation("Shows");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Kyoo.Postgresql
{
/// <summary>
/// A postgresql implementation of <see cref="DatabaseContext"/>.
/// </summary>
public class PostgresContext : DatabaseContext
{
/// <summary>
/// The connection string to use.
/// </summary>
private readonly string _connection;
/// <summary>
/// Is this instance in debug mode?
/// </summary>
private readonly bool _debugMode;
/// <summary>
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
/// </summary>
private readonly bool _skipConfigure;
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
public PostgresContext()
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
}
/// <summary>
/// Create a new <see cref="PostgresContext"/> using specific options
/// </summary>
/// <param name="options">The options to use.</param>
public PostgresContext(DbContextOptions options)
: base(options)
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
_skipConfigure = true;
}
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
/// <param name="connection">The connection string to use</param>
/// <param name="debugMode">Is this instance in debug mode?</param>
public PostgresContext(string connection, bool debugMode)
{
_connection = connection;
_debugMode = debugMode;
}
/// <summary>
/// Set connection information for this database context
/// </summary>
/// <param name="optionsBuilder">An option builder to fill.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!_skipConfigure)
{
if (_connection != null)
optionsBuilder.UseNpgsql(_connection);
else
optionsBuilder.UseNpgsql();
if (_debugMode)
optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging();
}
base.OnConfiguring(optionsBuilder);
}
/// <summary>
/// Set database parameters to support every types of Kyoo.
/// </summary>
/// <param name="modelBuilder">The database's model builder.</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresEnum<Status>();
modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<User>()
.Property(x => x.ExtraData)
.HasColumnType("jsonb");
base.OnModelCreating(modelBuilder);
}
/// <inheritdoc />
protected override bool IsDuplicateException(Exception ex)
{
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
}
/// <inheritdoc />
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
{
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(EF.Functions.ILike);
MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format));
return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Kyoo.Controllers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Kyoo.Postgresql
{
/// <summary>
/// A module to add postgresql capacity to the app.
/// </summary>
public class PostgresModule : IPlugin
{
/// <inheritdoc />
public string Slug => "postgresql";
/// <inheritdoc />
public string Name => "Postgresql";
/// <inheritdoc />
public string Description => "A database context for postgresql.";
/// <inheritdoc />
public ICollection<Type> Provides => new[]
{
typeof(DatabaseContext)
};
/// <inheritdoc />
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc />
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
/// <summary>
/// The configuration to use. The database connection string is pulled from it.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The host environment to check if the app is in debug mode.
/// </summary>
private readonly IWebHostEnvironment _environment;
/// <summary>
/// Create a new postgres module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="env">The environment that will be used (if the env is in development mode, more information will be displayed on errors.</param>
public PostgresModule(IConfiguration configuration, IWebHostEnvironment env)
{
_configuration = configuration;
_environment = env;
}
/// <inheritdoc />
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
{
services.AddDbContext<DatabaseContext, PostgresContext>(x =>
{
x.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
if (_environment.IsDevelopment())
x.EnableDetailedErrors().EnableSensitiveDataLogging();
});
// services.AddScoped<DatabaseContext>(_ => new PostgresContext(
// _configuration.GetDatabaseConnection("postgres"),
// _environment.IsDevelopment()));
// services.AddScoped<DbContext>(x => x.GetRequiredService<PostgresContext>());
}
/// <inheritdoc />
public void Initialize(IServiceProvider provider)
{
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
context.Database.Migrate();
}
}
}

View File

@ -14,14 +14,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -1,6 +1,3 @@
using System.Linq;
using Xunit;
namespace Kyoo.Tests
{
public class SetupTests

View File

@ -1,79 +1,79 @@
using Kyoo.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Tests
{
/// <summary>
/// Class responsible to fill and create in memory databases for unit tests.
/// </summary>
public class TestContext
{
/// <summary>
/// The context's options that specify to use an in memory Sqlite database.
/// </summary>
private readonly DbContextOptions<DatabaseContext> _context;
/// <summary>
/// Create a new database and fill it with informations.
/// </summary>
public TestContext()
{
SqliteConnection connection = new("DataSource=:memory:");
connection.Open();
try
{
_context = new DbContextOptionsBuilder<DatabaseContext>()
.UseSqlite(connection)
.Options;
FillDatabase();
}
finally
{
connection.Close();
}
}
/// <summary>
/// Fill the database with pre defined values using a clean context.
/// </summary>
private void FillDatabase()
{
using DatabaseContext context = new(_context);
context.Shows.Add(new Show
{
ID = 67,
Slug = "anohana",
Title = "Anohana: The Flower We Saw That Day",
Aliases = new[]
{
"Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
"AnoHana",
"We Still Don't Know the Name of the Flower We Saw That Day."
},
Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
"In time, however, these childhood friends drifted apart, and when they became high " +
"school students, they had long ceased to think of each other as friends.",
Status = Status.Finished,
TrailerUrl = null,
StartYear = 2011,
EndYear = 2011,
Poster = "poster",
Logo = "logo",
Backdrop = "backdrop",
IsMovie = false,
Studio = null
});
}
/// <summary>
/// Get a new databse context connected to a in memory Sqlite databse.
/// </summary>
/// <returns>A valid DatabaseContext</returns>
public DatabaseContext New()
{
return new(_context);
}
}
}
// using Kyoo.Models;
// using Microsoft.Data.Sqlite;
// using Microsoft.EntityFrameworkCore;
//
// namespace Kyoo.Tests
// {
// /// <summary>
// /// Class responsible to fill and create in memory databases for unit tests.
// /// </summary>
// public class TestContext
// {
// /// <summary>
// /// The context's options that specify to use an in memory Sqlite database.
// /// </summary>
// private readonly DbContextOptions<DatabaseContext> _context;
//
// /// <summary>
// /// Create a new database and fill it with information.
// /// </summary>
// public TestContext()
// {
// SqliteConnection connection = new("DataSource=:memory:");
// connection.Open();
//
// try
// {
// _context = new DbContextOptionsBuilder<DatabaseContext>()
// .UseSqlite(connection)
// .Options;
// FillDatabase();
// }
// finally
// {
// connection.Close();
// }
// }
//
// /// <summary>
// /// Fill the database with pre defined values using a clean context.
// /// </summary>
// private void FillDatabase()
// {
// using DatabaseContext context = new(_context);
// context.Shows.Add(new Show
// {
// ID = 67,
// Slug = "anohana",
// Title = "Anohana: The Flower We Saw That Day",
// Aliases = new[]
// {
// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
// "AnoHana",
// "We Still Don't Know the Name of the Flower We Saw That Day."
// },
// Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
// "In time, however, these childhood friends drifted apart, and when they became high " +
// "school students, they had long ceased to think of each other as friends.",
// Status = Status.Finished,
// TrailerUrl = null,
// StartYear = 2011,
// EndYear = 2011,
// Poster = "poster",
// Logo = "logo",
// Backdrop = "backdrop",
// IsMovie = false,
// Studio = null
// });
// }
//
// /// <summary>
// /// Get a new database context connected to a in memory Sqlite database.
// /// </summary>
// /// <returns>A valid DatabaseContext</returns>
// public DatabaseContext New()
// {
// return new(_context);
// }
// }
// }

@ -1 +1 @@
Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3
Subproject commit 22a02671918201d6d9d4e80a76f01b59b216a82d

View File

@ -3,11 +3,11 @@
<head>
<meta charset="UTF-8">
<title>Kyoo - Login</title>
<link rel="stylesheet" type="text/css" href="login/lib/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="login/login.css" />
<link rel="stylesheet" type="text/css" href="login/material-icons.css" />
<script src="login/lib/jquery.min.js"></script>
<script src="login/lib/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="lib/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="login.css" />
<link rel="stylesheet" type="text/css" href="material-icons.css" />
<script src="lib/jquery.min.js"></script>
<script src="lib/bootstrap.min.js"></script>
</head>
<body style="height: 100vh; align-items: center;" class="d-flex">
<div class="container pb-5">
@ -85,6 +85,6 @@
</div>
</div>
</div>
<script src="login/login.js"></script>
<script src="login.js"></script>
</body>
</html>
</html>

View File

@ -41,11 +41,11 @@ $("#login-btn").on("click", function (e)
success: function ()
{
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
if (returnUrl == null)
window.location.href = "/unauthorized";
else
window.location.href = returnUrl;
window.location.href = returnUrl;
},
error: function(xhr)
{
@ -56,7 +56,7 @@ $("#login-btn").on("click", function (e)
});
});
$("#register-btn").on("click", function (e)
$("#register-btn").on("click", function (e)
{
e.preventDefault();
@ -73,7 +73,7 @@ $("#register-btn").on("click", function (e)
error.text("Passwords don't match.");
return;
}
$.ajax(
{
url: "/api/account/register",
@ -81,19 +81,19 @@ $("#register-btn").on("click", function (e)
contentType: 'application/json;charset=UTF-8',
dataType: 'json',
data: JSON.stringify(user),
success: function(res)
success: function(res)
{
useOtac(res.otac);
},
error: function(xhr)
error: function(xhr)
{
let error = $("#register-error");
error.show();
error.text(JSON.parse(xhr.responseText)[0].description);
error.html(Object.values(JSON.parse(xhr.responseText).errors).map(x => x[0]).join("<br/>"));
}
});
});
function useOtac(otac)
{
$.ajax(
@ -101,7 +101,7 @@ function useOtac(otac)
url: "/api/account/otac-login",
type: "POST",
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({otac: otac, tayLoggedIn: $("#stay-logged-in")[0].checked}),
data: JSON.stringify({otac: otac, stayLoggedIn: $("#stay-logged-in")[0].checked}),
success: function()
{
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
@ -124,4 +124,4 @@ function useOtac(otac)
let otac = new URLSearchParams(window.location.search).get("otac");
if (otac != null)
useOtac(otac);
useOtac(otac);

View File

@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.Comm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Kyoo.Tests.csproj", "{D179D5FF-9F75-4B27-8E27-0DBDF1806611}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -29,5 +33,13 @@ Global
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D179D5FF-9F75-4B27-8E27-0DBDF1806611}.Release|Any CPU.Build.0 = Release|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = Release|Any CPU
{7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,137 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace Kyoo.Controllers
{
public static class AuthExtension
{
private const string CertificateFile = "certificate.pfx";
private const string OldCertificateFile = "oldCertificate.pfx";
public static IIdentityServerBuilder AddSigninKeys(this IIdentityServerBuilder builder, IConfiguration configuration)
{
X509Certificate2 certificate = GetSiginCredential(configuration);
builder.AddSigningCredential(certificate);
if (certificate.NotAfter.AddDays(7) <= DateTime.UtcNow)
{
Console.WriteLine("Signin certificate will expire soon, renewing it.");
if (File.Exists(OldCertificateFile))
File.Delete(OldCertificateFile);
File.Move(CertificateFile, OldCertificateFile);
builder.AddValidationKey(GenerateCertificate(CertificateFile, configuration.GetValue<string>("certificatePassword")));
}
else if (File.Exists(OldCertificateFile))
builder.AddValidationKey(GetExistingCredential(OldCertificateFile, configuration.GetValue<string>("certificatePassword")));
return builder;
}
private static X509Certificate2 GetSiginCredential(IConfiguration configuration)
{
if (File.Exists(CertificateFile))
return GetExistingCredential(CertificateFile, configuration.GetValue<string>("certificatePassword"));
return GenerateCertificate(CertificateFile, configuration.GetValue<string>("certificatePassword"));
}
private static X509Certificate2 GetExistingCredential(string file, string password)
{
return new X509Certificate2(file, password,
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable
);
}
private static X509Certificate2 GenerateCertificate(string file, string password)
{
SecureRandom random = new SecureRandom();
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.SetSerialNumber(BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random));
certificateGenerator.SetIssuerDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetSubjectDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetNotBefore(DateTime.UtcNow.Date);
certificateGenerator.SetNotAfter(DateTime.UtcNow.Date.AddMonths(3));
KeyGenerationParameters keyGenerationParameters = new KeyGenerationParameters(random, 2048);
RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
AsymmetricCipherKeyPair issuerKeyPair = subjectKeyPair;
const string signatureAlgorithm = "MD5WithRSA";
Asn1SignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private);
X509Certificate bouncyCert = certificateGenerator.Generate(signatureFactory);
X509Certificate2 certificate;
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
store.SetKeyEntry("Kyoo_key", new AsymmetricKeyEntry(subjectKeyPair.Private), new [] {new X509CertificateEntry(bouncyCert)});
using MemoryStream pfxStream = new MemoryStream();
store.Save(pfxStream, password.ToCharArray(), random);
certificate = new X509Certificate2(pfxStream.ToArray(), password, X509KeyStorageFlags.Exportable);
using FileStream fileStream = File.OpenWrite(file);
pfxStream.WriteTo(fileStream);
return certificate;
}
}
public class AuthorizationValidatorHandler : AuthorizationHandler<AuthorizationValidator>
{
private readonly IConfiguration _configuration;
public AuthorizationValidatorHandler(IConfiguration configuration)
{
_configuration = configuration;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizationValidator requirement)
{
if (!context.User.IsAuthenticated())
{
string defaultPerms = _configuration.GetValue<string>("defaultPermissions");
if (defaultPerms.Split(',').Contains(requirement.Permission.ToLower()))
context.Succeed(requirement);
}
else
{
Claim perms = context.User.Claims.FirstOrDefault(x => x.Type == "permissions");
if (perms != null && perms.Value.Split(",").Contains(requirement.Permission.ToLower()))
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class AuthorizationValidator : IAuthorizationRequirement
{
public string Permission;
public AuthorizationValidator(string permission)
{
Permission = permission;
}
}
}

View File

@ -8,10 +8,22 @@ using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Controllers
{
/// <summary>
/// A <see cref="IFileManager"/> for the local filesystem (using System.IO).
/// </summary>
public class FileManager : IFileManager
{
/// <summary>
/// An extension provider to get content types from files extensions.
/// </summary>
private FileExtensionContentTypeProvider _provider;
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private string _GetContentType(string path)
{
if (_provider == null)
@ -28,26 +40,36 @@ namespace Kyoo.Controllers
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
// TODO add a way to force content type
public IActionResult FileResult(string path, bool range)
/// <inheritdoc />
public IActionResult FileResult(string path, bool range = false, string type = null)
{
if (path == null)
return new NotFoundResult();
if (!File.Exists(path))
return new NotFoundResult();
return new PhysicalFileResult(Path.GetFullPath(path), _GetContentType(path))
return new PhysicalFileResult(Path.GetFullPath(path), type ?? _GetContentType(path))
{
EnableRangeProcessing = range
};
}
public StreamReader GetReader(string path)
/// <inheritdoc />
public Stream GetReader(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return new StreamReader(path);
return File.OpenRead(path);
}
/// <inheritdoc />
public Stream NewFile(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return File.Create(path);
}
/// <inheritdoc />
public Task<ICollection<string>> ListFiles(string path)
{
if (path == null)
@ -57,11 +79,13 @@ namespace Kyoo.Controllers
: Array.Empty<string>());
}
/// <inheritdoc />
public Task<bool> Exists(string path)
{
return Task.FromResult(File.Exists(path));
}
/// <inheritdoc />
public string GetExtraDirectory(Show show)
{
string path = Path.Combine(show.Path, "Extra");
@ -69,6 +93,7 @@ namespace Kyoo.Controllers
return path;
}
/// <inheritdoc />
public string GetExtraDirectory(Season season)
{
if (season.Show == null)
@ -79,6 +104,7 @@ namespace Kyoo.Controllers
return path;
}
/// <inheritdoc />
public string GetExtraDirectory(Episode episode)
{
string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra");

View File

@ -0,0 +1,35 @@
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace Kyoo.Controllers
{
/// <summary>
/// A permission validator that always validate permissions. This effectively disable the permission system.
/// </summary>
public class PassthroughPermissionValidator : IPermissionValidator
{
// ReSharper disable once SuggestBaseTypeForParameter
public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger)
{
logger.LogWarning("No permission validator has been enabled, all users will have all permissions");
}
/// <inheritdoc />
public IFilterMetadata Create(PermissionAttribute attribute)
{
return new PassthroughValidator();
}
/// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute)
{
return new PassthroughValidator();
}
/// <summary>
/// An useless filter that does nothing.
/// </summary>
private class PassthroughValidator : IFilterMetadata { }
}
}

View File

@ -4,100 +4,235 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Kyoo.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Kyoo.Controllers
{
public class PluginDependencyLoader : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginDependencyLoader(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
return base.Load(assemblyName);
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
return LoadUnmanagedDllFromPath(libraryPath);
return base.LoadUnmanagedDll(unmanagedDllName);
}
}
/// <summary>
/// An implementation of <see cref="IPluginManager"/>.
/// This is used to load plugins and retrieve information from them.
/// </summary>
public class PluginManager : IPluginManager
{
/// <summary>
/// The service provider. It allow plugin's activation.
/// </summary>
private readonly IServiceProvider _provider;
/// <summary>
/// The configuration to get the plugin's directory.
/// </summary>
private readonly IConfiguration _config;
private List<IPlugin> _plugins;
/// <summary>
/// The logger used by this class.
/// </summary>
private readonly ILogger<PluginManager> _logger;
/// <summary>
/// The list of plugins that are currently loaded.
/// </summary>
private readonly List<IPlugin> _plugins = new();
public PluginManager(IServiceProvider provider, IConfiguration config)
/// <summary>
/// Create a new <see cref="PluginManager"/> instance.
/// </summary>
/// <param name="provider">A service container to allow initialization of plugins</param>
/// <param name="config">The configuration instance, to get the plugin's directory path.</param>
/// <param name="logger">The logger used by this class.</param>
public PluginManager(IServiceProvider provider,
IConfiguration config,
ILogger<PluginManager> logger)
{
_provider = provider;
_config = config;
_logger = logger;
}
/// <inheritdoc />
public T GetPlugin<T>(string name)
{
return (T)_plugins?.FirstOrDefault(x => x.Name == name && x is T);
}
public IEnumerable<T> GetPlugins<T>()
/// <inheritdoc />
public ICollection<T> GetPlugins<T>()
{
return _plugins?.OfType<T>() ?? new List<T>();
return _plugins?.OfType<T>().ToArray();
}
public IEnumerable<IPlugin> GetAllPlugins()
/// <inheritdoc />
public ICollection<IPlugin> GetAllPlugins()
{
return _plugins ?? new List<IPlugin>();
return _plugins;
}
public void ReloadPlugins()
/// <summary>
/// Load a single plugin and return all IPlugin implementations contained in the Assembly.
/// </summary>
/// <param name="path">The path of the dll</param>
/// <returns>The list of dlls in hte assembly</returns>
private IPlugin[] LoadPlugin(string path)
{
path = Path.GetFullPath(path);
try
{
PluginDependencyLoader loader = new(path);
Assembly assembly = loader.LoadFromAssemblyPath(path);
return assembly.GetTypes()
.Where(x => typeof(IPlugin).IsAssignableFrom(x))
.Where(x => _plugins.All(y => y.GetType() != x))
.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x))
.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not load the plugin at {Path}", path);
return Array.Empty<IPlugin>();
}
}
/// <inheritdoc />
public void LoadPlugins(ICollection<IPlugin> plugins)
{
string pluginFolder = _config.GetValue<string>("plugins");
if (!Directory.Exists(pluginFolder))
Directory.CreateDirectory(pluginFolder);
string[] pluginsPaths = Directory.GetFiles(pluginFolder);
_plugins = pluginsPaths.SelectMany(path =>
_logger.LogTrace("Loading new plugins...");
string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories);
plugins = plugins.Concat(pluginsPaths.SelectMany(LoadPlugin))
.GroupBy(x => x.Name)
.Select(x => x.First())
.ToList();
ICollection<Type> available = GetProvidedTypes(plugins);
_plugins.AddRange(plugins.Where(plugin =>
{
path = Path.GetFullPath(path);
try
{
PluginDependencyLoader loader = new(path);
Assembly ass = loader.LoadFromAssemblyPath(path);
return ass.GetTypes()
.Where(x => typeof(IPlugin).IsAssignableFrom(x))
.Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x));
}
catch (Exception ex)
{
Console.Error.WriteLine($"\nError loading the plugin at {path}.\n{ex.GetType().Name}: {ex.Message}\n");
return Array.Empty<IPlugin>();
}
}).ToList();
Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x)));
if (missing == null)
return true;
_logger.LogCritical("No {Dependency} available in Kyoo but the plugin {Plugin} requires it",
missing.Name, plugin.Name);
return false;
}));
if (!_plugins.Any())
_logger.LogInformation("No plugin enabled");
else
_logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
}
/// <inheritdoc />
public void ConfigureServices(IServiceCollection services)
{
ICollection<Type> available = GetProvidedTypes(_plugins);
foreach (IPlugin plugin in _plugins)
plugin.Configure(services, available);
}
/// <inheritdoc />
public void ConfigureAspnet(IApplicationBuilder app)
{
foreach (IPlugin plugin in _plugins)
plugin.ConfigureAspNet(app);
}
/// <summary>
/// Get the list of types provided by the currently loaded plugins.
/// </summary>
/// <param name="plugins">The list of plugins that will be used as a plugin pool to get provided types.</param>
/// <returns>The list of types available.</returns>
private ICollection<Type> GetProvidedTypes(ICollection<IPlugin> plugins)
{
List<Type> available = plugins.SelectMany(x => x.Provides).ToList();
List<ConditionalProvide> conditionals = plugins
.SelectMany(x => x.ConditionalProvides)
.Where(x => x.Condition.Condition())
.ToList();
bool IsAvailable(ConditionalProvide conditional, bool log = false)
{
Console.WriteLine("\nNo plugin enabled.\n");
return;
if (!conditional.Condition.Condition())
return false;
ICollection<Type> needed = conditional.Condition.Needed
.Where(y => !available.Contains(y))
.ToList();
// TODO handle circular dependencies, actually it might stack overflow.
needed = needed.Where(x => !conditionals
.Where(y => y.Type == x)
.Any(y => IsAvailable(y)))
.ToList();
if (!needed.Any())
return true;
if (log && available.All(x => x != conditional.Type))
{
_logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met",
conditional.Type.Name,
needed.Select(x => x.Name));
}
return false;
}
Console.WriteLine("\nPlugin enabled:");
foreach (IPlugin plugin in _plugins)
Console.WriteLine($"\t{plugin.Name}");
Console.WriteLine();
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (ConditionalProvide conditional in conditionals)
{
if (IsAvailable(conditional, true))
available.Add(conditional.Type);
}
return available;
}
/// <summary>
/// A custom <see cref="AssemblyLoadContext"/> to load plugin's dependency if they are on the same folder.
/// </summary>
private class PluginDependencyLoader : AssemblyLoadContext
{
/// <summary>
/// The basic resolver that will be used to load dlls.
/// </summary>
private readonly AssemblyDependencyResolver _resolver;
/// <summary>
/// Create a new <see cref="PluginDependencyLoader"/> for the given path.
/// </summary>
/// <param name="pluginPath">The path of the plugin and it's dependencies</param>
public PluginDependencyLoader(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
/// <inheritdoc />
protected override Assembly Load(AssemblyName assemblyName)
{
Assembly existing = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(x =>
{
AssemblyName name = x.GetName();
return name.Name == assemblyName.Name && name.Version == assemblyName.Version;
});
if (existing != null)
return existing;
// TODO load the assembly from the common folder if the file exists (this would allow shared libraries)
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
return LoadFromAssemblyPath(assemblyPath);
return base.Load(assemblyName);
}
/// <inheritdoc />
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
return LoadUnmanagedDllFromPath(libraryPath);
return base.LoadUnmanagedDll(unmanagedDllName);
}
}
}
}

View File

@ -33,7 +33,7 @@ namespace Kyoo.Controllers
} catch (Exception ex)
{
await Console.Error.WriteLineAsync(
$"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
$"The provider {provider.Provider.Name} could not work for {what}. Exception: {ex.Message}");
}
}
return ret;

View File

@ -35,7 +35,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Collection>> Search(string query)
{
return await _database.Collections
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Where(_database.Like<Collection>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();

View File

@ -108,7 +108,7 @@ namespace Kyoo.Controllers
{
Episode ret = await GetOrDefault(showID, seasonNumber, episodeNumber);
if (ret == null)
throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}.");
throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}.");
return ret;
}
@ -117,7 +117,7 @@ namespace Kyoo.Controllers
{
Episode ret = await GetOrDefault(showSlug, seasonNumber, episodeNumber);
if (ret == null)
throw new ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}.");
throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}.");
return ret;
}
@ -156,7 +156,8 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Episode>> Search(string query)
{
List<Episode> episodes = await _database.Episodes
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%") && x.EpisodeNumber != -1)
.Where(x => x.EpisodeNumber != -1)
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -233,7 +234,7 @@ namespace Kyoo.Controllers
await base.Validate(resource);
resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x =>
{
x.Provider = await _providers.CreateIfNotExists(x.Provider, true);
x.Provider = await _providers.CreateIfNotExists(x.Provider);
x.ProviderID = x.Provider.ID;
_database.Entry(x.Provider).State = EntityState.Detached;
return x;

View File

@ -36,7 +36,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Genre>> Search(string query)
{
return await _database.Genres
.Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%"))
.Where(_database.Like<Genre>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
@ -20,10 +19,6 @@ namespace Kyoo.Controllers
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A lazy loaded library repository to validate queries (check if a library does exist)
/// </summary>
private readonly Lazy<ILibraryRepository> _libraries;
@ -44,18 +39,19 @@ namespace Kyoo.Controllers
/// Create a new <see cref="LibraryItemRepository"/>.
/// </summary>
/// <param name="database">The databse instance</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazilly request a library, show or collection repository.</param>
/// <param name="libraries">A lazy loaded library repository</param>
/// <param name="shows">A lazy loaded show repository</param>
/// <param name="collections">A lazy loaded collection repository</param>
public LibraryItemRepository(DatabaseContext database,
IProviderRepository providers,
IServiceProvider services)
Lazy<ILibraryRepository> libraries,
Lazy<IShowRepository> shows,
Lazy<ICollectionRepository> collections)
: base(database)
{
_database = database;
_providers = providers;
_libraries = new Lazy<ILibraryRepository>(services.GetRequiredService<ILibraryRepository>);
_shows = new Lazy<IShowRepository>(services.GetRequiredService<IShowRepository>);
_collections = new Lazy<ICollectionRepository>(services.GetRequiredService<ICollectionRepository>);
_libraries = libraries;
_shows = shows;
_collections = collections;
}
@ -105,7 +101,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<LibraryItem>> Search(string query)
{
return await ItemsQuery
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"))
.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -115,12 +111,7 @@ namespace Kyoo.Controllers
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj, bool silentFail = false)
{
if (silentFail)
return Task.FromResult<LibraryItem>(default);
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();
/// <inheritdoc />
@ -158,7 +149,7 @@ namespace Kyoo.Controllers
sort,
limit);
if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null)
throw new ItemNotFound();
throw new ItemNotFoundException();
return items;
}
@ -173,7 +164,7 @@ namespace Kyoo.Controllers
sort,
limit);
if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null)
throw new ItemNotFound();
throw new ItemNotFoundException();
return items;
}
}

View File

@ -43,7 +43,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Library>> Search(string query)
{
return await _database.Libraries
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Where(_database.Like<Library>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -53,9 +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 = obj.Providers?.Select(x => Link.Create(obj, x)).ToArray();
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;
}
@ -65,7 +64,7 @@ namespace Kyoo.Controllers
{
await base.Validate(resource);
resource.Providers = await resource.Providers
.SelectAsync(x => _providers.CreateIfNotExists(x, true))
.SelectAsync(x => _providers.CreateIfNotExists(x))
.ToListAsync();
}

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
@ -36,15 +35,15 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazy load a show repository</param>
/// <param name="shows">A lazy loaded show repository</param>
public PeopleRepository(DatabaseContext database,
IProviderRepository providers,
IServiceProvider services)
Lazy<IShowRepository> shows)
: base(database)
{
_database = database;
_providers = providers;
_shows = new Lazy<IShowRepository>(services.GetRequiredService<IShowRepository>);
_shows = shows;
}
@ -52,7 +51,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<People>> Search(string query)
{
return await _database.People
.Where(people => EF.Functions.ILike(people.Name, $"%{query}%"))
.Where(_database.Like<People>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -74,13 +73,13 @@ namespace Kyoo.Controllers
await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
id.Provider = await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
});
await resource.Roles.ForEachAsync(async role =>
{
role.Show = await _shows.Value.CreateIfNotExists(role.Show, true);
role.Show = await _shows.Value.CreateIfNotExists(role.Show);
role.ShowID = role.Show.ID;
_database.Entry(role.Show).State = EntityState.Detached;
});
@ -130,8 +129,8 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!people.Any() && await _shows.Value.Get(showID) == null)
throw new ItemNotFound();
if (!people.Any() && await _shows.Value.GetOrDefault(showID) == null)
throw new ItemNotFoundException();
foreach (PeopleRole role in people)
role.ForPeople = true;
return people;
@ -152,8 +151,8 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!people.Any() && await _shows.Value.Get(showSlug) == null)
throw new ItemNotFound();
if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null)
throw new ItemNotFoundException();
foreach (PeopleRole role in people)
role.ForPeople = true;
return people;
@ -173,8 +172,8 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!roles.Any() && await Get(id) == null)
throw new ItemNotFound();
if (!roles.Any() && await GetOrDefault(id) == null)
throw new ItemNotFoundException();
return roles;
}
@ -192,8 +191,8 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!roles.Any() && await Get(slug) == null)
throw new ItemNotFound();
if (!roles.Any() && await GetOrDefault(slug) == null)
throw new ItemNotFoundException();
return roles;
}
}

View File

@ -36,7 +36,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Provider>> Search(string query)
{
return await _database.Providers
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Where(_database.Like<Provider>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();

View File

@ -7,7 +7,6 @@ using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
@ -44,17 +43,17 @@ namespace Kyoo.Controllers
/// <param name="database">The database handle that will be used</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="services">A service provider to lazilly request an episode repository.</param>
/// <param name="episodes">A lazy loaded episode repository.</param>
public SeasonRepository(DatabaseContext database,
IProviderRepository providers,
IShowRepository shows,
IServiceProvider services)
Lazy<IEpisodeRepository> episodes)
: base(database)
{
_database = database;
_providers = providers;
_shows = shows;
_episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>);
_episodes = episodes;
}
@ -89,7 +88,7 @@ namespace Kyoo.Controllers
{
Season ret = await GetOrDefault(showID, seasonNumber);
if (ret == null)
throw new ItemNotFound($"No season {seasonNumber} found for the show {showID}");
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
}
@ -99,7 +98,7 @@ namespace Kyoo.Controllers
{
Season ret = await GetOrDefault(showSlug, seasonNumber);
if (ret == null)
throw new ItemNotFound($"No season {seasonNumber} found for the show {showSlug}");
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug;
return ret;
}
@ -122,7 +121,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Season>> Search(string query)
{
List<Season> seasons = await _database.Seasons
.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"))
.Where(_database.Like<Season>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -161,7 +160,7 @@ namespace Kyoo.Controllers
await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
id.Provider = await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
});

View File

@ -5,7 +5,6 @@ using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
@ -15,7 +14,7 @@ namespace Kyoo.Controllers
public class ShowRepository : LocalRepository<Show>, IShowRepository
{
/// <summary>
/// The databse handle
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
@ -54,13 +53,15 @@ namespace Kyoo.Controllers
/// <param name="people">A people repository</param>
/// <param name="genres">A genres repository</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazilly request a season and an episode repository</param>
/// <param name="seasons">A lazy loaded season repository</param>
/// <param name="episodes">A lazy loaded episode repository</param>
public ShowRepository(DatabaseContext database,
IStudioRepository studios,
IPeopleRepository people,
IGenreRepository genres,
IProviderRepository providers,
IServiceProvider services)
Lazy<ISeasonRepository> seasons,
Lazy<IEpisodeRepository> episodes)
: base(database)
{
_database = database;
@ -68,8 +69,8 @@ namespace Kyoo.Controllers
_people = people;
_genres = genres;
_providers = providers;
_seasons = new Lazy<ISeasonRepository>(services.GetRequiredService<ISeasonRepository>);
_episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>);
_seasons = seasons;
_episodes = episodes;
}
@ -78,9 +79,7 @@ namespace Kyoo.Controllers
{
query = $"%{query}%";
return await _database.Shows
.Where(x => EF.Functions.ILike(x.Title, query)
|| EF.Functions.ILike(x.Slug, query)
/*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE.
.Where(_database.Like<Show>(x => x.Title + " " + x.Slug, query))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
@ -103,22 +102,22 @@ namespace Kyoo.Controllers
{
await base.Validate(resource);
if (resource.Studio != null)
resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true);
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.Genres = await resource.Genres
.SelectAsync(x => _genres.CreateIfNotExists(x, true))
.SelectAsync(x => _genres.CreateIfNotExists(x))
.ToListAsync();
resource.GenreLinks = resource.Genres?
.Select(x => Link.UCreate(resource, x))
.ToList();
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider, true);
id.Provider = await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
});
await resource.People.ForEachAsync(async role =>
{
role.People = await _people.CreateIfNotExists(role.People, true);
role.People = await _people.CreateIfNotExists(role.People);
role.PeopleID = role.People.ID;
_database.Entry(role.People).State = EntityState.Detached;
});

View File

@ -36,7 +36,7 @@ namespace Kyoo.Controllers
public override async Task<ICollection<Studio>> Search(string query)
{
return await _database.Studios
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Where(_database.Like<Studio>(x => x.Name, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();

View File

@ -46,7 +46,7 @@ namespace Kyoo.Controllers
{
Track ret = await GetOrDefault(slug, type);
if (ret == null)
throw new ItemNotFound($"No track found with the slug {slug} and the type {type}.");
throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}.");
return ret;
}
@ -59,7 +59,7 @@ namespace Kyoo.Controllers
if (!match.Success)
{
if (int.TryParse(slug, out int id))
return Get(id);
return GetOrDefault(id);
match = Regex.Match(slug, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success)
throw new ArgumentException("Invalid track slug. " +
@ -102,6 +102,7 @@ namespace Kyoo.Controllers
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
await _database.SaveOrRetry(obj, (x, i) =>
{
if (i > 10)

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
{
/// <summary>
/// A repository for users.
/// </summary>
public class UserRepository : LocalRepository<User>, IUserRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<User, object>> DefaultSort => x => x.Username;
/// <summary>
/// Create a new <see cref="UserRepository"/>
/// </summary>
/// <param name="database">The database handle to use</param>
public UserRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
public override async Task<ICollection<User>> Search(string query)
{
return await _database.Users
.Where(_database.Like<User>(x => x.Username, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
public override async Task<User> Create(User obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated user (slug {obj.Slug} already exists).");
return obj;
}
/// <inheritdoc />
public override async Task Delete(User obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
}
}
}

View File

@ -1,56 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Tasks;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Kyoo.Controllers
{
/// <summary>
/// A service to handle long running tasks and a background runner.
/// </summary>
/// <remarks>Task will be queued, only one can run simultaneously.</remarks>
public class TaskManager : BackgroundService, ITaskManager
{
private readonly IServiceProvider _serviceProvider;
private readonly IPluginManager _pluginManager;
/// <summary>
/// The service provider used to activate
/// </summary>
private readonly IServiceProvider _provider;
/// <summary>
/// The configuration instance used to get schedule information
/// </summary>
private readonly IConfiguration _configuration;
private List<(ITask task, DateTime scheduledDate)> _tasks = new List<(ITask, DateTime)>();
private CancellationTokenSource _taskToken = new CancellationTokenSource();
/// <summary>
/// The logger instance.
/// </summary>
private readonly ILogger<TaskManager> _logger;
/// <summary>
/// The list of tasks and their next scheduled run.
/// </summary>
private readonly List<(ITask task, DateTime scheduledDate)> _tasks;
/// <summary>
/// The queue of tasks that should be run as soon as possible.
/// </summary>
private readonly Queue<(ITask, Dictionary<string, object>)> _queuedTasks = new();
/// <summary>
/// The currently running task.
/// </summary>
private ITask _runningTask;
private Queue<(ITask, string)> _queuedTasks = new Queue<(ITask, string)>();
public TaskManager(IServiceProvider serviceProvider, IPluginManager pluginManager, IConfiguration configuration)
/// <summary>
/// The cancellation token used to cancel the running task when the runner should shutdown.
/// </summary>
private readonly CancellationTokenSource _taskToken = new();
/// <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="configuration">The configuration to load schedule information.</param>
/// <param name="logger">The logger.</param>
public TaskManager(IEnumerable<ITask> tasks,
IServiceProvider provider,
IConfiguration configuration,
ILogger<TaskManager> logger)
{
_serviceProvider = serviceProvider;
_pluginManager = pluginManager;
_configuration = configuration;
_provider = provider;
_configuration = configuration.GetSection("scheduledTasks");
_logger = logger;
_tasks = tasks.Select(x => (x, GetNextTaskDate(x.Slug))).ToList();
if (_tasks.Any())
_logger.LogTrace("Task manager initiated with: {Tasks}", _tasks.Select(x => x.task.Name));
else
_logger.LogInformation("Task manager initiated without any tasks");
}
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <remarks>Start the runner in another thread.</remarks>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
public override Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(() => base.StartAsync(cancellationToken), CancellationToken.None);
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task StopAsync(CancellationToken cancellationToken)
{
_taskToken.Cancel();
return base.StopAsync(cancellationToken);
}
/// <summary>
/// The runner that will host tasks and run queued tasks.
/// </summary>
/// <param name="cancellationToken">A token to stop the runner</param>
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
ReloadTask();
IEnumerable<ITask> startupTasks = _tasks.Select(x => x.task)
.Where(x => x.RunOnStartup && x.Priority != Int32.MaxValue)
.OrderByDescending(x => x.Priority);
foreach (ITask task in startupTasks)
_queuedTasks.Enqueue((task, null));
EnqueueStartupTasks();
while (!cancellationToken.IsCancellationRequested)
{
if (_queuedTasks.Any())
{
(ITask task, string arguments) = _queuedTasks.Dequeue();
(ITask task, Dictionary<string, object> arguments) = _queuedTasks.Dequeue();
_runningTask = task;
try
{
await task.Run(_serviceProvider, _taskToken.Token, arguments);
await RunTask(task, arguments);
}
catch (Exception e)
{
Console.Error.WriteLine($"An unhandled exception occured while running the task {task.Name}.\nInner exception: {e.Message}\n\n");
_logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name);
}
}
else
@ -61,67 +124,122 @@ namespace Kyoo.Controllers
}
}
/// <summary>
/// Parse parameters, inject a task and run it.
/// </summary>
/// <param name="task">The task to run</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)
{
_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())
{
string invalidsStr = string.Join(", ", invalids);
throw new ArgumentException($"{invalidsStr} are invalid arguments for the 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.Name}" +
" but it was not specified.");
return x.CreateValue(value ?? x.DefaultValue);
}));
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);
}
/// <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);
foreach (string task in tasksToQueue)
StartTask(task);
{
_logger.LogDebug("Queuing task scheduled for running: {Task}", task);
StartTask(task, new Dictionary<string, object>());
}
}
public override Task StartAsync(CancellationToken cancellationToken)
/// <summary>
/// Queue startup tasks with respect to the priority rules.
/// </summary>
private void EnqueueStartupTasks()
{
Task.Run(() => base.StartAsync(cancellationToken));
return Task.CompletedTask;
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>()));
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_taskToken.Cancel();
return base.StopAsync(cancellationToken);
}
public bool StartTask(string taskSlug, string arguments = null)
/// <inheritdoc />
public void StartTask(string taskSlug, Dictionary<string, object> arguments = null)
{
arguments ??= new Dictionary<string, object>();
int index = _tasks.FindIndex(x => x.task.Slug == taskSlug);
if (index == -1)
return false;
throw new ItemNotFoundException($"No task found with the slug {taskSlug}");
_queuedTasks.Enqueue((_tasks[index].task, arguments));
_tasks[index] = (_tasks[index].task, DateTime.Now + GetTaskDelay(taskSlug));
return true;
_tasks[index] = (_tasks[index].task, GetNextTaskDate(taskSlug));
}
public TimeSpan GetTaskDelay(string taskSlug)
/// <summary>
/// Get the next date of the execution of the given task.
/// </summary>
/// <param name="taskSlug">The slug of the task</param>
/// <returns>The next date.</returns>
private DateTime GetNextTaskDate(string taskSlug)
{
TimeSpan delay = _configuration.GetSection("scheduledTasks").GetValue<TimeSpan>(taskSlug);
TimeSpan delay = _configuration.GetValue<TimeSpan>(taskSlug);
if (delay == default)
delay = TimeSpan.FromDays(365);
return delay;
return DateTime.MaxValue;
return DateTime.Now + delay;
}
public ITask GetRunningTask()
/// <inheritdoc />
public ICollection<ITask> GetRunningTasks()
{
return _runningTask;
return new[] {_runningTask};
}
public void ReloadTask()
/// <inheritdoc />
public ICollection<ITask> GetAllTasks()
{
_tasks.Clear();
_tasks.AddRange(CoreTaskHolder.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))));
IEnumerable<ITask> prerunTasks = _tasks.Select(x => x.task)
.Where(x => x.RunOnStartup && x.Priority == int.MaxValue);
foreach (ITask task in prerunTasks)
task.Run(_serviceProvider, _taskToken.Token);
foreach (IPlugin plugin in _pluginManager.GetAllPlugins())
if (plugin.Tasks != null)
_tasks.AddRange(plugin.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug))));
}
public IEnumerable<ITask> GetAllTasks()
{
return _tasks.Select(x => x.task);
return _tasks.Select(x => x.task).ToArray();
}
}
}

103
Kyoo/CoreModule.cs Normal file
View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Kyoo.Controllers;
using Kyoo.Models.Permissions;
using Kyoo.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo
{
/// <summary>
/// The core module containing default implementations
/// </summary>
public class CoreModule : IPlugin
{
/// <inheritdoc />
public string Slug => "core";
/// <inheritdoc />
public string Name => "Core";
/// <inheritdoc />
public string Description => "The core module containing default implementations.";
/// <inheritdoc />
public ICollection<Type> Provides => new[]
{
typeof(IFileManager),
typeof(ITranscoder),
typeof(IThumbnailsManager),
typeof(IProviderManager),
typeof(ITaskManager),
typeof(ILibraryManager)
};
/// <inheritdoc />
public ICollection<ConditionalProvide> ConditionalProvides => new ConditionalProvide[]
{
(typeof(ILibraryRepository), typeof(DatabaseContext)),
(typeof(ILibraryItemRepository), typeof(DatabaseContext)),
(typeof(ICollectionRepository), typeof(DatabaseContext)),
(typeof(IShowRepository), typeof(DatabaseContext)),
(typeof(ISeasonRepository), typeof(DatabaseContext)),
(typeof(IEpisodeRepository), typeof(DatabaseContext)),
(typeof(ITrackRepository), typeof(DatabaseContext)),
(typeof(IPeopleRepository), typeof(DatabaseContext)),
(typeof(IStudioRepository), typeof(DatabaseContext)),
(typeof(IGenreRepository), typeof(DatabaseContext)),
(typeof(IProviderRepository), typeof(DatabaseContext)),
(typeof(IUserRepository), typeof(DatabaseContext))
};
/// <inheritdoc />
public ICollection<Type> Requires => new []
{
typeof(ILibraryRepository),
typeof(ILibraryItemRepository),
typeof(ICollectionRepository),
typeof(IShowRepository),
typeof(ISeasonRepository),
typeof(IEpisodeRepository),
typeof(ITrackRepository),
typeof(IPeopleRepository),
typeof(IStudioRepository),
typeof(IGenreRepository),
typeof(IProviderRepository)
};
/// <inheritdoc />
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
{
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>();
}
}
}

View File

@ -6,7 +6,6 @@
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>../Kyoo.WebApp/</SpaRoot>
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
<TranscoderRoot>../Kyoo.Transcoder/</TranscoderRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules/**</DefaultItemExcludes>
@ -35,36 +34,25 @@
<ItemGroup>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
<PackageReference Include="IdentityServer4" Version="4.1.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.1" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.1" />
<PackageReference Include="IdentityServer4.EntityFramework.Storage" Version="4.1.1" />
<PackageReference Include="IdentityServer4.Storage" Version="4.1.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.9" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.12" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.14" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.Postgresql/Kyoo.Postgresql.csproj">
<!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference>
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj">
<!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules/**" Visible="false" />
<StaticFiles Include="$(SpaRoot)static/**" Visible="false" />
<LoginFiles Include="$(LoginRoot)**" Visible="false" />
<Content Remove="$(SpaRoot)**" />
</ItemGroup>
@ -84,18 +72,13 @@
</ItemGroup>
</Target>
<Target Name="Publish static and login" AfterTargets="ComputeFilesToPublish">
<Target Name="Publish static files" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Include="@(StaticFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot/%(StaticFiles.RecursiveDir)%(StaticFiles.Filename)%(StaticFiles.Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
<ResolvedFileToPublish Include="@(LoginFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
@ -103,9 +86,8 @@
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="Prepare static and login pages" AfterTargets="Build" Condition="$(Configuration) == 'Debug'">
<Target Name="Prepare static files" AfterTargets="Build" Condition="$(Configuration) == 'Debug'">
<Copy SourceFiles="@(StaticFiles)" DestinationFolder="$(OutputPath)/wwwroot/%(RecursiveDir)" />
<Copy SourceFiles="@(LoginFiles)" DestinationFolder="$(OutputPath)/wwwroot/%(RecursiveDir)" />
</Target>
<Target Name="Symlink views to output - Linux" AfterTargets="Build" Condition="$(Configuration) == 'Debug' And $(OS) == 'Unix'">
@ -124,4 +106,25 @@
<Visible>false</Visible>
</None>
</ItemGroup>
<!--TODO remove this once plugins are reworked. This is useful because the authentication plugin is loaded manually and not by the plugin manager-->
<PropertyGroup>
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
</PropertyGroup>
<ItemGroup>
<LoginFiles Include="$(LoginRoot)**" Visible="false" />
</ItemGroup>
<Target Name="Publish login files" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Include="@(LoginFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
<Target Name="Prepare static files" AfterTargets="Build" Condition="$(Configuration) == 'Debug'">
<Copy SourceFiles="@(LoginFiles)" DestinationFolder="$(OutputPath)/login/%(RecursiveDir)" />
</Target>
</Project>

View File

@ -1,985 +0,0 @@
// <auto-generated />
using System;
using IdentityServer4.EntityFramework.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
{
[DbContext(typeof(ConfigurationDbContext))]
[Migration("20210306161631_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AllowedAccessTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<DateTime?>("LastAccessed")
.HasColumnType("timestamp without time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ApiResources");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Emphasize")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ApiScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ScopeId");
b.ToTable("ApiScopeClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ScopeId");
b.ToTable("ApiScopeProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AbsoluteRefreshTokenLifetime")
.HasColumnType("integer");
b.Property<int>("AccessTokenLifetime")
.HasColumnType("integer");
b.Property<int>("AccessTokenType")
.HasColumnType("integer");
b.Property<bool>("AllowAccessTokensViaBrowser")
.HasColumnType("boolean");
b.Property<bool>("AllowOfflineAccess")
.HasColumnType("boolean");
b.Property<bool>("AllowPlainTextPkce")
.HasColumnType("boolean");
b.Property<bool>("AllowRememberConsent")
.HasColumnType("boolean");
b.Property<string>("AllowedIdentityTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<bool>("AlwaysIncludeUserClaimsInIdToken")
.HasColumnType("boolean");
b.Property<bool>("AlwaysSendClientClaims")
.HasColumnType("boolean");
b.Property<int>("AuthorizationCodeLifetime")
.HasColumnType("integer");
b.Property<bool>("BackChannelLogoutSessionRequired")
.HasColumnType("boolean");
b.Property<string>("BackChannelLogoutUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("ClientClaimsPrefix")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<int?>("ConsentLifetime")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<int>("DeviceCodeLifetime")
.HasColumnType("integer");
b.Property<bool>("EnableLocalLogin")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<bool>("FrontChannelLogoutSessionRequired")
.HasColumnType("boolean");
b.Property<string>("FrontChannelLogoutUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<int>("IdentityTokenLifetime")
.HasColumnType("integer");
b.Property<bool>("IncludeJwtId")
.HasColumnType("boolean");
b.Property<DateTime?>("LastAccessed")
.HasColumnType("timestamp without time zone");
b.Property<string>("LogoUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<string>("PairWiseSubjectSalt")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ProtocolType")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("RefreshTokenExpiration")
.HasColumnType("integer");
b.Property<int>("RefreshTokenUsage")
.HasColumnType("integer");
b.Property<bool>("RequireClientSecret")
.HasColumnType("boolean");
b.Property<bool>("RequireConsent")
.HasColumnType("boolean");
b.Property<bool>("RequirePkce")
.HasColumnType("boolean");
b.Property<bool>("RequireRequestObject")
.HasColumnType("boolean");
b.Property<int>("SlidingRefreshTokenLifetime")
.HasColumnType("integer");
b.Property<bool>("UpdateAccessTokenClaimsOnRefresh")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.Property<string>("UserCodeType")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int?>("UserSsoLifetime")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("Clients");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Origin")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientCorsOrigins");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("GrantType")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientGrantTypes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Provider")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientIdPRestrictions");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("PostLogoutRedirectUri")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientPostLogoutRedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("RedirectUri")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientRedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Emphasize")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("IdentityResources");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("UserClaims")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Properties")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Scopes")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Secrets")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("UserClaims")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("Properties")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("Claims")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedCorsOrigins")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedGrantTypes")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("IdentityProviderRestrictions")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("PostLogoutRedirectUris")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("Properties")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("RedirectUris")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedScopes")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("ClientSecrets")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("UserClaims")
.HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("Properties")
.HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Navigation("Properties");
b.Navigation("Scopes");
b.Navigation("Secrets");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Navigation("AllowedCorsOrigins");
b.Navigation("AllowedGrantTypes");
b.Navigation("AllowedScopes");
b.Navigation("Claims");
b.Navigation("ClientSecrets");
b.Navigation("IdentityProviderRestrictions");
b.Navigation("PostLogoutRedirectUris");
b.Navigation("Properties");
b.Navigation("RedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,658 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiResources",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false),
Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
LastAccessed = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
NonEditable = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResources", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ApiScopes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Required = table.Column<bool>(type: "boolean", nullable: false),
Emphasize = table.Column<bool>(type: "boolean", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Clients",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ProtocolType = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
RequireClientSecret = table.Column<bool>(type: "boolean", nullable: false),
ClientName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
ClientUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
LogoUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
RequireConsent = table.Column<bool>(type: "boolean", nullable: false),
AllowRememberConsent = table.Column<bool>(type: "boolean", nullable: false),
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "boolean", nullable: false),
RequirePkce = table.Column<bool>(type: "boolean", nullable: false),
AllowPlainTextPkce = table.Column<bool>(type: "boolean", nullable: false),
RequireRequestObject = table.Column<bool>(type: "boolean", nullable: false),
AllowAccessTokensViaBrowser = table.Column<bool>(type: "boolean", nullable: false),
FrontChannelLogoutUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "boolean", nullable: false),
BackChannelLogoutUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
BackChannelLogoutSessionRequired = table.Column<bool>(type: "boolean", nullable: false),
AllowOfflineAccess = table.Column<bool>(type: "boolean", nullable: false),
IdentityTokenLifetime = table.Column<int>(type: "integer", nullable: false),
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
AccessTokenLifetime = table.Column<int>(type: "integer", nullable: false),
AuthorizationCodeLifetime = table.Column<int>(type: "integer", nullable: false),
ConsentLifetime = table.Column<int>(type: "integer", nullable: true),
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "integer", nullable: false),
SlidingRefreshTokenLifetime = table.Column<int>(type: "integer", nullable: false),
RefreshTokenUsage = table.Column<int>(type: "integer", nullable: false),
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "boolean", nullable: false),
RefreshTokenExpiration = table.Column<int>(type: "integer", nullable: false),
AccessTokenType = table.Column<int>(type: "integer", nullable: false),
EnableLocalLogin = table.Column<bool>(type: "boolean", nullable: false),
IncludeJwtId = table.Column<bool>(type: "boolean", nullable: false),
AlwaysSendClientClaims = table.Column<bool>(type: "boolean", nullable: false),
ClientClaimsPrefix = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
PairWiseSubjectSalt = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
LastAccessed = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
UserSsoLifetime = table.Column<int>(type: "integer", nullable: true),
UserCodeType = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
DeviceCodeLifetime = table.Column<int>(type: "integer", nullable: false),
NonEditable = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clients", x => x.Id);
});
migrationBuilder.CreateTable(
name: "IdentityResources",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Required = table.Column<bool>(type: "boolean", nullable: false),
Emphasize = table.Column<bool>(type: "boolean", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(type: "boolean", nullable: false),
Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Updated = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
NonEditable = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResources", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ApiResourceClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ApiResourceId = table.Column<int>(type: "integer", nullable: false),
Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceProperties",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ApiResourceId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceScopes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Scope = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ApiResourceId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceSecrets",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ApiResourceId = table.Column<int>(type: "integer", nullable: false),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Value = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiScopeClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ScopeId = table.Column<int>(type: "integer", nullable: false),
Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiScopeProperties",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ScopeId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientClaims", x => x.Id);
table.ForeignKey(
name: "FK_ClientClaims_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientCorsOrigins",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Origin = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id);
table.ForeignKey(
name: "FK_ClientCorsOrigins_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientGrantTypes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
GrantType = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id);
table.ForeignKey(
name: "FK_ClientGrantTypes_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientIdPRestrictions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Provider = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id);
table.ForeignKey(
name: "FK_ClientIdPRestrictions_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientPostLogoutRedirectUris",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PostLogoutRedirectUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id);
table.ForeignKey(
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientProperties",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ClientId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientProperties", x => x.Id);
table.ForeignKey(
name: "FK_ClientProperties_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientRedirectUris",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RedirectUri = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id);
table.ForeignKey(
name: "FK_ClientRedirectUris_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientScopes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Scope = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientScopes", x => x.Id);
table.ForeignKey(
name: "FK_ClientScopes_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientSecrets",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ClientId = table.Column<int>(type: "integer", nullable: false),
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Value = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Type = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Created = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientSecrets", x => x.Id);
table.ForeignKey(
name: "FK_ClientSecrets_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IdentityResourceClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IdentityResourceId = table.Column<int>(type: "integer", nullable: false),
Type = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
table.ForeignKey(
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
column: x => x.IdentityResourceId,
principalTable: "IdentityResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IdentityResourceProperties",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IdentityResourceId = table.Column<int>(type: "integer", nullable: false),
Key = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
table.ForeignKey(
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResour~",
column: x => x.IdentityResourceId,
principalTable: "IdentityResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ApiResourceClaims_ApiResourceId",
table: "ApiResourceClaims",
column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiResourceProperties_ApiResourceId",
table: "ApiResourceProperties",
column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiResources_Name",
table: "ApiResources",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiResourceScopes_ApiResourceId",
table: "ApiResourceScopes",
column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiResourceSecrets_ApiResourceId",
table: "ApiResourceSecrets",
column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopeClaims_ScopeId",
table: "ApiScopeClaims",
column: "ScopeId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopeProperties_ScopeId",
table: "ApiScopeProperties",
column: "ScopeId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopes_Name",
table: "ApiScopes",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientClaims_ClientId",
table: "ClientClaims",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientCorsOrigins_ClientId",
table: "ClientCorsOrigins",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientGrantTypes_ClientId",
table: "ClientGrantTypes",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientIdPRestrictions_ClientId",
table: "ClientIdPRestrictions",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientPostLogoutRedirectUris_ClientId",
table: "ClientPostLogoutRedirectUris",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientProperties_ClientId",
table: "ClientProperties",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientRedirectUris_ClientId",
table: "ClientRedirectUris",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_Clients_ClientId",
table: "Clients",
column: "ClientId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientScopes_ClientId",
table: "ClientScopes",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_ClientSecrets_ClientId",
table: "ClientSecrets",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_IdentityResourceClaims_IdentityResourceId",
table: "IdentityResourceClaims",
column: "IdentityResourceId");
migrationBuilder.CreateIndex(
name: "IX_IdentityResourceProperties_IdentityResourceId",
table: "IdentityResourceProperties",
column: "IdentityResourceId");
migrationBuilder.CreateIndex(
name: "IX_IdentityResources_Name",
table: "IdentityResources",
column: "Name",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiResourceClaims");
migrationBuilder.DropTable(
name: "ApiResourceProperties");
migrationBuilder.DropTable(
name: "ApiResourceScopes");
migrationBuilder.DropTable(
name: "ApiResourceSecrets");
migrationBuilder.DropTable(
name: "ApiScopeClaims");
migrationBuilder.DropTable(
name: "ApiScopeProperties");
migrationBuilder.DropTable(
name: "ClientClaims");
migrationBuilder.DropTable(
name: "ClientCorsOrigins");
migrationBuilder.DropTable(
name: "ClientGrantTypes");
migrationBuilder.DropTable(
name: "ClientIdPRestrictions");
migrationBuilder.DropTable(
name: "ClientPostLogoutRedirectUris");
migrationBuilder.DropTable(
name: "ClientProperties");
migrationBuilder.DropTable(
name: "ClientRedirectUris");
migrationBuilder.DropTable(
name: "ClientScopes");
migrationBuilder.DropTable(
name: "ClientSecrets");
migrationBuilder.DropTable(
name: "IdentityResourceClaims");
migrationBuilder.DropTable(
name: "IdentityResourceProperties");
migrationBuilder.DropTable(
name: "ApiResources");
migrationBuilder.DropTable(
name: "ApiScopes");
migrationBuilder.DropTable(
name: "Clients");
migrationBuilder.DropTable(
name: "IdentityResources");
}
}
}

View File

@ -1,983 +0,0 @@
// <auto-generated />
using System;
using IdentityServer4.EntityFramework.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration
{
[DbContext(typeof(ConfigurationDbContext))]
partial class ConfigurationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AllowedAccessTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<DateTime?>("LastAccessed")
.HasColumnType("timestamp without time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ApiResources");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiResourceId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ApiResourceId");
b.ToTable("ApiResourceSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Emphasize")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ApiScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ScopeId");
b.ToTable("ApiScopeClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<int>("ScopeId")
.HasColumnType("integer");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ScopeId");
b.ToTable("ApiScopeProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AbsoluteRefreshTokenLifetime")
.HasColumnType("integer");
b.Property<int>("AccessTokenLifetime")
.HasColumnType("integer");
b.Property<int>("AccessTokenType")
.HasColumnType("integer");
b.Property<bool>("AllowAccessTokensViaBrowser")
.HasColumnType("boolean");
b.Property<bool>("AllowOfflineAccess")
.HasColumnType("boolean");
b.Property<bool>("AllowPlainTextPkce")
.HasColumnType("boolean");
b.Property<bool>("AllowRememberConsent")
.HasColumnType("boolean");
b.Property<string>("AllowedIdentityTokenSigningAlgorithms")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<bool>("AlwaysIncludeUserClaimsInIdToken")
.HasColumnType("boolean");
b.Property<bool>("AlwaysSendClientClaims")
.HasColumnType("boolean");
b.Property<int>("AuthorizationCodeLifetime")
.HasColumnType("integer");
b.Property<bool>("BackChannelLogoutSessionRequired")
.HasColumnType("boolean");
b.Property<string>("BackChannelLogoutUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("ClientClaimsPrefix")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<int?>("ConsentLifetime")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<int>("DeviceCodeLifetime")
.HasColumnType("integer");
b.Property<bool>("EnableLocalLogin")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<bool>("FrontChannelLogoutSessionRequired")
.HasColumnType("boolean");
b.Property<string>("FrontChannelLogoutUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<int>("IdentityTokenLifetime")
.HasColumnType("integer");
b.Property<bool>("IncludeJwtId")
.HasColumnType("boolean");
b.Property<DateTime?>("LastAccessed")
.HasColumnType("timestamp without time zone");
b.Property<string>("LogoUri")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<string>("PairWiseSubjectSalt")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ProtocolType")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("RefreshTokenExpiration")
.HasColumnType("integer");
b.Property<int>("RefreshTokenUsage")
.HasColumnType("integer");
b.Property<bool>("RequireClientSecret")
.HasColumnType("boolean");
b.Property<bool>("RequireConsent")
.HasColumnType("boolean");
b.Property<bool>("RequirePkce")
.HasColumnType("boolean");
b.Property<bool>("RequireRequestObject")
.HasColumnType("boolean");
b.Property<int>("SlidingRefreshTokenLifetime")
.HasColumnType("integer");
b.Property<bool>("UpdateAccessTokenClaimsOnRefresh")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.Property<string>("UserCodeType")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int?>("UserSsoLifetime")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("Clients");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Origin")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientCorsOrigins");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("GrantType")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientGrantTypes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Provider")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientIdPRestrictions");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("PostLogoutRedirectUri")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientPostLogoutRedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("RedirectUri")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientRedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<string>("Scope")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientScopes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.HasKey("Id");
b.HasIndex("ClientId");
b.ToTable("ClientSecrets");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime>("Created")
.HasColumnType("timestamp without time zone");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("Emphasize")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("NonEditable")
.HasColumnType("boolean");
b.Property<bool>("Required")
.HasColumnType("boolean");
b.Property<bool>("ShowInDiscoveryDocument")
.HasColumnType("boolean");
b.Property<DateTime?>("Updated")
.HasColumnType("timestamp without time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("IdentityResources");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("IdentityResourceId")
.HasColumnType("integer");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("character varying(250)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.HasKey("Id");
b.HasIndex("IdentityResourceId");
b.ToTable("IdentityResourceProperties");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("UserClaims")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Properties")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Scopes")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource")
.WithMany("Secrets")
.HasForeignKey("ApiResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApiResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("UserClaims")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope")
.WithMany("Properties")
.HasForeignKey("ScopeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scope");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("Claims")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedCorsOrigins")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedGrantTypes")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("IdentityProviderRestrictions")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("PostLogoutRedirectUris")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("Properties")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("RedirectUris")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedScopes")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client")
.WithMany("ClientSecrets")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("UserClaims")
.HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b =>
{
b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource")
.WithMany("Properties")
.HasForeignKey("IdentityResourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("IdentityResource");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b =>
{
b.Navigation("Properties");
b.Navigation("Scopes");
b.Navigation("Secrets");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b =>
{
b.Navigation("AllowedCorsOrigins");
b.Navigation("AllowedGrantTypes");
b.Navigation("AllowedScopes");
b.Navigation("Claims");
b.Navigation("ClientSecrets");
b.Navigation("IdentityProviderRestrictions");
b.Navigation("PostLogoutRedirectUris");
b.Navigation("Properties");
b.Navigation("RedirectUris");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b =>
{
b.Navigation("Properties");
b.Navigation("UserClaims");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,384 +0,0 @@
// <auto-generated />
using System;
using Kyoo;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{
[DbContext(typeof(IdentityDatabase))]
[Migration("20210216205030_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{
b.Property<string>("UserCode")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration")
.IsRequired()
.HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("UserCode");
b.HasIndex("DeviceCode")
.IsUnique();
b.HasIndex("Expiration");
b.ToTable("DeviceCodes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
{
b.Property<string>("Key")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("timestamp without time zone");
b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Key");
b.HasIndex("Expiration");
b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("OTAC")
.HasColumnType("text");
b.Property<DateTime?>("OTACExpires")
.HasColumnType("timestamp without time zone");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("UserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("UserRoleClaim");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaim");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogin");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRole");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserToken");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,291 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DeviceCodes",
columns: table => new
{
UserCode = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DeviceCode = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
SubjectId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
SessionId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
CreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Data = table.Column<string>(type: "character varying(50000)", maxLength: 50000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
});
migrationBuilder.CreateTable(
name: "PersistedGrants",
columns: table => new
{
Key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
SubjectId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
SessionId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ClientId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
CreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
Expiration = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
ConsumedTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
Data = table.Column<string>(type: "character varying(50000)", maxLength: 50000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PersistedGrants", x => x.Key);
});
migrationBuilder.CreateTable(
name: "User",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
OTAC = table.Column<string>(type: "text", nullable: true),
OTACExpires = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_User", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserClaim",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserClaim", x => x.Id);
table.ForeignKey(
name: "FK_UserClaim_User_UserId",
column: x => x.UserId,
principalTable: "User",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserLogin",
columns: table => new
{
LoginProvider = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
ProviderKey = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserLogin", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_UserLogin_User_UserId",
column: x => x.UserId,
principalTable: "User",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserToken",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserToken", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_UserToken_User_UserId",
column: x => x.UserId,
principalTable: "User",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserRole",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_UserRole_User_UserId",
column: x => x.UserId,
principalTable: "User",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserRole_UserRoles_RoleId",
column: x => x.RoleId,
principalTable: "UserRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserRoleClaim",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRoleClaim", x => x.Id);
table.ForeignKey(
name: "FK_UserRoleClaim_UserRoles_RoleId",
column: x => x.RoleId,
principalTable: "UserRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DeviceCodes_DeviceCode",
table: "DeviceCodes",
column: "DeviceCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DeviceCodes_Expiration",
table: "DeviceCodes",
column: "Expiration");
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_Expiration",
table: "PersistedGrants",
column: "Expiration");
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "ClientId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "SessionId", "Type" });
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "User",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "User",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserClaim_UserId",
table: "UserClaim",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_UserLogin_UserId",
table: "UserLogin",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_UserRole_RoleId",
table: "UserRole",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_UserRoleClaim_RoleId",
table: "UserRoleClaim",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "UserRoles",
column: "NormalizedName",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeviceCodes");
migrationBuilder.DropTable(
name: "PersistedGrants");
migrationBuilder.DropTable(
name: "UserClaim");
migrationBuilder.DropTable(
name: "UserLogin");
migrationBuilder.DropTable(
name: "UserRole");
migrationBuilder.DropTable(
name: "UserRoleClaim");
migrationBuilder.DropTable(
name: "UserToken");
migrationBuilder.DropTable(
name: "UserRoles");
migrationBuilder.DropTable(
name: "User");
}
}
}

View File

@ -1,382 +0,0 @@
// <auto-generated />
using System;
using Kyoo;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase
{
[DbContext(typeof(IdentityDatabase))]
partial class IdentityDatabaseModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.3")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{
b.Property<string>("UserCode")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration")
.IsRequired()
.HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("UserCode");
b.HasIndex("DeviceCode")
.IsUnique();
b.HasIndex("Expiration");
b.ToTable("DeviceCodes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
{
b.Property<string>("Key")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("timestamp without time zone");
b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp without time zone");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("character varying(50000)");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime?>("Expiration")
.HasColumnType("timestamp without time zone");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Key");
b.HasIndex("Expiration");
b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("OTAC")
.HasColumnType("text");
b.Property<DateTime?>("OTACExpires")
.HasColumnType("timestamp without time zone");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("UserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("UserRoleClaim");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaim");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogin");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRole");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserToken");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Kyoo.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,47 +0,0 @@
using System.Threading.Tasks;
using IdentityServer4.EntityFramework.Entities;
using IdentityServer4.EntityFramework.Extensions;
using IdentityServer4.EntityFramework.Interfaces;
using IdentityServer4.EntityFramework.Options;
using Kyoo.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Kyoo
{
// The configuration's database is named ConfigurationDbContext.
public class IdentityDatabase : IdentityDbContext<User>, IPersistedGrantDbContext
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
public IdentityDatabase(DbContextOptions<IdentityDatabase> options, IOptions<OperationalStoreOptions> operationalStoreOptions)
: base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
public DbSet<User> Accounts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
modelBuilder.Entity<User>().ToTable("User");
modelBuilder.Entity<IdentityUserRole<string>>().ToTable("UserRole");
modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("UserLogin");
modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("UserClaim");
modelBuilder.Entity<IdentityRole>().ToTable("UserRoles");
modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("UserRoleClaim");
modelBuilder.Entity<IdentityUserToken<string>>().ToTable("UserToken");
}
public Task<int> SaveChangesAsync() => base.SaveChangesAsync();
public DbSet<PersistedGrant> PersistedGrants { get; set; }
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
}
}

12
Kyoo/Models/LazyDi.cs Normal file
View File

@ -0,0 +1,12 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Models
{
public class LazyDi<T> : Lazy<T>
{
public LazyDi(IServiceProvider provider)
: base(provider.GetRequiredService<T>)
{ }
}
}

View File

@ -1,22 +0,0 @@
using System;
using IdentityModel;
using Microsoft.AspNetCore.Identity;
namespace Kyoo.Models
{
public class User : IdentityUser
{
public string OTAC { get; set; }
public DateTime? OTACExpires { get; set; }
public string GenerateOTAC(TimeSpan validFor)
{
string otac = CryptoRandom.CreateUniqueId();
string hashed = otac; // TODO should add a good hashing here.
OTAC = hashed;
OTACExpires = DateTime.UtcNow.Add(validFor);
return otac;
}
}
}

View File

@ -1,14 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Kyoo
{
/// <summary>
@ -39,15 +37,23 @@ namespace Kyoo
if (debug == null && Environment.GetEnvironmentVariable("ENVIRONMENT") != null)
Console.WriteLine($"Invalid ENVIRONMENT variable. Supported values are \"debug\" and \"prod\". Ignoring...");
#if DEBUG
debug ??= true;
#endif
Console.WriteLine($"Running as {Environment.UserName}.");
IWebHostBuilder host = CreateWebHostBuilder(args);
IWebHostBuilder builder = CreateWebHostBuilder(args);
if (debug != null)
host = host.UseEnvironment(debug == true ? "Development" : "Production");
await host.Build().RunAsync();
builder = builder.UseEnvironment(debug == true ? "Development" : "Production");
try
{
await builder.Build().RunAsync();
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Unhandled exception: {ex}");
}
}
/// <summary>
@ -62,16 +68,14 @@ namespace Kyoo
.AddEnvironmentVariables()
.AddCommandLine(args);
}
/// <summary>
/// Createa a web host
/// Create a a web host
/// </summary>
/// <param name="args">Command line parameters that can be handled by kestrel</param>
/// <returns>A new web host instance</returns>
private static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
WebHost.CreateDefaultBuilder(args);
return new WebHostBuilder()
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build())
@ -79,7 +83,10 @@ namespace Kyoo
.ConfigureLogging((context, builder) =>
{
builder.AddConfiguration(context.Configuration.GetSection("logging"))
.AddConsole()
.AddSimpleConsole(x =>
{
x.TimestampFormat = "[hh:mm:ss] ";
})
.AddDebug()
.AddEventSourceLogger();
})

View File

@ -1,20 +1,14 @@
using System;
using System.IO;
using System.Reflection;
using IdentityServer4.Extensions;
using IdentityServer4.Services;
using Kyoo.Api;
using Kyoo.Authentication;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Kyoo.Postgresql;
using Kyoo.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@ -28,20 +22,48 @@ namespace Kyoo
/// </summary>
public class Startup
{
/// <summary>
/// The configuration context
/// </summary>
private readonly IConfiguration _configuration;
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// A plugin manager used to load plugins and allow them to configure services / asp net.
/// </summary>
private readonly IPluginManager _plugins;
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
/// <summary>
/// Created from the DI container, those services are needed to load information and instantiate plugins.s
/// </summary>
/// <param name="hostProvider">
/// The ServiceProvider used to create this <see cref="Startup"/> instance.
/// The host provider that contains only well-known services that are Kyoo independent.
/// This is used to instantiate plugins that might need a logger, a configuration or an host environment.
/// </param>
/// <param name="configuration">The configuration context</param>
/// <param name="loggerFactory">A logger factory used to create a logger for the plugin manager.</param>
public Startup(IServiceProvider hostProvider, IConfiguration configuration, ILoggerFactory loggerFactory, IWebHostEnvironment host)
{
_configuration = configuration;
_loggerFactory = loggerFactory;
_plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>());
// TODO remove postgres from here and load it like a normal plugin.
_plugins.LoadPlugins(new IPlugin[] {new CoreModule(),
new PostgresModule(configuration, host),
new AuthenticationModule(configuration, loggerFactory, host)
});
}
/// <summary>
/// Configure the WebApp services context.
/// </summary>
/// <param name="services">The service collection to fill.</param>
public void ConfigureServices(IServiceCollection services)
{
string publicUrl = _configuration.GetValue<string>("public_url");
string publicUrl = _configuration.GetValue<string>("publicUrl");
services.AddMvc().AddControllersAsServices();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
@ -58,139 +80,23 @@ namespace Kyoo
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
});
services.AddHttpClient();
services.AddDbContext<DatabaseContext>(options =>
{
options.UseNpgsql(_configuration.GetDatabaseConnection());
// .EnableSensitiveDataLogging()
// .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
}, ServiceLifetime.Transient);
services.AddDbContext<IdentityDatabase>(options =>
{
options.UseNpgsql(_configuration.GetDatabaseConnection());
});
string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityCore<User>(o =>
{
o.Stores.MaxLengthForKeys = 128;
})
.AddSignInManager()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<IdentityDatabase>();
services.AddIdentityServer(options =>
{
options.IssuerUri = publicUrl;
options.UserInteraction.LoginUrl = publicUrl + "login";
options.UserInteraction.ErrorUrl = publicUrl + "error";
options.UserInteraction.LogoutUrl = publicUrl + "logout";
})
.AddAspNetIdentity<User>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseNpgsql(_configuration.GetDatabaseConnection(),
sql => sql.MigrationsAssembly(assemblyName));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseNpgsql(_configuration.GetDatabaseConnection(),
sql => sql.MigrationsAssembly(assemblyName));
options.EnableTokenCleanup = true;
})
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
.AddInMemoryApiScopes(IdentityContext.GetScopes())
.AddInMemoryApiResources(IdentityContext.GetApis())
.AddProfileService<AccountController>()
.AddSigninKeys(_configuration);
services.AddTransient(typeof(Lazy<>), typeof(LazyDi<>));
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(_ => { });
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = publicUrl;
options.Audience = "Kyoo";
options.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
AuthorizationPolicyBuilder scheme = new(IdentityConstants.ApplicationScheme, JwtBearerDefaults.AuthenticationScheme);
options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build();
string[] permissions = {"Read", "Write", "Play", "Admin"};
foreach (string permission in permissions)
{
options.AddPolicy(permission, policy =>
{
policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme);
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.AddRequirements(new AuthorizationValidator(permission));
// policy.RequireScope($"kyoo.{permission.ToLower()}");
});
}
});
services.AddSingleton<IAuthorizationHandler, AuthorizationValidatorHandler>();
services.AddSingleton<ICorsPolicyService>(new DefaultCorsPolicyService(_loggerFactory.CreateLogger<DefaultCorsPolicyService>())
{
AllowedOrigins = { new Uri(publicUrl).GetLeftPart(UriPartial.Authority) }
});
// TODO Add custom method to the service container and expose those methods to the plugin
// TODO Add for example a AddRepository that will automatically register the complex interface, the IRepository<T> and the IBaseRepository
services.AddScoped<IBaseRepository, LibraryRepository>();
services.AddScoped<IBaseRepository, LibraryItemRepository>();
services.AddScoped<IBaseRepository, CollectionRepository>();
services.AddScoped<IBaseRepository, ShowRepository>();
services.AddScoped<IBaseRepository, SeasonRepository>();
services.AddScoped<IBaseRepository, EpisodeRepository>();
services.AddScoped<IBaseRepository, TrackRepository>();
services.AddScoped<IBaseRepository, PeopleRepository>();
services.AddScoped<IBaseRepository, StudioRepository>();
services.AddScoped<IBaseRepository, GenreRepository>();
services.AddScoped<IBaseRepository, ProviderRepository>();
services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<ILibraryItemRepository, LibraryItemRepository>();
services.AddScoped<ICollectionRepository, CollectionRepository>();
services.AddScoped<IShowRepository, ShowRepository>();
services.AddScoped<ISeasonRepository, SeasonRepository>();
services.AddScoped<IEpisodeRepository, EpisodeRepository>();
services.AddScoped<ITrackRepository, TrackRepository>();
services.AddScoped<IPeopleRepository, PeopleRepository>();
services.AddScoped<IStudioRepository, StudioRepository>();
services.AddScoped<IGenreRepository, GenreRepository>();
services.AddScoped<IProviderRepository, ProviderRepository>();
services.AddScoped<DbContext, DatabaseContext>();
services.AddScoped<ILibraryManager, LibraryManager>();
services.AddSingleton<IFileManager, FileManager>();
services.AddSingleton<ITranscoder, Transcoder>();
services.AddSingleton<IThumbnailsManager, ThumbnailsManager>();
services.AddSingleton<IProviderManager, ProviderManager>();
services.AddSingleton<IPluginManager, PluginManager>();
services.AddSingleton<ITaskManager, TaskManager>();
services.AddHostedService(provider => (TaskManager)provider.GetService<ITaskManager>());
services.AddSingleton(_plugins);
services.AddTask<PluginInitializer>();
_plugins.ConfigureServices(services);
}
/// <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)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
@ -222,28 +128,19 @@ namespace Kyoo
return next();
});
app.UseResponseCompression();
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict
});
app.UseAuthentication();
app.Use((ctx, next) =>
{
ctx.SetIdentityServerOrigin(_configuration.GetValue<string>("public_url"));
return next();
});
app.UseIdentityServer();
app.UseAuthorization();
_plugins.ConfigureAspnet(app);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
if (env.IsDevelopment())
spa.UseAngularCliServer("start");
});

View File

@ -1,18 +0,0 @@
using Kyoo.Controllers;
using Kyoo.Models;
namespace Kyoo.Tasks
{
public static class CoreTaskHolder
{
public static readonly ITask[] Tasks =
{
new CreateDatabase(),
new PluginLoader(),
new Crawler(),
new MetadataProviderLoader(),
// new ReScan(),
new ExtractMetadata()
};
}
}

View File

@ -7,10 +7,12 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
namespace Kyoo.Tasks
{
public class Crawler : ITask
{
@ -21,19 +23,20 @@ namespace Kyoo.Controllers
public bool RunOnStartup => true;
public int Priority => 0;
private IServiceProvider _serviceProvider;
private IThumbnailsManager _thumbnailsManager;
private IProviderManager _metadataProvider;
private ITranscoder _transcoder;
private IConfiguration _config;
[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; }
private int _parallelTasks;
public async Task<IEnumerable<string>> GetPossibleParameters()
public TaskParameters GetParameters()
{
using IServiceScope serviceScope = _serviceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
return (await libraryManager!.GetAll<Library>()).Select(x => x.Slug);
return new()
{
TaskParameter.Create<string>("slug", "A library slug to restrict the scan to this library.")
};
}
public int? Progress()
@ -42,20 +45,16 @@ namespace Kyoo.Controllers
return null;
}
public async Task Run(IServiceProvider serviceProvider,
CancellationToken cancellationToken,
string argument = null)
public async Task Run(TaskParameters parameters,
CancellationToken cancellationToken)
{
_serviceProvider = serviceProvider;
_thumbnailsManager = serviceProvider.GetService<IThumbnailsManager>();
_metadataProvider = serviceProvider.GetService<IProviderManager>();
_transcoder = serviceProvider.GetService<ITranscoder>();
_config = serviceProvider.GetService<IConfiguration>();
_parallelTasks = _config.GetValue<int>("parallelTasks");
string argument = parameters["slug"].As<string>();
_parallelTasks = Config.GetValue<int>("parallelTasks");
if (_parallelTasks <= 0)
_parallelTasks = 30;
using IServiceScope serviceScope = _serviceProvider.CreateScope();
using IServiceScope serviceScope = ServiceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
foreach (Show show in await libraryManager!.GetAll<Show>())
@ -74,7 +73,7 @@ namespace Kyoo.Controllers
ICollection<Library> libraries = argument == null
? await libraryManager.GetAll<Library>()
: new [] { await libraryManager.Get<Library>(argument)};
: new [] { await libraryManager.GetOrDefault<Library>(argument)};
if (argument != null && libraries.First() == null)
throw new ArgumentException($"No library found with the name {argument}");
@ -148,10 +147,10 @@ namespace Kyoo.Controllers
{
if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles"))
return;
using IServiceScope serviceScope = _serviceProvider.CreateScope();
using IServiceScope serviceScope = ServiceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("subtitleRegex");
string patern = Config.GetValue<string>("subtitleRegex");
Regex regex = new(patern, RegexOptions.IgnoreCase);
Match match = regex.Match(path);
@ -178,7 +177,7 @@ namespace Kyoo.Controllers
await libraryManager.Create(track);
Console.WriteLine($"Registering subtitle at: {path}.");
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
}
@ -195,10 +194,10 @@ namespace Kyoo.Controllers
try
{
using IServiceScope serviceScope = _serviceProvider.CreateScope();
using IServiceScope serviceScope = ServiceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("regex");
string patern = Config.GetValue<string>("regex");
Regex regex = new(patern, RegexOptions.IgnoreCase);
Match match = regex.Match(relativePath);
@ -254,10 +253,10 @@ namespace Kyoo.Controllers
{
if (string.IsNullOrEmpty(collectionName))
return null;
Collection collection = await libraryManager.Get<Collection>(Utility.ToSlug(collectionName));
Collection collection = await libraryManager.GetOrDefault<Collection>(Utility.ToSlug(collectionName));
if (collection != null)
return collection;
collection = await _metadataProvider.GetCollectionFromName(collectionName, library);
collection = await MetadataProvider.GetCollectionFromName(collectionName, library);
try
{
@ -266,7 +265,7 @@ namespace Kyoo.Controllers
}
catch (DuplicatedItemException)
{
return await libraryManager.Get<Collection>(collection.Slug);
return await libraryManager.GetOrDefault<Collection>(collection.Slug);
}
}
@ -276,15 +275,15 @@ namespace Kyoo.Controllers
bool isMovie,
Library library)
{
Show old = await libraryManager.Get<Show>(x => x.Path == showPath);
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 show = await MetadataProvider.SearchShow(showTitle, isMovie, library);
show.Path = showPath;
show.People = await _metadataProvider.GetPeople(show, library);
show.People = await MetadataProvider.GetPeople(show, library);
try
{
@ -292,7 +291,7 @@ namespace Kyoo.Controllers
}
catch (DuplicatedItemException)
{
old = await libraryManager.Get<Show>(show.Slug);
old = await libraryManager.GetOrDefault<Show>(show.Slug);
if (old.Path == showPath)
{
await libraryManager.Load(old, x => x.ExternalIDs);
@ -301,7 +300,7 @@ namespace Kyoo.Controllers
show.Slug += $"-{show.StartYear}";
await libraryManager.Create(show);
}
await _thumbnailsManager.Validate(show);
await ThumbnailsManager.Validate(show);
return show;
}
@ -318,11 +317,18 @@ namespace Kyoo.Controllers
season.Show = show;
return season;
}
catch (ItemNotFound)
catch (ItemNotFoundException)
{
Season season = await _metadataProvider.GetSeason(show, seasonNumber, library);
await libraryManager.CreateIfNotExists(season);
await _thumbnailsManager.Validate(season);
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;
}
@ -336,7 +342,7 @@ namespace Kyoo.Controllers
string episodePath,
Library library)
{
Episode episode = await _metadataProvider.GetEpisode(show,
Episode episode = await MetadataProvider.GetEpisode(show,
episodePath,
season?.SeasonNumber ?? -1,
episodeNumber,
@ -346,7 +352,7 @@ namespace Kyoo.Controllers
season ??= await GetSeason(libraryManager, show, episode.SeasonNumber, library);
episode.Season = season;
episode.SeasonID = season?.ID;
await _thumbnailsManager.Validate(episode);
await ThumbnailsManager.Validate(episode);
await GetTracks(episode);
return episode;
}
@ -367,7 +373,7 @@ namespace Kyoo.Controllers
private async Task<ICollection<Track>> GetTracks(Episode episode)
{
episode.Tracks = (await _transcoder.ExtractInfos(episode, false))
episode.Tracks = (await Transcoder.ExtractInfos(episode, false))
.Where(x => x.Type != StreamType.Attachment)
.ToArray();
return episode.Tracks;

View File

@ -1,66 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.Models;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Tasks
{
public class CreateDatabase : ITask
{
public string Slug => "create-database";
public string Name => "Create the database";
public string Description => "Create the database if it does not exit and initialize it with defaults value.";
public string HelpMessage => null;
public bool RunOnStartup => true;
public int Priority => int.MaxValue;
public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null)
{
using IServiceScope serviceScope = serviceProvider.CreateScope();
DatabaseContext databaseContext = serviceScope.ServiceProvider.GetService<DatabaseContext>();
IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService<IdentityDatabase>();
ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>();
databaseContext!.Database.Migrate();
identityDatabase!.Database.Migrate();
identityContext!.Database.Migrate();
if (!identityContext.Clients.Any())
{
foreach (Client client in IdentityContext.GetClients())
identityContext.Clients.Add(client.ToEntity());
identityContext.SaveChanges();
}
if (!identityContext.IdentityResources.Any())
{
foreach (IdentityResource resource in IdentityContext.GetIdentityResources())
identityContext.IdentityResources.Add(resource.ToEntity());
identityContext.SaveChanges();
}
if (!identityContext.ApiResources.Any())
{
foreach (ApiResource resource in IdentityContext.GetApis())
identityContext.ApiResources.Add(resource.ToEntity());
identityContext.SaveChanges();
}
return Task.CompletedTask;
}
public Task<IEnumerable<string>> GetPossibleParameters()
{
return Task.FromResult<IEnumerable<string>>(null);
}
public int? Progress()
{
return null;
}
}
}
// using System;
// using System.Collections.Generic;
// using System.Linq;
// using System.Threading;
// using System.Threading.Tasks;
// using IdentityServer4.EntityFramework.DbContexts;
// using IdentityServer4.EntityFramework.Mappers;
// using IdentityServer4.Models;
// using Kyoo.Models;
// using Microsoft.EntityFrameworkCore;
// using Microsoft.Extensions.DependencyInjection;
//
// namespace Kyoo.Tasks
// {
// public class CreateDatabase : ITask
// {
// public string Slug => "create-database";
// public string Name => "Create the database";
// public string Description => "Create the database if it does not exit and initialize it with defaults value.";
// public string HelpMessage => null;
// public bool RunOnStartup => true;
// public int Priority => int.MaxValue;
//
// public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null)
// {
// using IServiceScope serviceScope = serviceProvider.CreateScope();
// DatabaseContext databaseContext = serviceScope.ServiceProvider.GetService<DatabaseContext>();
// IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService<IdentityDatabase>();
// ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>();
//
// databaseContext!.Database.Migrate();
// identityDatabase!.Database.Migrate();
// identityContext!.Database.Migrate();
//
// if (!identityContext.Clients.Any())
// {
// foreach (Client client in IdentityContext.GetClients())
// identityContext.Clients.Add(client.ToEntity());
// identityContext.SaveChanges();
// }
// if (!identityContext.IdentityResources.Any())
// {
// foreach (IdentityResource resource in IdentityContext.GetIdentityResources())
// identityContext.IdentityResources.Add(resource.ToEntity());
// identityContext.SaveChanges();
// }
// if (!identityContext.ApiResources.Any())
// {
// foreach (ApiResource resource in IdentityContext.GetApis())
// identityContext.ApiResources.Add(resource.ToEntity());
// identityContext.SaveChanges();
// }
// return Task.CompletedTask;
// }
//
// public Task<IEnumerable<string>> GetPossibleParameters()
// {
// return Task.FromResult<IEnumerable<string>>(null);
// }
//
// public int? Progress()
// {
// return null;
// }
// }
// }

View File

@ -1,120 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Tasks
{
public class ExtractMetadata : ITask
{
public string Slug => "extract";
public string Name => "Metadata Extractor";
public string Description => "Extract subtitles or download thumbnails for a show/episode.";
public string HelpMessage => null;
public bool RunOnStartup => false;
public int Priority => 0;
private ILibraryManager _library;
private IThumbnailsManager _thumbnails;
private ITranscoder _transcoder;
public async Task Run(IServiceProvider serviceProvider, CancellationToken token, string arguments = null)
{
string[] args = arguments?.Split('/');
if (args == null || args.Length < 2)
return;
string slug = args[1];
bool thumbs = args.Length < 3 || string.Equals(args[2], "thumbnails", StringComparison.InvariantCultureIgnoreCase);
bool subs = args.Length < 3 || string.Equals(args[2], "subs", StringComparison.InvariantCultureIgnoreCase);
using IServiceScope serviceScope = serviceProvider.CreateScope();
_library = serviceScope.ServiceProvider.GetService<ILibraryManager>();
_thumbnails = serviceScope.ServiceProvider.GetService<IThumbnailsManager>();
_transcoder = serviceScope.ServiceProvider.GetService<ITranscoder>();
int id;
switch (args[0].ToLowerInvariant())
{
case "show":
case "shows":
Show show = await (int.TryParse(slug, out id)
? _library!.Get<Show>(id)
: _library!.Get<Show>(slug));
await ExtractShow(show, thumbs, subs, token);
break;
case "season":
case "seasons":
Season season = await (int.TryParse(slug, out id)
? _library!.Get<Season>(id)
: _library!.Get<Season>(slug));
await ExtractSeason(season, thumbs, subs, token);
break;
case "episode":
case "episodes":
Episode episode = await (int.TryParse(slug, out id)
? _library!.Get<Episode>(id)
: _library!.Get<Episode>(slug));
await ExtractEpisode(episode, thumbs, subs);
break;
}
}
private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token)
{
if (thumbs)
await _thumbnails!.Validate(show, true);
await _library.Load(show, x => x.Seasons);
foreach (Season season in show.Seasons)
{
if (token.IsCancellationRequested)
return;
await ExtractSeason(season, thumbs, subs, token);
}
}
private async Task ExtractSeason(Season season, bool thumbs, bool subs, CancellationToken token)
{
if (thumbs)
await _thumbnails!.Validate(season, true);
await _library.Load(season, x => x.Episodes);
foreach (Episode episode in season.Episodes)
{
if (token.IsCancellationRequested)
return;
await ExtractEpisode(episode, thumbs, subs);
}
}
private async Task ExtractEpisode(Episode episode, bool thumbs, bool subs)
{
if (thumbs)
await _thumbnails!.Validate(episode, true);
if (subs)
{
await _library.Load(episode, x => x.Tracks);
episode.Tracks = (await _transcoder!.ExtractInfos(episode, true))
.Where(x => x.Type != StreamType.Attachment)
.Concat(episode.Tracks.Where(x => x.IsExternal))
.ToList();
await _library.Edit(episode, false);
}
}
public Task<IEnumerable<string>> GetPossibleParameters()
{
return Task.FromResult<IEnumerable<string>>(null);
}
public int? Progress()
{
return null;
}
}
}
// using System;
// using System.Collections.Generic;
// using System.Linq;
// using System.Threading;
// using System.Threading.Tasks;
// using Kyoo.Controllers;
// using Kyoo.Models;
// using Microsoft.Extensions.DependencyInjection;
//
// namespace Kyoo.Tasks
// {
// public class ExtractMetadata : ITask
// {
// public string Slug => "extract";
// public string Name => "Metadata Extractor";
// public string Description => "Extract subtitles or download thumbnails for a show/episode.";
// public string HelpMessage => null;
// public bool RunOnStartup => false;
// public int Priority => 0;
//
//
// private ILibraryManager _library;
// private IThumbnailsManager _thumbnails;
// private ITranscoder _transcoder;
//
// public async Task Run(IServiceProvider serviceProvider, CancellationToken token, string arguments = null)
// {
// string[] args = arguments?.Split('/');
//
// if (args == null || args.Length < 2)
// return;
//
// string slug = args[1];
// bool thumbs = args.Length < 3 || string.Equals(args[2], "thumbnails", StringComparison.InvariantCultureIgnoreCase);
// bool subs = args.Length < 3 || string.Equals(args[2], "subs", StringComparison.InvariantCultureIgnoreCase);
//
// using IServiceScope serviceScope = serviceProvider.CreateScope();
// _library = serviceScope.ServiceProvider.GetService<ILibraryManager>();
// _thumbnails = serviceScope.ServiceProvider.GetService<IThumbnailsManager>();
// _transcoder = serviceScope.ServiceProvider.GetService<ITranscoder>();
// int id;
//
// switch (args[0].ToLowerInvariant())
// {
// case "show":
// case "shows":
// Show show = await (int.TryParse(slug, out id)
// ? _library!.Get<Show>(id)
// : _library!.Get<Show>(slug));
// await ExtractShow(show, thumbs, subs, token);
// break;
// case "season":
// case "seasons":
// Season season = await (int.TryParse(slug, out id)
// ? _library!.Get<Season>(id)
// : _library!.Get<Season>(slug));
// await ExtractSeason(season, thumbs, subs, token);
// break;
// case "episode":
// case "episodes":
// Episode episode = await (int.TryParse(slug, out id)
// ? _library!.Get<Episode>(id)
// : _library!.Get<Episode>(slug));
// await ExtractEpisode(episode, thumbs, subs);
// break;
// }
// }
//
// private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token)
// {
// if (thumbs)
// await _thumbnails!.Validate(show, true);
// await _library.Load(show, x => x.Seasons);
// foreach (Season season in show.Seasons)
// {
// if (token.IsCancellationRequested)
// return;
// await ExtractSeason(season, thumbs, subs, token);
// }
// }
//
// private async Task ExtractSeason(Season season, bool thumbs, bool subs, CancellationToken token)
// {
// if (thumbs)
// await _thumbnails!.Validate(season, true);
// await _library.Load(season, x => x.Episodes);
// foreach (Episode episode in season.Episodes)
// {
// if (token.IsCancellationRequested)
// return;
// await ExtractEpisode(episode, thumbs, subs);
// }
// }
//
// private async Task ExtractEpisode(Episode episode, bool thumbs, bool subs)
// {
// if (thumbs)
// await _thumbnails!.Validate(episode, true);
// if (subs)
// {
// await _library.Load(episode, x => x.Tracks);
// episode.Tracks = (await _transcoder!.ExtractInfos(episode, true))
// .Where(x => x.Type != StreamType.Attachment)
// .Concat(episode.Tracks.Where(x => x.IsExternal))
// .ToList();
// await _library.Edit(episode, false);
// }
// }
//
// public Task<IEnumerable<string>> GetPossibleParameters()
// {
// return Task.FromResult<IEnumerable<string>>(null);
// }
//
// public int? Progress()
// {
// return null;
// }
// }
// }

Some files were not shown because too many files have changed in this diff Show More