mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Adding a new User class
This commit is contained in:
parent
d9cca97961
commit
77231f4f41
@ -4,6 +4,7 @@ using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Services;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@ -36,7 +37,10 @@ namespace Kyoo.Authentication
|
||||
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
|
||||
public ICollection<Type> Requires => new []
|
||||
{
|
||||
typeof(IUserRepository)
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -10,16 +10,29 @@ namespace Kyoo.Authentication
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
public static ClaimsPrincipal ToPrincipal(this User user)
|
||||
/// <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)
|
||||
{
|
||||
List<Claim> claims = new()
|
||||
return new[]
|
||||
{
|
||||
new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
|
||||
new Claim(JwtClaimTypes.Name, user.Username),
|
||||
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
|
||||
};
|
||||
}
|
||||
|
||||
ClaimsIdentity id = new (claims);
|
||||
/// <summary>
|
||||
/// Convert a user to a ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to convert</param>
|
||||
/// <returns>A ClaimsPrincipal representing the user</returns>
|
||||
public static ClaimsPrincipal ToPrincipal(this User user)
|
||||
{
|
||||
ClaimsIdentity id = new (user.GetClaims());
|
||||
return new ClaimsPrincipal(id);
|
||||
}
|
||||
}
|
||||
|
28
Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs
Normal file
28
Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs
Normal 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]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The new username of the user.
|
||||
/// </summary>
|
||||
[MinLength(4)]
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The picture icon.
|
||||
/// </summary>
|
||||
public IFormFile Picture { get; set; }
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using Kyoo.Authentication.Models.DTO;
|
||||
@ -12,24 +11,12 @@ using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
|
||||
|
||||
namespace Kyoo.Authentication.Views
|
||||
{
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The class responsible for login, logout, permissions and claims of a user.
|
||||
/// </summary>
|
||||
@ -46,6 +33,11 @@ namespace Kyoo.Authentication.Views
|
||||
/// The identity server interaction service to login users.
|
||||
/// </summary>
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
/// <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>
|
||||
@ -57,13 +49,16 @@ namespace Kyoo.Authentication.Views
|
||||
/// </summary>
|
||||
/// <param name="users">The user repository to create and manage users</param>
|
||||
/// <param name="interaction">The identity server interaction service to login 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,
|
||||
IIdentityServerInteractionService interaction,
|
||||
IFileManager files,
|
||||
IOptionsMonitor<AuthenticationOptions> options)
|
||||
{
|
||||
_users = users;
|
||||
_interaction = interaction;
|
||||
_files = files;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
@ -153,66 +148,51 @@ namespace Kyoo.Authentication.Views
|
||||
// TODO check with the extension method
|
||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
User user = await _userManager.GetUserAsync(context.Subject);
|
||||
if (user != null)
|
||||
{
|
||||
List<Claim> claims = new()
|
||||
{
|
||||
new Claim(JwtClaimTypes.Email, user.Email),
|
||||
new Claim(JwtClaimTypes.Name, user.Username),
|
||||
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
|
||||
};
|
||||
|
||||
Claim perms = (await _userManager.GetClaimsAsync(user)).FirstOrDefault(x => x.Type == "permissions");
|
||||
if (perms != null)
|
||||
claims.Add(perms);
|
||||
|
||||
context.IssuedClaims.AddRange(claims);
|
||||
}
|
||||
User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
|
||||
if (user == null)
|
||||
return;
|
||||
context.IssuedClaims.AddRange(user.GetClaims());
|
||||
}
|
||||
|
||||
public async Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
User user = await _userManager.GetUserAsync(context.Subject);
|
||||
User user = await _users.Get(int.Parse(context.Subject.GetSubjectId()));
|
||||
context.IsActive = user != null;
|
||||
}
|
||||
|
||||
[HttpGet("picture/{username}")]
|
||||
public async Task<IActionResult> GetPicture(string username)
|
||||
[HttpGet("picture/{slug}")]
|
||||
public async Task<IActionResult> GetPicture(string slug)
|
||||
{
|
||||
User user = await _userManager.FindByNameAsync(username);
|
||||
User user = await _users.GetOrDefault(slug);
|
||||
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");
|
||||
string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString());
|
||||
return _files.FileResult(path);
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
[HttpPut]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Update([FromForm] AccountData data)
|
||||
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
|
||||
{
|
||||
User user = await _userManager.GetUserAsync(HttpContext.User);
|
||||
User user = await _users.Get(int.Parse(HttpContext.User.GetSubjectId()));
|
||||
|
||||
if (!string.IsNullOrEmpty(data.Email))
|
||||
user.Email = data.Email;
|
||||
user.Email = data.Email;
|
||||
if (!string.IsNullOrEmpty(data.Username))
|
||||
user.UserName = 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);
|
||||
string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString());
|
||||
await using Stream file = _files.NewFile(path);
|
||||
await data.Picture.CopyToAsync(file);
|
||||
}
|
||||
await _userManager.UpdateAsync(user);
|
||||
return Ok();
|
||||
return await _users.Edit(user, false);
|
||||
}
|
||||
|
||||
[HttpGet("default-permissions")]
|
||||
[HttpGet("permissions")]
|
||||
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
|
||||
{
|
||||
return _configuration.GetValue<string>("defaultPermissions").Split(",");
|
||||
return _options.CurrentValue.Permissions.Default;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,21 +7,87 @@ 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, you should return a NotFoundResult or 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);
|
||||
}
|
||||
}
|
@ -53,11 +53,17 @@ namespace Kyoo.Models
|
||||
/// Links between Users and Shows.
|
||||
/// </summary>
|
||||
public ICollection<Link<User, Show>> ShowLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Links between Users and WatchedEpisodes.
|
||||
/// </summary>
|
||||
public ICollection<Link<User, WatchedEpisode>> EpisodeLinks { 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; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata of episode currently watching by an user
|
||||
/// </summary>
|
||||
public class WatchedEpisode : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[SerializeIgnore] public int ID
|
||||
{
|
||||
get => Episode.ID;
|
||||
set => Episode.ID = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[SerializeIgnore] public string Slug => Episode.Slug;
|
||||
|
||||
/// <summary>
|
||||
/// The episode currently watched
|
||||
/// </summary>
|
||||
public Episode Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
|
||||
/// </summary>
|
||||
public int WatchedPercentage { get; set; }
|
||||
}
|
||||
}
|
@ -74,6 +74,11 @@ namespace Kyoo
|
||||
/// </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.
|
||||
/// </summary>
|
||||
@ -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,6 +268,9 @@ 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>
|
||||
|
@ -2,17 +2,20 @@
|
||||
|
||||
<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>
|
||||
|
||||
<Company>SDG</Company>
|
||||
<Authors>Zoe Roux</Authors>
|
||||
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
|
||||
<LangVersion>default</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Postgresql;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -11,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
[DbContext(typeof(PostgresContext))]
|
||||
[Migration("20210505182627_Initial")]
|
||||
[Migration("20210507203809_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -225,6 +226,21 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -509,6 +525,58 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -621,6 +689,25 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -710,6 +797,25 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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");
|
||||
@ -780,6 +886,13 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Navigation("Shows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||
{
|
||||
b.Navigation("CurrentlyWatching");
|
||||
|
||||
b.Navigation("ShowLinks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
@ -104,6 +105,24 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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
|
||||
@ -256,6 +275,30 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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
|
||||
@ -420,6 +463,31 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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",
|
||||
@ -474,6 +542,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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",
|
||||
@ -549,6 +622,17 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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)
|
||||
@ -568,6 +652,9 @@ namespace Kyoo.Postgresql.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<Show, Genre>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link<User, Show>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataIds");
|
||||
|
||||
@ -577,6 +664,9 @@ namespace Kyoo.Postgresql.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tracks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WatchedEpisodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Collections");
|
||||
|
||||
@ -595,6 +685,9 @@ namespace Kyoo.Postgresql.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "Episodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Seasons");
|
||||
|
@ -1,5 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Postgresql;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -223,6 +224,21 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -507,6 +523,58 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -619,6 +687,25 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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")
|
||||
@ -708,6 +795,25 @@ namespace Kyoo.Postgresql.Migrations
|
||||
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");
|
||||
@ -778,6 +884,13 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Navigation("Shows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||
{
|
||||
b.Navigation("CurrentlyWatching");
|
||||
|
||||
b.Navigation("ShowLinks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,10 @@ namespace Kyoo.Postgresql
|
||||
modelBuilder.HasPostgresEnum<ItemType>();
|
||||
modelBuilder.HasPostgresEnum<StreamType>();
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.ExtraData)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3
|
||||
Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99
|
@ -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");
|
||||
|
@ -14,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>
|
||||
|
@ -44,7 +44,8 @@ namespace Kyoo
|
||||
(typeof(IPeopleRepository), typeof(DatabaseContext)),
|
||||
(typeof(IStudioRepository), typeof(DatabaseContext)),
|
||||
(typeof(IGenreRepository), typeof(DatabaseContext)),
|
||||
(typeof(IProviderRepository), typeof(DatabaseContext))
|
||||
(typeof(IProviderRepository), typeof(DatabaseContext)),
|
||||
(typeof(IUserRepository), typeof(DatabaseContext))
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -88,6 +89,7 @@ namespace Kyoo
|
||||
services.AddRepository<IStudioRepository, StudioRepository>();
|
||||
services.AddRepository<IGenreRepository, GenreRepository>();
|
||||
services.AddRepository<IProviderRepository, ProviderRepository>();
|
||||
services.AddRepository<IUserRepository, UserRepository>();
|
||||
}
|
||||
|
||||
services.AddTask<Crawler>();
|
||||
|
@ -126,14 +126,14 @@ namespace Kyoo
|
||||
app.UseResponseCompression();
|
||||
|
||||
_plugins.ConfigureAspnet(app);
|
||||
|
||||
app.UseSpa(spa =>
|
||||
{
|
||||
spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
|
||||
|
||||
if (env.IsDevelopment())
|
||||
spa.UseAngularCliServer("start");
|
||||
});
|
||||
//
|
||||
// app.UseSpa(spa =>
|
||||
// {
|
||||
// spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
|
||||
//
|
||||
// if (env.IsDevelopment())
|
||||
// spa.UseAngularCliServer("start");
|
||||
// });
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
@ -71,7 +71,7 @@ namespace Kyoo.Api
|
||||
await writer.WriteLineAsync("");
|
||||
await writer.WriteLineAsync("");
|
||||
|
||||
using StreamReader reader = _files.GetReader(_path);
|
||||
using StreamReader reader = new(_files.GetReader(_path));
|
||||
string line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user