diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index c45cbf82..4e75a971 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -60,6 +60,10 @@ namespace Kyoo.Controllers Task GetGenre(Expression> where); Task GetStudio(Expression> where); Task GetPerson(Expression> where); + + Task Load([NotNull] T obj, Expression> member) + where T : class, IResource + where T2 : class; // Library Items relations Task> GetItemsFromLibrary(int id, diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index f741de7b..522154c3 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -33,12 +33,8 @@ namespace Kyoo.Controllers Key = ExpressionRewrite.Rewrite>(key); Descendant = descendant; - if (Key == null || - Key.Body is MemberExpression || - Key.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)Key.Body).Operand is MemberExpression) - return; - - throw new ArgumentException("The given sort key is not valid."); + if (!Utility.IsPropertyExpression(Key)) + throw new ArgumentException("The given sort key is not valid."); } public Sort(string sortBy) diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/ALibraryManager.cs similarity index 98% rename from Kyoo.Common/Controllers/Implementations/LibraryManager.cs rename to Kyoo.Common/Controllers/Implementations/ALibraryManager.cs index bfc3cfa6..ab99f4f8 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/ALibraryManager.cs @@ -6,7 +6,7 @@ using Kyoo.Models; namespace Kyoo.Controllers { - public class LibraryManager : ILibraryManager + public abstract class ALibraryManager : ILibraryManager { public ILibraryRepository LibraryRepository { get; } public ILibraryItemRepository LibraryItemRepository { get; } @@ -20,7 +20,7 @@ namespace Kyoo.Controllers public IPeopleRepository PeopleRepository { get; } public IProviderRepository ProviderRepository { get; } - public LibraryManager(ILibraryRepository libraryRepository, + public ALibraryManager(ILibraryRepository libraryRepository, ILibraryItemRepository libraryItemRepository, ICollectionRepository collectionRepository, IShowRepository showRepository, @@ -235,6 +235,10 @@ namespace Kyoo.Controllers return PeopleRepository.Get(where); } + public abstract Task Load(T obj, Expression> member) + where T : class, IResource + where T2 : class; + public Task> GetLibraries(Expression> where = null, Sort sort = default, Pagination page = default) diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 8d5a57d2..8d3adcd9 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -19,7 +19,7 @@ - + diff --git a/Kyoo.Common/Models/Attributes/ComposedSlug.cs b/Kyoo.Common/Models/Attributes/ComposedSlug.cs new file mode 100644 index 00000000..a66fef71 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/ComposedSlug.cs @@ -0,0 +1,7 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class ComposedSlug : Attribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index d596b6e1..cd7c1a24 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -4,6 +4,7 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + [ComposedSlug] public class Episode : IResource, IOnMerge { public int ID { get; set; } @@ -28,7 +29,7 @@ namespace Kyoo.Models [JsonIgnore] public virtual IEnumerable Tracks { get; set; } public string ShowTitle => Show.Title; - public string Slug => GetSlug(Show.Slug, SeasonNumber, EpisodeNumber); + public string Slug => Show != null ? GetSlug(Show.Slug, SeasonNumber, EpisodeNumber) : ID.ToString(); public string Thumb { get diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 9bdd2f10..d7843e5c 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -3,6 +3,7 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + [ComposedSlug] public class Season : IResource { [JsonIgnore] public int ID { get; set; } @@ -10,7 +11,7 @@ namespace Kyoo.Models public int SeasonNumber { get; set; } = -1; - public string Slug => $"{Show.Slug}-s{SeasonNumber}"; + public string Slug => Show != null ? $"{Show.Slug}-s{SeasonNumber}" : ID.ToString(); public string Title { get; set; } public string Overview { get; set; } public int? Year { get; set; } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 8000f1be..01a0c366 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -17,6 +17,13 @@ namespace Kyoo { public static class Utility { + public static bool IsPropertyExpression(Expression> ex) + { + return ex == null || + ex.Body is MemberExpression || + ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; + } + public static string ToSlug(string str) { if (str == null) diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index b16c5e99..3a2b6456 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 5d81f7c4..d4197e08 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -153,6 +153,7 @@ namespace Kyoo.Controllers if (edited == null) throw new ArgumentNullException(nameof(edited)); + bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; Database.ChangeTracker.LazyLoadingEnabled = false; try { @@ -193,7 +194,7 @@ namespace Kyoo.Controllers } finally { - Database.ChangeTracker.LazyLoadingEnabled = true; + Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; } } @@ -206,7 +207,7 @@ namespace Kyoo.Controllers { if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); - if (int.TryParse(resource.Slug, out int _)) + if (int.TryParse(resource.Slug, out int _) && typeof(T).GetCustomAttribute() == null) { try { @@ -352,7 +353,7 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(edited)); if (edited is TInternal intern) return Edit(intern, resetOld).Cast(); - TInternal obj = new TInternal(); + TInternal obj = new(); Utility.Assign(obj, edited); return base.Edit(obj, resetOld).Cast(); } @@ -365,7 +366,7 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); if (obj is TInternal intern) return Delete(intern); - TInternal item = new TInternal(); + TInternal item = new(); Utility.Assign(item, obj); return Delete(item); } diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs new file mode 100644 index 00000000..c44e0a0c --- /dev/null +++ b/Kyoo/Controllers/LibraryManager.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.Models; + +namespace Kyoo.Controllers +{ + public class LibaryManager : ALibraryManager + { + private readonly DatabaseContext _database; + + public LibaryManager(ILibraryRepository libraryRepository, + ILibraryItemRepository libraryItemRepository, + ICollectionRepository collectionRepository, + IShowRepository showRepository, + ISeasonRepository seasonRepository, + IEpisodeRepository episodeRepository, + ITrackRepository trackRepository, + IGenreRepository genreRepository, + IStudioRepository studioRepository, + IProviderRepository providerRepository, + IPeopleRepository peopleRepository, + DatabaseContext database) + : base(libraryRepository, + libraryItemRepository, + collectionRepository, + showRepository, + seasonRepository, + episodeRepository, + trackRepository, + genreRepository, + studioRepository, + providerRepository, + peopleRepository) + { + _database = database; + } + + public override Task Load(T obj, Expression> member) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + if (!Utility.IsPropertyExpression(member) || member == null) + throw new ArgumentException($"{nameof(member)} is not a property."); + if (typeof(IEnumerable).IsAssignableFrom(typeof(T2))) + return _database.Entry(obj).Collection(member).LoadAsync(); + return _database.Entry(obj).Reference(member).LoadAsync(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5ab21e23..cb1faf12 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -20,30 +20,30 @@ - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index 4730cea2..96744687 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -10,6 +11,8 @@ using Npgsql; namespace Kyoo { + //TODO disable lazy loading a provide a LoadAsync method in the library manager. + public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) { } @@ -41,10 +44,10 @@ namespace Kyoo NpgsqlConnection.GlobalTypeMapper.MapEnum(); } - private readonly ValueComparer> _stringArrayComparer = - new ValueComparer>( - (l1, l2) => l1.SequenceEqual(l2), - arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); + private readonly ValueComparer> _stringArrayComparer = new( + (l1, l2) => l1.SequenceEqual(l2), + arr => arr.Aggregate(0, (i, s) => s.GetHashCode()) + ); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -266,7 +269,7 @@ namespace Kyoo } public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = new CancellationToken()) + CancellationToken cancellationToken = new()) { try { @@ -281,7 +284,7 @@ namespace Kyoo } } - public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { try { @@ -297,7 +300,7 @@ namespace Kyoo } public async Task SaveChangesAsync(string duplicateMessage, - CancellationToken cancellationToken = new CancellationToken()) + CancellationToken cancellationToken = new()) { try { @@ -312,7 +315,7 @@ namespace Kyoo } } - public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new CancellationToken()) + public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new()) { try { @@ -324,13 +327,12 @@ namespace Kyoo } } - public static bool IsDuplicateException(DbUpdateException ex) + private static bool IsDuplicateException(Exception ex) { - return ex.InnerException is PostgresException inner - && inner.SqlState == PostgresErrorCodes.UniqueViolation; + return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; } - public void DiscardChanges() + private void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)) diff --git a/Kyoo/Models/IdentityContext.cs b/Kyoo/Models/IdentityContext.cs index 7502c34d..c414efcc 100644 --- a/Kyoo/Models/IdentityContext.cs +++ b/Kyoo/Models/IdentityContext.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using System.Linq; using IdentityServer4.Models; namespace Kyoo { - public class IdentityContext + public static class IdentityContext { public static IEnumerable GetIdentityResources() { @@ -19,7 +20,7 @@ namespace Kyoo { return new List { - new Client + new() { ClientId = "kyoo.webapp", @@ -38,6 +39,38 @@ namespace Kyoo } }; } + + public static IEnumerable GetScopes() + { + return new[] + { + new ApiScope + { + Name = "kyoo.read", + DisplayName = "Read only access to the API.", + }, + new ApiScope + { + Name = "kyoo.write", + DisplayName = "Read and write access to the public API" + }, + new ApiScope + { + Name = "kyoo.play", + DisplayName = "Allow playback of movies and episodes." + }, + new ApiScope + { + Name = "kyoo.download", + DisplayName = "Allow downloading of episodes and movies from kyoo." + }, + new ApiScope + { + Name = "kyoo.admin", + DisplayName = "Full access to the admin's API and the public API." + } + }; + } public static IEnumerable GetApis() { @@ -46,34 +79,7 @@ namespace Kyoo new ApiResource { Name = "Kyoo", - Scopes = - { - new Scope - { - Name = "kyoo.read", - DisplayName = "Read only access to the API.", - }, - new Scope - { - Name = "kyoo.write", - DisplayName = "Read and write access to the public API" - }, - new Scope - { - Name = "kyoo.play", - DisplayName = "Allow playback of movies and episodes." - }, - new Scope - { - Name = "kyoo.download", - DisplayName = "Allow downloading of episodes and movies from kyoo." - }, - new Scope - { - Name = "kyoo.admin", - DisplayName = "Full access to the admin's API and the public API." - } - } + Scopes = GetScopes().Select(x => x.Name).ToArray() } }; } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 4eaa185c..9f32b56d 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Reflection; +using IdentityServer4.Extensions; using IdentityServer4.Services; using Kyoo.Api; using Kyoo.Controllers; @@ -47,8 +48,7 @@ namespace Kyoo services.AddDbContext(options => { - options.UseLazyLoadingProxies() - .UseNpgsql(_configuration.GetConnectionString("Database")); + options.UseNpgsql(_configuration.GetConnectionString("Database")); // .EnableSensitiveDataLogging() // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); }, ServiceLifetime.Transient); @@ -72,7 +72,6 @@ namespace Kyoo services.AddIdentityServer(options => { options.IssuerUri = publicUrl; - options.PublicOrigin = publicUrl; options.UserInteraction.LoginUrl = publicUrl + "login"; options.UserInteraction.ErrorUrl = publicUrl + "error"; options.UserInteraction.LogoutUrl = publicUrl + "logout"; @@ -92,6 +91,7 @@ namespace Kyoo options.EnableTokenCleanup = true; }) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) + .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) .AddProfileService() .AddSigninKeys(_configuration); @@ -157,7 +157,7 @@ namespace Kyoo services.AddHostedService(provider => (TaskManager)provider.GetService()); } - public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -186,6 +186,11 @@ namespace Kyoo MinimumSameSitePolicy = SameSiteMode.Strict }); app.UseAuthentication(); + app.Use((ctx, next) => + { + ctx.SetIdentityServerOrigin(_configuration.GetValue("public_url")); + return next(); + }); app.UseIdentityServer(); app.UseAuthorization(); diff --git a/Kyoo/Tasks/ExtractMetadata.cs b/Kyoo/Tasks/ExtractMetadata.cs index f65d1b42..ac613e0e 100644 --- a/Kyoo/Tasks/ExtractMetadata.cs +++ b/Kyoo/Tasks/ExtractMetadata.cs @@ -98,10 +98,9 @@ namespace Kyoo.Tasks await _thumbnails!.Validate(episode, true); if (subs) { - // TODO this doesn't work. - IEnumerable tracks = (await _transcoder!.ExtractInfos(episode.Path)) + // TODO handle external subtites. + episode.Tracks = (await _transcoder!.ExtractInfos(episode.Path)) .Where(x => x.Type != StreamType.Font); - episode.Tracks = tracks; await _library.EditEpisode(episode, false); } }