From 77231f4f417603f6fa633ad00a1e77da4252784e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 7 May 2021 23:13:33 +0200 Subject: [PATCH] Adding a new User class --- Kyoo.Authentication/AuthenticationModule.cs | 6 +- Kyoo.Authentication/Extensions.cs | 21 +++- .../Models/DTO/AccountUpdateRequest.cs | 28 +++++ Kyoo.Authentication/Views/AccountApi.cs | 78 +++++------- Kyoo.Common/Controllers/IFileManager.cs | 80 ++++++++++-- Kyoo.Common/Models/Resources/User.cs | 16 ++- Kyoo.Common/Models/WatchedEpisode.cs | 30 ----- Kyoo.CommonAPI/DatabaseContext.cs | 23 ++++ Kyoo.Postgresql/Kyoo.Postgresql.csproj | 13 +- ....cs => 20210507203809_Initial.Designer.cs} | 115 +++++++++++++++++- ...7_Initial.cs => 20210507203809_Initial.cs} | 93 ++++++++++++++ .../PostgresContextModelSnapshot.cs | 113 +++++++++++++++++ Kyoo.Postgresql/PostgresContext.cs | 4 + Kyoo.WebApp | 2 +- Kyoo/Controllers/FileManager.cs | 36 +++++- .../Repositories/ShowRepository.cs | 2 +- Kyoo/CoreModule.cs | 4 +- Kyoo/Startup.cs | 16 +-- Kyoo/Views/SubtitleApi.cs | 2 +- 19 files changed, 563 insertions(+), 119 deletions(-) create mode 100644 Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs delete mode 100644 Kyoo.Common/Models/WatchedEpisode.cs rename Kyoo.Postgresql/Migrations/{20210505182627_Initial.Designer.cs => 20210507203809_Initial.Designer.cs} (87%) rename Kyoo.Postgresql/Migrations/{20210505182627_Initial.cs => 20210507203809_Initial.cs} (86%) diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 82d05cce..02a0f982 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -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 ConditionalProvides => ArraySegment.Empty; /// - public ICollection Requires => ArraySegment.Empty; + public ICollection Requires => new [] + { + typeof(IUserRepository) + }; /// diff --git a/Kyoo.Authentication/Extensions.cs b/Kyoo.Authentication/Extensions.cs index b844ce5f..4cac9dc8 100644 --- a/Kyoo.Authentication/Extensions.cs +++ b/Kyoo.Authentication/Extensions.cs @@ -10,16 +10,29 @@ namespace Kyoo.Authentication /// public static class Extensions { - public static ClaimsPrincipal ToPrincipal(this User user) + /// + /// Get claims of an user. + /// + /// The user concerned + /// The list of claims the user has + public static ICollection GetClaims(this User user) { - List 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); + } + + /// + /// Convert a user to a ClaimsPrincipal. + /// + /// The user to convert + /// A ClaimsPrincipal representing the user + public static ClaimsPrincipal ToPrincipal(this User user) + { + ClaimsIdentity id = new (user.GetClaims()); return new ClaimsPrincipal(id); } } diff --git a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs new file mode 100644 index 00000000..4ec474d5 --- /dev/null +++ b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; + +namespace Kyoo.Authentication.Models.DTO +{ + /// + /// A model only used on account update requests. + /// + public class AccountUpdateRequest + { + /// + /// The new email address of the user + /// + [EmailAddress] + public string Email { get; set; } + + /// + /// The new username of the user. + /// + [MinLength(4)] + public string Username { get; set; } + + /// + /// The picture icon. + /// + public IFormFile Picture { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 94cae0ad..903c7e93 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -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; } - } - - /// /// The class responsible for login, logout, permissions and claims of a user. /// @@ -46,6 +33,11 @@ namespace Kyoo.Authentication.Views /// The identity server interaction service to login users. /// private readonly IIdentityServerInteractionService _interaction; + /// + /// A file manager to send profile pictures + /// + private readonly IFileManager _files; + /// /// Options about authentication. Those options are monitored and reloads are supported. /// @@ -57,13 +49,16 @@ namespace Kyoo.Authentication.Views /// /// The user repository to create and manage users /// The identity server interaction service to login users. + /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, IIdentityServerInteractionService interaction, + IFileManager files, IOptionsMonitor 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 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 GetPicture(string username) + [HttpGet("picture/{slug}")] + public async Task 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 Update([FromForm] AccountData data) + public async Task> 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> GetDefaultPermissions() { - return _configuration.GetValue("defaultPermissions").Split(","); + return _options.CurrentValue.Permissions.Default; } } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs index cc3c70bb..0765c1d0 100644 --- a/Kyoo.Common/Controllers/IFileManager.cs +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -7,21 +7,87 @@ using Microsoft.AspNetCore.Mvc; namespace Kyoo.Controllers { + /// + /// A service to abstract the file system to allow custom file systems (like distant file systems or external providers) + /// public interface IFileManager { - public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false); - - public StreamReader GetReader([NotNull] string path); - - public Task> ListFiles([NotNull] string path); - - public Task Exists([NotNull] string path); // TODO find a way to handle Transmux/Transcode with this system. + /// + /// Used for http queries returning a file. This should be used to return local files + /// or proxy them from a distant server + /// + /// + /// If no file exists at the given path, you should return a NotFoundResult or handle it gracefully. + /// + /// The path of the file. + /// + /// Should the file be downloaded at once or is the client allowed to request only part of the file + /// + /// + /// You can manually specify the content type of your file. + /// For example you can force a file to be returned as plain text using text/plain. + /// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file). + /// + /// An representing the file returned. + public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null); + + /// + /// Read a file present at . The reader can be used in an arbitrary context. + /// To return files from an http endpoint, use . + /// + /// The path of the file + /// If the file could not be found. + /// A reader to read the file. + public Stream GetReader([NotNull] string path); + + /// + /// Create a new file at . + /// + /// The path of the new file. + /// A writer to write to the new file. + public Stream NewFile([NotNull] string path); + + /// + /// List files in a directory. + /// + /// The path of the directory + /// A list of files's path. + public Task> ListFiles([NotNull] string path); + + /// + /// Check if a file exists at the given path. + /// + /// The path to check + /// True if the path exists, false otherwise + public Task Exists([NotNull] string path); + + /// + /// 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. + /// + /// The show to proceed + /// The extra directory of the show public string GetExtraDirectory(Show show); + /// + /// 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. + /// + /// The season to proceed + /// The extra directory of the season public string GetExtraDirectory(Season season); + /// + /// 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. + /// + /// The episode to proceed + /// The extra directory of the episode public string GetExtraDirectory(Episode episode); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index da85af0b..94afa240 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -53,11 +53,17 @@ namespace Kyoo.Models /// Links between Users and Shows. /// public ICollection> ShowLinks { get; set; } - - /// - /// Links between Users and WatchedEpisodes. - /// - public ICollection> EpisodeLinks { get; set; } #endif } + + /// + /// Metadata of episode currently watching by an user + /// + public class WatchedEpisode : Link + { + /// + /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). + /// + public int WatchedPercentage { get; set; } + } } \ No newline at end of file diff --git a/Kyoo.Common/Models/WatchedEpisode.cs b/Kyoo.Common/Models/WatchedEpisode.cs deleted file mode 100644 index 631c7572..00000000 --- a/Kyoo.Common/Models/WatchedEpisode.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - /// - /// Metadata of episode currently watching by an user - /// - public class WatchedEpisode : IResource - { - /// - [SerializeIgnore] public int ID - { - get => Episode.ID; - set => Episode.ID = value; - } - - /// - [SerializeIgnore] public string Slug => Episode.Slug; - - /// - /// The episode currently watched - /// - public Episode Episode { get; set; } - - /// - /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). - /// - public int WatchedPercentage { get; set; } - } -} \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 4a4b416b..584230b9 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -73,6 +73,11 @@ namespace Kyoo /// All people's role. See . /// public DbSet PeopleRoles { get; set; } + + /// + /// Episodes with a watch percentage. See + /// + public DbSet WatchedEpisodes { get; set; } /// /// Get a generic link between two resource types. @@ -188,6 +193,17 @@ namespace Kyoo .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); + modelBuilder.Entity() + .HasMany(x => x.Watched) + .WithMany("users") + .UsingEntity>( + y => y + .HasOne(x => x.Second) + .WithMany(), + y => y + .HasOne(x => x.First) + .WithMany(x => x.ShowLinks), + y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasOne(x => x.Show) @@ -210,6 +226,9 @@ namespace Kyoo .WithMany(x => x.MetadataLinks) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasKey(x => new {First = x.FirstID, Second = x.SecondID}); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); @@ -217,6 +236,7 @@ namespace Kyoo modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity() .HasIndex(x => x.Slug) @@ -248,6 +268,9 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); } /// diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 56479e95..c9067984 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -2,17 +2,20 @@ net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + + + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql false false false false true - - SDG - Zoe Roux - https://github.com/AnonymusRaccoon/Kyoo - default diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs similarity index 87% rename from Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs index 20ac842f..834321b2 100644 --- a/Kyoo.Postgresql/Migrations/20210505182627_Initial.Designer.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs @@ -1,5 +1,6 @@ // 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"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("ID") @@ -509,6 +525,58 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Tracks"); }); + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text"); + + b.Property>("ExtraData") + .HasColumnType("jsonb"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("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", 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 } } diff --git a/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs similarity index 86% rename from Kyoo.Postgresql/Migrations/20210505182627_Initial.cs rename to Kyoo.Postgresql/Migrations/20210507203809_Initial.cs index eacd436d..678e90ee 100644 --- a/Kyoo.Postgresql/Migrations/20210505182627_Initial.cs +++ b/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs @@ -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(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(type: "text", nullable: false), + Username = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + Password = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text[]", nullable: true), + ExtraData = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.ID); + }); + migrationBuilder.CreateTable( name: "Link", columns: table => new @@ -256,6 +275,30 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Link", + columns: table => new + { + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_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(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false), + WatchedPercentage = table.Column(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", column: "SecondID"); + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + 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"); + migrationBuilder.DropTable( + name: "Link"); + 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"); diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 05d8772d..4c6ceac7 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -1,5 +1,6 @@ // 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"); }); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { b.Property("ID") @@ -507,6 +523,58 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Tracks"); }); + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text"); + + b.Property>("ExtraData") + .HasColumnType("jsonb"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text[]"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer"); + + b.Property("SecondID") + .HasColumnType("integer"); + + b.Property("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", 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 } } diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index b5e4febe..4836601c 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -89,6 +89,10 @@ namespace Kyoo.Postgresql modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); + + modelBuilder.Entity() + .Property(x => x.ExtraData) + .HasColumnType("jsonb"); base.OnModelCreating(modelBuilder); } diff --git a/Kyoo.WebApp b/Kyoo.WebApp index da35a725..6802bc11 160000 --- a/Kyoo.WebApp +++ b/Kyoo.WebApp @@ -1 +1 @@ -Subproject commit da35a725a3e47db0994a697595aec4a10a4886e3 +Subproject commit 6802bc11e66331f0e77d7604838c8f1c219bef99 diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileManager.cs index 3fd3cf75..43b808b8 100644 --- a/Kyoo/Controllers/FileManager.cs +++ b/Kyoo/Controllers/FileManager.cs @@ -8,10 +8,22 @@ using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Controllers { + /// + /// A for the local filesystem (using System.IO). + /// public class FileManager : IFileManager { + /// + /// An extension provider to get content types from files extensions. + /// private FileExtensionContentTypeProvider _provider; + /// + /// Get the content type of a file using it's extension. + /// + /// The path of the file + /// The extension of the file is not known. + /// The content type of the file 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) + /// + 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) + /// + public Stream GetReader(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return new StreamReader(path); + return File.OpenRead(path); } + /// + public Stream NewFile(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return File.Create(path); + } + + /// public Task> ListFiles(string path) { if (path == null) @@ -57,11 +79,13 @@ namespace Kyoo.Controllers : Array.Empty()); } + /// public Task Exists(string path) { return Task.FromResult(File.Exists(path)); } + /// public string GetExtraDirectory(Show show) { string path = Path.Combine(show.Path, "Extra"); @@ -69,6 +93,7 @@ namespace Kyoo.Controllers return path; } + /// public string GetExtraDirectory(Season season) { if (season.Show == null) @@ -79,6 +104,7 @@ namespace Kyoo.Controllers return path; } + /// public string GetExtraDirectory(Episode episode) { string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra"); diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 41a1bb0a..2498e607 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -14,7 +14,7 @@ namespace Kyoo.Controllers public class ShowRepository : LocalRepository, IShowRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; /// diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index f7d5dea5..c2d5e321 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -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)) }; /// @@ -88,6 +89,7 @@ namespace Kyoo services.AddRepository(); services.AddRepository(); services.AddRepository(); + services.AddRepository(); } services.AddTask(); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 33eb9a67..3c7ac57e 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -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 => { diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 4ae053de..7052d9eb 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -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) {