diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs
index e49cee9b..82d05cce 100644
--- a/Kyoo.Authentication/AuthenticationModule.cs
+++ b/Kyoo.Authentication/AuthenticationModule.cs
@@ -64,7 +64,7 @@ namespace Kyoo.Authentication
///
public void Configure(IServiceCollection services, ICollection availableTypes)
{
- string publicUrl = _configuration.GetValue("public_url");
+ string publicUrl = _configuration.GetValue("public_url").TrimEnd('/');
// services.AddDbContext(options =>
// {
@@ -86,9 +86,9 @@ namespace Kyoo.Authentication
services.AddIdentityServer(options =>
{
options.IssuerUri = publicUrl;
- options.UserInteraction.LoginUrl = publicUrl + "login";
- options.UserInteraction.ErrorUrl = publicUrl + "error";
- options.UserInteraction.LogoutUrl = publicUrl + "logout";
+ options.UserInteraction.LoginUrl = $"{publicUrl}/login";
+ options.UserInteraction.ErrorUrl = $"{publicUrl}/error";
+ options.UserInteraction.LogoutUrl = $"{publicUrl}/logout";
})
// .AddAspNetIdentity()
// .AddConfigurationStore(options =>
@@ -105,11 +105,13 @@ namespace Kyoo.Authentication
// options.EnableTokenCleanup = true;
// })
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
- .AddInMemoryApiScopes(IdentityContext.GetScopes())
.AddInMemoryApiResources(IdentityContext.GetApis())
.AddInMemoryClients(IdentityContext.GetClients())
- // .AddProfileService()
- .AddSigninKeys(certificateOptions);
+ .AddDeveloperSigningCredential();
+ // .AddProfileService()
+ // .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 =>
{
diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj
index 17107035..7bb0a7fb 100644
--- a/Kyoo.Authentication/Kyoo.Authentication.csproj
+++ b/Kyoo.Authentication/Kyoo.Authentication.csproj
@@ -17,9 +17,6 @@
-
-
-
diff --git a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs
new file mode 100644
index 00000000..4d8efa2c
--- /dev/null
+++ b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+using Kyoo.Models;
+
+namespace Kyoo.Authentication.Models.DTO
+{
+ ///
+ /// A model only used on register requests.
+ ///
+ public class RegisterRequest
+ {
+ ///
+ /// The user email address
+ ///
+ [EmailAddress]
+ public string Email { get; set; }
+
+ ///
+ /// The user's username.
+ ///
+ [MinLength(4)]
+ public string Username { get; set; }
+
+ ///
+ /// The user's password.
+ ///
+ [MinLength(8)]
+ public string Password { get; set; }
+
+
+ ///
+ /// Convert this register request to a new class.
+ ///
+ ///
+ public User ToUser()
+ {
+ return new()
+ {
+ Slug = Utility.ToSlug(Username),
+ Username = Username,
+ Password = Password,
+ Email = Email
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Authentication/User.cs b/Kyoo.Authentication/User.cs
index 2a65d409..896d3004 100644
--- a/Kyoo.Authentication/User.cs
+++ b/Kyoo.Authentication/User.cs
@@ -1,22 +1,22 @@
-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;
- }
- }
-}
\ No newline at end of file
+// 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;
+// }
+// }
+// }
\ No newline at end of file
diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs
index 7acf5226..de47d57d 100644
--- a/Kyoo.Authentication/Views/AccountApi.cs
+++ b/Kyoo.Authentication/Views/AccountApi.cs
@@ -1,191 +1,183 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.IO;
-using System.Linq;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using IdentityServer4.Models;
-using IdentityServer4.Services;
-using Kyoo.Models;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
-
-namespace Kyoo.Api
-{
- public class RegisterRequest
- {
- public string Email { get; set; }
- public string Username { get; set; }
- public string Password { get; set; }
- }
-
- public class LoginRequest
- {
- public string Username { get; set; }
- public string Password { get; set; }
- public bool StayLoggedIn { get; set; }
- }
-
- public class OtacRequest
- {
- public string Otac { get; set; }
- public bool StayLoggedIn { get; set; }
- }
-
- public class AccountData
- {
- [FromForm(Name = "email")]
- public string Email { get; set; }
- [FromForm(Name = "username")]
- public string Username { get; set; }
- [FromForm(Name = "picture")]
- public IFormFile Picture { get; set; }
- }
-
-
- [Route("api/[controller]")]
- [ApiController]
- public class AccountController : Controller, IProfileService
- {
- private readonly UserManager _userManager;
- private readonly SignInManager _signInManager;
- private readonly IConfiguration _configuration;
- private readonly string _picturePath;
-
- public AccountController(UserManager userManager,
- SignInManager siginInManager,
- IConfiguration configuration)
- {
- _userManager = userManager;
- _signInManager = siginInManager;
- _picturePath = configuration.GetValue("profilePicturePath");
- _configuration = configuration;
- if (!Path.IsPathRooted(_picturePath))
- _picturePath = Path.GetFullPath(_picturePath);
- }
-
- [HttpPost("register")]
- public async Task Register([FromBody] RegisterRequest user)
- {
- if (!ModelState.IsValid)
- return BadRequest(user);
- if (user.Username.Length < 4)
- return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}});
- if (!new EmailAddressAttribute().IsValid(user.Email))
- return BadRequest(new[] {new {code = "email", description = "Email must be valid."}});
- User account = new() {UserName = user.Username, Email = user.Email};
- IdentityResult result = await _userManager.CreateAsync(account, user.Password);
- if (!result.Succeeded)
- return BadRequest(result.Errors);
- string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
- await _userManager.UpdateAsync(account);
- await _userManager.AddClaimAsync(account, new Claim(
- "permissions",
- _configuration.GetValue("newUserPermissions")));
- return Ok(new {otac});
- }
-
- [HttpPost("login")]
- public async Task Login([FromBody] LoginRequest login)
- {
- if (!ModelState.IsValid)
- return BadRequest(login);
- SignInResult result = await _signInManager
- .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false);
- if (result.Succeeded)
- return Ok();
- return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}});
- }
-
- [HttpPost("otac-login")]
- public async Task OtacLogin([FromBody] OtacRequest otac)
- {
- if (!ModelState.IsValid)
- return BadRequest(otac);
- User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac);
- if (user == null)
- return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}});
- if (user.OTACExpires <= DateTime.UtcNow)
- return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
- await _signInManager.SignInAsync(user, otac.StayLoggedIn);
- return Ok();
- }
-
- [HttpGet("logout")]
- [Authorize]
- public async Task Logout()
- {
- await _signInManager.SignOutAsync();
- return Ok();
- }
-
- public async Task GetProfileDataAsync(ProfileDataRequestContext context)
- {
- User user = await _userManager.GetUserAsync(context.Subject);
- if (user != null)
- {
- List claims = new()
- {
- new Claim("email", user.Email),
- new Claim("username", user.UserName),
- new Claim("picture", $"api/account/picture/{user.UserName}")
- };
-
- Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
- if (perms != null)
- claims.Add(perms);
-
- context.IssuedClaims.AddRange(claims);
- }
- }
-
- public async Task IsActiveAsync(IsActiveContext context)
- {
- User user = await _userManager.GetUserAsync(context.Subject);
- context.IsActive = user != null;
- }
-
- [HttpGet("picture/{username}")]
- public async Task GetPicture(string username)
- {
- User user = await _userManager.FindByNameAsync(username);
- if (user == null)
- return BadRequest();
- string path = Path.Combine(_picturePath, user.Id);
- if (!System.IO.File.Exists(path))
- return NotFound();
- return new PhysicalFileResult(path, "image/png");
- }
-
- [HttpPost("update")]
- [Authorize]
- public async Task Update([FromForm] AccountData data)
- {
- User user = await _userManager.GetUserAsync(HttpContext.User);
-
- 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(_picturePath, user.Id);
- await using FileStream file = System.IO.File.Create(path);
- await data.Picture.CopyToAsync(file);
- }
- await _userManager.UpdateAsync(user);
- return Ok();
- }
-
- [HttpGet("default-permissions")]
- public ActionResult> GetDefaultPermissions()
- {
- return _configuration.GetValue("defaultPermissions").Split(",");
- }
- }
-}
\ No newline at end of file
+// using System;
+// using System.Collections.Generic;
+// using System.IO;
+// using System.Linq;
+// using System.Security.Claims;
+// using System.Threading.Tasks;
+// using IdentityServer4.Models;
+// using IdentityServer4.Services;
+// using Kyoo.Authentication.Models.DTO;
+// using Kyoo.Models;
+// using Microsoft.AspNetCore.Authorization;
+// using Microsoft.AspNetCore.Http;
+// using Microsoft.AspNetCore.Identity;
+// using Microsoft.AspNetCore.Mvc;
+// using Microsoft.Extensions.Configuration;
+// using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
+//
+// namespace Kyoo.Authentication.Views
+// {
+// public class LoginRequest
+// {
+// public string Username { get; set; }
+// public string Password { get; set; }
+// public bool StayLoggedIn { get; set; }
+// }
+//
+// public class OtacRequest
+// {
+// public string Otac { get; set; }
+// public bool StayLoggedIn { get; set; }
+// }
+//
+// public class AccountData
+// {
+// [FromForm(Name = "email")]
+// public string Email { get; set; }
+// [FromForm(Name = "username")]
+// public string Username { get; set; }
+// [FromForm(Name = "picture")]
+// public IFormFile Picture { get; set; }
+// }
+//
+//
+// ///
+// /// The class responsible for login, logout, permissions and claims of a user.
+// ///
+// [Route("api/account")]
+// [Route("api/accounts")]
+// [ApiController]
+// public class AccountApi : Controller, IProfileService
+// {
+// private readonly UserManager _userManager;
+// private readonly SignInManager _signInManager;
+// private readonly IConfiguration _configuration;
+// private readonly string _picturePath;
+//
+// // TODO find how SignInManager & UserManager are implement and check if they can be used or not.
+// public AccountApi(UserManager userManager,
+// SignInManager signInManager,
+// IConfiguration configuration)
+// {
+// _userManager = userManager;
+// _signInManager = signInManager;
+// _picturePath = configuration.GetValue("profilePicturePath");
+// _configuration = configuration;
+// if (!Path.IsPathRooted(_picturePath))
+// _picturePath = Path.GetFullPath(_picturePath);
+// }
+//
+// [HttpPost("register")]
+// public async Task Register([FromBody] RegisterRequest request)
+// {
+// User user = request.ToUser();
+// IdentityResult result = await _userManager.CreateAsync(user, user.Password);
+// if (!result.Succeeded)
+// return BadRequest(result.Errors);
+// string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
+// await _userManager.UpdateAsync(account);
+// await _userManager.AddClaimAsync(account, new Claim(
+// "permissions",
+// _configuration.GetValue("newUserPermissions")));
+// return Ok(new {otac});
+// }
+//
+// [HttpPost("login")]
+// public async Task Login([FromBody] LoginRequest login)
+// {
+// if (!ModelState.IsValid)
+// return BadRequest(login);
+// SignInResult result = await _signInManager
+// .PasswordSignInAsync(login.Username, login.Password, login.StayLoggedIn, false);
+// if (result.Succeeded)
+// return Ok();
+// return BadRequest(new [] { new {code = "InvalidCredentials", description = "Invalid username/password"}});
+// }
+//
+// [HttpPost("otac-login")]
+// public async Task OtacLogin([FromBody] OtacRequest otac)
+// {
+// if (!ModelState.IsValid)
+// return BadRequest(otac);
+// User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac);
+// if (user == null)
+// return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}});
+// if (user.OTACExpires <= DateTime.UtcNow)
+// return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
+// await _signInManager.SignInAsync(user, otac.StayLoggedIn);
+// return Ok();
+// }
+//
+// [HttpGet("logout")]
+// [Authorize]
+// public async Task Logout()
+// {
+// await _signInManager.SignOutAsync();
+// return Ok();
+// }
+//
+// public async Task GetProfileDataAsync(ProfileDataRequestContext context)
+// {
+// User user = await _userManager.GetUserAsync(context.Subject);
+// if (user != null)
+// {
+// List claims = new()
+// {
+// new Claim("email", user.Email),
+// new Claim("username", user.UserName),
+// new Claim("picture", $"api/account/picture/{user.UserName}")
+// };
+//
+// Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
+// if (perms != null)
+// claims.Add(perms);
+//
+// context.IssuedClaims.AddRange(claims);
+// }
+// }
+//
+// public async Task IsActiveAsync(IsActiveContext context)
+// {
+// User user = await _userManager.GetUserAsync(context.Subject);
+// context.IsActive = user != null;
+// }
+//
+// [HttpGet("picture/{username}")]
+// public async Task GetPicture(string username)
+// {
+// User user = await _userManager.FindByNameAsync(username);
+// if (user == null)
+// return BadRequest();
+// string path = Path.Combine(_picturePath, user.Id);
+// if (!System.IO.File.Exists(path))
+// return NotFound();
+// return new PhysicalFileResult(path, "image/png");
+// }
+//
+// [HttpPost("update")]
+// [Authorize]
+// public async Task Update([FromForm] AccountData data)
+// {
+// User user = await _userManager.GetUserAsync(HttpContext.User);
+//
+// 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(_picturePath, user.Id);
+// await using FileStream file = System.IO.File.Create(path);
+// await data.Picture.CopyToAsync(file);
+// }
+// await _userManager.UpdateAsync(user);
+// return Ok();
+// }
+//
+// [HttpGet("default-permissions")]
+// public ActionResult> GetDefaultPermissions()
+// {
+// return _configuration.GetValue("defaultPermissions").Split(",");
+// }
+// }
+// }
\ No newline at end of file
diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs
index 297f3b1d..c4c4231b 100644
--- a/Kyoo.Common/Models/Resources/IResource.cs
+++ b/Kyoo.Common/Models/Resources/IResource.cs
@@ -1,30 +1,23 @@
-using System;
-using System.Collections.Generic;
-
namespace Kyoo.Models
{
+ ///
+ /// An interface to represent a resource that can be retrieved from the database.
+ ///
public interface IResource
{
+ ///
+ /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
+ ///
public int ID { get; set; }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// There is no setter for a slug since it can be computed from other fields.
+ /// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
+ ///
public string Slug { get; }
}
-
- public class ResourceComparer : IEqualityComparer 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);
- }
- }
}
\ No newline at end of file
diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs
new file mode 100644
index 00000000..a4367c11
--- /dev/null
+++ b/Kyoo.Common/Models/Resources/User.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+
+namespace Kyoo.Models
+{
+ ///
+ /// A single user of the app.
+ ///
+ public class User : IResource
+ {
+ ///
+ public int ID { get; set; }
+
+ ///
+ public string Slug { get; set; }
+
+ ///
+ /// A username displayed to the user.
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// The user email address.
+ ///
+ public string Email { get; set; }
+
+ ///
+ /// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// The list of shows the user has finished.
+ ///
+ public ICollection Watched { get; set; }
+
+ ///
+ /// The list of episodes the user is watching (stopped in progress or the next episode of the show)
+ ///
+ public ICollection<(Episode episode, int watchedPercentage)> CurrentlyWatching { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs
index bbbe177b..ff44761a 100644
--- a/Kyoo.Common/Utility.cs
+++ b/Kyoo.Common/Utility.cs
@@ -712,72 +712,6 @@ namespace Kyoo
}, TaskContinuationOptions.ExecuteSynchronously);
}
- public static Expression> ResourceEquals(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 Func ResourceEqualsFunc(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(), ens.Cast());
- return RunGenericMethod(typeof(Enumerable), "SequenceEqual", type, first, second);
- }
-
- public static bool ResourceEquals([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([CanBeNull] IEnumerable first, [CanBeNull] IEnumerable second)
- where T : IResource
- {
- if (ReferenceEquals(first, second))
- return true;
- if (first == null || second == null)
- return false;
- return first.SequenceEqual(second, new ResourceComparer());
- }
-
- public static bool LinkEquals([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;
- }
-
///
/// Get a friendly type name (supporting generics)
/// For example a list of string will be displayed as List<string> and not as List`1.
diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs
index d92c208f..a795723e 100644
--- a/Kyoo.CommonAPI/DatabaseContext.cs
+++ b/Kyoo.CommonAPI/DatabaseContext.cs
@@ -64,6 +64,10 @@ namespace Kyoo
/// All metadataIDs (ExternalIDs) of Kyoo. See .
///
public DbSet MetadataIds { get; set; }
+ ///
+ /// The list of registered users.
+ ///
+ // public DbSet Users { get; set; }
///
/// All people's role. See .
diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs
index c172fd67..33eb9a67 100644
--- a/Kyoo/Startup.cs
+++ b/Kyoo/Startup.cs
@@ -124,6 +124,8 @@ namespace Kyoo
return next();
});
app.UseResponseCompression();
+
+ _plugins.ConfigureAspnet(app);
app.UseSpa(spa =>
{
@@ -133,8 +135,6 @@ namespace Kyoo
spa.UseAngularCliServer("start");
});
- _plugins.ConfigureAspnet(app);
-
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
diff --git a/Kyoo/settings.json b/Kyoo/settings.json
index 246b84de..b521f8a8 100644
--- a/Kyoo/settings.json
+++ b/Kyoo/settings.json
@@ -33,7 +33,8 @@
"permissions": {
"default": ["read", "play", "write", "admin"],
"newUser": ["read", "play", "write", "admin"]
- }
+ },
+ "profilePicturePath": "users/"
},
@@ -47,7 +48,6 @@
"transcodeTempPath": "cached/kyoo/transcode",
"peoplePath": "people",
"providerPath": "providers",
- "profilePicturePath": "users/",
"plugins": "plugins/",
"regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$",
"subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$"