mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Working oidc
This commit is contained in:
parent
429af9b252
commit
d7972704dd
@ -3,16 +3,19 @@ using System.Collections.Generic;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Services;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Authentication.Views;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
{
|
||||
@ -53,16 +56,25 @@ namespace Kyoo.Authentication
|
||||
/// </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>
|
||||
public AuthenticationModule(IConfiguration configuration, ILoggerFactory loggerFactory)
|
||||
/// <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 />
|
||||
@ -70,8 +82,16 @@ namespace Kyoo.Authentication
|
||||
{
|
||||
string publicUrl = _configuration.GetValue<string>("public_url").TrimEnd('/');
|
||||
|
||||
if (_environment.IsDevelopment())
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
// services.AddIdentityCore<User>()
|
||||
// .AddSignInManager()
|
||||
// .AddDefaultTokenProviders()
|
||||
// .AddUserStore<UserStore>();
|
||||
|
||||
// services.AddDbContext<IdentityDatabase>(options =>
|
||||
// {
|
||||
// options.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
|
||||
@ -113,25 +133,25 @@ namespace Kyoo.Authentication
|
||||
// options.EnableTokenCleanup = true;
|
||||
// })
|
||||
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
|
||||
.AddInMemoryApiScopes(IdentityContext.GetScopes())
|
||||
.AddInMemoryApiResources(IdentityContext.GetApis())
|
||||
.AddInMemoryClients(IdentityContext.GetClients())
|
||||
.AddDeveloperSigningCredential();
|
||||
// .AddProfileService<AccountApi>()
|
||||
// .AddSigninKeys(certificateOptions);
|
||||
.AddProfileService<AccountApi>()
|
||||
.AddSigninKeys(certificateOptions);
|
||||
// TODO implement means to add clients or api scopes for other plugins.
|
||||
// TODO split scopes (kyoo.read should be task.read, video.read etc)
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultScheme = IdentityConstants.ApplicationScheme;
|
||||
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
||||
})
|
||||
.AddIdentityCookies(_ => { });
|
||||
// services.AddAuthentication(o =>
|
||||
// {
|
||||
// o.DefaultScheme = IdentityConstants.ApplicationScheme;
|
||||
// o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
||||
// })
|
||||
// .AddIdentityCookies(_ => { });
|
||||
services.AddAuthentication()
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = publicUrl;
|
||||
options.Audience = "Kyoo";
|
||||
options.Audience = "kyoo";
|
||||
options.RequireHttpsMetadata = false;
|
||||
});
|
||||
|
||||
@ -146,10 +166,10 @@ namespace Kyoo.Authentication
|
||||
{
|
||||
options.AddPolicy(permission, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme);
|
||||
// policy.AuthenticationSchemes.Add(IdentityConstants.ApplicationScheme);
|
||||
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.AddRequirements(new AuthRequirement(permission));
|
||||
policy.RequireScope($"kyoo.{permission.ToLower()}");
|
||||
// policy.RequireScope($"kyoo.{permission.ToLower()}");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ namespace Kyoo.Authentication
|
||||
else
|
||||
{
|
||||
ICollection<string> defaultPerms = _options.CurrentValue.Default;
|
||||
if (defaultPerms.Contains(requirement.Permission.ToLower()))
|
||||
if (defaultPerms?.Contains(requirement.Permission.ToLower()) == true)
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
|
133
Kyoo.Authentication/Controllers/UserStore.cs
Normal file
133
Kyoo.Authentication/Controllers/UserStore.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// An implementation of an <see cref="IUserStore{TUser}"/> that uses an <see cref="IUserRepository"/>.
|
||||
/// </summary>
|
||||
public class UserStore : IUserStore<User>
|
||||
{
|
||||
/// <summary>
|
||||
/// The user repository used to store users.
|
||||
/// </summary>
|
||||
private readonly IUserRepository _users;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="UserStore"/>.
|
||||
/// </summary>
|
||||
/// <param name="users">The user repository to use</param>
|
||||
public UserStore(IUserRepository users)
|
||||
{
|
||||
_users = users;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of the IDisposable pattern
|
||||
/// </summary>
|
||||
/// <param name="disposing">True if this class should be disposed.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
bool _ = disposing;
|
||||
// Not implemented because this class has nothing to dispose.
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.ID.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.Username);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
user.Username = userName;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.Slug);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken)
|
||||
{
|
||||
user.Slug = normalizedName;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _users.Create(user);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _users.Edit(user, false);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _users.Delete(user);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return IdentityResult.Failed(new IdentityError {Code = ex.GetType().Name, Description = ex.Message});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
return _users.GetOrDefault(int.Parse(userId));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
|
||||
{
|
||||
return _users.GetOrDefault(normalizedUserName);
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ namespace Kyoo.Authentication
|
||||
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" }
|
||||
}
|
||||
@ -60,11 +60,6 @@ namespace Kyoo.Authentication
|
||||
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."
|
||||
@ -76,9 +71,8 @@ namespace Kyoo.Authentication
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ApiResource
|
||||
new ApiResource("kyoo", "Kyoo")
|
||||
{
|
||||
Name = "Kyoo",
|
||||
Scopes = GetScopes().Select(x => x.Name).ToArray()
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Authentication.Models
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -3,7 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
@ -14,6 +16,7 @@ using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@ -39,6 +42,7 @@ namespace Kyoo.Authentication.Views
|
||||
/// A file manager to send profile pictures
|
||||
/// </summary>
|
||||
private readonly IFileManager _files;
|
||||
// private readonly SignInManager<User> _signInManager;
|
||||
|
||||
/// <summary>
|
||||
/// Options about authentication. Those options are monitored and reloads are supported.
|
||||
@ -57,11 +61,13 @@ namespace Kyoo.Authentication.Views
|
||||
// IIdentityServerInteractionService interaction,
|
||||
IFileManager files,
|
||||
IOptions<AuthenticationOption> options)
|
||||
//, SignInManager<User> signInManager)
|
||||
{
|
||||
_users = users;
|
||||
// _interaction = interaction;
|
||||
_files = files;
|
||||
_options = options;
|
||||
// _signInManager = signInManager;
|
||||
}
|
||||
|
||||
|
||||
@ -114,14 +120,14 @@ namespace Kyoo.Authentication.Views
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest login)
|
||||
{
|
||||
// AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL);
|
||||
User user = await _users.Get(x => x.Username == login.Username);
|
||||
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.ID.ToString(), user.ToPrincipal(), StayLogged(login.StayLoggedIn));
|
||||
// await _signInManager.SignInAsync(user, login.StayLoggedIn);
|
||||
return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
|
||||
}
|
||||
|
||||
@ -142,7 +148,15 @@ namespace Kyoo.Authentication.Views
|
||||
{
|
||||
code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
|
||||
});
|
||||
await HttpContext.SignInAsync(user.ID.ToString(), user.ToPrincipal(), StayLogged(otac.StayLoggedIn));
|
||||
|
||||
|
||||
IdentityServerUser iduser = new(user.ID.ToString())
|
||||
{
|
||||
DisplayName = user.Username
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(iduser, StayLogged(otac.StayLoggedIn));
|
||||
// await _signInManager.SignInAsync(user, otac.StayLoggedIn);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -153,22 +167,23 @@ namespace Kyoo.Authentication.Views
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync();
|
||||
// await _signInManager.SignOutAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// TODO check with the extension method
|
||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
|
||||
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)));
|
||||
}
|
||||
|
||||
public async Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
|
||||
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
|
||||
context.IsActive = user != null;
|
||||
}
|
||||
|
||||
@ -186,8 +201,10 @@ namespace Kyoo.Authentication.Views
|
||||
[Authorize]
|
||||
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
|
||||
{
|
||||
User user = await _users.Get(int.Parse(HttpContext.User.GetSubjectId()));
|
||||
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))
|
||||
@ -204,7 +221,7 @@ namespace Kyoo.Authentication.Views
|
||||
[HttpGet("permissions")]
|
||||
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
|
||||
{
|
||||
return _options.Value.Permissions.Default;
|
||||
return _options.Value.Permissions.Default ?? Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
@ -84,6 +84,7 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the resource</typeparam>
|
||||
/// <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>
|
||||
@ -93,6 +94,7 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the resource</typeparam>
|
||||
/// <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>
|
||||
@ -102,6 +104,7 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the resource</typeparam>
|
||||
/// <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>
|
||||
@ -111,6 +114,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="seasonNumber">The season's number</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The season found</returns>
|
||||
[ItemNotNull]
|
||||
Task<Season> Get(int showID, int seasonNumber);
|
||||
|
||||
/// <summary>
|
||||
@ -120,6 +124,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="seasonNumber">The season's number</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The season found</returns>
|
||||
[ItemNotNull]
|
||||
Task<Season> Get(string showSlug, int seasonNumber);
|
||||
|
||||
/// <summary>
|
||||
@ -130,6 +135,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="episodeNumber">The episode's number</param>
|
||||
/// <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>
|
||||
@ -140,6 +146,7 @@ namespace Kyoo.Controllers
|
||||
/// <param name="episodeNumber">The episode's number</param>
|
||||
/// <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>
|
||||
@ -148,7 +155,8 @@ namespace Kyoo.Controllers
|
||||
/// <param name="slug">The slug of the track</param>
|
||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The tracl found</returns>
|
||||
/// <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);
|
||||
|
||||
|
||||
|
@ -212,9 +212,8 @@ namespace Kyoo.Controllers
|
||||
/// 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 caught 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
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Kyoo.Models.Exceptions
|
||||
{
|
||||
@ -22,5 +23,14 @@ namespace Kyoo.Models.Exceptions
|
||||
public DuplicatedItemException(string 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)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Kyoo.Models.Exceptions
|
||||
{
|
||||
@ -20,5 +21,14 @@ namespace Kyoo.Models.Exceptions
|
||||
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)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -29,28 +29,20 @@ namespace Kyoo.CommonApi
|
||||
[Authorize(Policy = "Read")]
|
||||
public virtual async Task<ActionResult<T>> Get(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _repository.Get(id);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
T ret = await _repository.GetOrDefault(id);
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
[HttpGet("{slug}")]
|
||||
[Authorize(Policy = "Read")]
|
||||
public virtual async Task<ActionResult<T>> Get(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _repository.Get(slug);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
T ret = await _repository.Get(slug);
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
[HttpGet("count")]
|
||||
@ -111,7 +103,7 @@ namespace Kyoo.CommonApi
|
||||
}
|
||||
catch (DuplicatedItemException)
|
||||
{
|
||||
T existing = await _repository.Get(resource.Slug);
|
||||
T existing = await _repository.GetOrDefault(resource.Slug);
|
||||
return Conflict(existing);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace Kyoo
|
||||
{
|
||||
|
@ -125,7 +125,7 @@ namespace Kyoo.Controllers
|
||||
Sort<T> sort = default,
|
||||
Pagination limit = default)
|
||||
{
|
||||
return ApplyFilters(query, Get, DefaultSort, where, sort, limit);
|
||||
return ApplyFilters(query, GetOrDefault, DefaultSort, where, sort, limit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ namespace Kyoo.Postgresql
|
||||
services.AddDbContext<DatabaseContext, PostgresContext>(x =>
|
||||
{
|
||||
x.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
|
||||
if (_configuration.GetValue<bool>("logging:dotnet-ef"))
|
||||
if (_environment.IsDevelopment())
|
||||
x.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||
});
|
||||
// services.AddScoped<DatabaseContext>(_ => new PostgresContext(
|
||||
|
@ -1,6 +1,3 @@
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
{
|
||||
public class SetupTests
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99
|
||||
Subproject commit d3a860fa8ffccade9e3b17022482e11c9a18303e
|
@ -234,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;
|
||||
|
@ -111,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 />
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -73,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;
|
||||
});
|
||||
@ -129,7 +129,7 @@ namespace Kyoo.Controllers
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
if (!people.Any() && await _shows.Value.Get(showID) == null)
|
||||
if (!people.Any() && await _shows.Value.GetOrDefault(showID) == null)
|
||||
throw new ItemNotFoundException();
|
||||
foreach (PeopleRole role in people)
|
||||
role.ForPeople = true;
|
||||
@ -151,7 +151,7 @@ namespace Kyoo.Controllers
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
if (!people.Any() && await _shows.Value.Get(showSlug) == null)
|
||||
if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null)
|
||||
throw new ItemNotFoundException();
|
||||
foreach (PeopleRole role in people)
|
||||
role.ForPeople = true;
|
||||
@ -172,7 +172,7 @@ namespace Kyoo.Controllers
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
if (!roles.Any() && await Get(id) == null)
|
||||
if (!roles.Any() && await GetOrDefault(id) == null)
|
||||
throw new ItemNotFoundException();
|
||||
return roles;
|
||||
}
|
||||
@ -191,7 +191,7 @@ namespace Kyoo.Controllers
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
if (!roles.Any() && await Get(slug) == null)
|
||||
if (!roles.Any() && await GetOrDefault(slug) == null)
|
||||
throw new ItemNotFoundException();
|
||||
return roles;
|
||||
}
|
||||
|
@ -160,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;
|
||||
});
|
||||
|
@ -102,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;
|
||||
});
|
||||
|
@ -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)
|
||||
|
@ -83,7 +83,10 @@ namespace Kyoo
|
||||
.ConfigureLogging((context, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(context.Configuration.GetSection("logging"))
|
||||
.AddConsole()
|
||||
.AddSimpleConsole(x =>
|
||||
{
|
||||
x.TimestampFormat = "[hh:mm:ss] ";
|
||||
})
|
||||
.AddDebug()
|
||||
.AddEventSourceLogger();
|
||||
})
|
||||
|
@ -47,7 +47,8 @@ namespace Kyoo
|
||||
_configuration = configuration;
|
||||
_plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>());
|
||||
|
||||
_plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), new AuthenticationModule(configuration, loggerFactory)});
|
||||
_plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host),
|
||||
new AuthenticationModule(configuration, loggerFactory, host)});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,7 +73,7 @@ namespace Kyoo.Tasks
|
||||
|
||||
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}");
|
||||
@ -253,7 +253,7 @@ namespace Kyoo.Tasks
|
||||
{
|
||||
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);
|
||||
@ -265,7 +265,7 @@ namespace Kyoo.Tasks
|
||||
}
|
||||
catch (DuplicatedItemException)
|
||||
{
|
||||
return await libraryManager.Get<Collection>(collection.Slug);
|
||||
return await libraryManager.GetOrDefault<Collection>(collection.Slug);
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,7 +275,7 @@ namespace Kyoo.Tasks
|
||||
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);
|
||||
@ -291,7 +291,7 @@ namespace Kyoo.Tasks
|
||||
}
|
||||
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);
|
||||
@ -320,8 +320,15 @@ namespace Kyoo.Tasks
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
Season season = await MetadataProvider.GetSeason(show, seasonNumber, library);
|
||||
await libraryManager.CreateIfNotExists(season);
|
||||
try
|
||||
{
|
||||
await libraryManager.Create(season);
|
||||
await ThumbnailsManager.Validate(season);
|
||||
}
|
||||
catch (DuplicatedItemException)
|
||||
{
|
||||
season = await libraryManager.Get(show.Slug, seasonNumber);
|
||||
}
|
||||
season.Show = show;
|
||||
return season;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace Kyoo.Api
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -92,7 +92,7 @@ namespace Kyoo.Api
|
||||
new Sort<Library>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Collection>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
|
@ -50,14 +50,20 @@ namespace Kyoo.Api
|
||||
[Authorize(Policy = "Read")]
|
||||
public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber, int episodeNumber)
|
||||
{
|
||||
return await _libraryManager.Get<Show>(showID);
|
||||
Show ret = await _libraryManager.GetOrDefault<Show>(showID);
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
return ret;
|
||||
}
|
||||
|
||||
[HttpGet("{episodeID:int}/season")]
|
||||
[Authorize(Policy = "Read")]
|
||||
public async Task<ActionResult<Season>> GetSeason(int episodeID)
|
||||
{
|
||||
return await _libraryManager.Get<Season>(x => x.Episodes.Any(y => y.ID == episodeID));
|
||||
Season ret = await _libraryManager.GetOrDefault<Season>(x => x.Episodes.Any(y => y.ID == episodeID));
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
return ret;
|
||||
}
|
||||
|
||||
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")]
|
||||
@ -104,7 +110,7 @@ namespace Kyoo.Api
|
||||
new Sort<Track>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Episode>(episodeID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Episode>(episodeID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -175,7 +181,6 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/thumb")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetThumb(int id)
|
||||
{
|
||||
try
|
||||
@ -190,7 +195,6 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/thumb")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetThumb(string slug)
|
||||
{
|
||||
try
|
||||
|
@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using Kyoo.CommonApi;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -41,7 +40,7 @@ namespace Kyoo.Api
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Genre>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.CommonApi;
|
||||
using Kyoo.Models.Exceptions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@ -27,7 +26,7 @@ namespace Kyoo.Api
|
||||
_taskManager = taskManager;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Authorize(Policy = "Write")]
|
||||
public override async Task<ActionResult<Library>> Create(Library resource)
|
||||
{
|
||||
ActionResult<Library> result = await base.Create(resource);
|
||||
@ -52,7 +51,7 @@ namespace Kyoo.Api
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -104,7 +103,7 @@ namespace Kyoo.Api
|
||||
new Sort<Collection>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -156,7 +155,7 @@ namespace Kyoo.Api
|
||||
new Sort<LibraryItem>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
|
@ -87,18 +87,20 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/poster")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetPeopleIcon(int id)
|
||||
{
|
||||
People people = await _libraryManager.Get<People>(id);
|
||||
People people = await _libraryManager.GetOrDefault<People>(id);
|
||||
if (people == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbs.GetPeoplePoster(people));
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/poster")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetPeopleIcon(string slug)
|
||||
{
|
||||
People people = await _libraryManager.Get<People>(slug);
|
||||
People people = await _libraryManager.GetOrDefault<People>(slug);
|
||||
if (people == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbs.GetPeoplePoster(people));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ using System.Threading.Tasks;
|
||||
using Kyoo.CommonApi;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@ -29,18 +28,20 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/logo")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetLogo(int id)
|
||||
{
|
||||
Provider provider = await _libraryManager.Get<Provider>(id);
|
||||
Provider provider = await _libraryManager.GetOrDefault<Provider>(id);
|
||||
if (provider == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/logo")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetLogo(string slug)
|
||||
{
|
||||
Provider provider = await _libraryManager.Get<Provider>(slug);
|
||||
Provider provider = await _libraryManager.GetOrDefault<Provider>(slug);
|
||||
if (provider == null)
|
||||
return NotFound();
|
||||
return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace Kyoo.Api
|
||||
new Sort<Episode>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Season>(seasonID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Season>(seasonID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -130,23 +130,28 @@ namespace Kyoo.Api
|
||||
[Authorize(Policy = "Read")]
|
||||
public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber)
|
||||
{
|
||||
return await _libraryManager.Get<Show>(showID);
|
||||
Show ret = await _libraryManager.GetOrDefault<Show>(showID);
|
||||
if (ret == null)
|
||||
return NotFound();
|
||||
return ret;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/thumb")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetThumb(int id)
|
||||
{
|
||||
Season season = await _libraryManager.Get<Season>(id);
|
||||
Season season = await _libraryManager.GetOrDefault<Season>(id);
|
||||
if (season == null)
|
||||
return NotFound();
|
||||
await _libraryManager.Load(season, x => x.Show);
|
||||
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/thumb")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetThumb(string slug)
|
||||
{
|
||||
Season season = await _libraryManager.Get<Season>(slug);
|
||||
Season season = await _libraryManager.GetOrDefault<Season>(slug);
|
||||
if (season == null)
|
||||
return NotFound();
|
||||
await _libraryManager.Load(season, x => x.Show);
|
||||
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ namespace Kyoo.Api
|
||||
new Sort<Season>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -75,7 +75,7 @@ namespace Kyoo.Api
|
||||
new Sort<Season>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -101,7 +101,7 @@ namespace Kyoo.Api
|
||||
new Sort<Episode>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -127,7 +127,7 @@ namespace Kyoo.Api
|
||||
new Sort<Episode>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -152,7 +152,7 @@ namespace Kyoo.Api
|
||||
new Sort<PeopleRole>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -177,7 +177,7 @@ namespace Kyoo.Api
|
||||
new Sort<PeopleRole>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -203,7 +203,7 @@ namespace Kyoo.Api
|
||||
new Sort<Genre>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -229,7 +229,7 @@ namespace Kyoo.Api
|
||||
new Sort<Genre>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -283,7 +283,7 @@ namespace Kyoo.Api
|
||||
new Sort<Library>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -309,7 +309,7 @@ namespace Kyoo.Api
|
||||
new Sort<Library>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -335,7 +335,7 @@ namespace Kyoo.Api
|
||||
new Sort<Collection>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(showID) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -361,7 +361,7 @@ namespace Kyoo.Api
|
||||
new Sort<Collection>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Show>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -408,7 +408,6 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/poster")]
|
||||
[Authorize(Policy = "Read")]
|
||||
public async Task<IActionResult> GetPoster(string slug)
|
||||
{
|
||||
try
|
||||
@ -423,7 +422,6 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/logo")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetLogo(string slug)
|
||||
{
|
||||
try
|
||||
@ -438,7 +436,6 @@ namespace Kyoo.Api
|
||||
}
|
||||
|
||||
[HttpGet("{slug}/backdrop")]
|
||||
[Authorize(Policy="Read")]
|
||||
public async Task<IActionResult> GetBackdrop(string slug)
|
||||
{
|
||||
try
|
||||
|
@ -40,7 +40,7 @@ namespace Kyoo.Api
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Studio>(id) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Studio>(id) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
@ -66,7 +66,7 @@ namespace Kyoo.Api
|
||||
new Sort<Show>(sortBy),
|
||||
new Pagination(limit, afterID));
|
||||
|
||||
if (!resources.Any() && await _libraryManager.Get<Studio>(slug) == null)
|
||||
if (!resources.Any() && await _libraryManager.GetOrDefault<Studio>(slug) == null)
|
||||
return NotFound();
|
||||
return Page(resources, limit);
|
||||
}
|
||||
|
@ -20,12 +20,9 @@
|
||||
"default": "Trace",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.EntityFrameworkCore.DbUpdateException": "None",
|
||||
"Microsoft.EntityFrameworkCore.Update": "None",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "None",
|
||||
"Microsoft.EntityFrameworkCore": "None",
|
||||
"Kyoo": "Trace"
|
||||
},
|
||||
"dotnet-ef": "false"
|
||||
}
|
||||
},
|
||||
|
||||
"authentication": {
|
||||
@ -35,7 +32,7 @@
|
||||
"password": "passphrase"
|
||||
},
|
||||
"permissions": {
|
||||
"default": ["read", "play", "write", "admin"],
|
||||
"default": [],
|
||||
"newUser": ["read", "play", "write", "admin"]
|
||||
},
|
||||
"profilePicturePath": "users/"
|
||||
|
Loading…
x
Reference in New Issue
Block a user