diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileManager.cs new file mode 100644 index 00000000..cc3c70bb --- /dev/null +++ b/Kyoo.Common/Controllers/IFileManager.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Controllers +{ + 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. + + public string GetExtraDirectory(Show show); + + public string GetExtraDirectory(Season season); + + public string GetExtraDirectory(Episode episode); + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index c45cbf82..ae1d0ccb 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -35,6 +35,7 @@ namespace Kyoo.Controllers Task GetTrack(int id); Task GetStudio(int id); Task GetPeople(int id); + Task GetProvider(int id); // Get by slug Task GetLibrary(string slug); @@ -49,6 +50,7 @@ namespace Kyoo.Controllers Task GetGenre(string slug); Task GetStudio(string slug); Task GetPeople(string slug); + Task GetProvider(string slug); // Get by predicate Task GetLibrary(Expression> where); @@ -60,7 +62,20 @@ 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, IResource, new(); + + Task Load([NotNull] T obj, Expression>> member) + where T : class, IResource + where T2 : class, new(); + + Task Load([NotNull] T obj, string memberName) + where T : class, IResource; + + Task Load([NotNull] IResource obj, string memberName); + // Library Items relations Task> GetItemsFromLibrary(int id, Expression> where = null, @@ -104,25 +119,25 @@ namespace Kyoo.Controllers ) => GetPeopleFromShow(showSlug, where, new Sort(sort), limit); // Show Role relations - Task> GetRolesFromPeople(int showID, - Expression> where = null, - Sort sort = default, + Task> GetRolesFromPeople(int showID, + Expression> where = null, + Sort sort = default, Pagination limit = default); - Task> GetRolesFromPeople(int showID, - [Optional] Expression> where, - Expression> sort, + Task> GetRolesFromPeople(int showID, + [Optional] Expression> where, + Expression> sort, Pagination limit = default - ) => GetRolesFromPeople(showID, where, new Sort(sort), limit); + ) => GetRolesFromPeople(showID, where, new Sort(sort), limit); - Task> GetRolesFromPeople(string showSlug, - Expression> where = null, - Sort sort = default, + Task> GetRolesFromPeople(string showSlug, + Expression> where = null, + Sort sort = default, Pagination limit = default); - Task> GetRolesFromPeople(string showSlug, - [Optional] Expression> where, - Expression> sort, + Task> GetRolesFromPeople(string showSlug, + [Optional] Expression> where, + Expression> sort, Pagination limit = default - ) => GetRolesFromPeople(showSlug, where, new Sort(sort), limit); + ) => GetRolesFromPeople(showSlug, where, new Sort(sort), limit); // Helpers Task AddShowLink(int showID, int? libraryID, int? collectionID); @@ -236,10 +251,9 @@ namespace Kyoo.Controllers Task EditGenre(Genre genre, bool resetOld); Task EditStudio(Studio studio, bool resetOld); Task EditPeople(People people, bool resetOld); - // Delete values - Task DelteLibrary(Library library); + Task DeleteLibrary(Library library); Task DeleteCollection(Collection collection); Task DeleteShow(Show show); Task DeleteSeason(Season season); @@ -250,7 +264,7 @@ namespace Kyoo.Controllers Task DeletePeople(People people); //Delete by slug - Task DelteLibrary(string slug); + Task DeleteLibrary(string slug); Task DeleteCollection(string slug); Task DeleteShow(string slug); Task DeleteSeason(string slug); @@ -261,7 +275,7 @@ namespace Kyoo.Controllers Task DeletePeople(string slug); //Delete by id - Task DelteLibrary(int id); + Task DeleteLibrary(int id); Task DeleteCollection(int id); Task DeleteShow(int id); Task DeleteSeason(int id); diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs index c552d5e3..6cf8a6ac 100644 --- a/Kyoo.Common/Controllers/IMetadataProvider.cs +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -11,8 +11,8 @@ namespace Kyoo.Controllers Task GetCollectionFromName(string name); Task GetShowByID(Show show); - Task> SearchShows(string showName, bool isMovie); - Task> GetPeople(Show show); + Task> SearchShows(string showName, bool isMovie); + Task> GetPeople(Show show); Task GetSeason(Show show, int seasonNumber); diff --git a/Kyoo.Common/Controllers/IProviderManager.cs b/Kyoo.Common/Controllers/IProviderManager.cs index 07f09cb2..d1136052 100644 --- a/Kyoo.Common/Controllers/IProviderManager.cs +++ b/Kyoo.Common/Controllers/IProviderManager.cs @@ -12,6 +12,6 @@ namespace Kyoo.Controllers Task> SearchShows(string showName, bool isMovie, Library library); Task GetSeason(Show show, int seasonNumber, Library library); Task GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library); - Task> GetPeople(Show show, Library library); + Task> GetPeople(Show show, Library library); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index f741de7b..5beee98b 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -20,7 +20,7 @@ namespace Kyoo.Controllers AfterID = afterID; } - public static implicit operator Pagination(int limit) => new Pagination(limit); + public static implicit operator Pagination(int limit) => new(limit); } public struct Sort @@ -30,15 +30,11 @@ namespace Kyoo.Controllers public Sort(Expression> key, bool descendant = false) { - Key = ExpressionRewrite.Rewrite>(key); + Key = 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) @@ -58,8 +54,7 @@ namespace Kyoo.Controllers Key = property.Type.IsValueType ? Expression.Lambda>(Expression.Convert(property, typeof(object)), param) : Expression.Lambda>(property, param); - Key = ExpressionRewrite.Rewrite>(Key); - + Descendant = order switch { "desc" => true, @@ -68,11 +63,6 @@ namespace Kyoo.Controllers _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") }; } - - public Sort To() - { - return new Sort(Key.Convert>(), Descendant); - } } public interface IRepository : IDisposable, IAsyncDisposable where T : class, IResource @@ -108,11 +98,14 @@ namespace Kyoo.Controllers Task DeleteRange(IEnumerable ids); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(IEnumerable slugs); + Task DeleteRange([NotNull] Expression> where); } public interface IShowRepository : IRepository { Task AddShowLink(int showID, int? libraryID, int? collectionID); + + Task GetSlug(int showID); } public interface ISeasonRepository : IRepository @@ -190,26 +183,36 @@ namespace Kyoo.Controllers Pagination limit = default ) => GetFromShow(showSlug, where, new Sort(sort), limit); - Task> GetFromPeople(int showID, - Expression> where = null, - Sort sort = default, + Task> GetFromPeople(int showID, + Expression> where = null, + Sort sort = default, Pagination limit = default); - Task> GetFromPeople(int showID, - [Optional] Expression> where, - Expression> sort, + Task> GetFromPeople(int showID, + [Optional] Expression> where, + Expression> sort, Pagination limit = default - ) => GetFromPeople(showID, where, new Sort(sort), limit); + ) => GetFromPeople(showID, where, new Sort(sort), limit); - Task> GetFromPeople(string showSlug, - Expression> where = null, - Sort sort = default, + Task> GetFromPeople(string showSlug, + Expression> where = null, + Sort sort = default, Pagination limit = default); - Task> GetFromPeople(string showSlug, - [Optional] Expression> where, - Expression> sort, + Task> GetFromPeople(string showSlug, + [Optional] Expression> where, + Expression> sort, Pagination limit = default - ) => GetFromPeople(showSlug, where, new Sort(sort), limit); + ) => GetFromPeople(showSlug, where, new Sort(sort), limit); + } + + public interface IProviderRepository : IRepository + { + Task> GetMetadataID(Expression> where = null, + Sort sort = default, + Pagination limit = default); + + Task> GetMetadataID([Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetMetadataID(where, new Sort(sort), limit); } - - public interface IProviderRepository : IRepository {} } diff --git a/Kyoo.Common/Controllers/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs index 0e966b12..5d2597cf 100644 --- a/Kyoo.Common/Controllers/IThumbnailsManager.cs +++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs @@ -1,14 +1,24 @@ using Kyoo.Models; using System.Collections.Generic; using System.Threading.Tasks; +using JetBrains.Annotations; namespace Kyoo.Controllers { public interface IThumbnailsManager { - Task Validate(Show show, bool alwaysDownload = false); - Task Validate(Season season, bool alwaysDownload = false); - Task Validate(Episode episode, bool alwaysDownload = false); - Task> Validate(IEnumerable actors, bool alwaysDownload = false); + Task Validate(Show show, bool alwaysDownload = false); + Task Validate(Season season, bool alwaysDownload = false); + Task Validate(Episode episode, bool alwaysDownload = false); + Task Validate(People actors, bool alwaysDownload = false); + Task Validate(ProviderID actors, bool alwaysDownload = false); + + Task GetShowPoster([NotNull] Show show); + Task GetShowLogo([NotNull] Show show); + Task GetShowBackdrop([NotNull] Show show); + Task GetSeasonPoster([NotNull] Season season); + Task GetEpisodeThumb([NotNull] Episode episode); + Task GetPeoplePoster([NotNull] People people); + Task GetProviderLogo([NotNull] ProviderID provider); } } diff --git a/Kyoo.Common/Controllers/ITranscoder.cs b/Kyoo.Common/Controllers/ITranscoder.cs index 0eee0f59..4fc109d1 100644 --- a/Kyoo.Common/Controllers/ITranscoder.cs +++ b/Kyoo.Common/Controllers/ITranscoder.cs @@ -1,12 +1,11 @@ using Kyoo.Models; -using Kyoo.Models.Watch; using System.Threading.Tasks; namespace Kyoo.Controllers { public interface ITranscoder { - Task ExtractInfos(string path); + Task ExtractInfos(Episode episode, bool reextract); Task Transmux(Episode episode); Task Transcode(Episode episode); } diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index bfc3cfa6..7cae9b4d 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; @@ -19,7 +20,7 @@ namespace Kyoo.Controllers public IStudioRepository StudioRepository { get; } public IPeopleRepository PeopleRepository { get; } public IProviderRepository ProviderRepository { get; } - + public LibraryManager(ILibraryRepository libraryRepository, ILibraryItemRepository libraryItemRepository, ICollectionRepository collectionRepository, @@ -130,6 +131,11 @@ namespace Kyoo.Controllers return PeopleRepository.Get(id); } + public Task GetProvider(int id) + { + return ProviderRepository.Get(id); + } + public Task GetLibrary(string slug) { return LibraryRepository.Get(slug); @@ -189,6 +195,11 @@ namespace Kyoo.Controllers { return PeopleRepository.Get(slug); } + + public Task GetProvider(string slug) + { + return ProviderRepository.Get(slug); + } public Task GetLibrary(Expression> where) { @@ -235,6 +246,195 @@ namespace Kyoo.Controllers return PeopleRepository.Get(where); } + public Task Load(T obj, Expression> member) + where T : class, IResource + where T2 : class, IResource, new() + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + return Load(obj, Utility.GetPropertyName(member)); + } + + public Task Load(T obj, Expression>> member) + where T : class, IResource + where T2 : class, new() + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + return Load(obj, Utility.GetPropertyName(member)); + } + + public async Task Load(T obj, string member) + where T : class, IResource + { + await Load(obj as IResource, member); + return obj; + } + + private async Task SetRelation(T1 obj, + Task> loader, + Action> setter, + Action inverse) + { + ICollection loaded = await loader; + setter(obj, loaded); + foreach (T2 item in loaded) + inverse(item, obj); + } + + public Task Load(IResource obj, string member) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + return (obj, member) switch + { + (Library l, nameof(Library.Providers)) => ProviderRepository + .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) + .Then(x => l.Providers = x), + + (Library l, nameof(Library.Shows)) => ShowRepository + .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) + .Then(x => l.Shows = x), + + (Library l, nameof(Library.Collections)) => CollectionRepository + .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) + .Then(x => l.Collections = x), + + + (Collection c, nameof(Library.Shows)) => ShowRepository + .GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) + .Then(x => c.Shows = x), + + (Collection c, nameof(Collection.Libraries)) => LibraryRepository + .GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) + .Then(x => c.Libraries = x), + + + (Show s, nameof(Show.ExternalIDs)) => SetRelation(s, + ProviderRepository.GetMetadataID(x => x.ShowID == obj.ID), + (x, y) => x.ExternalIDs = y, + (x, y) => { x.Show = y; x.ShowID = y.ID; }), + + (Show s, nameof(Show.Genres)) => GenreRepository + .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) + .Then(x => s.Genres = x), + + (Show s, nameof(Show.People)) => PeopleRepository + .GetFromShow(obj.ID) + .Then(x => s.People = x), + + (Show s, nameof(Show.Seasons)) => SetRelation(s, + SeasonRepository.GetAll(x => x.Show.ID == obj.ID), + (x, y) => x.Seasons = y, + (x, y) => { x.Show = y; x.ShowID = y.ID; }), + + (Show s, nameof(Show.Episodes)) => SetRelation(s, + EpisodeRepository.GetAll(x => x.Show.ID == obj.ID), + (x, y) => x.Episodes = y, + (x, y) => { x.Show = y; x.ShowID = y.ID; }), + + (Show s, nameof(Show.Libraries)) => LibraryRepository + .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) + .Then(x => s.Libraries = x), + + (Show s, nameof(Show.Collections)) => CollectionRepository + .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) + .Then(x => s.Collections = x), + + (Show s, nameof(Show.Studio)) => StudioRepository + .Get(x => x.Shows.Any(y => y.ID == obj.ID)) + .Then(x => + { + s.Studio = x; + s.StudioID = x?.ID ?? 0; + }), + + + (Season s, nameof(Season.ExternalIDs)) => SetRelation(s, + ProviderRepository.GetMetadataID(x => x.SeasonID == obj.ID), + (x, y) => x.ExternalIDs = y, + (x, y) => { x.Season = y; x.SeasonID = y.ID; }), + + (Season s, nameof(Season.Episodes)) => SetRelation(s, + EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), + (x, y) => x.Episodes = y, + (x, y) => { x.Season = y; x.SeasonID = y.ID; }), + + (Season s, nameof(Season.Show)) => ShowRepository + .Get(x => x.Seasons.Any(y => y.ID == obj.ID)) + .Then(x => + { + s.Show = x; + s.ShowID = x?.ID ?? 0; + }), + + + (Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e, + ProviderRepository.GetMetadataID(x => x.EpisodeID == obj.ID), + (x, y) => x.ExternalIDs = y, + (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), + + (Episode e, nameof(Episode.Tracks)) => SetRelation(e, + TrackRepository.GetAll(x => x.Episode.ID == obj.ID), + (x, y) => x.Tracks = y, + (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), + + (Episode e, nameof(Episode.Show)) => ShowRepository + .Get(x => x.Episodes.Any(y => y.ID == obj.ID)) + .Then(x => + { + e.Show = x; + e.ShowID = x?.ID ?? 0; + }), + + (Episode e, nameof(Episode.Season)) => SeasonRepository + .Get(x => x.Episodes.Any(y => y.ID == e.ID)) + .Then(x => + { + e.Season = x; + e.SeasonID = x?.ID ?? 0; + }), + + + (Track t, nameof(Track.Episode)) => EpisodeRepository + .Get(x => x.Tracks.Any(y => y.ID == obj.ID)) + .Then(x => + { + t.Episode = x; + t.EpisodeID = x?.ID ?? 0; + }), + + + (Genre g, nameof(Genre.Shows)) => ShowRepository + .GetAll(x => x.Genres.Any(y => y.ID == obj.ID)) + .Then(x => g.Shows = x), + + + (Studio s, nameof(Studio.Shows)) => ShowRepository + .GetAll(x => x.Studio.ID == obj.ID) + .Then(x => s.Shows = x), + + + (People p, nameof(People.ExternalIDs)) => SetRelation(p, + ProviderRepository.GetMetadataID(x => x.PeopleID == obj.ID), + (x, y) => x.ExternalIDs = y, + (x, y) => { x.People = y; x.PeopleID = y.ID; }), + + (People p, nameof(People.Roles)) => PeopleRepository + .GetFromPeople(obj.ID) + .Then(x => p.Roles = x), + + + (ProviderID p, nameof(ProviderID.Libraries)) => LibraryRepository + .GetAll(x => x.Providers.Any(y => y.ID == obj.ID)) + .Then(x => p.Libraries = x), + + + _ => throw new ArgumentException($"Couldn't find a way to load {member} of {obj.Slug}.") + }; + } + public Task> GetLibraries(Expression> where = null, Sort sort = default, Pagination page = default) @@ -337,17 +537,17 @@ namespace Kyoo.Controllers return PeopleRepository.GetFromShow(showSlug, where, sort, limit); } - public Task> GetRolesFromPeople(int id, - Expression> where = null, - Sort sort = default, + public Task> GetRolesFromPeople(int id, + Expression> where = null, + Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromPeople(id, where, sort, limit); } - public Task> GetRolesFromPeople(string slug, - Expression> where = null, - Sort sort = default, + public Task> GetRolesFromPeople(string slug, + Expression> where = null, + Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromPeople(slug, where, sort, limit); @@ -540,7 +740,7 @@ namespace Kyoo.Controllers return PeopleRepository.Edit(people, resetOld); } - public Task DelteLibrary(Library library) + public Task DeleteLibrary(Library library) { return LibraryRepository.Delete(library); } @@ -585,7 +785,7 @@ namespace Kyoo.Controllers return PeopleRepository.Delete(people); } - public Task DelteLibrary(string library) + public Task DeleteLibrary(string library) { return LibraryRepository.Delete(library); } @@ -630,7 +830,7 @@ namespace Kyoo.Controllers return PeopleRepository.Delete(people); } - public Task DelteLibrary(int library) + public Task DeleteLibrary(int library) { return LibraryRepository.Delete(library); } diff --git a/Kyoo.Common/ExpressionRewrite.cs b/Kyoo.Common/ExpressionRewrite.cs deleted file mode 100644 index c7db9636..00000000 --- a/Kyoo.Common/ExpressionRewrite.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Kyoo -{ - public class ExpressionRewriteAttribute : Attribute - { - public string Link { get; } - public string Inner { get; } - - public ExpressionRewriteAttribute(string link, string inner = null) - { - Link = link; - Inner = inner; - } - } - - public class ExpressionRewrite : ExpressionVisitor - { - private string _inner; - private readonly List<(string inner, ParameterExpression param, ParameterExpression newParam)> _innerRewrites; - - private ExpressionRewrite() - { - _innerRewrites = new List<(string, ParameterExpression, ParameterExpression)>(); - } - - public static Expression Rewrite(Expression expression) - { - return new ExpressionRewrite().Visit(expression); - } - - public static Expression Rewrite(Expression expression) where T : Delegate - { - return (Expression)new ExpressionRewrite().Visit(expression); - } - - protected override Expression VisitMember(MemberExpression node) - { - (string inner, _, ParameterExpression p) = _innerRewrites.FirstOrDefault(x => x.param == node.Expression); - if (inner != null) - { - Expression param = p; - foreach (string accessor in inner.Split('.')) - param = Expression.Property(param, accessor); - node = Expression.Property(param, node.Member.Name); - } - - // Can't use node.Member directly because we want to support attribute override - MemberInfo member = node.Expression.Type.GetProperty(node.Member.Name) ?? node.Member; - ExpressionRewriteAttribute attr = member!.GetCustomAttribute(); - if (attr == null) - return base.VisitMember(node); - - Expression property = node.Expression; - foreach (string child in attr.Link.Split('.')) - property = Expression.Property(property, child); - - if (property is MemberExpression expr) - Visit(expr.Expression); - _inner = attr.Inner; - return property; - } - - protected override Expression VisitLambda(Expression node) - { - (_, ParameterExpression oldParam, ParameterExpression param) = _innerRewrites - .FirstOrDefault(x => node.Parameters.Any(y => y == x.param)); - if (param == null) - return base.VisitLambda(node); - - ParameterExpression[] newParams = node.Parameters.Where(x => x != oldParam).Append(param).ToArray(); - return Expression.Lambda(Visit(node.Body)!, newParams); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - int count = node.Arguments.Count; - if (node.Object != null) - count++; - if (count != 2) - return base.VisitMethodCall(node); - - Expression instance = node.Object ?? node.Arguments.First(); - Expression argument = node.Object != null - ? node.Arguments.First() - : node.Arguments[1]; - - Type oldType = instance.Type; - instance = Visit(instance); - if (instance!.Type == oldType) - return base.VisitMethodCall(node); - - if (_inner != null && argument is LambdaExpression lambda) - { - // TODO this type handler will usually work with IEnumerable & others but won't work with everything. - Type type = oldType.GetGenericArguments().First(); - ParameterExpression oldParam = lambda.Parameters.FirstOrDefault(x => x.Type == type); - if (oldParam != null) - { - Type newType = instance.Type.GetGenericArguments().First(); - ParameterExpression newParam = Expression.Parameter(newType, oldParam.Name); - _innerRewrites.Add((_inner, oldParam, newParam)); - } - } - argument = Visit(argument); - - // TODO this method handler may not work for some methods (ex: method taking a Fun<> method won't have good generic arguments) - MethodInfo method = node.Method.IsGenericMethod - ? node.Method.GetGenericMethodDefinition().MakeGenericMethod(instance.Type.GetGenericArguments()) - : node.Method; - return node.Object != null - ? Expression.Call(instance, method!, argument) - : Expression.Call(null, method!, instance, argument!); - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 8d5a57d2..9a1ba82b 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -12,14 +12,17 @@ SDG GPL-3.0-or-later true - 1.0.22 + 1.0.23 true snupkg default + + ENABLE_INTERNAL_LINKS - + + diff --git a/Kyoo.Common/Models/Attributes/EditableRelation.cs b/Kyoo.Common/Models/Attributes/EditableRelation.cs deleted file mode 100644 index 4275dca4..00000000 --- a/Kyoo.Common/Models/Attributes/EditableRelation.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace Kyoo.Models.Attributes -{ - [AttributeUsage(AttributeTargets.Property, Inherited = false)] - public class EditableRelation : Attribute { } -} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/MergeAttributes.cs b/Kyoo.Common/Models/Attributes/MergeAttributes.cs index 1944c89b..399f5389 100644 --- a/Kyoo.Common/Models/Attributes/MergeAttributes.cs +++ b/Kyoo.Common/Models/Attributes/MergeAttributes.cs @@ -8,7 +8,4 @@ namespace Kyoo.Models.Attributes { void OnMerge(object merged); } - - public class JsonReadOnly : Attribute { } - public class JsonIgnore : JsonReadOnly { } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/RelationAttributes.cs b/Kyoo.Common/Models/Attributes/RelationAttributes.cs new file mode 100644 index 00000000..aac0e633 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/RelationAttributes.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + [AttributeUsage(AttributeTargets.Property, Inherited = false)] + public class EditableRelationAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public class LoadableRelationAttribute : Attribute + { + public string RelationID { get; } + + public LoadableRelationAttribute() {} + + public LoadableRelationAttribute(string relationID) + { + RelationID = relationID; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/SerializeAttribute.cs b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs new file mode 100644 index 00000000..3eafb90c --- /dev/null +++ b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class SerializeIgnoreAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class DeserializeIgnoreAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class SerializeAsAttribute : Attribute + { + public string Format { get; } + + public SerializeAsAttribute(string format) + { + Format = format; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs index 4264a469..6fe964d4 100644 --- a/Kyoo.Common/Models/LibraryItem.cs +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using Kyoo.Models.Attributes; namespace Kyoo.Models { @@ -20,7 +21,8 @@ namespace Kyoo.Models public string TrailerUrl { get; set; } public int? StartYear { get; set; } public int? EndYear { get; set; } - public string Poster { get; set; } + [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } + private string _type => Type == ItemType.Collection ? "collection" : "show"; public ItemType Type { get; set; } public LibraryItem() {} diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs new file mode 100644 index 00000000..f504b5a0 --- /dev/null +++ b/Kyoo.Common/Models/Link.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq.Expressions; + +namespace Kyoo.Models +{ + public class Link + { + public int FirstID { get; set; } + public int SecondID { get; set; } + + public Link() {} + + public Link(int firstID, int secondID) + { + FirstID = firstID; + SecondID = secondID; + } + + public Link(IResource first, IResource second) + { + FirstID = first.ID; + SecondID = second.ID; + } + + public static Link Create(IResource first, IResource second) + { + return new(first, second); + } + + public static Link Create(T first, T2 second) + where T : class, IResource + where T2 : class, IResource + { + return new(first, second); + } + + public static Link UCreate(T first, T2 second) + where T : class, IResource + where T2 : class, IResource + { + return new(first, second, true); + } + + public static Expression> PrimaryKey + { + get + { + return x => new {First = x.FirstID, Second = x.SecondID}; + } + } + } + + public class Link : Link + where T1 : class, IResource + where T2 : class, IResource + { + public virtual T1 First { get; set; } + public virtual T2 Second { get; set; } + + + public Link() {} + + public Link(T1 first, T2 second, bool privateItems = false) + : base(first, second) + { + if (privateItems) + return; + First = first; + Second = second; + } + + public Link(int firstID, int secondID) + : base(firstID, secondID) + { } + + public new static Expression, object>> PrimaryKey + { + get + { + return x => new {First = x.FirstID, Second = x.SecondID}; + } + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index 6ea84afa..71e1946a 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -1,25 +1,24 @@ -using System; using Kyoo.Models.Attributes; namespace Kyoo.Models { public class MetadataID { - [JsonIgnore] public int ID { get; set; } - [JsonIgnore] public int ProviderID { get; set; } + [SerializeIgnore] public int ID { get; set; } + [SerializeIgnore] public int ProviderID { get; set; } public virtual ProviderID Provider {get; set; } - [JsonIgnore] public int? ShowID { get; set; } - [JsonIgnore] public virtual Show Show { get; set; } + [SerializeIgnore] public int? ShowID { get; set; } + [SerializeIgnore] public virtual Show Show { get; set; } - [JsonIgnore] public int? EpisodeID { get; set; } - [JsonIgnore] public virtual Episode Episode { get; set; } + [SerializeIgnore] public int? EpisodeID { get; set; } + [SerializeIgnore] public virtual Episode Episode { get; set; } - [JsonIgnore] public int? SeasonID { get; set; } - [JsonIgnore] public virtual Season Season { get; set; } + [SerializeIgnore] public int? SeasonID { get; set; } + [SerializeIgnore] public virtual Season Season { get; set; } - [JsonIgnore] public int? PeopleID { get; set; } - [JsonIgnore] public virtual People People { get; set; } + [SerializeIgnore] public int? PeopleID { get; set; } + [SerializeIgnore] public virtual People People { get; set; } public string DataID { get; set; } public string Link { get; set; } diff --git a/Kyoo.Common/Models/PeopleRole.cs b/Kyoo.Common/Models/PeopleRole.cs index 54fe48cb..fe027682 100644 --- a/Kyoo.Common/Models/PeopleRole.cs +++ b/Kyoo.Common/Models/PeopleRole.cs @@ -1,46 +1,17 @@ -using System; using System.Collections.Generic; -using System.Linq.Expressions; using Kyoo.Models.Attributes; namespace Kyoo.Models { public class PeopleRole : IResource { - [JsonIgnore] public int ID { get; set; } - [JsonIgnore] public int PeopleID { get; set; } - [JsonIgnore] public virtual People People { get; set; } - - [ExpressionRewrite(nameof(People) + "." + nameof(Models.People.Slug))] - public string Slug - { - get => People.Slug; - set => People.Slug = value; - } - - [ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.Name))] - public string Name - { - get => People.Name; - set => People.Name = value; - } - - [ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.Poster))] - public string Poster - { - get => People.Poster; - set => People.Poster = value; - } - - [ExpressionRewrite(nameof(People) + "."+ nameof(Models.People.ExternalIDs))] - public IEnumerable ExternalIDs - { - get => People.ExternalIDs; - set => People.ExternalIDs = value; - } - - [JsonIgnore] public int ShowID { get; set; } - [JsonIgnore] public virtual Show Show { get; set; } + [SerializeIgnore] public int ID { get; set; } + [SerializeIgnore] public string Slug => ForPeople ? Show.Slug : People.Slug; + [SerializeIgnore] public bool ForPeople; + [SerializeIgnore] public int PeopleID { get; set; } + [SerializeIgnore] public virtual People People { get; set; } + [SerializeIgnore] public int ShowID { get; set; } + [SerializeIgnore] public virtual Show Show { get; set; } public string Role { get; set; } public string Type { get; set; } @@ -66,67 +37,4 @@ namespace Kyoo.Models Type = type; } } - - public class ShowRole : IResource - { - public int ID { get; set; } - public string Role { get; set; } - public string Type { get; set; } - - public string Slug { get; set; } - public string Title { get; set; } - public IEnumerable Aliases { get; set; } - [JsonIgnore] public string Path { get; set; } - public string Overview { get; set; } - public Status? Status { get; set; } - public string TrailerUrl { get; set; } - public int? StartYear { get; set; } - public int? EndYear { get; set; } - public string Poster { get; set; } - public string Logo { get; set; } - public string Backdrop { get; set; } - public bool IsMovie { get; set; } - - public ShowRole() {} - - public ShowRole(PeopleRole x) - { - ID = x.ID; - Role = x.Role; - Type = x.Type; - Slug = x.Show.Slug; - Title = x.Show.Title; - Aliases = x.Show.Aliases; - Path = x.Show.Path; - Overview = x.Show.Overview; - Status = x.Show.Status; - TrailerUrl = x.Show.TrailerUrl; - StartYear = x.Show.StartYear; - EndYear = x.Show.EndYear; - Poster = x.Show.Poster; - Logo = x.Show.Logo; - Backdrop = x.Show.Backdrop; - IsMovie = x.Show.IsMovie; - } - - public static Expression> FromPeopleRole => x => new ShowRole - { - ID = x.ID, - Role = x.Role, - Type = x.Type, - Slug = x.Show.Slug, - Title = x.Show.Title, - Aliases = x.Show.Aliases, - Path = x.Show.Path, - Overview = x.Show.Overview, - Status = x.Show.Status, - TrailerUrl = x.Show.TrailerUrl, - StartYear = x.Show.StartYear, - EndYear = x.Show.EndYear, - Poster = x.Show.Poster, - Logo = x.Show.Logo, - Backdrop = x.Show.Backdrop, - IsMovie = x.Show.IsMovie - }; - } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Plugin.cs b/Kyoo.Common/Models/Plugin.cs index 9965d1ba..7947d542 100644 --- a/Kyoo.Common/Models/Plugin.cs +++ b/Kyoo.Common/Models/Plugin.cs @@ -5,6 +5,6 @@ namespace Kyoo.Models public interface IPlugin { public string Name { get; } - public IEnumerable Tasks { get; } + public ICollection Tasks { get; } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Collection.cs b/Kyoo.Common/Models/Resources/Collection.cs index 2e8c85bc..3c7bed25 100644 --- a/Kyoo.Common/Models/Resources/Collection.cs +++ b/Kyoo.Common/Models/Resources/Collection.cs @@ -8,11 +8,16 @@ namespace Kyoo.Models public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - public string Poster { get; set; } + [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; } public string Overview { get; set; } - [JsonIgnore] public virtual IEnumerable Shows { get; set; } - [JsonIgnore] public virtual IEnumerable Libraries { get; set; } + [LoadableRelation] public virtual ICollection Shows { get; set; } + [LoadableRelation] public virtual ICollection Libraries { get; set; } +#if ENABLE_INTERNAL_LINKS + [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } + [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } +#endif + public Collection() { } public Collection(string slug, string name, string overview, string poster) diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index d596b6e1..c346be0e 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -7,38 +8,29 @@ namespace Kyoo.Models public class Episode : IResource, IOnMerge { public int ID { get; set; } - public int ShowID { get; set; } - [JsonIgnore] public virtual Show Show { get; set; } - public int? SeasonID { get; set; } - [JsonIgnore] public virtual Season Season { get; set; } + public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber); + [SerializeIgnore] public string ShowSlug { private get; set; } + [SerializeIgnore] public int ShowID { get; set; } + [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + [SerializeIgnore] public int? SeasonID { get; set; } + [LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; } public int SeasonNumber { get; set; } = -1; public int EpisodeNumber { get; set; } = -1; public int AbsoluteNumber { get; set; } = -1; - [JsonIgnore] public string Path { get; set; } + [SerializeIgnore] public string Path { get; set; } + + [SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; } public string Title { get; set; } public string Overview { get; set; } public DateTime? ReleaseDate { get; set; } public int Runtime { get; set; } //This runtime variable should be in minutes - [JsonIgnore] public string Poster { get; set; } - [EditableRelation] public virtual IEnumerable ExternalIDs { get; set; } - - [JsonIgnore] public virtual IEnumerable Tracks { get; set; } - - public string ShowTitle => Show.Title; - public string Slug => GetSlug(Show.Slug, SeasonNumber, EpisodeNumber); - public string Thumb - { - get - { - if (Show != null) - return "thumb/" + Slug; - return Poster; - } - } + [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection Tracks { get; set; } + public Episode() { } @@ -49,7 +41,7 @@ namespace Kyoo.Models string overview, DateTime? releaseDate, int runtime, - string poster, + string thumb, IEnumerable externalIDs) { SeasonNumber = seasonNumber; @@ -59,8 +51,8 @@ namespace Kyoo.Models Overview = overview; ReleaseDate = releaseDate; Runtime = runtime; - Poster = poster; - ExternalIDs = externalIDs; + Thumb = thumb; + ExternalIDs = externalIDs?.ToArray(); } public Episode(int showID, @@ -75,23 +67,17 @@ namespace Kyoo.Models int runtime, string poster, IEnumerable externalIDs) + : this(seasonNumber, episodeNumber, absoluteNumber, title, overview, releaseDate, runtime, poster, externalIDs) { ShowID = showID; SeasonID = seasonID; - SeasonNumber = seasonNumber; - EpisodeNumber = episodeNumber; - AbsoluteNumber = absoluteNumber; Path = path; - Title = title; - Overview = overview; - ReleaseDate = releaseDate; - Runtime = runtime; - Poster = poster; - ExternalIDs = externalIDs; } public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber) { + if (showSlug == null) + throw new ArgumentException("Show's slug is null. Can't find episode's slug."); if (seasonNumber == -1) return showSlug; return $"{showSlug}-s{seasonNumber}e{episodeNumber}"; diff --git a/Kyoo.Common/Models/Resources/Genre.cs b/Kyoo.Common/Models/Resources/Genre.cs index 947a2866..cd6086df 100644 --- a/Kyoo.Common/Models/Resources/Genre.cs +++ b/Kyoo.Common/Models/Resources/Genre.cs @@ -5,12 +5,17 @@ namespace Kyoo.Models { public class Genre : IResource { - [JsonIgnore] public int ID { get; set; } + public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - [JsonIgnore] public virtual IEnumerable Shows { get; set; } - + [LoadableRelation] public virtual ICollection Shows { get; set; } + +#if ENABLE_INTERNAL_LINKS + [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } +#endif + + public Genre() {} public Genre(string name) diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs index 83806ff5..297f3b1d 100644 --- a/Kyoo.Common/Models/Resources/IResource.cs +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -9,16 +9,6 @@ namespace Kyoo.Models public string Slug { get; } } - public interface IResourceLink - where T : IResource - where T2 : IResource - { - public T Parent { get; } - public int ParentID { get; } - public T2 Child { get; } - public int ChildID { get; } - } - public class ResourceComparer : IEqualityComparer where T : IResource { public bool Equals(T x, T y) @@ -37,27 +27,4 @@ namespace Kyoo.Models return HashCode.Combine(obj.ID, obj.Slug); } } - - public class LinkComparer : IEqualityComparer - where T : IResourceLink - where T1 : IResource - where T2 : 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 Utility.LinkEquals(x.Parent, x.ParentID, y.Parent, y.ParentID) - && Utility.LinkEquals(x.Child, x.ChildID, y.Child, y.ChildID); - } - - public int GetHashCode(T obj) - { - return HashCode.Combine(obj.Parent, obj.ParentID, obj.Child, obj.ChildID); - } - } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index 765d48d3..c1e17b56 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -1,19 +1,26 @@ using System.Collections.Generic; +using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models { public class Library : IResource { - [JsonIgnore] public int ID { get; set; } + public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - public IEnumerable Paths { get; set; } + public string[] Paths { get; set; } - [EditableRelation] public virtual IEnumerable Providers { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } - [JsonIgnore] public virtual IEnumerable Shows { get; set; } - [JsonIgnore] public virtual IEnumerable Collections { get; set; } + [LoadableRelation] public virtual ICollection Shows { get; set; } + [LoadableRelation] public virtual ICollection Collections { get; set; } + +#if ENABLE_INTERNAL_LINKS + [SerializeIgnore] public virtual ICollection> ProviderLinks { get; set; } + [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } + [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } +#endif public Library() { } @@ -21,8 +28,8 @@ namespace Kyoo.Models { Slug = slug; Name = name; - Paths = paths; - Providers = providers; + Paths = paths?.ToArray(); + Providers = providers?.ToArray(); } } } diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 5b8828b8..2743cf3c 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models @@ -8,10 +9,10 @@ namespace Kyoo.Models public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - public string Poster { get; set; } - [EditableRelation] public virtual IEnumerable ExternalIDs { get; set; } + [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [EditableRelation] [JsonReadOnly] public virtual IEnumerable Roles { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection Roles { get; set; } public People() {} @@ -20,7 +21,7 @@ namespace Kyoo.Models Slug = slug; Name = name; Poster = poster; - ExternalIDs = externalIDs; + ExternalIDs = externalIDs?.ToArray(); } } } diff --git a/Kyoo.Common/Models/Resources/ProviderID.cs b/Kyoo.Common/Models/Resources/ProviderID.cs index 49e19774..6500f9f7 100644 --- a/Kyoo.Common/Models/Resources/ProviderID.cs +++ b/Kyoo.Common/Models/Resources/ProviderID.cs @@ -1,14 +1,22 @@ +using System.Collections.Generic; using Kyoo.Models.Attributes; namespace Kyoo.Models { public class ProviderID : IResource { - [JsonIgnore] public int ID { get; set; } + public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - public string Logo { get; set; } - + [SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; } + + [LoadableRelation] public virtual ICollection Libraries { get; set; } + +#if ENABLE_INTERNAL_LINKS + [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } + [SerializeIgnore] public virtual ICollection MetadataLinks { get; set; } +#endif + public ProviderID() { } public ProviderID(string name, string logo) diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index 9bdd2f10..827b24f7 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,25 +1,27 @@ using System.Collections.Generic; +using System.Linq; using Kyoo.Models.Attributes; namespace Kyoo.Models { public class Season : IResource { - [JsonIgnore] public int ID { get; set; } - [JsonIgnore] public int ShowID { get; set; } + public int ID { get; set; } + public string Slug => $"{ShowSlug}-s{SeasonNumber}"; + [SerializeIgnore] public int ShowID { get; set; } + [SerializeIgnore] public string ShowSlug { private get; set; } + [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } public int SeasonNumber { get; set; } = -1; - public string Slug => $"{Show.Slug}-s{SeasonNumber}"; public string Title { get; set; } public string Overview { get; set; } public int? Year { get; set; } - [JsonIgnore] public string Poster { get; set; } - [EditableRelation] public virtual IEnumerable ExternalIDs { get; set; } + [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [JsonIgnore] public virtual Show Show { get; set; } - [JsonIgnore] public virtual IEnumerable Episodes { get; set; } + [LoadableRelation] public virtual ICollection Episodes { get; set; } public Season() { } @@ -37,7 +39,7 @@ namespace Kyoo.Models Overview = overview; Year = year; Poster = poster; - ExternalIDs = externalIDs; + ExternalIDs = externalIDs?.ToArray(); } } } diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 69bf72ec..7b948b55 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -9,8 +9,8 @@ namespace Kyoo.Models public int ID { get; set; } public string Slug { get; set; } public string Title { get; set; } - [EditableRelation] public IEnumerable Aliases { get; set; } - [JsonIgnore] public string Path { get; set; } + [EditableRelation] public string[] Aliases { get; set; } + [SerializeIgnore] public string Path { get; set; } public string Overview { get; set; } public Status? Status { get; set; } public string TrailerUrl { get; set; } @@ -18,23 +18,30 @@ namespace Kyoo.Models public int? StartYear { get; set; } public int? EndYear { get; set; } - public string Poster { get; set; } - public string Logo { get; set; } - public string Backdrop { get; set; } + [SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; } + [SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; } + [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; } public bool IsMovie { get; set; } - [EditableRelation] public virtual IEnumerable ExternalIDs { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [JsonIgnore] public int? StudioID { get; set; } - [EditableRelation] [JsonReadOnly] public virtual Studio Studio { get; set; } - [EditableRelation] [JsonReadOnly] public virtual IEnumerable Genres { get; set; } - [EditableRelation] [JsonReadOnly] public virtual IEnumerable People { get; set; } - [JsonIgnore] public virtual IEnumerable Seasons { get; set; } - [JsonIgnore] public virtual IEnumerable Episodes { get; set; } - [JsonIgnore] public virtual IEnumerable Libraries { get; set; } - [JsonIgnore] public virtual IEnumerable Collections { get; set; } + [SerializeIgnore] public int? StudioID { get; set; } + [LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; } + [LoadableRelation] [EditableRelation] public virtual ICollection Genres { get; set; } + [LoadableRelation] [EditableRelation] public virtual ICollection People { get; set; } + [LoadableRelation] public virtual ICollection Seasons { get; set; } + [LoadableRelation] public virtual ICollection Episodes { get; set; } + [LoadableRelation] public virtual ICollection Libraries { get; set; } + [LoadableRelation] public virtual ICollection Collections { get; set; } + +#if ENABLE_INTERNAL_LINKS + [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } + [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } + [SerializeIgnore] public virtual ICollection> GenreLinks { get; set; } +#endif + public Show() { } @@ -51,15 +58,15 @@ namespace Kyoo.Models { Slug = slug; Title = title; - Aliases = aliases; + Aliases = aliases?.ToArray(); Path = path; Overview = overview; TrailerUrl = trailerUrl; - Genres = genres; + Genres = genres?.ToArray(); Status = status; StartYear = startYear; EndYear = endYear; - ExternalIDs = externalIDs; + ExternalIDs = externalIDs?.ToArray(); } public Show(string slug, @@ -78,7 +85,7 @@ namespace Kyoo.Models { Slug = slug; Title = title; - Aliases = aliases; + Aliases = aliases?.ToArray(); Path = path; Overview = overview; TrailerUrl = trailerUrl; @@ -88,7 +95,7 @@ namespace Kyoo.Models Poster = poster; Logo = logo; Backdrop = backdrop; - ExternalIDs = externalIDs; + ExternalIDs = externalIDs?.ToArray(); } public string GetID(string provider) diff --git a/Kyoo.Common/Models/Resources/Studio.cs b/Kyoo.Common/Models/Resources/Studio.cs index 1bbb6014..9eea3a7b 100644 --- a/Kyoo.Common/Models/Resources/Studio.cs +++ b/Kyoo.Common/Models/Resources/Studio.cs @@ -5,11 +5,11 @@ namespace Kyoo.Models { public class Studio : IResource { - [JsonIgnore] public int ID { get; set; } + public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - [JsonIgnore] public virtual IEnumerable Shows { get; set; } + [LoadableRelation] public virtual ICollection Shows { get; set; } public Studio() { } diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 86e4bdbb..92438e0a 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -12,7 +12,7 @@ namespace Kyoo.Models Video = 1, Audio = 2, Subtitle = 3, - Font = 4 + Attachment = 4 } namespace Watch @@ -25,8 +25,8 @@ namespace Kyoo.Models public string Codec { get; set; } [MarshalAs(UnmanagedType.I1)] public bool isDefault; [MarshalAs(UnmanagedType.I1)] public bool isForced; - [JsonIgnore] public string Path { get; set; } - [JsonIgnore] public StreamType Type { get; set; } + [SerializeIgnore] public string Path { get; set; } + [SerializeIgnore] public StreamType Type { get; set; } public Stream() {} @@ -56,8 +56,9 @@ namespace Kyoo.Models public class Track : Stream, IResource { - [JsonIgnore] public int ID { get; set; } - [JsonIgnore] public int EpisodeID { get; set; } + public int ID { get; set; } + [SerializeIgnore] public int EpisodeID { get; set; } + public int TrackIndex { get; set; } public bool IsDefault { get => isDefault; @@ -76,7 +77,7 @@ namespace Kyoo.Models string language = GetLanguage(Language); if (language == null) - return $"Unknown Language (id: {ID.ToString()})"; + return $"Unknown (index: {TrackIndex})"; CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures) .FirstOrDefault(x => x.ThreeLetterISOLanguageName == language); string name = info?.EnglishName ?? language; @@ -94,31 +95,37 @@ namespace Kyoo.Models { get { - if (Type != StreamType.Subtitle) - return null; - - string slug = string.IsNullOrEmpty(Language) - ? ID.ToString() - : $"{Episode.Slug}.{Language}{(IsForced ? "-forced" : "")}"; - switch (Codec) + string type = Type switch { - case "ass": - slug += ".ass"; - break; - case "subrip": - slug += ".srt"; - break; - } - return slug; + StreamType.Subtitle => "", + StreamType.Video => "video.", + StreamType.Audio => "audio.", + StreamType.Attachment => "font.", + _ => "" + }; + string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; + string codec = Codec switch + { + "subrip" => ".srt", + {} x => $".{x}" + }; + return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}"; } } - [JsonIgnore] public bool IsExternal { get; set; } - [JsonIgnore] public virtual Episode Episode { get; set; } + public bool IsExternal { get; set; } + [LoadableRelation(nameof(EpisodeID))] public virtual Episode Episode { get; set; } public Track() { } - public Track(StreamType type, string title, string language, bool isDefault, bool isForced, string codec, bool isExternal, string path) + public Track(StreamType type, + string title, + string language, + bool isDefault, + bool isForced, + string codec, + bool isExternal, + string path) : base(title, language, codec, isDefault, isForced, path, type) { IsExternal = isExternal; @@ -136,6 +143,7 @@ namespace Kyoo.Models return mkvLanguage switch { "fre" => "fra", + null => "und", _ => mkvLanguage }; } diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index a86f5490..fc260699 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models.Attributes; -using Kyoo.Models.Watch; using PathIO = System.IO.Path; namespace Kyoo.Models @@ -26,31 +25,34 @@ namespace Kyoo.Models public class WatchItem { - [JsonIgnore] public readonly int EpisodeID = -1; + public int EpisodeID { get; set; } - public string ShowTitle; - public string ShowSlug; - public int SeasonNumber; - public int EpisodeNumber; - public string Title; - public string Slug; - public DateTime? ReleaseDate; - [JsonIgnore] public string Path; - public Episode PreviousEpisode; - public Episode NextEpisode; - public bool IsMovie; + public string ShowTitle { get; set; } + public string ShowSlug { get; set; } + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public string Title { get; set; } + public string Slug { get; set; } + public DateTime? ReleaseDate { get; set; } + [SerializeIgnore] public string Path { get; set; } + public Episode PreviousEpisode { get; set; } + public Episode NextEpisode { get; set; } + public bool IsMovie { get; set; } - public string Container; - public Track Video; - public IEnumerable Audios; - public IEnumerable Subtitles; - public IEnumerable Chapters; + [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; } + [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; } + [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; } + + public string Container { get; set; } + public Track Video { get; set; } + public ICollection Audios { get; set; } + public ICollection Subtitles { get; set; } + public ICollection Chapters { get; set; } public WatchItem() { } private WatchItem(int episodeID, - string showTitle, - string showSlug, + Show show, int seasonNumber, int episodeNumber, string title, @@ -58,30 +60,34 @@ namespace Kyoo.Models string path) { EpisodeID = episodeID; - ShowTitle = showTitle; - ShowSlug = showSlug; + ShowTitle = show.Title; + ShowSlug = show.Slug; SeasonNumber = seasonNumber; EpisodeNumber = episodeNumber; Title = title; ReleaseDate = releaseDate; Path = path; + IsMovie = show.IsMovie; + + Poster = show.Poster; + Logo = show.Logo; + Backdrop = show.Backdrop; Container = Path.Substring(Path.LastIndexOf('.') + 1); Slug = Episode.GetSlug(ShowSlug, seasonNumber, episodeNumber); } private WatchItem(int episodeID, - string showTitle, - string showSlug, + Show show, int seasonNumber, int episodeNumber, string title, DateTime? releaseDate, string path, Track video, - IEnumerable audios, - IEnumerable subtitles) - : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path) + ICollection audios, + ICollection subtitles) + : this(episodeID, show, seasonNumber, episodeNumber, title, releaseDate, path) { Video = video; Audios = audios; @@ -90,11 +96,13 @@ namespace Kyoo.Models public static async Task FromEpisode(Episode ep, ILibraryManager library) { - Show show = await library.GetShow(ep.ShowID); // TODO load only the title, the slug & the IsMovie with the library manager. Episode previous = null; Episode next = null; - if (!show.IsMovie) + await library.Load(ep, x => x.Show); + await library.Load(ep, x => x.Tracks); + + if (!ep.Show.IsMovie) { if (ep.EpisodeNumber > 1) previous = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); @@ -110,27 +118,25 @@ namespace Kyoo.Models else next = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); } - + return new WatchItem(ep.ID, - show.Title, - show.Slug, + ep.Show, ep.SeasonNumber, ep.EpisodeNumber, ep.Title, ep.ReleaseDate, ep.Path, - await library.GetTrack(x => x.EpisodeID == ep.ID && x.Type == StreamType.Video), - await library.GetTracks(x => x.EpisodeID == ep.ID && x.Type == StreamType.Audio), - await library.GetTracks(x => x.EpisodeID == ep.ID && x.Type == StreamType.Subtitle)) + ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), + ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), + ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray()) { - IsMovie = show.IsMovie, PreviousEpisode = previous, NextEpisode = next, Chapters = await GetChapters(ep.Path) }; } - private static async Task> GetChapters(string episodePath) + private static async Task> GetChapters(string episodePath) { string path = PathIO.Combine( PathIO.GetDirectoryName(episodePath)!, @@ -138,7 +144,7 @@ namespace Kyoo.Models PathIO.GetFileNameWithoutExtension(episodePath) + ".txt" ); if (!File.Exists(path)) - return new Chapter[0]; + return Array.Empty(); try { return (await File.ReadAllLinesAsync(path)) @@ -152,7 +158,7 @@ namespace Kyoo.Models catch { await Console.Error.WriteLineAsync($"Invalid chapter file at {path}"); - return new Chapter[0]; + return Array.Empty(); } } } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index 8000f1be..0ee0c881 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -17,6 +17,37 @@ namespace Kyoo { public static class Utility { + public static bool IsPropertyExpression(LambdaExpression ex) + { + return ex == null || + ex.Body is MemberExpression || + ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; + } + + public static string GetPropertyName(LambdaExpression ex) + { + if (!IsPropertyExpression(ex)) + throw new ArgumentException($"{ex} is not a property expression."); + MemberExpression member = ex.Body.NodeType == ExpressionType.Convert + ? ((UnaryExpression)ex.Body).Operand as MemberExpression + : ex.Body as MemberExpression; + return member!.Member.Name; + } + + public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return member switch + { + PropertyInfo property => property.GetValue(obj), + FieldInfo field => field.GetValue(obj), + _ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).") + }; + } + public static string ToSlug(string str) { if (str == null) @@ -25,7 +56,7 @@ namespace Kyoo str = str.ToLowerInvariant(); string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new(); foreach (char c in normalizedString) { UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); @@ -93,7 +124,7 @@ namespace Kyoo return first; } - public static T Complete(T first, T second) + public static T Complete(T first, T second, Func where = null) { if (first == null) throw new ArgumentNullException(nameof(first)); @@ -104,6 +135,9 @@ namespace Kyoo IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergableAttribute)) == null); + + if (where != null) + properties = properties.Where(where); foreach (PropertyInfo property in properties) { @@ -112,7 +146,7 @@ namespace Kyoo ? Activator.CreateInstance(property.PropertyType) : null; - if (value?.Equals(defaultValue) == false) + if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) property.SetValue(first, value); } @@ -221,6 +255,154 @@ namespace Kyoo : type.GetInheritanceTree(); return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); } + + public static IEnumerable Map([CanBeNull] this IEnumerable self, + [NotNull] Func mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return mapper(enumerator.Current, index); + index++; + } + } + + public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } + } + + public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + + while (enumerator.MoveNext()) + yield return await mapper(enumerator.Current); + } + + public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + + List ret = new(); + + await foreach(T i in self) + ret.Add(i); + return ret; + } + + public static IEnumerable IfEmpty(this IEnumerable self, Action action) + { + using IEnumerator enumerator = self.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + action(); + yield break; + } + + do + { + yield return enumerator.Current; + } + while (enumerator.MoveNext()); + } + + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (T i in self) + action(i); + } + + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (object i in self) + action(i); + } + + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (T i in self) + await action(i); + } + + public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) + { + if (self == null) + return; + await foreach (T i in self) + action(i); + } + + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (object i in self) + await action(i); + } + + private static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) + { + MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == name) + .Where(x => x.GetGenericArguments().Length == generics.Length) + .Where(x => x.GetParameters().Length == args.Length) + .IfEmpty(() => throw new NullReferenceException($"A method named {name} with " + + $"{args.Length} arguments and {generics.Length} generic " + + $"types could not be found on {type.Name}.")) + .Where(x => + { + int i = 0; + return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); + }) + .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) + .Where(x => + { + int i = 0; + return x.GetParameters().All(y => y.ParameterType == args[i++].GetType()); + }) + .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) + .Take(2) + .ToArray(); + + if (methods.Length == 1) + return methods[0]; + throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints."); + } public static T RunGenericMethod( [NotNull] Type owner, @@ -245,29 +427,33 @@ namespace Kyoo throw new ArgumentNullException(nameof(types)); if (types.Length < 1) throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); - MethodInfo method = owner.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) - .SingleOrDefault(x => x.Name == methodName && x.GetParameters().Length == args.Length); - if (method == null) - throw new NullReferenceException($"A method named {methodName} with {args.Length} arguments could not be found on {owner.FullName}"); + MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray()); } - + + public static T RunGenericMethod( + [NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(instance, methodName, new[] {type}, args); + } + public static T RunGenericMethod( [NotNull] object instance, [NotNull] string methodName, - [NotNull] Type type, + [NotNull] Type[] types, params object[] args) { if (instance == null) throw new ArgumentNullException(nameof(instance)); if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (type == null) - throw new ArgumentNullException(nameof(type)); - MethodInfo method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - if (method == null) - throw new NullReferenceException($"A method named {methodName} could not be found on {instance.GetType().FullName}"); - return (T)method.MakeGenericMethod(type).Invoke(instance, args?.ToArray()); + if (types == null || types.Length == 0) + throw new ArgumentNullException(nameof(types)); + MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args); + return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray()); } [NotNull] @@ -368,6 +554,22 @@ namespace Kyoo return (T)((dynamic)x).Result; }, 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) { @@ -382,13 +584,9 @@ namespace Kyoo Type type = GetEnumerableType(eno); if (typeof(IResource).IsAssignableFrom(type)) return ResourceEquals(eno.Cast(), ens.Cast()); - Type genericDefinition = GetGenericDefinition(type, typeof(IResourceLink<,>)); - if (genericDefinition == null) - return RunGenericMethod(typeof(Enumerable), "SequenceEqual", type, first, second); - Type[] types = genericDefinition.GetGenericArguments().Prepend(type).ToArray(); - return RunGenericMethod(typeof(Utility), "LinkEquals", types, eno, ens); + return RunGenericMethod(typeof(Enumerable), "SequenceEqual", type, first, second); } - + public static bool ResourceEquals([CanBeNull] T first, [CanBeNull] T second) where T : IResource { @@ -422,69 +620,5 @@ namespace Kyoo return true; return firstID == secondID; } - - public static bool LinkEquals([CanBeNull] IEnumerable first, [CanBeNull] IEnumerable second) - where T : IResourceLink - where T1 : IResource - where T2 : IResource - { - if (ReferenceEquals(first, second)) - return true; - if (first == null || second == null) - return false; - return first.SequenceEqual(second, new LinkComparer()); - } - - public static Expression Convert([CanBeNull] this Expression expr) - where T : Delegate - { - Expression e = expr switch - { - null => null, - LambdaExpression lambda => new ExpressionConverter(lambda).VisitAndConvert(), - _ => throw new ArgumentException("Can't convert a non lambda.") - }; - - return ExpressionRewrite.Rewrite(e); - } - - private class ExpressionConverter : ExpressionVisitor - where TTo : Delegate - { - private readonly LambdaExpression _expression; - private readonly ParameterExpression[] _newParams; - - internal ExpressionConverter(LambdaExpression expression) - { - _expression = expression; - - Type[] paramTypes = typeof(TTo).GetGenericArguments()[..^1]; - if (paramTypes.Length != _expression.Parameters.Count) - throw new ArgumentException("Parameter count from internal and external lambda are not matched."); - - _newParams = new ParameterExpression[paramTypes.Length]; - for (int i = 0; i < paramTypes.Length; i++) - { - if (_expression.Parameters[i].Type == paramTypes[i]) - _newParams[i] = _expression.Parameters[i]; - else - _newParams[i] = Expression.Parameter(paramTypes[i], _expression.Parameters[i].Name); - } - } - - internal Expression VisitAndConvert() - { - Type returnType = _expression.Type.GetGenericArguments().Last(); - Expression body = _expression.ReturnType == returnType - ? Visit(_expression.Body) - : Expression.Convert(Visit(_expression.Body)!, returnType); - return Expression.Lambda(body!, _newParams); - } - - protected override Expression VisitParameter(ParameterExpression node) - { - return _newParams.FirstOrDefault(x => x.Name == node.Name) ?? node; - } - } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/ApiHelper.cs b/Kyoo.CommonAPI/ApiHelper.cs index 312ac908..d3d8d951 100644 --- a/Kyoo.CommonAPI/ApiHelper.cs +++ b/Kyoo.CommonAPI/ApiHelper.cs @@ -26,12 +26,7 @@ namespace Kyoo.CommonApi Expression> defaultWhere = null) { if (where == null || where.Count == 0) - { - if (defaultWhere == null) - return null; - Expression body = ExpressionRewrite.Rewrite(defaultWhere.Body); - return Expression.Lambda>(body, defaultWhere.Parameters.First()); - } + return defaultWhere; ParameterExpression param = defaultWhere?.Parameters.First() ?? Expression.Parameter(typeof(T)); Expression expression = defaultWhere?.Body; @@ -97,8 +92,7 @@ namespace Kyoo.CommonApi expression = condition; } - expression = ExpressionRewrite.Rewrite(expression); - return Expression.Lambda>(expression, param); + return Expression.Lambda>(expression!, param); } private static Expression ResourceEqual(Expression parameter, string value, bool notEqual = false) diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 23d409ec..0bbf7733 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration; namespace Kyoo.CommonApi { [ApiController] + [ResourceView] public class CrudApi : ControllerBase where T : class, IResource { private readonly IRepository _repository; @@ -22,10 +23,10 @@ namespace Kyoo.CommonApi _repository = repository; BaseURL = configuration.GetValue("public_url").TrimEnd('/'); } - + + [HttpGet("{id:int}")] [Authorize(Policy = "Read")] - [JsonDetailed] public virtual async Task> Get(int id) { T resource = await _repository.Get(id); @@ -37,7 +38,6 @@ namespace Kyoo.CommonApi [HttpGet("{slug}")] [Authorize(Policy = "Read")] - [JsonDetailed] public virtual async Task> Get(string slug) { T resource = await _repository.Get(slug); @@ -68,10 +68,6 @@ namespace Kyoo.CommonApi [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _repository.GetAll(ApiHelper.ParseWhere(where), @@ -89,7 +85,7 @@ namespace Kyoo.CommonApi protected Page Page(ICollection resources, int limit) where TResult : IResource { - return new Page(resources, + return new(resources, BaseURL + Request.Path, Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), limit); @@ -186,5 +182,20 @@ namespace Kyoo.CommonApi return Ok(); } + + [Authorize(Policy = "Write")] + public virtual async Task Delete(Dictionary where) + { + try + { + await _repository.DeleteRange(ApiHelper.ParseWhere(where)); + } + catch (ItemNotFound) + { + return NotFound(); + } + + return Ok(); + } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/JsonSerializer.cs b/Kyoo.CommonAPI/JsonSerializer.cs index 6cbc9c89..c9238049 100644 --- a/Kyoo.CommonAPI/JsonSerializer.cs +++ b/Kyoo.CommonAPI/JsonSerializer.cs @@ -1,103 +1,138 @@ using System; -using System.Buffers; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Kyoo.Models; using Kyoo.Models.Attributes; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Kyoo.Controllers { public class JsonPropertyIgnorer : CamelCasePropertyNamesContractResolver { + private int _depth = -1; + private string _host; + + public JsonPropertyIgnorer(string host) + { + _host = host; + } + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty property = base.CreateProperty(member, memberSerialization); - - property.ShouldSerialize = i => member.GetCustomAttribute(true) == null; - property.ShouldDeserialize = i => member.GetCustomAttribute(true) == null; + + LoadableRelationAttribute relation = member?.GetCustomAttribute(); + if (relation != null) + { + if (relation.RelationID == null) + property.ShouldSerialize = x => _depth == 0 && member.GetValue(x) != null; + else + property.ShouldSerialize = x => + { + if (_depth != 0) + return false; + if (member.GetValue(x) != null) + return true; + return x.GetType().GetProperty(relation.RelationID)?.GetValue(x) != null; + }; + } + + if (member?.GetCustomAttribute() != null) + property.ShouldSerialize = _ => false; + if (member?.GetCustomAttribute() != null) + property.ShouldDeserialize = _ => false; + + SerializeAsAttribute serializeAs = member?.GetCustomAttribute(); + if (serializeAs != null) + property.ValueProvider = new SerializeAsProvider(serializeAs.Format, _host); return property; } - } - - public class JsonPropertySelector : JsonPropertyIgnorer - { - private readonly Dictionary> _ignored; - private readonly Dictionary> _forceSerialize; - public JsonPropertySelector() + protected override JsonContract CreateContract(Type objectType) { - _ignored = new Dictionary>(); - _forceSerialize = new Dictionary>(); + JsonContract contract = base.CreateContract(objectType); + if (Utility.GetGenericDefinition(objectType, typeof(Page<>)) == null + && !objectType.IsAssignableTo(typeof(IEnumerable))) + { + contract.OnSerializingCallbacks.Add((_, _) => _depth++); + contract.OnSerializedCallbacks.Add((_, _) => _depth--); + } + + return contract; + } + } + + public class PeopleRoleConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, PeopleRole value, JsonSerializer serializer) + { + ICollection oldPeople = value.Show?.People; + ICollection oldRoles = value.People?.Roles; + if (value.Show != null) + value.Show.People = null; + if (value.People != null) + value.People.Roles = null; + + JObject obj = JObject.FromObject(value.ForPeople ? value.People : value.Show, serializer); + obj.Add("role", value.Role); + obj.Add("type", value.Type); + obj.WriteTo(writer); + + if (value.Show != null) + value.Show.People = oldPeople; + if (value.People != null) + value.People.Roles = oldRoles; + } + + public override PeopleRole ReadJson(JsonReader reader, + Type objectType, + PeopleRole existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } + + public class SerializeAsProvider : IValueProvider + { + private string _format; + private string _host; + + public SerializeAsProvider(string format, string host) + { + _format = format; + _host = host.TrimEnd('/'); } - public JsonPropertySelector(Dictionary> ignored, - Dictionary> forceSerialize = null) + public object GetValue(object target) { - _ignored = ignored ?? new Dictionary>(); - _forceSerialize = forceSerialize ?? new Dictionary>(); - } - - private bool IsIgnored(Type type, string propertyName) - { - while (type != null) + return Regex.Replace(_format, @"(? { - if (_ignored.ContainsKey(type) && _ignored[type].Contains(propertyName)) - return true; - type = type.BaseType; - } + string value = x.Groups[1].Value; - return false; + if (value == "HOST") + return _host; + + PropertyInfo properties = target.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .FirstOrDefault(y => y.Name == value); + if (properties == null) + return null; + if (properties.GetValue(target) is string ret) + return ret; + throw new ArgumentException($"Invalid serializer replacement {value}"); + }); } - - private bool IsSerializationForced(Type type, string propertyName) + + public void SetValue(object target, object value) { - while (type != null) - { - if (_forceSerialize.ContainsKey(type) && _forceSerialize[type].Contains(propertyName)) - return true; - type = type.BaseType; - } - - return false; - } - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty property = base.CreateProperty(member, memberSerialization); - - if (IsSerializationForced(property.DeclaringType, property.PropertyName)) - property.ShouldSerialize = i => true; - else if (IsIgnored(property.DeclaringType, property.PropertyName)) - property.ShouldSerialize = i => false; - return property; - } - } - - public class JsonDetailed : ActionFilterAttribute - { - public override void OnActionExecuted(ActionExecutedContext context) - { - if (context.Result is ObjectResult result) - { - result.Formatters.Add(new NewtonsoftJsonOutputFormatter( - new JsonSerializerSettings - { - ContractResolver = new JsonPropertySelector(null, new Dictionary> - { - {typeof(Show), new HashSet {"genres", "studio"}}, - {typeof(Episode), new HashSet {"tracks"}}, - {typeof(PeopleRole), new HashSet {"show"}} - }) - }, - context.HttpContext.RequestServices.GetRequiredService>(), - new MvcOptions())); - } + // Values are ignored and should not be editable, except if the internal value is set. } } } \ No newline at end of file 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..5da8fcc0 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -1,22 +1,18 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; -using JetBrains.Annotations; using Kyoo.CommonApi; using Kyoo.Models; using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata; namespace Kyoo.Controllers { - public abstract class LocalRepository + public abstract class LocalRepository : IRepository where T : class, IResource { protected readonly DbContext Database; @@ -32,6 +28,7 @@ namespace Kyoo.Controllers public virtual void Dispose() { Database.Dispose(); + GC.SuppressFinalize(this); } public virtual ValueTask DisposeAsync() @@ -43,6 +40,11 @@ namespace Kyoo.Controllers { return Database.Set().FirstOrDefaultAsync(x => x.ID == id); } + + public virtual Task GetWithTracking(int id) + { + return Database.Set().AsTracking().FirstOrDefaultAsync(x => x.ID == id); + } public virtual Task Get(string slug) { @@ -54,6 +56,8 @@ namespace Kyoo.Controllers return Database.Set().FirstOrDefaultAsync(predicate); } + public abstract Task> Search(string query); + public virtual Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) @@ -112,7 +116,7 @@ namespace Kyoo.Controllers return query.CountAsync(); } - public virtual async Task Create([NotNull] T obj) + public virtual async Task Create(T obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -147,61 +151,38 @@ namespace Kyoo.Controllers throw; } } - + public virtual async Task Edit(T edited, bool resetOld) { if (edited == null) throw new ArgumentNullException(nameof(edited)); + bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; Database.ChangeTracker.LazyLoadingEnabled = false; try { - T old = await Get(edited.ID); - + T old = await GetWithTracking(edited.ID); if (old == null) throw new ItemNotFound($"No resource found with the ID {edited.ID}."); - - foreach (NavigationEntry navigation in Database.Entry(old).Navigations) - { - if (navigation.Metadata.PropertyInfo.GetCustomAttribute() != null) - { - if (resetOld) - { - await navigation.LoadAsync(); - continue; - } - IClrPropertyGetter getter = navigation.Metadata.GetGetter(); - - if (getter.HasDefaultValue(edited)) - continue; - await navigation.LoadAsync(); - // TODO this may be usless for lists since the API does not return IDs but the - // TODO LinkEquality does not check slugs (their are lazy loaded and only the ID is available) - if (Utility.ResourceEquals(getter.GetClrValue(edited), getter.GetClrValue(old))) - navigation.Metadata.PropertyInfo.SetValue(edited, default); - } - else - navigation.Metadata.PropertyInfo.SetValue(edited, default); - } - + if (resetOld) Utility.Nullify(old); - Utility.Complete(old, edited); - await Validate(old); + Utility.Complete(old, edited, x => x.GetCustomAttribute() == null); + await EditRelations(old, edited, resetOld); await Database.SaveChangesAsync(); return old; } finally { - Database.ChangeTracker.LazyLoadingEnabled = true; + Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; } } - - protected bool ShouldValidate(T2 value) + + protected virtual Task EditRelations(T resource, T changed, bool resetOld) { - return value != null && Database.Entry(value).State == EntityState.Detached; + return Validate(resource); } - + protected virtual Task Validate(T resource) { if (string.IsNullOrEmpty(resource.Slug)) @@ -221,21 +202,9 @@ namespace Kyoo.Controllers throw new ArgumentException("Resources slug can't be number only."); } } - - foreach (PropertyInfo property in typeof(T).GetProperties() - .Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType) - && !typeof(string).IsAssignableFrom(x.PropertyType) - && x.GetCustomAttribute() != null)) - { - object value = property.GetValue(resource); - if (value == null || value is ICollection || Utility.IsOfGenericType(value, typeof(ICollection<>))) - continue; - value = Utility.RunGenericMethod(typeof(Enumerable), "ToList", Utility.GetEnumerableType((IEnumerable)value), value); - property.SetValue(resource, value); - } return Task.CompletedTask; } - + public virtual async Task Delete(int id) { T resource = await Get(id); @@ -249,7 +218,7 @@ namespace Kyoo.Controllers } public abstract Task Delete(T obj); - + public virtual async Task DeleteRange(IEnumerable objs) { foreach (T obj in objs) @@ -267,113 +236,11 @@ namespace Kyoo.Controllers foreach (string slug in slugs) await Delete(slug); } - } - - public abstract class LocalRepository : LocalRepository, IRepository - where T : class, IResource - where TInternal : class, T, new() - { - protected LocalRepository(DbContext database) : base(database) { } - - public new Task Get(int id) - { - return base.Get(id).Cast(); - } - public new Task Get(string slug) + public async Task DeleteRange(Expression> where) { - return base.Get(slug).Cast(); - } - - public Task Get(Expression> predicate) - { - return Get(predicate.Convert>()).Cast(); - } - - public abstract Task> Search(string query); - - public virtual Task> GetAll(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return ApplyFilters(Database.Set(), where, sort, limit); - } - - protected virtual async Task> ApplyFilters(IQueryable query, - Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - ICollection items = await ApplyFilters(query, - base.Get, - DefaultSort, - where.Convert>(), - sort.To(), - limit); - - return items.ToList(); - } - - public virtual Task GetCount(Expression> where = null) - { - IQueryable query = Database.Set(); - if (where != null) - query = query.Where(where.Convert>()); - return query.CountAsync(); - } - - Task IRepository.Create(T item) - { - if (item == null) - throw new ArgumentNullException(nameof(item)); - TInternal obj = item as TInternal ?? new TInternal(); - if (!(item is TInternal)) - Utility.Assign(obj, item); - - return Create(obj).Cast() - .Then(x => item.ID = x.ID); - } - - Task IRepository.CreateIfNotExists(T item, bool silentFail) - { - if (item == null) - throw new ArgumentNullException(nameof(item)); - TInternal obj = item as TInternal ?? new TInternal(); - if (!(item is TInternal)) - Utility.Assign(obj, item); - - return CreateIfNotExists(obj, silentFail).Cast() - .Then(x => item.ID = x.ID); - } - - public Task Edit(T edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - if (edited is TInternal intern) - return Edit(intern, resetOld).Cast(); - TInternal obj = new TInternal(); - Utility.Assign(obj, edited); - return base.Edit(obj, resetOld).Cast(); - } - - public abstract override Task Delete([NotNull] TInternal obj); - - Task IRepository.Delete(T obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - if (obj is TInternal intern) - return Delete(intern); - TInternal item = new TInternal(); - Utility.Assign(item, obj); - return Delete(item); - } - - public virtual async Task DeleteRange(IEnumerable objs) - { - foreach (T obj in objs) - await ((IRepository)this).Delete(obj); + ICollection resources = await GetAll(where); + await DeleteRange(resources); } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/ResourceViewAttribute.cs b/Kyoo.CommonAPI/ResourceViewAttribute.cs new file mode 100644 index 00000000..fa177342 --- /dev/null +++ b/Kyoo.CommonAPI/ResourceViewAttribute.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Attributes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Kyoo.CommonApi +{ + public class ResourceViewAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.ActionArguments.TryGetValue("where", out object dic) && dic is Dictionary where) + { + where.Remove("fields"); + foreach ((string key, _) in context.ActionArguments) + where.Remove(key); + } + + List fields = context.HttpContext.Request.Query["fields"] + .SelectMany(x => x.Split(',')) + .ToList(); + if (fields.Contains("internal")) + { + fields.Remove("internal"); + context.HttpContext.Items["internal"] = true; + // TODO disable SerializeAs attributes when this is true. + } + if (context.ActionDescriptor is ControllerActionDescriptor descriptor) + { + Type type = descriptor.MethodInfo.ReturnType; + type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type; + type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type; + type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type; + + PropertyInfo[] properties = type.GetProperties() + .Where(x => x.GetCustomAttribute() != null) + .ToArray(); + fields = fields.Select(x => + { + string property = properties + .FirstOrDefault(y => string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase)) + ?.Name; + if (property != null) + return property; + context.Result = new BadRequestObjectResult(new + { + Error = $"{x} does not exist on {type.Name}." + }); + return null; + }) + .ToList(); + if (context.Result != null) + return; + } + context.HttpContext.Items["fields"] = fields; + base.OnActionExecuting(context); + } + + public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + if (context.Result is ObjectResult result) + await LoadResultRelations(context, result); + await base.OnResultExecutionAsync(context, next); + } + + private static async Task LoadResultRelations(ActionContext context, ObjectResult result) + { + if (result.DeclaredType == null) + return; + + await using ILibraryManager library = context.HttpContext.RequestServices.GetService(); + ICollection fields = (ICollection)context.HttpContext.Items["fields"]; + Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>)); + + + if (pageType != null) + { + foreach (IResource resource in ((dynamic)result.Value).Items) + { + foreach (string field in fields!) + await library!.Load(resource, field); + } + } + else if (result.DeclaredType.IsAssignableTo(typeof(IResource))) + { + foreach (string field in fields!) + await library!.Load((IResource)result.Value, field); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileManager.cs new file mode 100644 index 00000000..71dfbb13 --- /dev/null +++ b/Kyoo/Controllers/FileManager.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; + +namespace Kyoo.Controllers +{ + public class FileManager : IFileManager + { + private FileExtensionContentTypeProvider _provider; + + private string _GetContentType(string path) + { + if (_provider == null) + { + _provider = new FileExtensionContentTypeProvider(); + _provider.Mappings[".mkv"] = "video/x-matroska"; + _provider.Mappings[".ass"] = "text/x-ssa"; + _provider.Mappings[".srt"] = "application/x-subrip"; + } + + if (_provider.TryGetContentType(path, out string contentType)) + return contentType; + 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) + { + if (path == null) + return new NotFoundResult(); + if (!File.Exists(path)) + return new NotFoundResult(); + return new PhysicalFileResult(Path.GetFullPath(path), _GetContentType(path)) + { + EnableRangeProcessing = range + }; + } + + public StreamReader GetReader(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return new StreamReader(path); + } + + public Task> ListFiles(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return Task.FromResult>(Directory.GetFiles(path)); + } + + public Task Exists(string path) + { + return Task.FromResult(File.Exists(path)); + } + + public string GetExtraDirectory(Show show) + { + string path = Path.Combine(show.Path, "Extra"); + Directory.CreateDirectory(path); + return path; + } + + public string GetExtraDirectory(Season season) + { + if (season.Show == null) + throw new NotImplementedException("Can't get season's extra directory when season.Show == null."); + // TODO use a season.Path here. + string path = Path.Combine(season.Show.Path, "Extra"); + Directory.CreateDirectory(path); + return path; + } + + public string GetExtraDirectory(Episode episode) + { + string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra"); + Directory.CreateDirectory(path); + return path; + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 346f71be..d025c31c 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -18,7 +18,7 @@ namespace Kyoo.Controllers private async Task GetMetadata(Func> providerCall, Library library, string what) where T : new() { - T ret = new T(); + T ret = new(); IEnumerable providers = library?.Providers .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) @@ -40,11 +40,11 @@ namespace Kyoo.Controllers } private async Task> GetMetadata( - Func>> providerCall, + Func>> providerCall, Library library, string what) { - List ret = new List(); + List ret = new(); IEnumerable providers = library?.Providers .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) @@ -121,6 +121,7 @@ namespace Kyoo.Controllers $"the season {seasonNumber} of {show.Title}"); season.Show = show; season.ShowID = show.ID; + season.ShowSlug = show.Slug; season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; season.Title ??= $"Season {season.SeasonNumber}"; return season; @@ -139,6 +140,7 @@ namespace Kyoo.Controllers "an episode"); episode.Show = show; episode.ShowID = show.ID; + episode.ShowSlug = show.Slug; episode.Path = episodePath; episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber; @@ -146,7 +148,7 @@ namespace Kyoo.Controllers return episode; } - public async Task> GetPeople(Show show, Library library) + public async Task> GetPeople(Show show, Library library) { List people = await GetMetadata( provider => provider.GetPeople(show), diff --git a/Kyoo/Controllers/Repositories/CollectionRepository.cs b/Kyoo/Controllers/Repositories/CollectionRepository.cs index 230e79ae..ccebc298 100644 --- a/Kyoo/Controllers/Repositories/CollectionRepository.cs +++ b/Kyoo/Controllers/Repositories/CollectionRepository.cs @@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class CollectionRepository : LocalRepository, ICollectionRepository + public class CollectionRepository : LocalRepository, ICollectionRepository { private bool _disposed; private readonly DatabaseContext _database; - protected override Expression> DefaultSort => x => x.Name; + protected override Expression> DefaultSort => x => x.Name; public CollectionRepository(DatabaseContext database) : base(database) { @@ -25,6 +25,7 @@ namespace Kyoo.Controllers return; _disposed = true; _database.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -39,11 +40,12 @@ namespace Kyoo.Controllers { return await _database.Collections .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) - .ToListAsync(); + .ToListAsync(); } - public override async Task Create(CollectionDE obj) + public override async Task Create(Collection obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; @@ -51,18 +53,12 @@ namespace Kyoo.Controllers return obj; } - public override async Task Delete(CollectionDE obj) + public override async Task Delete(Collection obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.Links != null) - foreach (CollectionLink link in obj.Links) - _database.Entry(link).State = EntityState.Deleted; - if (obj.LibraryLinks != null) - foreach (LibraryLink link in obj.LibraryLinks) - _database.Entry(link).State = EntityState.Deleted; await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index a95bfcad..77901701 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers @@ -15,13 +14,21 @@ namespace Kyoo.Controllers private bool _disposed; private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly IShowRepository _shows; + private readonly ITrackRepository _tracks; protected override Expression> DefaultSort => x => x.EpisodeNumber; - public EpisodeRepository(DatabaseContext database, IProviderRepository providers) : base(database) + public EpisodeRepository(DatabaseContext database, + IProviderRepository providers, + IShowRepository shows, + ITrackRepository tracks) + : base(database) { _database = database; _providers = providers; + _shows = shows; + _tracks = tracks; } @@ -32,6 +39,8 @@ namespace Kyoo.Controllers _disposed = true; _database.Dispose(); _providers.Dispose(); + _shows.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -41,11 +50,20 @@ namespace Kyoo.Controllers _disposed = true; await _database.DisposeAsync(); await _providers.DisposeAsync(); + await _shows.DisposeAsync(); + } + + public override async Task Get(int id) + { + Episode ret = await base.Get(id); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } public override Task Get(string slug) { - Match match = Regex.Match(slug, @"(?.*)-s(?\d*)-e(?\d*)"); + Match match = Regex.Match(slug, @"(?.*)-s(?\d*)e(?\d*)"); if (!match.Success) return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); @@ -53,76 +71,138 @@ namespace Kyoo.Controllers int.Parse(match.Groups["season"].Value), int.Parse(match.Groups["episode"].Value)); } - - public Task Get(string showSlug, int seasonNumber, int episodeNumber) + + public override async Task Get(Expression> predicate) { - return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + Episode ret = await base.Get(predicate); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } - public Task Get(int showID, int seasonNumber, int episodeNumber) + public async Task Get(string showSlug, int seasonNumber, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber - && x.EpisodeNumber == episodeNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; } - public Task Get(int seasonID, int episodeNumber) + public async Task Get(int showID, int seasonNumber, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID - && x.EpisodeNumber == episodeNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; } - public Task GetAbsolute(int showID, int absoluteNumber) + public async Task Get(int seasonID, int episodeNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID - && x.AbsoluteNumber == absoluteNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID + && x.EpisodeNumber == episodeNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; } - public Task GetAbsolute(string showSlug, int absoluteNumber) + public async Task GetAbsolute(int showID, int absoluteNumber) { - return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.AbsoluteNumber == absoluteNumber); + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.AbsoluteNumber == absoluteNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; } - public async Task> Search(string query) + public async Task GetAbsolute(string showSlug, int absoluteNumber) { - return await _database.Episodes - .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.AbsoluteNumber == absoluteNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; + } + + public override async Task> Search(string query) + { + List episodes = await _database.Episodes + .Where(x => EF.Functions.ILike(x.Title, $"%{query}%") && x.EpisodeNumber != -1) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); + foreach (Episode episode in episodes) + episode.ShowSlug = await _shows.GetSlug(episode.ShowID); + return episodes; + } + + public override async Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection episodes = await base.GetAll(where, sort, limit); + foreach (Episode episode in episodes) + episode.ShowSlug = await _shows.GetSlug(episode.ShowID); + return episodes; } public override async Task Create(Episode obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Added; - - if (obj.Tracks != null) - foreach (Track entry in obj.Tracks) - _database.Entry(entry).State = EntityState.Added; - + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); - return obj; + return await ValidateTracks(obj); } - protected override async Task Validate(Episode resource) + protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld) { if (resource.ShowID <= 0) throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {resource.ShowID})."); - - await base.Validate(resource); - - if (resource.ExternalIDs != null) + + if (changed.Tracks != null || resetOld) { - foreach (MetadataID link in resource.ExternalIDs) - if (ShouldValidate(link)) - link.Provider = await _providers.CreateIfNotExists(link.Provider, true); + await _tracks.DeleteRange(x => x.EpisodeID == resource.ID); + resource.Tracks = changed.Tracks; + await ValidateTracks(resource); } + + if (changed.ExternalIDs != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); + resource.ExternalIDs = changed.ExternalIDs; + } + + await Validate(resource); + } + + private async Task ValidateTracks(Episode resource) + { + resource.Tracks = await resource.Tracks.MapAsync((x, i) => + { + x.Episode = resource; + x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language + && x.IsForced == y.IsForced + && x.Codec == y.Codec + && x.Type == y.Type); + return _tracks.Create(x); + }).ToListAsync(); + return resource; + } + + protected override async Task Validate(Episode resource) + { + await base.Validate(resource); + resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => + { + x.Provider = await _providers.CreateIfNotExists(x.Provider, true); + x.ProviderID = x.Provider.ID; + _database.Entry(x.Provider).State = EntityState.Detached; + return x; + }).ToListAsync(); } public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) @@ -137,10 +217,8 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Deleted; - // Since Tracks & Episodes are on the same database and handled by dotnet-ef, we can't use the repository to delete them. + await obj.Tracks.ForEachAsync(x => _tracks.Delete(x)); + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Controllers/Repositories/GenreRepository.cs b/Kyoo/Controllers/Repositories/GenreRepository.cs index 3a2261c6..bfbc1e3c 100644 --- a/Kyoo/Controllers/Repositories/GenreRepository.cs +++ b/Kyoo/Controllers/Repositories/GenreRepository.cs @@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class GenreRepository : LocalRepository, IGenreRepository + public class GenreRepository : LocalRepository, IGenreRepository { private bool _disposed; private readonly DatabaseContext _database; - protected override Expression> DefaultSort => x => x.Slug; + protected override Expression> DefaultSort => x => x.Slug; public GenreRepository(DatabaseContext database) : base(database) @@ -26,6 +26,7 @@ namespace Kyoo.Controllers return; _disposed = true; _database.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -40,11 +41,12 @@ namespace Kyoo.Controllers { return await _database.Genres .Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) - .ToListAsync(); + .ToListAsync(); } - public override async Task Create(GenreDE obj) + public override async Task Create(Genre obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; @@ -52,15 +54,12 @@ namespace Kyoo.Controllers return obj; } - public override async Task Delete(GenreDE obj) + public override async Task Delete(Genre obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.Links != null) - foreach (GenreLink link in obj.Links) - _database.Entry(link).State = EntityState.Deleted; await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index b8c5cc17..57f293b5 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -42,6 +42,7 @@ namespace Kyoo.Controllers _shows.Value.Dispose(); if (_collections.IsValueCreated) _collections.Value.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -71,7 +72,7 @@ namespace Kyoo.Controllers private IQueryable ItemsQuery => _database.Shows - .Where(x => !_database.CollectionLinks.Any(y => y.ChildID == x.ID)) + .Where(x => !x.Collections.Any()) .Select(LibraryItem.FromShow) .Concat(_database.Collections .Select(LibraryItem.FromCollection)); @@ -91,10 +92,11 @@ namespace Kyoo.Controllers return query.CountAsync(); } - public async Task> Search(string query) + public override async Task> Search(string query) { return await ItemsQuery .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); } @@ -108,22 +110,19 @@ namespace Kyoo.Controllers throw new InvalidOperationException(); } public override Task Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); - protected override Task Validate(LibraryItem resource) => throw new InvalidOperationException(); public override Task Delete(int id) => throw new InvalidOperationException(); public override Task Delete(string slug) => throw new InvalidOperationException(); public override Task Delete(LibraryItem obj) => throw new InvalidOperationException(); - private IQueryable LibraryRelatedQuery(Expression> selector) - => _database.LibraryLinks + private IQueryable LibraryRelatedQuery(Expression> selector) + => _database.Libraries .Where(selector) - .Select(x => x.Show) - .Where(x => x != null) - .Where(x => !_database.CollectionLinks.Any(y => y.ChildID == x.ID)) + .SelectMany(x => x.Shows) + .Where(x => !x.Collections.Any()) .Select(LibraryItem.FromShow) - .Concat(_database.LibraryLinks + .Concat(_database.Libraries .Where(selector) - .Select(x => x.Collection) - .Where(x => x != null) + .SelectMany(x => x.Collections) .Select(LibraryItem.FromCollection)); public async Task> GetFromLibrary(int id, @@ -131,7 +130,7 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default) { - ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.LibraryID == id), + ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.ID == id), where, sort, limit); @@ -145,7 +144,7 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default) { - ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.Library.Slug == slug), + ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.Slug == slug), where, sort, limit); diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index 86cdee90..b4d725c1 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -4,18 +4,16 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class LibraryRepository : LocalRepository, ILibraryRepository + public class LibraryRepository : LocalRepository, ILibraryRepository { private bool _disposed; private readonly DatabaseContext _database; private readonly IProviderRepository _providers; - protected override Expression> DefaultSort => x => x.ID; + protected override Expression> DefaultSort => x => x.ID; public LibraryRepository(DatabaseContext database, IProviderRepository providers) @@ -32,6 +30,7 @@ namespace Kyoo.Controllers _disposed = true; _database.Dispose(); _providers.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -47,23 +46,30 @@ namespace Kyoo.Controllers { return await _database.Libraries .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) - .ToListAsync(); + .ToListAsync(); } - public override async Task Create(LibraryDE obj) + public override async Task Create(Library obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - if (obj.ProviderLinks != null) - foreach (ProviderLink entry in obj.ProviderLinks) - _database.Entry(entry).State = EntityState.Added; - + obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToArray(); + obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); return obj; } - protected override async Task Validate(LibraryDE resource) + protected override async Task Validate(Library resource) + { + await base.Validate(resource); + resource.Providers = await resource.Providers + .SelectAsync(x => _providers.CreateIfNotExists(x, true)) + .ToListAsync(); + } + + protected override async Task EditRelations(Library resource, Library changed, bool resetOld) { if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("The library's slug must be set and not empty"); @@ -71,27 +77,21 @@ namespace Kyoo.Controllers throw new ArgumentException("The library's name must be set and not empty"); if (resource.Paths == null || !resource.Paths.Any()) throw new ArgumentException("The library should have a least one path."); - - await base.Validate(resource); - - if (resource.ProviderLinks != null) - foreach (ProviderLink link in resource.ProviderLinks) - if (ShouldValidate(link)) - link.Child = await _providers.CreateIfNotExists(link.Child, true); + + if (changed.Providers != null || resetOld) + { + await Validate(changed); + await Database.Entry(resource).Collection(x => x.Providers).LoadAsync(); + resource.Providers = changed.Providers; + } } - public override async Task Delete(LibraryDE obj) + public override async Task Delete(Library obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.ProviderLinks != null) - foreach (ProviderLink entry in obj.ProviderLinks) - _database.Entry(entry).State = EntityState.Deleted; - if (obj.Links != null) - foreach (LibraryLink entry in obj.Links) - _database.Entry(entry).State = EntityState.Deleted; await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index a0f7c0bb..9097bea8 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -36,6 +36,7 @@ namespace Kyoo.Controllers _providers.Dispose(); if (_shows.IsValueCreated) _shows.Value.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -49,10 +50,11 @@ namespace Kyoo.Controllers await _shows.Value.DisposeAsync(); } - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.People .Where(people => EF.Functions.ILike(people.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); } @@ -61,10 +63,7 @@ namespace Kyoo.Controllers { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Added; - + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists)."); return obj; } @@ -72,25 +71,45 @@ namespace Kyoo.Controllers protected override async Task Validate(People resource) { await base.Validate(resource); - - if (resource.ExternalIDs != null) - foreach (MetadataID link in resource.ExternalIDs) - if (ShouldValidate(link)) - link.Provider = await _providers.CreateIfNotExists(link.Provider, true); + await resource.ExternalIDs.ForEachAsync(async id => + { + id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.ProviderID = id.Provider.ID; + _database.Entry(id.Provider).State = EntityState.Detached; + }); + await resource.Roles.ForEachAsync(async role => + { + role.Show = await _shows.Value.CreateIfNotExists(role.Show, true); + role.ShowID = role.Show.ID; + _database.Entry(role.Show).State = EntityState.Detached; + }); } - + + protected override async Task EditRelations(People resource, People changed, bool resetOld) + { + if (changed.Roles != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.Roles).LoadAsync(); + resource.Roles = changed.Roles; + } + + if (changed.ExternalIDs != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); + resource.ExternalIDs = changed.ExternalIDs; + + } + await base.EditRelations(resource, changed, resetOld); + } + public override async Task Delete(People obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Deleted; - if (obj.Roles != null) - foreach (PeopleRole link in obj.Roles) - _database.Entry(link).State = EntityState.Deleted; + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); + obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); } @@ -99,7 +118,9 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default) { - ICollection people = await ApplyFilters(_database.PeopleRoles.Where(x => x.ShowID == showID), + ICollection people = await ApplyFilters(_database.PeopleRoles + .Where(x => x.ShowID == showID) + .Include(x => x.People), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), x => x.People.Name, where, @@ -107,6 +128,8 @@ namespace Kyoo.Controllers limit); if (!people.Any() && await _shows.Value.Get(showID) == null) throw new ItemNotFound(); + foreach (PeopleRole role in people) + role.ForPeople = true; return people; } @@ -115,7 +138,10 @@ namespace Kyoo.Controllers Sort sort = default, Pagination limit = default) { - ICollection people = await ApplyFilters(_database.PeopleRoles.Where(x => x.Show.Slug == showSlug), + ICollection people = await ApplyFilters(_database.PeopleRoles + .Where(x => x.Show.Slug == showSlug) + .Include(x => x.People) + .Include(x => x.Show), id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), x => x.People.Name, where, @@ -123,18 +149,21 @@ namespace Kyoo.Controllers limit); if (!people.Any() && await _shows.Value.Get(showSlug) == null) throw new ItemNotFound(); + foreach (PeopleRole role in people) + role.ForPeople = true; return people; } - public async Task> GetFromPeople(int peopleID, - Expression> where = null, - Sort sort = default, + public async Task> GetFromPeople(int peopleID, + Expression> where = null, + Sort sort = default, Pagination limit = default) { - ICollection roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.PeopleID == peopleID) - .Select(ShowRole.FromPeopleRole), - async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), - x => x.Title, + ICollection roles = await ApplyFilters(_database.PeopleRoles + .Where(x => x.PeopleID == peopleID) + .Include(x => x.Show), + id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), + x => x.Show.Title, where, sort, limit); @@ -143,15 +172,16 @@ namespace Kyoo.Controllers return roles; } - public async Task> GetFromPeople(string slug, - Expression> where = null, - Sort sort = default, + public async Task> GetFromPeople(string slug, + Expression> where = null, + Sort sort = default, Pagination limit = default) { - ICollection roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.People.Slug == slug) - .Select(ShowRole.FromPeopleRole), - async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), - x => x.Title, + ICollection roles = await ApplyFilters(_database.PeopleRoles + .Where(x => x.People.Slug == slug) + .Include(x => x.Show), + id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), + x => x.Show.Title, where, sort, limit); diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index 6219a919..79cc80b1 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -19,10 +19,11 @@ namespace Kyoo.Controllers _database = database; } - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Providers .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); } @@ -31,7 +32,6 @@ namespace Kyoo.Controllers { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync($"Trying to insert a duplicated provider (slug {obj.Slug} already exists)."); return obj; } @@ -42,8 +42,20 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - // TODO handle ExternalID deletion when they refer to this providerID. + obj.MetadataLinks.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); } + + public Task> GetMetadataID(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ApplyFilters(_database.MetadataIds.Include(y => y.Provider), + x => _database.MetadataIds.FirstOrDefaultAsync(y => y.ID == x), + x => x.ID, + where, + sort, + limit); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 97bd8319..29c3e400 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -16,17 +15,20 @@ namespace Kyoo.Controllers private bool _disposed; private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly IShowRepository _shows; private readonly Lazy _episodes; protected override Expression> DefaultSort => x => x.SeasonNumber; public SeasonRepository(DatabaseContext database, IProviderRepository providers, + IShowRepository shows, IServiceProvider services) : base(database) { _database = database; _providers = providers; + _shows = shows; _episodes = new Lazy(services.GetRequiredService); } @@ -38,8 +40,10 @@ namespace Kyoo.Controllers _disposed = true; _database.Dispose(); _providers.Dispose(); + _shows.Dispose(); if (_episodes.IsValueCreated) _episodes.Value.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -49,10 +53,27 @@ namespace Kyoo.Controllers _disposed = true; await _database.DisposeAsync(); await _providers.DisposeAsync(); + await _shows.DisposeAsync(); if (_episodes.IsValueCreated) await _episodes.Value.DisposeAsync(); } + public override async Task Get(int id) + { + Season ret = await base.Get(id); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; + } + + public override async Task Get(Expression> predicate) + { + Season ret = await base.Get(predicate); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + return ret; + } + public override Task Get(string slug) { Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); @@ -62,34 +83,51 @@ namespace Kyoo.Controllers return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); } - public Task Get(int showID, int seasonNumber) + public async Task Get(int showID, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber); + Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber); + if (ret != null) + ret.ShowSlug = await _shows.GetSlug(showID); + return ret; } - public Task Get(string showSlug, int seasonNumber) + public async Task Get(string showSlug, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug && x.SeasonNumber == seasonNumber); + if (ret != null) + ret.ShowSlug = showSlug; + return ret; } - public async Task> Search(string query) + public override async Task> Search(string query) { - return await _database.Seasons + List seasons = await _database.Seasons .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); + foreach (Season season in seasons) + season.ShowSlug = await _shows.GetSlug(season.ShowID); + return seasons; + } + + public override async Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection seasons = await base.GetAll(where, sort, limit); + foreach (Season season in seasons) + season.ShowSlug = await _shows.GetSlug(season.ShowID); + return seasons; } public override async Task Create(Season obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Added; - + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists)."); return obj; } @@ -100,15 +138,24 @@ namespace Kyoo.Controllers throw new InvalidOperationException($"Can't store a season not related to any show (showID: {resource.ShowID})."); await base.Validate(resource); - - if (resource.ExternalIDs != null) + await resource.ExternalIDs.ForEachAsync(async id => { - foreach (MetadataID link in resource.ExternalIDs) - if (ShouldValidate(link)) - link.Provider = await _providers.CreateIfNotExists(link.Provider, true); - } + id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.ProviderID = id.Provider.ID; + _database.Entry(id.Provider).State = EntityState.Detached; + }); } - + + protected override async Task EditRelations(Season resource, Season changed, bool resetOld) + { + if (changed.ExternalIDs != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); + resource.ExternalIDs = changed.ExternalIDs; + } + await base.EditRelations(resource, changed, resetOld); + } + public async Task Delete(string showSlug, int seasonNumber) { Season obj = await Get(showSlug, seasonNumber); @@ -121,11 +168,7 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Deleted; - + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); if (obj.Episodes != null) diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index f1f0fef4..f07e4f5a 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class ShowRepository : LocalRepository, IShowRepository + public class ShowRepository : LocalRepository, IShowRepository { private bool _disposed; private readonly DatabaseContext _database; @@ -19,7 +19,7 @@ namespace Kyoo.Controllers private readonly IProviderRepository _providers; private readonly Lazy _seasons; private readonly Lazy _episodes; - protected override Expression> DefaultSort => x => x.Title; + protected override Expression> DefaultSort => x => x.Title; public ShowRepository(DatabaseContext database, IStudioRepository studios, @@ -52,6 +52,7 @@ namespace Kyoo.Controllers _seasons.Value.Dispose(); if (_episodes.IsValueCreated) _episodes.Value.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -77,89 +78,110 @@ namespace Kyoo.Controllers .Where(x => EF.Functions.ILike(x.Title, query) || EF.Functions.ILike(x.Slug, query) /*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE. + .OrderBy(DefaultSort) .Take(20) - .ToListAsync(); + .ToListAsync(); } - public override async Task Create(ShowDE obj) + public override async Task Create(Show obj) { await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - - if (obj.GenreLinks != null) - { - foreach (GenreLink entry in obj.GenreLinks) - { - if (!(entry.Child is GenreDE)) - entry.Child = new GenreDE(entry.Child); - _database.Entry(entry).State = EntityState.Added; - } - } - - if (obj.People != null) - foreach (PeopleRole entry in obj.People) - _database.Entry(entry).State = EntityState.Added; - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Added; - + obj.GenreLinks.ForEach(x => _database.Entry(x).State = EntityState.Added); + obj.People.ForEach(x => _database.Entry(x).State = EntityState.Added); + obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists)."); return obj; } - protected override async Task Validate(ShowDE resource) + protected override async Task Validate(Show resource) { await base.Validate(resource); - - if (ShouldValidate(resource.Studio)) + if (resource.Studio != null) resource.Studio = await _studios.CreateIfNotExists(resource.Studio, true); - - if (resource.GenreLinks != null) - foreach (GenreLink link in resource.GenreLinks) - if (ShouldValidate(link)) - link.Child = await _genres.CreateIfNotExists(link.Child, true); - - if (resource.People != null) - foreach (PeopleRole link in resource.People) - if (ShouldValidate(link)) - link.People = await _people.CreateIfNotExists(link.People, true); - - if (resource.ExternalIDs != null) - foreach (MetadataID link in resource.ExternalIDs) - if (ShouldValidate(link)) - link.Provider = await _providers.CreateIfNotExists(link.Provider, true); + resource.Genres = await resource.Genres + .SelectAsync(x => _genres.CreateIfNotExists(x, true)) + .ToListAsync(); + resource.GenreLinks = resource.Genres? + .Select(x => Link.UCreate(resource, x)) + .ToList(); + await resource.ExternalIDs.ForEachAsync(async id => + { + id.Provider = await _providers.CreateIfNotExists(id.Provider, true); + id.ProviderID = id.Provider.ID; + _database.Entry(id.Provider).State = EntityState.Detached; + }); + await resource.People.ForEachAsync(async role => + { + role.People = await _people.CreateIfNotExists(role.People, true); + role.PeopleID = role.People.ID; + _database.Entry(role.People).State = EntityState.Detached; + }); } - + + protected override async Task EditRelations(Show resource, Show changed, bool resetOld) + { + await Validate(changed); + + if (changed.Aliases != null || resetOld) + resource.Aliases = changed.Aliases; + + if (changed.Genres != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.GenreLinks).LoadAsync(); + resource.GenreLinks = changed.Genres?.Select(x => Link.UCreate(resource, x)).ToList(); + } + + if (changed.People != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.People).LoadAsync(); + resource.People = changed.People; + } + + if (changed.ExternalIDs != null || resetOld) + { + await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync(); + resource.ExternalIDs = changed.ExternalIDs; + } + } + public async Task AddShowLink(int showID, int? libraryID, int? collectionID) { if (collectionID != null) { - await _database.CollectionLinks.AddAsync(new CollectionLink {ParentID = collectionID.Value, ChildID = showID}); + await _database.Links() + .AddAsync(new Link(collectionID.Value, showID)); await _database.SaveIfNoDuplicates(); + + if (libraryID != null) + { + await _database.Links() + .AddAsync(new Link(libraryID.Value, collectionID.Value)); + await _database.SaveIfNoDuplicates(); + } } if (libraryID != null) { - await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, ShowID = showID}); - await _database.SaveIfNoDuplicates(); - } - - if (libraryID != null && collectionID != null) - { - await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, CollectionID = collectionID.Value}); + await _database.Links() + .AddAsync(new Link(libraryID.Value, showID)); await _database.SaveIfNoDuplicates(); } } - public override async Task Delete(ShowDE obj) + public Task GetSlug(int showID) + { + return _database.Shows.Where(x => x.ID == showID) + .Select(x => x.Slug) + .FirstOrDefaultAsync(); + } + + public override async Task Delete(Show obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - if (obj.GenreLinks != null) - foreach (GenreLink entry in obj.GenreLinks) - _database.Entry(entry).State = EntityState.Deleted; if (obj.People != null) foreach (PeopleRole entry in obj.People) @@ -168,14 +190,6 @@ namespace Kyoo.Controllers if (obj.ExternalIDs != null) foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Deleted; - - if (obj.CollectionLinks != null) - foreach (CollectionLink entry in obj.CollectionLinks) - _database.Entry(entry).State = EntityState.Deleted; - - if (obj.LibraryLinks != null) - foreach (LibraryLink entry in obj.LibraryLinks) - _database.Entry(entry).State = EntityState.Deleted; await _database.SaveChangesAsync(); diff --git a/Kyoo/Controllers/Repositories/StudioRepository.cs b/Kyoo/Controllers/Repositories/StudioRepository.cs index eebf0fdc..f8afc757 100644 --- a/Kyoo/Controllers/Repositories/StudioRepository.cs +++ b/Kyoo/Controllers/Repositories/StudioRepository.cs @@ -19,10 +19,11 @@ namespace Kyoo.Controllers _database = database; } - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Studios .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) + .OrderBy(DefaultSort) .Take(20) .ToListAsync(); } @@ -41,10 +42,6 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - - // Using Dotnet-EF change discovery service to remove references to this studio on shows. - foreach (Show show in obj.Shows) - show.StudioID = null; await _database.SaveChangesAsync(); } } diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index ee5366aa..d4e04376 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; +using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers @@ -12,7 +13,7 @@ namespace Kyoo.Controllers { private bool _disposed; private readonly DatabaseContext _database; - protected override Expression> DefaultSort => x => x.ID; + protected override Expression> DefaultSort => x => x.TrackIndex; public TrackRepository(DatabaseContext database) : base(database) @@ -36,10 +37,15 @@ namespace Kyoo.Controllers await _database.DisposeAsync(); } - public Task Get(string slug, StreamType type = StreamType.Unknown) + public override Task Get(string slug) + { + return Get(slug, StreamType.Unknown); + } + + public Task Get(string slug, StreamType type) { Match match = Regex.Match(slug, - @"(?.*)-s(?\d+)e(?\d+)\.(?.{0,3})(?-forced)?(\..*)?"); + @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); if (!match.Success) { @@ -56,6 +62,8 @@ namespace Kyoo.Controllers int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1; string language = match.Groups["language"].Value; bool forced = match.Groups["forced"].Success; + if (match.Groups["type"].Success) + type = Enum.Parse(match.Groups["type"].Value, true); if (type == StreamType.Unknown) { @@ -73,7 +81,7 @@ namespace Kyoo.Controllers && x.IsForced == forced); } - public Task> Search(string query) + public override Task> Search(string query) { throw new InvalidOperationException("Tracks do not support the search method."); } @@ -82,22 +90,23 @@ namespace Kyoo.Controllers { if (obj.EpisodeID <= 0) { - obj.EpisodeID = obj.Episode?.ID ?? -1; + obj.EpisodeID = obj.Episode?.ID ?? 0; if (obj.EpisodeID <= 0) throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); } await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync($"Trying to insert a duplicated track (slug {obj.Slug} already exists)."); + await _database.SaveOrRetry(obj, (x, i) => + { + if (i > 10) + throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting..."); + x.TrackIndex++; + return x; + }); return obj; } - - protected override Task Validate(Track resource) - { - return Task.CompletedTask; - } - + public override async Task Delete(Track obj) { if (obj == null) diff --git a/Kyoo/Controllers/TaskManager.cs b/Kyoo/Controllers/TaskManager.cs index 9a791fdc..519fb43f 100644 --- a/Kyoo/Controllers/TaskManager.cs +++ b/Kyoo/Controllers/TaskManager.cs @@ -110,7 +110,7 @@ namespace Kyoo.Controllers _tasks.AddRange(CoreTaskHolder.Tasks.Select(x => (x, DateTime.Now + GetTaskDelay(x.Slug)))); IEnumerable prerunTasks = _tasks.Select(x => x.task) - .Where(x => x.RunOnStartup && x.Priority == Int32.MaxValue); + .Where(x => x.RunOnStartup && x.Priority == int.MaxValue); foreach (ITask task in prerunTasks) task.Run(_serviceProvider, _taskToken.Token); diff --git a/Kyoo/Controllers/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs index 07a1d709..bc91f1af 100644 --- a/Kyoo/Controllers/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -1,20 +1,28 @@ using Kyoo.Models; using Microsoft.Extensions.Configuration; using System; -using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; +using JetBrains.Annotations; namespace Kyoo.Controllers { public class ThumbnailsManager : IThumbnailsManager { private readonly IConfiguration _config; + private readonly IFileManager _files; + private readonly string _peoplePath; + private readonly string _providerPath; - public ThumbnailsManager(IConfiguration configuration) + public ThumbnailsManager(IConfiguration configuration, IFileManager files) { _config = configuration; + _files = files; + _peoplePath = Path.GetFullPath(configuration.GetValue("peoplePath")); + _providerPath = Path.GetFullPath(configuration.GetValue("providerPath")); + Directory.CreateDirectory(_peoplePath); + Directory.CreateDirectory(_providerPath); } private static async Task DownloadImage(string url, string localPath, string what) @@ -26,83 +34,133 @@ namespace Kyoo.Controllers } catch (WebException exception) { - await Console.Error.WriteLineAsync($"{what} could not be downloaded.\n\tError: {exception.Message}."); + await Console.Error.WriteLineAsync($"{what} could not be downloaded. Error: {exception.Message}."); } } - public async Task Validate(Show show, bool alwaysDownload) + public async Task Validate(Show show, bool alwaysDownload) { - if (show?.Path == null) - return default; - if (show.Poster != null) { - string posterPath = Path.Combine(show.Path, "poster.jpg"); + string posterPath = await GetShowPoster(show); if (alwaysDownload || !File.Exists(posterPath)) await DownloadImage(show.Poster, posterPath, $"The poster of {show.Title}"); } if (show.Logo != null) { - string logoPath = Path.Combine(show.Path, "logo.png"); + string logoPath = await GetShowLogo(show); if (alwaysDownload || !File.Exists(logoPath)) await DownloadImage(show.Logo, logoPath, $"The logo of {show.Title}"); } if (show.Backdrop != null) { - string backdropPath = Path.Combine(show.Path, "backdrop.jpg"); + string backdropPath = await GetShowBackdrop(show); if (alwaysDownload || !File.Exists(backdropPath)) await DownloadImage(show.Backdrop, backdropPath, $"The backdrop of {show.Title}"); } - - return show; + + foreach (PeopleRole role in show.People) + await Validate(role.People, alwaysDownload); } - public async Task> Validate(IEnumerable people, bool alwaysDownload) + public async Task Validate([NotNull] People people, bool alwaysDownload) { if (people == null) - return null; - + throw new ArgumentNullException(nameof(people)); string root = _config.GetValue("peoplePath"); + string localPath = Path.Combine(root, people.Slug + ".jpg"); + Directory.CreateDirectory(root); - - foreach (PeopleRole peop in people) - { - string localPath = Path.Combine(root, peop.People.Slug + ".jpg"); - if (peop.People.Poster == null) - continue; - if (alwaysDownload || !File.Exists(localPath)) - await DownloadImage(peop.People.Poster, localPath, $"The profile picture of {peop.People.Name}"); - } - - return people; + if (alwaysDownload || !File.Exists(localPath)) + await DownloadImage(people.Poster, localPath, $"The profile picture of {people.Name}"); } - public async Task Validate(Season season, bool alwaysDownload) + public async Task Validate(Season season, bool alwaysDownload) { - if (season?.Show?.Path == null) - return default; + if (season?.Show?.Path == null || season.Poster == null) + return; - if (season.Poster != null) - { - string localPath = Path.Combine(season.Show.Path, $"season-{season.SeasonNumber}.jpg"); - if (alwaysDownload || !File.Exists(localPath)) - await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}"); - } - return season; + string localPath = await GetSeasonPoster(season); + if (alwaysDownload || !File.Exists(localPath)) + await DownloadImage(season.Poster, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}"); } - public async Task Validate(Episode episode, bool alwaysDownload) + public async Task Validate(Episode episode, bool alwaysDownload) { - if (episode?.Path == null) - return default; + if (episode?.Path == null || episode.Thumb == null) + return; - if (episode.Poster != null) - { - string localPath = Path.ChangeExtension(episode.Path, "jpg"); - if (alwaysDownload || !File.Exists(localPath)) - await DownloadImage(episode.Poster, localPath, $"The thumbnail of {episode.Show.Title}"); - } - return episode; + string localPath = await GetEpisodeThumb(episode); + if (alwaysDownload || !File.Exists(localPath)) + await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}"); + } + + public async Task Validate(ProviderID provider, bool alwaysDownload) + { + if (provider.Logo == null) + return; + + string root = _config.GetValue("providerPath"); + string localPath = Path.Combine(root, provider.Slug + ".jpg"); + + Directory.CreateDirectory(root); + if (alwaysDownload || !File.Exists(localPath)) + await DownloadImage(provider.Logo, localPath, $"The logo of {provider.Slug}"); + } + + public Task GetShowBackdrop(Show show) + { + if (show?.Path == null) + throw new ArgumentNullException(nameof(show)); + return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "backdrop.jpg")); + } + + public Task GetShowLogo(Show show) + { + if (show?.Path == null) + throw new ArgumentNullException(nameof(show)); + return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "logo.png")); + } + + public Task GetShowPoster(Show show) + { + if (show?.Path == null) + throw new ArgumentNullException(nameof(show)); + return Task.FromResult(Path.Combine(_files.GetExtraDirectory(show), "poster.jpg")); + } + + public Task GetSeasonPoster(Season season) + { + if (season == null) + throw new ArgumentNullException(nameof(season)); + return Task.FromResult(Path.Combine(_files.GetExtraDirectory(season), $"season-{season.SeasonNumber}.jpg")); + } + + public Task GetEpisodeThumb(Episode episode) + { + string dir = Path.Combine(_files.GetExtraDirectory(episode), "Thumbnails"); + Directory.CreateDirectory(dir); + return Task.FromResult(Path.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg")); + } + + public Task GetPeoplePoster(People people) + { + if (people == null) + throw new ArgumentNullException(nameof(people)); + string thumbPath = Path.GetFullPath(Path.Combine(_peoplePath, $"{people.Slug}.jpg")); + if (!thumbPath.StartsWith(_peoplePath)) + return Task.FromResult(null); + return Task.FromResult(thumbPath); + } + + public Task GetProviderLogo(ProviderID provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + string thumbPath = Path.GetFullPath(Path.Combine(_providerPath, $"{provider.Slug}.jpg")); + if (!thumbPath.StartsWith(_providerPath)) + return Task.FromResult(null); + return Task.FromResult(thumbPath.StartsWith(_providerPath) ? thumbPath : null); } } } diff --git a/Kyoo/Controllers/Transcoder.cs b/Kyoo/Controllers/Transcoder.cs new file mode 100644 index 00000000..898f7ba4 --- /dev/null +++ b/Kyoo/Controllers/Transcoder.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Kyoo.Models; +using Microsoft.Extensions.Configuration; +using Stream = Kyoo.Models.Watch.Stream; + +// We use threads so tasks are not always awaited. +#pragma warning disable 4014 + +namespace Kyoo.Controllers +{ + public class BadTranscoderException : Exception {} + + public class Transcoder : ITranscoder + { + private static class TranscoderAPI + { + private const string TranscoderPath = "libtranscoder.so"; + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int init(); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int transmux(string path, string outpath, out float playableDuration); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + public static extern int transcode(string path, string outpath, out float playableDuration); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr extract_infos(string path, + string outpath, + out int length, + out int trackCount, + bool reextracct); + + [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] + private static extern void free(IntPtr ptr); + + + public static Track[] ExtractInfos(string path, string outPath, bool reextract) + { + int size = Marshal.SizeOf(); + IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount, reextract); + IntPtr streamsPtr = ptr; + Track[] tracks; + + if (trackCount > 0 && ptr != IntPtr.Zero) + { + tracks = new Track[trackCount]; + + int j = 0; + for (int i = 0; i < arrayLength; i++) + { + Stream stream = Marshal.PtrToStructure(streamsPtr); + if (stream!.Type != StreamType.Unknown) + { + tracks[j] = new Track(stream); + j++; + } + streamsPtr += size; + } + } + else + tracks = Array.Empty(); + + if (ptr != IntPtr.Zero) + free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers. + return tracks; + } + } + + private readonly IFileManager _files; + private readonly string _transmuxPath; + private readonly string _transcodePath; + + public Transcoder(IConfiguration config, IFileManager files) + { + _files = files; + _transmuxPath = Path.GetFullPath(config.GetValue("transmuxTempPath")); + _transcodePath = Path.GetFullPath(config.GetValue("transcodeTempPath")); + + if (TranscoderAPI.init() != Marshal.SizeOf()) + throw new BadTranscoderException(); + } + + public Task ExtractInfos(Episode episode, bool reextract) + { + string dir = _files.GetExtraDirectory(episode); + if (dir == null) + throw new ArgumentException("Invalid path."); + return Task.Factory.StartNew( + () => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract), + TaskCreationOptions.LongRunning); + } + + public async Task Transmux(Episode episode) + { + if (!File.Exists(episode.Path)) + throw new ArgumentException("Path does not exists. Can't transcode."); + + string folder = Path.Combine(_transmuxPath, episode.Slug); + string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); + float playableDuration = 0; + bool transmuxFailed = false; + + try + { + Directory.CreateDirectory(folder); + if (File.Exists(manifest)) + return manifest; + } + catch (UnauthorizedAccessException) + { + await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); + return null; + } + + Task.Factory.StartNew(() => + { + string cleanManifest = manifest.Replace('\\', '/'); + transmuxFailed = TranscoderAPI.transmux(episode.Path, cleanManifest, out playableDuration) != 0; + }, TaskCreationOptions.LongRunning); + while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed) + await Task.Delay(10); + return transmuxFailed ? null : manifest; + } + + public Task Transcode(Episode episode) + { + return Task.FromResult(null); // Not implemented yet. + } + } +} diff --git a/Kyoo/Controllers/Transcoder/Transcoder.cs b/Kyoo/Controllers/Transcoder/Transcoder.cs deleted file mode 100644 index 479ac32b..00000000 --- a/Kyoo/Controllers/Transcoder/Transcoder.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Kyoo.Models; -using Microsoft.Extensions.Configuration; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Kyoo.Controllers.TranscoderLink; -#pragma warning disable 4014 - -namespace Kyoo.Controllers -{ - public class BadTranscoderException : Exception {} - - public class Transcoder : ITranscoder - { - private readonly string _transmuxPath; - private readonly string _transcodePath; - - public Transcoder(IConfiguration config) - { - _transmuxPath = Path.GetFullPath(config.GetValue("transmuxTempPath")); - _transcodePath = Path.GetFullPath(config.GetValue("transcodeTempPath")); - - if (TranscoderAPI.init() != Marshal.SizeOf()) - throw new BadTranscoderException(); - } - - public Task ExtractInfos(string path) - { - string dir = Path.GetDirectoryName(path); - if (dir == null) - throw new ArgumentException("Invalid path."); - - return Task.Factory.StartNew(() => TranscoderAPI.ExtractInfos(path, dir), TaskCreationOptions.LongRunning); - } - - public async Task Transmux(Episode episode) - { - string folder = Path.Combine(_transmuxPath, episode.Slug); - string manifest = Path.Combine(folder, episode.Slug + ".m3u8"); - float playableDuration = 0; - bool transmuxFailed = false; - - try - { - Directory.CreateDirectory(folder); - if (File.Exists(manifest)) - return manifest; - } - catch (UnauthorizedAccessException) - { - await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config."); - return null; - } - - Task.Factory.StartNew(() => - { - transmuxFailed = TranscoderAPI.transmux(episode.Path, manifest.Replace('\\', '/'), out playableDuration) != 0; - }, TaskCreationOptions.LongRunning); - while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed) - await Task.Delay(10); - return transmuxFailed ? null : manifest; - } - - public Task Transcode(Episode episode) - { - return Task.FromResult(null); // Not implemented yet. - } - } -} diff --git a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs deleted file mode 100644 index 6abb5a51..00000000 --- a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Kyoo.Models; -using Kyoo.Models.Watch; -// ReSharper disable InconsistentNaming - -namespace Kyoo.Controllers.TranscoderLink -{ - public static class TranscoderAPI - { - private const string TranscoderPath = "libtranscoder.so"; - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int init(); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int transmux(string path, string out_path, out float playableDuration); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - public static extern int transcode(string path, string out_path, out float playableDuration); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr extract_infos(string path, string outpath, out int length, out int track_count); - - [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] - private static extern void free(IntPtr stream_ptr); - - - public static Track[] ExtractInfos(string path, string outPath) - { - int size = Marshal.SizeOf(); - IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount); - IntPtr streamsPtr = ptr; - Track[] tracks; - - if (trackCount > 0 && ptr != IntPtr.Zero) - { - tracks = new Track[trackCount]; - - int j = 0; - for (int i = 0; i < arrayLength; i++) - { - Stream stream = Marshal.PtrToStructure(streamsPtr); - if (stream!.Type != StreamType.Unknown) - { - tracks[j] = new Track(stream); - j++; - } - streamsPtr += size; - } - } - else - tracks = Array.Empty(); - - if (ptr != IntPtr.Zero) - free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers. - return tracks; - } - } -} diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 607e76e8..97b151f2 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -21,32 +21,32 @@ - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - @@ -90,7 +90,7 @@ - + diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index 4730cea2..435b01f7 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,39 +12,43 @@ namespace Kyoo { public class DatabaseContext : DbContext { - public DatabaseContext(DbContextOptions options) : base(options) { } + public DatabaseContext(DbContextOptions options) : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.LazyLoadingEnabled = false; + } - public DbSet Libraries { get; set; } - public DbSet Collections { get; set; } - public DbSet Shows { get; set; } + public DbSet Libraries { get; set; } + public DbSet Collections { get; set; } + public DbSet Shows { get; set; } public DbSet Seasons { get; set; } public DbSet Episodes { get; set; } public DbSet Tracks { get; set; } - public DbSet Genres { get; set; } + public DbSet Genres { get; set; } public DbSet People { get; set; } public DbSet Studios { get; set; } public DbSet Providers { get; set; } public DbSet MetadataIds { get; set; } public DbSet PeopleRoles { get; set; } + + public DbSet> Links() + where T1 : class, IResource + where T2 : class, IResource + { + return Set>(); + } - - public DbSet LibraryLinks { get; set; } - public DbSet CollectionLinks { get; set; } - public DbSet GenreLinks { get; set; } - public DbSet ProviderLinks { get; set; } public DatabaseContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); + + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.LazyLoadingEnabled = false; } - - private readonly ValueComparer> _stringArrayComparer = - new ValueComparer>( - (l1, l2) => l1.SequenceEqual(l2), - arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -54,20 +58,13 @@ namespace Kyoo modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - modelBuilder.Ignore(); - modelBuilder.Ignore(); - modelBuilder.Ignore(); - modelBuilder.Ignore(); - - modelBuilder.Entity() + modelBuilder.Entity() .Property(x => x.Paths) - .HasColumnType("text[]") - .Metadata.SetValueComparer(_stringArrayComparer); + .HasColumnType("text[]"); - modelBuilder.Entity() + modelBuilder.Entity() .Property(x => x.Aliases) - .HasColumnType("text[]") - .Metadata.SetValueComparer(_stringArrayComparer); + .HasColumnType("text[]"); modelBuilder.Entity() .Property(t => t.IsDefault) @@ -77,91 +74,69 @@ namespace Kyoo .Property(t => t.IsForced) .ValueGeneratedNever(); - - modelBuilder.Entity() - .HasKey(x => new {ShowID = x.ParentID, GenreID = x.ChildID}); - - modelBuilder.Entity() - .HasKey(x => new {CollectionID = x.ParentID, ShowID = x.ChildID}); + modelBuilder.Entity() + .HasMany(x => x.Libraries) + .WithMany(x => x.Providers) + .UsingEntity>( + y => y + .HasOne(x => x.First) + .WithMany(x => x.ProviderLinks), + y => y + .HasOne(x => x.Second) + .WithMany(x => x.LibraryLinks), + y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .HasKey(x => new {LibraryID = x.ParentID, ProviderID = x.ChildID}); - - - modelBuilder.Entity() - .Ignore(x => x.Shows) - .Ignore(x => x.Collections) - .Ignore(x => x.Providers); + modelBuilder.Entity() + .HasMany(x => x.Libraries) + .WithMany(x => x.Collections) + .UsingEntity>( + y => y + .HasOne(x => x.First) + .WithMany(x => x.CollectionLinks), + y => y + .HasOne(x => x.Second) + .WithMany(x => x.LibraryLinks), + y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .Ignore(x => x.Shows) - .Ignore(x => x.Libraries); + modelBuilder.Entity() + .HasMany(x => x.Libraries) + .WithMany(x => x.Shows) + .UsingEntity>( + y => y + .HasOne(x => x.First) + .WithMany(x => x.ShowLinks), + y => y + .HasOne(x => x.Second) + .WithMany(x => x.LibraryLinks), + y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .Ignore(x => x.Genres) - .Ignore(x => x.Libraries) - .Ignore(x => x.Collections); + modelBuilder.Entity() + .HasMany(x => x.Collections) + .WithMany(x => x.Shows) + .UsingEntity>( + y => y + .HasOne(x => x.First) + .WithMany(x => x.ShowLinks), + y => y + .HasOne(x => x.Second) + .WithMany(x => x.CollectionLinks), + y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .Ignore(x => x.Slug) - .Ignore(x => x.Name) - .Ignore(x => x.Poster) - .Ignore(x => x.ExternalIDs); - - modelBuilder.Entity() - .Ignore(x => x.Shows); - - - modelBuilder.Entity() - .HasOne(x => x.Library as LibraryDE) - .WithMany(x => x.Links) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Show as ShowDE) - .WithMany(x => x.LibraryLinks) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Collection as CollectionDE) - .WithMany(x => x.LibraryLinks) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(x => x.Parent as CollectionDE) - .WithMany(x => x.Links) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Child as ShowDE) - .WithMany(x => x.CollectionLinks) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(x => x.Child as GenreDE) - .WithMany(x => x.Links) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Parent as ShowDE) - .WithMany(x => x.GenreLinks) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(x => x.Parent as LibraryDE) - .WithMany(x => x.ProviderLinks) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(x => x.Show as ShowDE) - .WithMany(x => x.Seasons); - modelBuilder.Entity() - .HasOne(x => x.Show as ShowDE) - .WithMany(x => x.Episodes); - modelBuilder.Entity() - .HasOne(x => x.Show as ShowDE) - .WithMany(x => x.People); - + modelBuilder.Entity() + .HasMany(x => x.Shows) + .WithMany(x => x.Genres) + .UsingEntity>( + y => y + .HasOne(x => x.First) + .WithMany(x => x.GenreLinks), + y => y + .HasOne(x => x.Second) + .WithMany(x => x.ShowLinks), + y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() - .HasOne(x => x.Show as ShowDE) + .HasOne(x => x.Show) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() @@ -176,28 +151,32 @@ namespace Kyoo .HasOne(x => x.People) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasOne(x => x.Provider) + .WithMany(x => x.MetadataLinks) + .OnDelete(DeleteBehavior.Cascade); - 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().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().Property(x => x.Slug).IsRequired(); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() @@ -212,14 +191,21 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => new {x.LibraryID, x.ShowID}) - .IsUnique(); - modelBuilder.Entity() - .HasIndex(x => new {x.LibraryID, x.CollectionID}) + modelBuilder.Entity() + .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .IsUnique(); } + public T GetTemporaryObject(T model) + where T : class, IResource + { + T tmp = Set().Local.FirstOrDefault(x => x.ID == model.ID); + if (tmp != null) + return tmp; + Entry(model).State = EntityState.Unchanged; + return model; + } + public override int SaveChanges() { try @@ -266,7 +252,7 @@ namespace Kyoo } public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = new CancellationToken()) + CancellationToken cancellationToken = new()) { try { @@ -281,7 +267,7 @@ namespace Kyoo } } - public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { try { @@ -297,7 +283,7 @@ namespace Kyoo } public async Task SaveChangesAsync(string duplicateMessage, - CancellationToken cancellationToken = new CancellationToken()) + CancellationToken cancellationToken = new()) { try { @@ -312,7 +298,7 @@ namespace Kyoo } } - public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new CancellationToken()) + public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new()) { try { @@ -324,13 +310,39 @@ namespace Kyoo } } - public static bool IsDuplicateException(DbUpdateException ex) + public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) { - return ex.InnerException is PostgresException inner - && inner.SqlState == PostgresErrorCodes.UniqueViolation; + return SaveOrRetry(obj, onFail, 0, cancellationToken); + } + + public async Task SaveOrRetry(T obj, + Func onFail, + int recurse, + CancellationToken cancellationToken = new()) + { + try + { + await base.SaveChangesAsync(true, cancellationToken); + return obj; + } + catch (DbUpdateException ex) when (IsDuplicateException(ex)) + { + recurse++; + return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken); + } + catch (DbUpdateException) + { + DiscardChanges(); + throw; + } } - public void DiscardChanges() + private static bool IsDuplicateException(Exception ex) + { + return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; + } + + private void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)) diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs similarity index 79% rename from Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs index 02ebc8c3..5d406433 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.Designer.cs @@ -10,16 +10,16 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration { [DbContext(typeof(ConfigurationDbContext))] - [Migration("20200526235342_Initial")] + [Migration("20210306161631_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => { @@ -28,16 +28,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("Created") .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Enabled") .HasColumnType("boolean"); @@ -47,12 +51,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("NonEditable") .HasColumnType("boolean"); + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + b.Property("Updated") .HasColumnType("timestamp without time zone"); @@ -76,14 +83,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Type") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); b.HasIndex("ApiResourceId"); - b.ToTable("ApiClaims"); + b.ToTable("ApiResourceClaims"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => @@ -98,22 +105,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); b.HasIndex("ApiResourceId"); - b.ToTable("ApiProperties"); + b.ToTable("ApiResourceProperties"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -123,21 +130,80 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("ApiResourceId") .HasColumnType("integer"); + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Emphasize") .HasColumnType("boolean"); + b.Property("Enabled") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Required") .HasColumnType("boolean"); @@ -147,8 +213,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.HasKey("Id"); - b.HasIndex("ApiResourceId"); - b.HasIndex("Name") .IsUnique(); @@ -162,56 +226,46 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ApiScopeId") + b.Property("ScopeId") .HasColumnType("integer"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("ApiScopeId"); + b.HasIndex("ScopeId"); b.ToTable("ApiScopeClaims"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") + b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("ScopeId") + .HasColumnType("integer"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(4000)") - .HasMaxLength(4000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); - b.HasIndex("ApiResourceId"); + b.HasIndex("ScopeId"); - b.ToTable("ApiSecrets"); + b.ToTable("ApiScopeProperties"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => @@ -242,6 +296,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("AllowRememberConsent") .HasColumnType("boolean"); + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("AlwaysIncludeUserClaimsInIdToken") .HasColumnType("boolean"); @@ -255,25 +313,25 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("boolean"); b.Property("BackChannelLogoutUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("ClientClaimsPrefix") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("ConsentLifetime") .HasColumnType("integer"); @@ -282,8 +340,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DeviceCodeLifetime") .HasColumnType("integer"); @@ -298,8 +356,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("boolean"); b.Property("FrontChannelLogoutUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("IdentityTokenLifetime") .HasColumnType("integer"); @@ -311,20 +369,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("LogoUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("NonEditable") .HasColumnType("boolean"); b.Property("PairWiseSubjectSalt") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ProtocolType") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("RefreshTokenExpiration") .HasColumnType("integer"); @@ -341,6 +399,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("RequirePkce") .HasColumnType("boolean"); + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + b.Property("SlidingRefreshTokenLifetime") .HasColumnType("integer"); @@ -351,8 +412,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("UserCodeType") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("UserSsoLifetime") .HasColumnType("integer"); @@ -377,13 +438,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Type") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.HasKey("Id"); @@ -404,8 +465,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Origin") .IsRequired() - .HasColumnType("character varying(150)") - .HasMaxLength(150); + .HasMaxLength(150) + .HasColumnType("character varying(150)"); b.HasKey("Id"); @@ -426,8 +487,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("GrantType") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.HasKey("Id"); @@ -448,8 +509,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Provider") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); @@ -470,8 +531,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("PostLogoutRedirectUri") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -492,13 +553,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -519,8 +580,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("RedirectUri") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -541,8 +602,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Scope") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); @@ -565,21 +626,21 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("Expiration") .HasColumnType("timestamp without time zone"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(4000)") - .HasMaxLength(4000); + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); b.HasKey("Id"); @@ -588,28 +649,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.ToTable("ClientSecrets"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityClaims"); - }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => { b.Property("Id") @@ -621,12 +660,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Emphasize") .HasColumnType("boolean"); @@ -636,8 +675,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("NonEditable") .HasColumnType("boolean"); @@ -659,6 +698,28 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.ToTable("IdentityResources"); }); + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => { b.Property("Id") @@ -671,19 +732,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); b.HasIndex("IdentityResourceId"); - b.ToTable("IdentityProperties"); + b.ToTable("IdentityResourceProperties"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => @@ -693,6 +754,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => @@ -702,33 +765,52 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") .WithMany("Scopes") .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope") - .WithMany("UserClaims") - .HasForeignKey("ApiScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") .WithMany("Secrets") .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => @@ -738,6 +820,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => @@ -747,6 +831,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => @@ -756,6 +842,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => @@ -765,6 +853,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => @@ -774,6 +864,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => @@ -783,6 +875,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => @@ -792,6 +886,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => @@ -801,6 +897,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => @@ -810,15 +908,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") .WithMany("UserClaims") .HasForeignKey("IdentityResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("IdentityResource"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => @@ -828,6 +930,54 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("IdentityResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); }); #pragma warning restore 612, 618 } diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs similarity index 52% rename from Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.cs rename to Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs index 447f9385..fec7b0f0 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20200526235342_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/20210306161631_Initial.cs @@ -12,69 +12,92 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ApiResources", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(nullable: false), - Name = table.Column(maxLength: 200, nullable: false), - DisplayName = table.Column(maxLength: 200, nullable: true), - Description = table.Column(maxLength: 1000, nullable: true), - Created = table.Column(nullable: false), - Updated = table.Column(nullable: true), - LastAccessed = table.Column(nullable: true), - NonEditable = table.Column(nullable: false) + Enabled = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + AllowedAccessTokenSigningAlgorithms = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false), + Updated = table.Column(type: "timestamp without time zone", nullable: true), + LastAccessed = table.Column(type: "timestamp without time zone", nullable: true), + NonEditable = table.Column(type: "boolean", nullable: false) }, constraints: table => { table.PrimaryKey("PK_ApiResources", x => x.Id); }); + migrationBuilder.CreateTable( + name: "ApiScopes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Enabled = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Required = table.Column(type: "boolean", nullable: false), + Emphasize = table.Column(type: "boolean", nullable: false), + ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiScopes", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Clients", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(nullable: false), - ClientId = table.Column(maxLength: 200, nullable: false), - ProtocolType = table.Column(maxLength: 200, nullable: false), - RequireClientSecret = table.Column(nullable: false), - ClientName = table.Column(maxLength: 200, nullable: true), - Description = table.Column(maxLength: 1000, nullable: true), - ClientUri = table.Column(maxLength: 2000, nullable: true), - LogoUri = table.Column(maxLength: 2000, nullable: true), - RequireConsent = table.Column(nullable: false), - AllowRememberConsent = table.Column(nullable: false), - AlwaysIncludeUserClaimsInIdToken = table.Column(nullable: false), - RequirePkce = table.Column(nullable: false), - AllowPlainTextPkce = table.Column(nullable: false), - AllowAccessTokensViaBrowser = table.Column(nullable: false), - FrontChannelLogoutUri = table.Column(maxLength: 2000, nullable: true), - FrontChannelLogoutSessionRequired = table.Column(nullable: false), - BackChannelLogoutUri = table.Column(maxLength: 2000, nullable: true), - BackChannelLogoutSessionRequired = table.Column(nullable: false), - AllowOfflineAccess = table.Column(nullable: false), - IdentityTokenLifetime = table.Column(nullable: false), - AccessTokenLifetime = table.Column(nullable: false), - AuthorizationCodeLifetime = table.Column(nullable: false), - ConsentLifetime = table.Column(nullable: true), - AbsoluteRefreshTokenLifetime = table.Column(nullable: false), - SlidingRefreshTokenLifetime = table.Column(nullable: false), - RefreshTokenUsage = table.Column(nullable: false), - UpdateAccessTokenClaimsOnRefresh = table.Column(nullable: false), - RefreshTokenExpiration = table.Column(nullable: false), - AccessTokenType = table.Column(nullable: false), - EnableLocalLogin = table.Column(nullable: false), - IncludeJwtId = table.Column(nullable: false), - AlwaysSendClientClaims = table.Column(nullable: false), - ClientClaimsPrefix = table.Column(maxLength: 200, nullable: true), - PairWiseSubjectSalt = table.Column(maxLength: 200, nullable: true), - Created = table.Column(nullable: false), - Updated = table.Column(nullable: true), - LastAccessed = table.Column(nullable: true), - UserSsoLifetime = table.Column(nullable: true), - UserCodeType = table.Column(maxLength: 100, nullable: true), - DeviceCodeLifetime = table.Column(nullable: false), - NonEditable = table.Column(nullable: false) + Enabled = table.Column(type: "boolean", nullable: false), + ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + ProtocolType = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + RequireClientSecret = table.Column(type: "boolean", nullable: false), + ClientName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + ClientUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + LogoUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + RequireConsent = table.Column(type: "boolean", nullable: false), + AllowRememberConsent = table.Column(type: "boolean", nullable: false), + AlwaysIncludeUserClaimsInIdToken = table.Column(type: "boolean", nullable: false), + RequirePkce = table.Column(type: "boolean", nullable: false), + AllowPlainTextPkce = table.Column(type: "boolean", nullable: false), + RequireRequestObject = table.Column(type: "boolean", nullable: false), + AllowAccessTokensViaBrowser = table.Column(type: "boolean", nullable: false), + FrontChannelLogoutUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + FrontChannelLogoutSessionRequired = table.Column(type: "boolean", nullable: false), + BackChannelLogoutUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + BackChannelLogoutSessionRequired = table.Column(type: "boolean", nullable: false), + AllowOfflineAccess = table.Column(type: "boolean", nullable: false), + IdentityTokenLifetime = table.Column(type: "integer", nullable: false), + AllowedIdentityTokenSigningAlgorithms = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + AccessTokenLifetime = table.Column(type: "integer", nullable: false), + AuthorizationCodeLifetime = table.Column(type: "integer", nullable: false), + ConsentLifetime = table.Column(type: "integer", nullable: true), + AbsoluteRefreshTokenLifetime = table.Column(type: "integer", nullable: false), + SlidingRefreshTokenLifetime = table.Column(type: "integer", nullable: false), + RefreshTokenUsage = table.Column(type: "integer", nullable: false), + UpdateAccessTokenClaimsOnRefresh = table.Column(type: "boolean", nullable: false), + RefreshTokenExpiration = table.Column(type: "integer", nullable: false), + AccessTokenType = table.Column(type: "integer", nullable: false), + EnableLocalLogin = table.Column(type: "boolean", nullable: false), + IncludeJwtId = table.Column(type: "boolean", nullable: false), + AlwaysSendClientClaims = table.Column(type: "boolean", nullable: false), + ClientClaimsPrefix = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + PairWiseSubjectSalt = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Created = table.Column(type: "timestamp without time zone", nullable: false), + Updated = table.Column(type: "timestamp without time zone", nullable: true), + LastAccessed = table.Column(type: "timestamp without time zone", nullable: true), + UserSsoLifetime = table.Column(type: "integer", nullable: true), + UserCodeType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + DeviceCodeLifetime = table.Column(type: "integer", nullable: false), + NonEditable = table.Column(type: "boolean", nullable: false) }, constraints: table => { @@ -85,18 +108,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "IdentityResources", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Enabled = table.Column(nullable: false), - Name = table.Column(maxLength: 200, nullable: false), - DisplayName = table.Column(maxLength: 200, nullable: true), - Description = table.Column(maxLength: 1000, nullable: true), - Required = table.Column(nullable: false), - Emphasize = table.Column(nullable: false), - ShowInDiscoveryDocument = table.Column(nullable: false), - Created = table.Column(nullable: false), - Updated = table.Column(nullable: true), - NonEditable = table.Column(nullable: false) + Enabled = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Required = table.Column(type: "boolean", nullable: false), + Emphasize = table.Column(type: "boolean", nullable: false), + ShowInDiscoveryDocument = table.Column(type: "boolean", nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false), + Updated = table.Column(type: "timestamp without time zone", nullable: true), + NonEditable = table.Column(type: "boolean", nullable: false) }, constraints: table => { @@ -104,19 +127,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "ApiClaims", + name: "ApiResourceClaims", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Type = table.Column(maxLength: 200, nullable: false), - ApiResourceId = table.Column(nullable: false) + ApiResourceId = table.Column(type: "integer", nullable: false), + Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_ApiClaims", x => x.Id); + table.PrimaryKey("PK_ApiResourceClaims", x => x.Id); table.ForeignKey( - name: "FK_ApiClaims_ApiResources_ApiResourceId", + name: "FK_ApiResourceClaims_ApiResources_ApiResourceId", column: x => x.ApiResourceId, principalTable: "ApiResources", principalColumn: "Id", @@ -124,20 +147,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "ApiProperties", + name: "ApiResourceProperties", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Key = table.Column(maxLength: 250, nullable: false), - Value = table.Column(maxLength: 2000, nullable: false), - ApiResourceId = table.Column(nullable: false) + ApiResourceId = table.Column(type: "integer", nullable: false), + Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_ApiProperties", x => x.Id); + table.PrimaryKey("PK_ApiResourceProperties", x => x.Id); table.ForeignKey( - name: "FK_ApiProperties_ApiResources_ApiResourceId", + name: "FK_ApiResourceProperties_ApiResources_ApiResourceId", column: x => x.ApiResourceId, principalTable: "ApiResources", principalColumn: "Id", @@ -145,24 +168,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "ApiScopes", + name: "ApiResourceScopes", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(maxLength: 200, nullable: false), - DisplayName = table.Column(maxLength: 200, nullable: true), - Description = table.Column(maxLength: 1000, nullable: true), - Required = table.Column(nullable: false), - Emphasize = table.Column(nullable: false), - ShowInDiscoveryDocument = table.Column(nullable: false), - ApiResourceId = table.Column(nullable: false) + Scope = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + ApiResourceId = table.Column(type: "integer", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_ApiScopes", x => x.Id); + table.PrimaryKey("PK_ApiResourceScopes", x => x.Id); table.ForeignKey( - name: "FK_ApiScopes_ApiResources_ApiResourceId", + name: "FK_ApiResourceScopes_ApiResources_ApiResourceId", column: x => x.ApiResourceId, principalTable: "ApiResources", principalColumn: "Id", @@ -170,38 +188,79 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "ApiSecrets", + name: "ApiResourceSecrets", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Description = table.Column(maxLength: 1000, nullable: true), - Value = table.Column(maxLength: 4000, nullable: false), - Expiration = table.Column(nullable: true), - Type = table.Column(maxLength: 250, nullable: false), - Created = table.Column(nullable: false), - ApiResourceId = table.Column(nullable: false) + ApiResourceId = table.Column(type: "integer", nullable: false), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Value = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), + Expiration = table.Column(type: "timestamp without time zone", nullable: true), + Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_ApiSecrets", x => x.Id); + table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id); table.ForeignKey( - name: "FK_ApiSecrets_ApiResources_ApiResourceId", + name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId", column: x => x.ApiResourceId, principalTable: "ApiResources", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "ApiScopeClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ScopeId = table.Column(type: "integer", nullable: false), + Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiScopeClaims", x => x.Id); + table.ForeignKey( + name: "FK_ApiScopeClaims_ApiScopes_ScopeId", + column: x => x.ScopeId, + principalTable: "ApiScopes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ApiScopeProperties", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ScopeId = table.Column(type: "integer", nullable: false), + Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiScopeProperties", x => x.Id); + table.ForeignKey( + name: "FK_ApiScopeProperties_ApiScopes_ScopeId", + column: x => x.ScopeId, + principalTable: "ApiScopes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "ClientClaims", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Type = table.Column(maxLength: 250, nullable: false), - Value = table.Column(maxLength: 250, nullable: false), - ClientId = table.Column(nullable: false) + Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Value = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -218,10 +277,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientCorsOrigins", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Origin = table.Column(maxLength: 150, nullable: false), - ClientId = table.Column(nullable: false) + Origin = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -238,10 +297,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientGrantTypes", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - GrantType = table.Column(maxLength: 250, nullable: false), - ClientId = table.Column(nullable: false) + GrantType = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -258,10 +317,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientIdPRestrictions", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Provider = table.Column(maxLength: 200, nullable: false), - ClientId = table.Column(nullable: false) + Provider = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -278,10 +337,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientPostLogoutRedirectUris", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - PostLogoutRedirectUri = table.Column(maxLength: 2000, nullable: false), - ClientId = table.Column(nullable: false) + PostLogoutRedirectUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -298,11 +357,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientProperties", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Key = table.Column(maxLength: 250, nullable: false), - Value = table.Column(maxLength: 2000, nullable: false), - ClientId = table.Column(nullable: false) + ClientId = table.Column(type: "integer", nullable: false), + Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) }, constraints: table => { @@ -319,10 +378,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientRedirectUris", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RedirectUri = table.Column(maxLength: 2000, nullable: false), - ClientId = table.Column(nullable: false) + RedirectUri = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -339,10 +398,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientScopes", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Scope = table.Column(maxLength: 200, nullable: false), - ClientId = table.Column(nullable: false) + Scope = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + ClientId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -359,14 +418,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientSecrets", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Description = table.Column(maxLength: 2000, nullable: true), - Value = table.Column(maxLength: 4000, nullable: false), - Expiration = table.Column(nullable: true), - Type = table.Column(maxLength: 250, nullable: false), - Created = table.Column(nullable: false), - ClientId = table.Column(nullable: false) + ClientId = table.Column(type: "integer", nullable: false), + Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + Value = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), + Expiration = table.Column(type: "timestamp without time zone", nullable: true), + Type = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false) }, constraints: table => { @@ -380,19 +439,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "IdentityClaims", + name: "IdentityResourceClaims", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Type = table.Column(maxLength: 200, nullable: false), - IdentityResourceId = table.Column(nullable: false) + IdentityResourceId = table.Column(type: "integer", nullable: false), + Type = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_IdentityClaims", x => x.Id); + table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id); table.ForeignKey( - name: "FK_IdentityClaims_IdentityResources_IdentityResourceId", + name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId", column: x => x.IdentityResourceId, principalTable: "IdentityResources", principalColumn: "Id", @@ -400,54 +459,34 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration }); migrationBuilder.CreateTable( - name: "IdentityProperties", + name: "IdentityResourceProperties", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Key = table.Column(maxLength: 250, nullable: false), - Value = table.Column(maxLength: 2000, nullable: false), - IdentityResourceId = table.Column(nullable: false) + IdentityResourceId = table.Column(type: "integer", nullable: false), + Key = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), + Value = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_IdentityProperties", x => x.Id); + table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id); table.ForeignKey( - name: "FK_IdentityProperties_IdentityResources_IdentityResourceId", + name: "FK_IdentityResourceProperties_IdentityResources_IdentityResour~", column: x => x.IdentityResourceId, principalTable: "IdentityResources", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "ApiScopeClaims", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Type = table.Column(maxLength: 200, nullable: false), - ApiScopeId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiScopeClaims", x => x.Id); - table.ForeignKey( - name: "FK_ApiScopeClaims_ApiScopes_ApiScopeId", - column: x => x.ApiScopeId, - principalTable: "ApiScopes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - migrationBuilder.CreateIndex( - name: "IX_ApiClaims_ApiResourceId", - table: "ApiClaims", + name: "IX_ApiResourceClaims_ApiResourceId", + table: "ApiResourceClaims", column: "ApiResourceId"); migrationBuilder.CreateIndex( - name: "IX_ApiProperties_ApiResourceId", - table: "ApiProperties", + name: "IX_ApiResourceProperties_ApiResourceId", + table: "ApiResourceProperties", column: "ApiResourceId"); migrationBuilder.CreateIndex( @@ -457,26 +496,31 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration unique: true); migrationBuilder.CreateIndex( - name: "IX_ApiScopeClaims_ApiScopeId", - table: "ApiScopeClaims", - column: "ApiScopeId"); + name: "IX_ApiResourceScopes_ApiResourceId", + table: "ApiResourceScopes", + column: "ApiResourceId"); migrationBuilder.CreateIndex( - name: "IX_ApiScopes_ApiResourceId", - table: "ApiScopes", + name: "IX_ApiResourceSecrets_ApiResourceId", + table: "ApiResourceSecrets", column: "ApiResourceId"); + migrationBuilder.CreateIndex( + name: "IX_ApiScopeClaims_ScopeId", + table: "ApiScopeClaims", + column: "ScopeId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiScopeProperties_ScopeId", + table: "ApiScopeProperties", + column: "ScopeId"); + migrationBuilder.CreateIndex( name: "IX_ApiScopes_Name", table: "ApiScopes", column: "Name", unique: true); - migrationBuilder.CreateIndex( - name: "IX_ApiSecrets_ApiResourceId", - table: "ApiSecrets", - column: "ApiResourceId"); - migrationBuilder.CreateIndex( name: "IX_ClientClaims_ClientId", table: "ClientClaims", @@ -529,13 +573,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration column: "ClientId"); migrationBuilder.CreateIndex( - name: "IX_IdentityClaims_IdentityResourceId", - table: "IdentityClaims", + name: "IX_IdentityResourceClaims_IdentityResourceId", + table: "IdentityResourceClaims", column: "IdentityResourceId"); migrationBuilder.CreateIndex( - name: "IX_IdentityProperties_IdentityResourceId", - table: "IdentityProperties", + name: "IX_IdentityResourceProperties_IdentityResourceId", + table: "IdentityResourceProperties", column: "IdentityResourceId"); migrationBuilder.CreateIndex( @@ -548,16 +592,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "ApiClaims"); + name: "ApiResourceClaims"); migrationBuilder.DropTable( - name: "ApiProperties"); + name: "ApiResourceProperties"); + + migrationBuilder.DropTable( + name: "ApiResourceScopes"); + + migrationBuilder.DropTable( + name: "ApiResourceSecrets"); migrationBuilder.DropTable( name: "ApiScopeClaims"); migrationBuilder.DropTable( - name: "ApiSecrets"); + name: "ApiScopeProperties"); migrationBuilder.DropTable( name: "ClientClaims"); @@ -587,10 +637,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration name: "ClientSecrets"); migrationBuilder.DropTable( - name: "IdentityClaims"); + name: "IdentityResourceClaims"); migrationBuilder.DropTable( - name: "IdentityProperties"); + name: "IdentityResourceProperties"); + + migrationBuilder.DropTable( + name: "ApiResources"); migrationBuilder.DropTable( name: "ApiScopes"); @@ -600,9 +653,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration migrationBuilder.DropTable( name: "IdentityResources"); - - migrationBuilder.DropTable( - name: "ApiResources"); } } } diff --git a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs index f83d80e5..c11ac33a 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityConfiguration/ConfigurationDbContextModelSnapshot.cs @@ -15,9 +15,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => { @@ -26,16 +26,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("Created") .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Enabled") .HasColumnType("boolean"); @@ -45,12 +49,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("NonEditable") .HasColumnType("boolean"); + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + b.Property("Updated") .HasColumnType("timestamp without time zone"); @@ -74,14 +81,14 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Type") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); b.HasIndex("ApiResourceId"); - b.ToTable("ApiClaims"); + b.ToTable("ApiResourceClaims"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => @@ -96,22 +103,22 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); b.HasIndex("ApiResourceId"); - b.ToTable("ApiProperties"); + b.ToTable("ApiResourceProperties"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -121,21 +128,80 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("ApiResourceId") .HasColumnType("integer"); + b.Property("Scope") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Emphasize") .HasColumnType("boolean"); + b.Property("Enabled") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Required") .HasColumnType("boolean"); @@ -145,8 +211,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.HasKey("Id"); - b.HasIndex("ApiResourceId"); - b.HasIndex("Name") .IsUnique(); @@ -160,56 +224,46 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ApiScopeId") + b.Property("ScopeId") .HasColumnType("integer"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("ApiScopeId"); + b.HasIndex("ScopeId"); b.ToTable("ApiScopeClaims"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ApiResourceId") - .HasColumnType("integer"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); - - b.Property("Expiration") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") + b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("ScopeId") + .HasColumnType("integer"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(4000)") - .HasMaxLength(4000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); - b.HasIndex("ApiResourceId"); + b.HasIndex("ScopeId"); - b.ToTable("ApiSecrets"); + b.ToTable("ApiScopeProperties"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => @@ -240,6 +294,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("AllowRememberConsent") .HasColumnType("boolean"); + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("AlwaysIncludeUserClaimsInIdToken") .HasColumnType("boolean"); @@ -253,25 +311,25 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("boolean"); b.Property("BackChannelLogoutUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("ClientClaimsPrefix") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("ConsentLifetime") .HasColumnType("integer"); @@ -280,8 +338,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DeviceCodeLifetime") .HasColumnType("integer"); @@ -296,8 +354,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("boolean"); b.Property("FrontChannelLogoutUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("IdentityTokenLifetime") .HasColumnType("integer"); @@ -309,20 +367,20 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("LogoUri") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("NonEditable") .HasColumnType("boolean"); b.Property("PairWiseSubjectSalt") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ProtocolType") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("RefreshTokenExpiration") .HasColumnType("integer"); @@ -339,6 +397,9 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("RequirePkce") .HasColumnType("boolean"); + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + b.Property("SlidingRefreshTokenLifetime") .HasColumnType("integer"); @@ -349,8 +410,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("UserCodeType") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("UserSsoLifetime") .HasColumnType("integer"); @@ -375,13 +436,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Type") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.HasKey("Id"); @@ -402,8 +463,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Origin") .IsRequired() - .HasColumnType("character varying(150)") - .HasMaxLength(150); + .HasMaxLength(150) + .HasColumnType("character varying(150)"); b.HasKey("Id"); @@ -424,8 +485,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("GrantType") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.HasKey("Id"); @@ -446,8 +507,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Provider") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); @@ -468,8 +529,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("PostLogoutRedirectUri") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -490,13 +551,13 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -517,8 +578,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("RedirectUri") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); @@ -539,8 +600,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Scope") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("Id"); @@ -563,21 +624,21 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.Property("Expiration") .HasColumnType("timestamp without time zone"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(4000)") - .HasMaxLength(4000); + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); b.HasKey("Id"); @@ -586,28 +647,6 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.ToTable("ClientSecrets"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("IdentityResourceId") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.HasIndex("IdentityResourceId"); - - b.ToTable("IdentityClaims"); - }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => { b.Property("Id") @@ -619,12 +658,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasColumnType("timestamp without time zone"); b.Property("Description") - .HasColumnType("character varying(1000)") - .HasMaxLength(1000); + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); b.Property("DisplayName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Emphasize") .HasColumnType("boolean"); @@ -634,8 +673,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Name") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("NonEditable") .HasColumnType("boolean"); @@ -657,6 +696,28 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.ToTable("IdentityResources"); }); + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => { b.Property("Id") @@ -669,19 +730,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration b.Property("Key") .IsRequired() - .HasColumnType("character varying(250)") - .HasMaxLength(250); + .HasMaxLength(250) + .HasColumnType("character varying(250)"); b.Property("Value") .IsRequired() - .HasColumnType("character varying(2000)") - .HasMaxLength(2000); + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); b.HasKey("Id"); b.HasIndex("IdentityResourceId"); - b.ToTable("IdentityProperties"); + b.ToTable("IdentityResourceProperties"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => @@ -691,6 +752,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceProperty", b => @@ -700,33 +763,52 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceScope", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") .WithMany("Scopes") .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => - { - b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope") - .WithMany("UserClaims") - .HasForeignKey("ApiScopeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceSecret", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") .WithMany("Secrets") .HasForeignKey("ApiResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "Scope") + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scope"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => @@ -736,6 +818,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => @@ -745,6 +829,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => @@ -754,6 +840,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => @@ -763,6 +851,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => @@ -772,6 +862,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientProperty", b => @@ -781,6 +873,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => @@ -790,6 +884,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => @@ -799,6 +895,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => @@ -808,15 +906,19 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Client"); }); - modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceClaim", b => { b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") .WithMany("UserClaims") .HasForeignKey("IdentityResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("IdentityResource"); }); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResourceProperty", b => @@ -826,6 +928,54 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityConfiguration .HasForeignKey("IdentityResourceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); }); #pragma warning restore 612, 618 } diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs similarity index 84% rename from Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs index ced0460b..10626a22 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.Designer.cs @@ -7,51 +7,59 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase +namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase { [DbContext(typeof(IdentityDatabase))] - [Migration("20200526235424_Initial")] + [Migration("20210216205030_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => { b.Property("UserCode") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("CreationTime") .HasColumnType("timestamp without time zone"); b.Property("Data") .IsRequired() - .HasColumnType("character varying(50000)") - .HasMaxLength(50000); + .HasMaxLength(50000) + .HasColumnType("character varying(50000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("DeviceCode") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Expiration") .IsRequired() .HasColumnType("timestamp without time zone"); + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("SubjectId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("UserCode"); @@ -66,33 +74,44 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => { b.Property("Key") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp without time zone"); b.Property("CreationTime") .HasColumnType("timestamp without time zone"); b.Property("Data") .IsRequired() - .HasColumnType("character varying(50000)") - .HasMaxLength(50000); + .HasMaxLength(50000) + .HasColumnType("character varying(50000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Expiration") .HasColumnType("timestamp without time zone"); + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("SubjectId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(50)") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("character varying(50)"); b.HasKey("Key"); @@ -100,6 +119,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase b.HasIndex("SubjectId", "ClientId", "Type"); + b.HasIndex("SubjectId", "SessionId", "Type"); + b.ToTable("PersistedGrants"); }); @@ -116,8 +137,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("Email") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("EmailConfirmed") .HasColumnType("boolean"); @@ -129,12 +150,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("timestamp with time zone"); b.Property("NormalizedEmail") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("NormalizedUserName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("OTAC") .HasColumnType("text"); @@ -158,17 +179,17 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("boolean"); b.Property("UserName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); + .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() - .HasName("UserNameIndex"); + .HasDatabaseName("UserNameIndex"); b.ToTable("User"); }); @@ -183,18 +204,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("Name") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("NormalizedName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() - .HasName("RoleNameIndex"); + .HasDatabaseName("RoleNameIndex"); b.ToTable("UserRoles"); }); @@ -250,12 +271,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("ProviderKey") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("ProviderDisplayName") .HasColumnType("text"); @@ -292,12 +313,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("LoginProvider") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Name") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Value") .HasColumnType("text"); diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs similarity index 56% rename from Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.cs rename to Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs index 15abc8d3..e0cffa4f 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20200526235424_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/20210216205030_Initial.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase +namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase { public partial class Initial : Migration { @@ -12,13 +12,15 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "DeviceCodes", columns: table => new { - UserCode = table.Column(maxLength: 200, nullable: false), - DeviceCode = table.Column(maxLength: 200, nullable: false), - SubjectId = table.Column(maxLength: 200, nullable: true), - ClientId = table.Column(maxLength: 200, nullable: false), - CreationTime = table.Column(nullable: false), - Expiration = table.Column(nullable: false), - Data = table.Column(maxLength: 50000, nullable: false) + UserCode = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DeviceCode = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + SubjectId = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + Expiration = table.Column(type: "timestamp without time zone", nullable: false), + Data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) }, constraints: table => { @@ -29,13 +31,16 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "PersistedGrants", columns: table => new { - Key = table.Column(maxLength: 200, nullable: false), - Type = table.Column(maxLength: 50, nullable: false), - SubjectId = table.Column(maxLength: 200, nullable: true), - ClientId = table.Column(maxLength: 200, nullable: false), - CreationTime = table.Column(nullable: false), - Expiration = table.Column(nullable: true), - Data = table.Column(maxLength: 50000, nullable: false) + Key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + SubjectId = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + Expiration = table.Column(type: "timestamp without time zone", nullable: true), + ConsumedTime = table.Column(type: "timestamp without time zone", nullable: true), + Data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) }, constraints: table => { @@ -46,23 +51,23 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "User", columns: table => new { - Id = table.Column(nullable: false), - UserName = table.Column(maxLength: 256, nullable: true), - NormalizedUserName = table.Column(maxLength: 256, nullable: true), - Email = table.Column(maxLength: 256, nullable: true), - NormalizedEmail = table.Column(maxLength: 256, nullable: true), - EmailConfirmed = table.Column(nullable: false), - PasswordHash = table.Column(nullable: true), - SecurityStamp = table.Column(nullable: true), - ConcurrencyStamp = table.Column(nullable: true), - PhoneNumber = table.Column(nullable: true), - PhoneNumberConfirmed = table.Column(nullable: false), - TwoFactorEnabled = table.Column(nullable: false), - LockoutEnd = table.Column(nullable: true), - LockoutEnabled = table.Column(nullable: false), - AccessFailedCount = table.Column(nullable: false), - OTAC = table.Column(nullable: true), - OTACExpires = table.Column(nullable: true) + Id = table.Column(type: "text", nullable: false), + OTAC = table.Column(type: "text", nullable: true), + OTACExpires = table.Column(type: "timestamp without time zone", nullable: true), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -73,10 +78,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "UserRoles", columns: table => new { - Id = table.Column(nullable: false), - Name = table.Column(maxLength: 256, nullable: true), - NormalizedName = table.Column(maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(nullable: true) + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -87,11 +92,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "UserClaim", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(nullable: false), - ClaimType = table.Column(nullable: true), - ClaimValue = table.Column(nullable: true) + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -108,10 +113,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "UserLogin", columns: table => new { - LoginProvider = table.Column(maxLength: 128, nullable: false), - ProviderKey = table.Column(maxLength: 128, nullable: false), - ProviderDisplayName = table.Column(nullable: true), - UserId = table.Column(nullable: false) + LoginProvider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ProviderKey = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) }, constraints: table => { @@ -128,10 +133,10 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "UserToken", columns: table => new { - UserId = table.Column(nullable: false), - LoginProvider = table.Column(maxLength: 128, nullable: false), - Name = table.Column(maxLength: 128, nullable: false), - Value = table.Column(nullable: true) + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Value = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -148,35 +153,35 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase name: "UserRole", columns: table => new { - UserId = table.Column(nullable: false), - RoleId = table.Column(nullable: false) + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) }, constraints: table => { table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_UserRole_UserRoles_RoleId", - column: x => x.RoleId, - principalTable: "UserRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_UserRole_User_UserId", column: x => x.UserId, principalTable: "User", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRole_UserRoles_RoleId", + column: x => x.RoleId, + principalTable: "UserRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "UserRoleClaim", columns: table => new { - Id = table.Column(nullable: false) + Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RoleId = table.Column(nullable: false), - ClaimType = table.Column(nullable: true), - ClaimValue = table.Column(nullable: true) + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -210,6 +215,11 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase table: "PersistedGrants", columns: new[] { "SubjectId", "ClientId", "Type" }); + migrationBuilder.CreateIndex( + name: "IX_PersistedGrants_SubjectId_SessionId_Type", + table: "PersistedGrants", + columns: new[] { "SubjectId", "SessionId", "Type" }); + migrationBuilder.CreateIndex( name: "EmailIndex", table: "User", diff --git a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs index 995c15db..fa2da994 100644 --- a/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/IdentityDatbase/IdentityDatabaseModelSnapshot.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase +namespace Kyoo.Kyoo.Models.DatabaseMigrations.IdentityDatbase { [DbContext(typeof(IdentityDatabase))] partial class IdentityDatabaseModelSnapshot : ModelSnapshot @@ -15,41 +15,49 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => { b.Property("UserCode") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("CreationTime") .HasColumnType("timestamp without time zone"); b.Property("Data") .IsRequired() - .HasColumnType("character varying(50000)") - .HasMaxLength(50000); + .HasMaxLength(50000) + .HasColumnType("character varying(50000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("DeviceCode") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Expiration") .IsRequired() .HasColumnType("timestamp without time zone"); + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("SubjectId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("UserCode"); @@ -64,33 +72,44 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => { b.Property("Key") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ClientId") .IsRequired() - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp without time zone"); b.Property("CreationTime") .HasColumnType("timestamp without time zone"); b.Property("Data") .IsRequired() - .HasColumnType("character varying(50000)") - .HasMaxLength(50000); + .HasMaxLength(50000) + .HasColumnType("character varying(50000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Expiration") .HasColumnType("timestamp without time zone"); + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("SubjectId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Type") .IsRequired() - .HasColumnType("character varying(50)") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("character varying(50)"); b.HasKey("Key"); @@ -98,6 +117,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase b.HasIndex("SubjectId", "ClientId", "Type"); + b.HasIndex("SubjectId", "SessionId", "Type"); + b.ToTable("PersistedGrants"); }); @@ -114,8 +135,8 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("Email") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("EmailConfirmed") .HasColumnType("boolean"); @@ -127,12 +148,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("timestamp with time zone"); b.Property("NormalizedEmail") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("NormalizedUserName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("OTAC") .HasColumnType("text"); @@ -156,17 +177,17 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("boolean"); b.Property("UserName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); + .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() - .HasName("UserNameIndex"); + .HasDatabaseName("UserNameIndex"); b.ToTable("User"); }); @@ -181,18 +202,18 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("Name") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("NormalizedName") - .HasColumnType("character varying(256)") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() - .HasName("RoleNameIndex"); + .HasDatabaseName("RoleNameIndex"); b.ToTable("UserRoles"); }); @@ -248,12 +269,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("ProviderKey") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("ProviderDisplayName") .HasColumnType("text"); @@ -290,12 +311,12 @@ namespace Kyoo.Models.DatabaseMigrations.IdentityDatbase .HasColumnType("text"); b.Property("LoginProvider") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Name") - .HasColumnType("character varying(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Value") .HasColumnType("text"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs similarity index 69% rename from Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs index e9e02cef..b713dc6b 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.Designer.cs @@ -1,6 +1,5 @@ // using System; -using System.Collections.Generic; using Kyoo; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -11,21 +10,21 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Models.DatabaseMigrations.Internal { [DbContext(typeof(DatabaseContext))] - [Migration("20210128212212_Initial")] + [Migration("20210317220956_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") - .HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") - .HasAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,font") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) + .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) + .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "font" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - modelBuilder.Entity("Kyoo.Models.CollectionDE", b => + modelBuilder.Entity("Kyoo.Models.Collection", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -51,23 +50,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Collections"); - - b.HasDiscriminator(); - }); - - modelBuilder.Entity("Kyoo.Models.CollectionLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("CollectionLinks"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -89,9 +71,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Path") .HasColumnType("text"); - b.Property("Poster") - .HasColumnType("text"); - b.Property("ReleaseDate") .HasColumnType("timestamp without time zone"); @@ -107,6 +86,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("ShowID") .HasColumnType("integer"); + b.Property("Thumb") + .HasColumnType("text"); + b.Property("Title") .HasColumnType("text"); @@ -120,7 +102,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Episodes"); }); - modelBuilder.Entity("Kyoo.Models.GenreDE", b => + modelBuilder.Entity("Kyoo.Models.Genre", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -140,26 +122,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Genres"); - - b.HasDiscriminator(); }); - modelBuilder.Entity("Kyoo.Models.GenreLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("GenreLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.LibraryDE", b => + modelBuilder.Entity("Kyoo.Models.Library", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -169,7 +134,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); - b.Property>("Paths") + b.Property("Paths") .HasColumnType("text[]"); b.Property("Slug") @@ -182,39 +147,81 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Libraries"); - - b.HasDiscriminator(); }); - modelBuilder.Entity("Kyoo.Models.LibraryLink", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("CollectionID") + b.Property("FirstID") .HasColumnType("integer"); - b.Property("LibraryID") + b.Property("SecondID") .HasColumnType("integer"); - b.Property("ShowID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.HasKey("ID"); + b.Property("SecondID") + .HasColumnType("integer"); - b.HasIndex("CollectionID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ShowID"); + b.HasIndex("SecondID"); - b.HasIndex("LibraryID", "CollectionID") - .IsUnique(); + b.ToTable("Link"); + }); - b.HasIndex("LibraryID", "ShowID") - .IsUnique(); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); - b.ToTable("LibraryLinks"); + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + 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.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 => @@ -338,21 +345,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Providers"); }); - modelBuilder.Entity("Kyoo.Models.ProviderLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("ProviderLinks"); - }); - modelBuilder.Entity("Kyoo.Models.Season", b => { b.Property("ID") @@ -386,14 +378,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Seasons"); }); - modelBuilder.Entity("Kyoo.Models.ShowDE", b => + modelBuilder.Entity("Kyoo.Models.Show", b => { b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property>("Aliases") + b.Property("Aliases") .HasColumnType("text[]"); b.Property("Backdrop") @@ -444,8 +436,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("StudioID"); b.ToTable("Shows"); - - b.HasDiscriminator(); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -501,76 +491,130 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Title") .HasColumnType("text"); + b.Property("TrackIndex") + .HasColumnType("integer"); + b.Property("Type") .HasColumnType("integer"); b.HasKey("ID"); - b.HasIndex("EpisodeID"); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); b.ToTable("Tracks"); }); - modelBuilder.Entity("Kyoo.Models.CollectionLink", b => - { - b.HasOne("Kyoo.Models.ShowDE", "Child") - .WithMany("CollectionLinks") - .HasForeignKey("ChildID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.CollectionDE", "Parent") - .WithMany("Links") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Kyoo.Models.Episode", b => { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") .HasForeignKey("SeasonID"); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); }); - modelBuilder.Entity("Kyoo.Models.GenreLink", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { - b.HasOne("Kyoo.Models.GenreDE", "Child") - .WithMany("Links") - .HasForeignKey("ChildID") + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Parent") + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.ProviderID", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Kyoo.Models.LibraryLink", b => - { - b.HasOne("Kyoo.Models.CollectionDE", "Collection") - .WithMany("LibraryLinks") - .HasForeignKey("CollectionID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.LibraryDE", "Library") - .WithMany("Links") - .HasForeignKey("LibraryID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Show") - .WithMany("LibraryLinks") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => @@ -586,7 +630,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.ProviderID", "Provider") - .WithMany() + .WithMany("MetadataLinks") .HasForeignKey("ProviderID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -596,10 +640,20 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasForeignKey("SeasonID") .OnDelete(DeleteBehavior.Cascade); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("ExternalIDs") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -610,42 +664,35 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - }); - modelBuilder.Entity("Kyoo.Models.ProviderLink", b => - { - b.HasOne("Kyoo.Models.ProviderID", "Child") - .WithMany() - .HasForeignKey("ChildID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.Navigation("People"); - b.HasOne("Kyoo.Models.LibraryDE", "Parent") - .WithMany("ProviderLinks") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.Navigation("Show"); }); modelBuilder.Entity("Kyoo.Models.Season", b => { - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Show"); }); - modelBuilder.Entity("Kyoo.Models.ShowDE", b => + modelBuilder.Entity("Kyoo.Models.Show", b => { b.HasOne("Kyoo.Models.Studio", "Studio") - .WithMany() + .WithMany("Shows") .HasForeignKey("StudioID"); + + b.Navigation("Studio"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -655,6 +702,79 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasForeignKey("EpisodeID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.ProviderID", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); }); #pragma warning restore 612, 618 } diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs similarity index 61% rename from Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs index 04349e48..b464f251 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210128212212_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210317220956_Initial.cs @@ -17,12 +17,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Collections", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true), - Poster = table.Column(nullable: true), - Overview = table.Column(nullable: true) + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Poster = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -33,10 +33,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Genres", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true) + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -47,10 +47,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Libraries", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true), + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), Paths = table.Column(type: "text[]", nullable: true) }, constraints: table => @@ -62,11 +62,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "People", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true), - Poster = table.Column(nullable: true) + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Poster = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -77,11 +77,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Providers", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true), - Logo = table.Column(nullable: true) + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + Logo = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -92,10 +92,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Studios", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Name = table.Column(nullable: true) + Slug = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -103,49 +103,73 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }); migrationBuilder.CreateTable( - name: "ProviderLinks", + name: "Link", columns: table => new { - ParentID = table.Column(nullable: false), - ChildID = table.Column(nullable: false) + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_ProviderLinks", x => new { x.ParentID, x.ChildID }); + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_ProviderLinks_Providers_ChildID", - column: x => x.ChildID, - principalTable: "Providers", + name: "FK_Link_Collections_SecondID", + column: x => x.SecondID, + principalTable: "Collections", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_ProviderLinks_Libraries_ParentID", - column: x => x.ParentID, + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, principalTable: "Libraries", principalColumn: "ID", 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_Libraries_FirstID", + column: x => x.FirstID, + principalTable: "Libraries", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Shows", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(nullable: false), - Title = table.Column(nullable: true), + Slug = table.Column(type: "text", nullable: false), + Title = table.Column(type: "text", nullable: true), Aliases = table.Column(type: "text[]", nullable: true), - Path = table.Column(nullable: true), - Overview = table.Column(nullable: true), - Status = table.Column(nullable: true), - TrailerUrl = table.Column(nullable: true), - StartYear = table.Column(nullable: true), - EndYear = table.Column(nullable: true), - Poster = table.Column(nullable: true), - Logo = table.Column(nullable: true), - Backdrop = table.Column(nullable: true), - IsMovie = table.Column(nullable: false), - StudioID = table.Column(nullable: true) + Path = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: true), + TrailerUrl = table.Column(type: "text", nullable: true), + StartYear = table.Column(type: "integer", nullable: true), + EndYear = table.Column(type: "integer", nullable: true), + Poster = table.Column(type: "text", nullable: true), + Logo = table.Column(type: "text", nullable: true), + Backdrop = table.Column(type: "text", nullable: true), + IsMovie = table.Column(type: "boolean", nullable: false), + StudioID = table.Column(type: "integer", nullable: true) }, constraints: table => { @@ -159,81 +183,72 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }); migrationBuilder.CreateTable( - name: "CollectionLinks", + name: "Link", columns: table => new { - ParentID = table.Column(nullable: false), - ChildID = table.Column(nullable: false) + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_CollectionLinks", x => new { x.ParentID, x.ChildID }); + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_CollectionLinks_Shows_ChildID", - column: x => x.ChildID, - principalTable: "Shows", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_CollectionLinks_Collections_ParentID", - column: x => x.ParentID, + name: "FK_Link_Collections_FirstID", + column: x => x.FirstID, principalTable: "Collections", principalColumn: "ID", onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "GenreLinks", - columns: table => new - { - ParentID = table.Column(nullable: false), - ChildID = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_GenreLinks", x => new { x.ParentID, x.ChildID }); table.ForeignKey( - name: "FK_GenreLinks_Genres_ChildID", - column: x => x.ChildID, - principalTable: "Genres", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_GenreLinks_Shows_ParentID", - column: x => x.ParentID, + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, principalTable: "Shows", principalColumn: "ID", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( - name: "LibraryLinks", + name: "Link", columns: table => new { - ID = table.Column(nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - LibraryID = table.Column(nullable: false), - ShowID = table.Column(nullable: true), - CollectionID = table.Column(nullable: true) + FirstID = table.Column(type: "integer", nullable: false), + SecondID = table.Column(type: "integer", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_LibraryLinks", x => x.ID); + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_LibraryLinks_Collections_CollectionID", - column: x => x.CollectionID, - principalTable: "Collections", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_LibraryLinks_Libraries_LibraryID", - column: x => x.LibraryID, + name: "FK_Link_Libraries_FirstID", + column: x => x.FirstID, principalTable: "Libraries", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_LibraryLinks_Shows_ShowID", - column: x => x.ShowID, + name: "FK_Link_Shows_SecondID", + column: x => x.SecondID, + principalTable: "Shows", + principalColumn: "ID", + 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_Genres_SecondID", + column: x => x.SecondID, + principalTable: "Genres", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Link_Shows_FirstID", + column: x => x.FirstID, principalTable: "Shows", principalColumn: "ID", onDelete: ReferentialAction.Cascade); @@ -243,12 +258,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "PeopleRoles", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - PeopleID = table.Column(nullable: false), - ShowID = table.Column(nullable: false), - Role = table.Column(nullable: true), - Type = table.Column(nullable: true) + PeopleID = table.Column(type: "integer", nullable: false), + ShowID = table.Column(type: "integer", nullable: false), + Role = table.Column(type: "text", nullable: true), + Type = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -271,14 +286,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Seasons", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ShowID = table.Column(nullable: false), - SeasonNumber = table.Column(nullable: false), - Title = table.Column(nullable: true), - Overview = table.Column(nullable: true), - Year = table.Column(nullable: true), - Poster = table.Column(nullable: true) + ShowID = table.Column(type: "integer", nullable: false), + SeasonNumber = table.Column(type: "integer", nullable: false), + Title = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + Year = table.Column(type: "integer", nullable: true), + Poster = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -295,19 +310,19 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Episodes", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ShowID = table.Column(nullable: false), - SeasonID = table.Column(nullable: true), - SeasonNumber = table.Column(nullable: false), - EpisodeNumber = table.Column(nullable: false), - AbsoluteNumber = table.Column(nullable: false), - Path = table.Column(nullable: true), - Title = table.Column(nullable: true), - Overview = table.Column(nullable: true), - ReleaseDate = table.Column(nullable: true), - Runtime = table.Column(nullable: false), - Poster = table.Column(nullable: true) + ShowID = table.Column(type: "integer", nullable: false), + SeasonID = table.Column(type: "integer", nullable: true), + SeasonNumber = table.Column(type: "integer", nullable: false), + EpisodeNumber = table.Column(type: "integer", nullable: false), + AbsoluteNumber = table.Column(type: "integer", nullable: false), + Path = table.Column(type: "text", nullable: true), + Thumb = table.Column(type: "text", nullable: true), + Title = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + ReleaseDate = table.Column(type: "timestamp without time zone", nullable: true), + Runtime = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -330,15 +345,15 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "MetadataIds", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProviderID = table.Column(nullable: false), - ShowID = table.Column(nullable: true), - EpisodeID = table.Column(nullable: true), - SeasonID = table.Column(nullable: true), - PeopleID = table.Column(nullable: true), - DataID = table.Column(nullable: true), - Link = table.Column(nullable: true) + ProviderID = table.Column(type: "integer", nullable: false), + ShowID = table.Column(type: "integer", nullable: true), + EpisodeID = table.Column(type: "integer", nullable: true), + SeasonID = table.Column(type: "integer", nullable: true), + PeopleID = table.Column(type: "integer", nullable: true), + DataID = table.Column(type: "text", nullable: true), + Link = table.Column(type: "text", nullable: true) }, constraints: table => { @@ -379,17 +394,18 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Tracks", columns: table => new { - ID = table.Column(nullable: false) + ID = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Title = table.Column(nullable: true), - Language = table.Column(nullable: true), - Codec = table.Column(nullable: true), - Path = table.Column(nullable: true), - Type = table.Column(nullable: false), - EpisodeID = table.Column(nullable: false), - IsDefault = table.Column(nullable: false), - IsForced = table.Column(nullable: false), - IsExternal = table.Column(nullable: false) + EpisodeID = table.Column(type: "integer", nullable: false), + TrackIndex = table.Column(type: "integer", nullable: false), + IsDefault = table.Column(type: "boolean", nullable: false), + IsForced = table.Column(type: "boolean", nullable: false), + IsExternal = table.Column(type: "boolean", nullable: false), + Title = table.Column(type: "text", nullable: true), + Language = table.Column(type: "text", nullable: true), + Codec = table.Column(type: "text", nullable: true), + Path = table.Column(type: "text", nullable: true), + Type = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -402,11 +418,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_CollectionLinks_ChildID", - table: "CollectionLinks", - column: "ChildID"); - migrationBuilder.CreateIndex( name: "IX_Collections_Slug", table: "Collections", @@ -424,11 +435,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, unique: true); - migrationBuilder.CreateIndex( - name: "IX_GenreLinks_ChildID", - table: "GenreLinks", - column: "ChildID"); - migrationBuilder.CreateIndex( name: "IX_Genres_Slug", table: "Genres", @@ -442,26 +448,29 @@ namespace Kyoo.Models.DatabaseMigrations.Internal unique: true); migrationBuilder.CreateIndex( - name: "IX_LibraryLinks_CollectionID", - table: "LibraryLinks", - column: "CollectionID"); + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_LibraryLinks_ShowID", - table: "LibraryLinks", - column: "ShowID"); + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_LibraryLinks_LibraryID_CollectionID", - table: "LibraryLinks", - columns: new[] { "LibraryID", "CollectionID" }, - unique: true); + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_LibraryLinks_LibraryID_ShowID", - table: "LibraryLinks", - columns: new[] { "LibraryID", "ShowID" }, - unique: true); + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); + + migrationBuilder.CreateIndex( + name: "IX_Link_SecondID", + table: "Link", + column: "SecondID"); migrationBuilder.CreateIndex( name: "IX_MetadataIds_EpisodeID", @@ -504,11 +513,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal table: "PeopleRoles", column: "ShowID"); - migrationBuilder.CreateIndex( - name: "IX_ProviderLinks_ChildID", - table: "ProviderLinks", - column: "ChildID"); - migrationBuilder.CreateIndex( name: "IX_Providers_Slug", table: "Providers", @@ -539,21 +543,28 @@ namespace Kyoo.Models.DatabaseMigrations.Internal unique: true); migrationBuilder.CreateIndex( - name: "IX_Tracks_EpisodeID", + name: "IX_Tracks_EpisodeID_Type_Language_TrackIndex_IsForced", table: "Tracks", - column: "EpisodeID"); + columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, + unique: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "CollectionLinks"); + name: "Link"); migrationBuilder.DropTable( - name: "GenreLinks"); + name: "Link"); migrationBuilder.DropTable( - name: "LibraryLinks"); + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); + + migrationBuilder.DropTable( + name: "Link"); migrationBuilder.DropTable( name: "MetadataIds"); @@ -561,26 +572,23 @@ namespace Kyoo.Models.DatabaseMigrations.Internal migrationBuilder.DropTable( name: "PeopleRoles"); - migrationBuilder.DropTable( - name: "ProviderLinks"); - migrationBuilder.DropTable( name: "Tracks"); - migrationBuilder.DropTable( - name: "Genres"); - migrationBuilder.DropTable( name: "Collections"); migrationBuilder.DropTable( - name: "People"); + name: "Libraries"); + + migrationBuilder.DropTable( + name: "Genres"); migrationBuilder.DropTable( name: "Providers"); migrationBuilder.DropTable( - name: "Libraries"); + name: "People"); migrationBuilder.DropTable( name: "Episodes"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs index ac3e38f6..f3b66bd3 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs @@ -1,6 +1,5 @@ // using System; -using System.Collections.Generic; using Kyoo; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -16,14 +15,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") - .HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") - .HasAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,font") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) + .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) + .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "font" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.3") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - modelBuilder.Entity("Kyoo.Models.CollectionDE", b => + modelBuilder.Entity("Kyoo.Models.Collection", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -49,23 +48,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Collections"); - - b.HasDiscriminator(); - }); - - modelBuilder.Entity("Kyoo.Models.CollectionLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("CollectionLinks"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -87,9 +69,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Path") .HasColumnType("text"); - b.Property("Poster") - .HasColumnType("text"); - b.Property("ReleaseDate") .HasColumnType("timestamp without time zone"); @@ -105,6 +84,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("ShowID") .HasColumnType("integer"); + b.Property("Thumb") + .HasColumnType("text"); + b.Property("Title") .HasColumnType("text"); @@ -118,7 +100,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Episodes"); }); - modelBuilder.Entity("Kyoo.Models.GenreDE", b => + modelBuilder.Entity("Kyoo.Models.Genre", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -138,26 +120,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Genres"); - - b.HasDiscriminator(); }); - modelBuilder.Entity("Kyoo.Models.GenreLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("GenreLinks"); - }); - - modelBuilder.Entity("Kyoo.Models.LibraryDE", b => + modelBuilder.Entity("Kyoo.Models.Library", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -167,7 +132,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); - b.Property>("Paths") + b.Property("Paths") .HasColumnType("text[]"); b.Property("Slug") @@ -180,39 +145,81 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .IsUnique(); b.ToTable("Libraries"); - - b.HasDiscriminator(); }); - modelBuilder.Entity("Kyoo.Models.LibraryLink", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("CollectionID") + b.Property("FirstID") .HasColumnType("integer"); - b.Property("LibraryID") + b.Property("SecondID") .HasColumnType("integer"); - b.Property("ShowID") + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") .HasColumnType("integer"); - b.HasKey("ID"); + b.Property("SecondID") + .HasColumnType("integer"); - b.HasIndex("CollectionID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("ShowID"); + b.HasIndex("SecondID"); - b.HasIndex("LibraryID", "CollectionID") - .IsUnique(); + b.ToTable("Link"); + }); - b.HasIndex("LibraryID", "ShowID") - .IsUnique(); + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer"); - b.ToTable("LibraryLinks"); + b.Property("SecondID") + .HasColumnType("integer"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + 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.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 => @@ -336,21 +343,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Providers"); }); - modelBuilder.Entity("Kyoo.Models.ProviderLink", b => - { - b.Property("ParentID") - .HasColumnType("integer"); - - b.Property("ChildID") - .HasColumnType("integer"); - - b.HasKey("ParentID", "ChildID"); - - b.HasIndex("ChildID"); - - b.ToTable("ProviderLinks"); - }); - modelBuilder.Entity("Kyoo.Models.Season", b => { b.Property("ID") @@ -384,14 +376,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Seasons"); }); - modelBuilder.Entity("Kyoo.Models.ShowDE", b => + modelBuilder.Entity("Kyoo.Models.Show", b => { b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property>("Aliases") + b.Property("Aliases") .HasColumnType("text[]"); b.Property("Backdrop") @@ -442,8 +434,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("StudioID"); b.ToTable("Shows"); - - b.HasDiscriminator(); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -499,76 +489,130 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Title") .HasColumnType("text"); + b.Property("TrackIndex") + .HasColumnType("integer"); + b.Property("Type") .HasColumnType("integer"); b.HasKey("ID"); - b.HasIndex("EpisodeID"); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); b.ToTable("Tracks"); }); - modelBuilder.Entity("Kyoo.Models.CollectionLink", b => - { - b.HasOne("Kyoo.Models.ShowDE", "Child") - .WithMany("CollectionLinks") - .HasForeignKey("ChildID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Kyoo.Models.CollectionDE", "Parent") - .WithMany("Links") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Kyoo.Models.Episode", b => { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") .HasForeignKey("SeasonID"); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); }); - modelBuilder.Entity("Kyoo.Models.GenreLink", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { - b.HasOne("Kyoo.Models.GenreDE", "Child") - .WithMany("Links") - .HasForeignKey("ChildID") + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Parent") + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("CollectionLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ProviderLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.ProviderID", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Library", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Kyoo.Models.LibraryLink", b => - { - b.HasOne("Kyoo.Models.CollectionDE", "Collection") - .WithMany("LibraryLinks") - .HasForeignKey("CollectionID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.LibraryDE", "Library") - .WithMany("Links") - .HasForeignKey("LibraryID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Show") - .WithMany("LibraryLinks") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.MetadataID", b => @@ -584,7 +628,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.ProviderID", "Provider") - .WithMany() + .WithMany("MetadataLinks") .HasForeignKey("ProviderID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -594,10 +638,20 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasForeignKey("SeasonID") .OnDelete(DeleteBehavior.Cascade); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("ExternalIDs") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Episode"); + + b.Navigation("People"); + + b.Navigation("Provider"); + + b.Navigation("Season"); + + b.Navigation("Show"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -608,42 +662,35 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - }); - modelBuilder.Entity("Kyoo.Models.ProviderLink", b => - { - b.HasOne("Kyoo.Models.ProviderID", "Child") - .WithMany() - .HasForeignKey("ChildID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.Navigation("People"); - b.HasOne("Kyoo.Models.LibraryDE", "Parent") - .WithMany("ProviderLinks") - .HasForeignKey("ParentID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.Navigation("Show"); }); modelBuilder.Entity("Kyoo.Models.Season", b => { - b.HasOne("Kyoo.Models.ShowDE", "Show") + b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Show"); }); - modelBuilder.Entity("Kyoo.Models.ShowDE", b => + modelBuilder.Entity("Kyoo.Models.Show", b => { b.HasOne("Kyoo.Models.Studio", "Studio") - .WithMany() + .WithMany("Shows") .HasForeignKey("StudioID"); + + b.Navigation("Studio"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -653,6 +700,79 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasForeignKey("EpisodeID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("ProviderLinks"); + + b.Navigation("ShowLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Navigation("ExternalIDs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Kyoo.Models.ProviderID", b => + { + b.Navigation("LibraryLinks"); + + b.Navigation("MetadataLinks"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Navigation("CollectionLinks"); + + b.Navigation("Episodes"); + + b.Navigation("ExternalIDs"); + + b.Navigation("GenreLinks"); + + b.Navigation("LibraryLinks"); + + b.Navigation("People"); + + b.Navigation("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Navigation("Shows"); }); #pragma warning restore 612, 618 } 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/Models/IdentityDatabase.cs b/Kyoo/Models/IdentityDatabase.cs index 99cc5384..f70f07b3 100644 --- a/Kyoo/Models/IdentityDatabase.cs +++ b/Kyoo/Models/IdentityDatabase.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; namespace Kyoo { + // The configuration's database is named ConfigurationDbContext. public class IdentityDatabase : IdentityDbContext, IPersistedGrantDbContext { private readonly IOptions _operationalStoreOptions; diff --git a/Kyoo/Models/Links/CollectionLink.cs b/Kyoo/Models/Links/CollectionLink.cs deleted file mode 100644 index 9af6d1df..00000000 --- a/Kyoo/Models/Links/CollectionLink.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Kyoo.Models -{ - public class CollectionLink : IResourceLink - { - public int ParentID { get; set; } - public virtual Collection Parent { get; set; } - public int ChildID { get; set; } - public virtual Show Child { get; set; } - - public CollectionLink() { } - - public CollectionLink(Collection parent, Show child) - { - Parent = parent; - ParentID = parent.ID; - Child = child; - ChildID = child.ID; - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Links/GenreLink.cs b/Kyoo/Models/Links/GenreLink.cs deleted file mode 100644 index 09dbde70..00000000 --- a/Kyoo/Models/Links/GenreLink.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Kyoo.Models -{ - public class GenreLink : IResourceLink - { - public int ParentID { get; set; } - public virtual Show Parent { get; set; } - public int ChildID { get; set; } - public virtual Genre Child { get; set; } - - public GenreLink() {} - - public GenreLink(Show parent, Genre child) - { - Parent = parent; - Child = child; - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Links/LibraryLink.cs b/Kyoo/Models/Links/LibraryLink.cs deleted file mode 100644 index 22120348..00000000 --- a/Kyoo/Models/Links/LibraryLink.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Kyoo.Models -{ - public class LibraryLink - { - public int ID { get; set; } - public int LibraryID { get; set; } - public virtual Library Library { get; set; } - public int? ShowID { get; set; } - public virtual Show Show { get; set; } - public int? CollectionID { get; set; } - public virtual Collection Collection { get; set; } - - public LibraryLink() { } - - public LibraryLink(Library library, Show show) - { - Library = library; - Show = show; - } - - public LibraryLink(Library library, Collection collection) - { - Library = library; - Collection = collection; - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Links/ProviderLink.cs b/Kyoo/Models/Links/ProviderLink.cs deleted file mode 100644 index 584af900..00000000 --- a/Kyoo/Models/Links/ProviderLink.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Newtonsoft.Json; - -namespace Kyoo.Models -{ - public class ProviderLink : IResourceLink - { - [JsonIgnore] public int ParentID { get; set; } - [JsonIgnore] public virtual Library Parent { get; set; } - [JsonIgnore] public int ChildID { get; set; } - [JsonIgnore] public virtual ProviderID Child { get; set; } - - public ProviderLink() { } - - public ProviderLink(ProviderID child, Library parent) - { - Child = child; - Parent = parent; - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Resources/CollectionDE.cs b/Kyoo/Models/Resources/CollectionDE.cs deleted file mode 100644 index ddc02b55..00000000 --- a/Kyoo/Models/Resources/CollectionDE.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - public class CollectionDE : Collection - { - [JsonIgnore] [NotMergable] public virtual ICollection Links { get; set; } - [ExpressionRewrite(nameof(Links), nameof(CollectionLink.Child))] - public override IEnumerable Shows - { - get => Links?.Select(x => x.Child); - set => Links = value?.Select(x => new CollectionLink(this, x)).ToList(); - } - - [JsonIgnore] [NotMergable] public virtual ICollection LibraryLinks { get; set; } - - [ExpressionRewrite(nameof(LibraryLinks), nameof(GenreLink.Child))] - public override IEnumerable Libraries - { - get => LibraryLinks?.Select(x => x.Library); - set => LibraryLinks = value?.Select(x => new LibraryLink(x, this)).ToList(); - } - - public CollectionDE() {} - - public CollectionDE(Collection collection) - { - Utility.Assign(this, collection); - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Resources/GenreDE.cs b/Kyoo/Models/Resources/GenreDE.cs deleted file mode 100644 index 1832cd38..00000000 --- a/Kyoo/Models/Resources/GenreDE.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - public class GenreDE : Genre - { - [JsonIgnore] [NotMergable] public virtual ICollection Links { get; set; } - - [ExpressionRewrite(nameof(Links), nameof(GenreLink.Child))] - [JsonIgnore] [NotMergable] public override IEnumerable Shows - { - get => Links?.Select(x => x.Parent); - set => Links = value?.Select(x => new GenreLink(x, this)).ToList(); - } - - public GenreDE() {} - - public GenreDE(Genre item) - { - Utility.Assign(this, item); - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Resources/LibraryDE.cs b/Kyoo/Models/Resources/LibraryDE.cs deleted file mode 100644 index 1bd097bf..00000000 --- a/Kyoo/Models/Resources/LibraryDE.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - public class LibraryDE : Library - { - [EditableRelation] [JsonIgnore] [NotMergable] public virtual ICollection ProviderLinks { get; set; } - [ExpressionRewrite(nameof(ProviderLinks), nameof(ProviderLink.Child))] - public override IEnumerable Providers - { - get => ProviderLinks?.Select(x => x.Child); - set => ProviderLinks = value?.Select(x => new ProviderLink(x, this)).ToList(); - } - - [JsonIgnore] [NotMergable] public virtual ICollection Links { get; set; } - [ExpressionRewrite(nameof(Links), nameof(LibraryLink.Show))] - public override IEnumerable Shows - { - get => Links?.Where(x => x.Show != null).Select(x => x.Show); - set => Links = Utility.MergeLists( - value?.Select(x => new LibraryLink(this, x)), - Links?.Where(x => x.Show == null))?.ToList(); - } - [ExpressionRewrite(nameof(Links), nameof(LibraryLink.Collection))] - public override IEnumerable Collections - { - get => Links?.Where(x => x.Collection != null).Select(x => x.Collection); - set => Links = Utility.MergeLists( - value?.Select(x => new LibraryLink(this, x)), - Links?.Where(x => x.Collection == null))?.ToList(); - } - - public LibraryDE() {} - - public LibraryDE(Library item) - { - Utility.Assign(this, item); - } - } -} \ No newline at end of file diff --git a/Kyoo/Models/Resources/ShowDE.cs b/Kyoo/Models/Resources/ShowDE.cs deleted file mode 100644 index 04e26994..00000000 --- a/Kyoo/Models/Resources/ShowDE.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Kyoo.Models.Attributes; - -namespace Kyoo.Models -{ - public class ShowDE : Show - { - [EditableRelation] [JsonReadOnly] [NotMergable] public virtual ICollection GenreLinks { get; set; } - [ExpressionRewrite(nameof(GenreLinks), nameof(GenreLink.Child))] - public override IEnumerable Genres - { - get => GenreLinks?.Select(x => x.Child); - set => GenreLinks = value?.Select(x => new GenreLink(this, x)).ToList(); - } - - [JsonReadOnly] [NotMergable] public virtual ICollection LibraryLinks { get; set; } - [ExpressionRewrite(nameof(LibraryLinks), nameof(LibraryLink.Library))] - public override IEnumerable Libraries - { - get => LibraryLinks?.Select(x => x.Library); - set => LibraryLinks = value?.Select(x => new LibraryLink(x, this)).ToList(); - } - - [JsonReadOnly] [NotMergable] public virtual ICollection CollectionLinks { get; set; } - [ExpressionRewrite(nameof(CollectionLinks), nameof(CollectionLink.Parent))] - public override IEnumerable Collections - { - get => CollectionLinks?.Select(x => x.Parent); - set => CollectionLinks = value?.Select(x => new CollectionLink(x, this)).ToList(); - } - - public ShowDE() {} - - public ShowDE(Show show) - { - Utility.Assign(this, show); - } - } -} \ No newline at end of file diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 9a029e5b..7f25909c 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; @@ -36,21 +37,30 @@ namespace Kyoo public void ConfigureServices(IServiceCollection services) { + string publicUrl = _configuration.GetValue("public_url"); + services.AddSpaStaticFiles(configuration => { configuration.RootPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "wwwroot"); }); + services.AddResponseCompression(x => + { + x.EnableForHttps = true; + }); services.AddControllers() - .AddNewtonsoftJson(x => x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer()); + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); + x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + }); services.AddHttpClient(); services.AddDbContext(options => { - options.UseLazyLoadingProxies() - .UseNpgsql(_configuration.GetConnectionString("Database")); - // .EnableSensitiveDataLogging() - // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); + options.UseNpgsql(_configuration.GetConnectionString("Database")); + // .EnableSensitiveDataLogging() + // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); }, ServiceLifetime.Transient); services.AddDbContext(options => @@ -59,7 +69,6 @@ namespace Kyoo }); string assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; - string publicUrl = _configuration.GetValue("public_url"); services.AddIdentityCore(o => { @@ -72,7 +81,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 +100,7 @@ namespace Kyoo options.EnableTokenCleanup = true; }) .AddInMemoryIdentityResources(IdentityContext.GetIdentityResources()) + .AddInMemoryApiScopes(IdentityContext.GetScopes()) .AddInMemoryApiResources(IdentityContext.GetApis()) .AddProfileService() .AddSigninKeys(_configuration); @@ -146,8 +155,10 @@ namespace Kyoo services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -157,7 +168,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()) { @@ -181,11 +192,29 @@ namespace Kyoo app.UseRouting(); - app.UseCookiePolicy(new CookiePolicyOptions + app.Use((ctx, next) => + { + ctx.Response.Headers.Remove("X-Powered-By"); + ctx.Response.Headers.Remove("Server"); + ctx.Response.Headers.Add("Feature-Policy", "autoplay 'self'; fullscreen"); + ctx.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"); + ctx.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); + ctx.Response.Headers.Add("Referrer-Policy", "no-referrer"); + ctx.Response.Headers.Add("Access-Control-Allow-Origin", "null"); + ctx.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + return next(); + }); + app.UseResponseCompression(); + app.UseCookiePolicy(new CookiePolicyOptions { 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/CoreTaskHolder.cs b/Kyoo/Tasks/CoreTaskHolder.cs index 5d5948e7..867c83f4 100644 --- a/Kyoo/Tasks/CoreTaskHolder.cs +++ b/Kyoo/Tasks/CoreTaskHolder.cs @@ -11,7 +11,7 @@ namespace Kyoo.Tasks new PluginLoader(), new Crawler(), new MetadataProviderLoader(), - new ReScan(), + // new ReScan(), new ExtractMetadata() }; } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 35eb2ae1..71da1381 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -75,9 +75,12 @@ namespace Kyoo.Controllers ICollection libraries = argument == null ? await libraryManager.GetLibraries() : new [] { await libraryManager.GetLibrary(argument)}; - // TODO replace this grotesque way to load the providers. + + if (argument != null && libraries.First() == null) + throw new ArgumentException($"No library found with the name {argument}"); + foreach (Library library in libraries) - library.Providers = library.Providers; + await libraryManager.Load(library, x => x.Providers); foreach (Library library in libraries) await Scan(library, episodes, tracks, cancellationToken); @@ -123,6 +126,7 @@ namespace Kyoo.Controllers .GroupBy(Path.GetDirectoryName) .ToList(); + // TODO If the library's path end with a /, the regex is broken. IEnumerable tasks = shows.Select(x => x.First()); foreach (string[] showTasks in tasks.BatchBy(_parallelTasks)) await Task.WhenAll(showTasks @@ -287,7 +291,6 @@ namespace Kyoo.Controllers show.Slug += $"-{show.StartYear}"; await libraryManager.RegisterShow(show); } - await _thumbnailsManager.Validate(show.People); await _thumbnailsManager.Validate(show); return show; } @@ -347,16 +350,17 @@ namespace Kyoo.Controllers Title = show.Title, Path = episodePath, Show = show, - ShowID = show.ID + ShowID = show.ID, + ShowSlug = show.Slug }; episode.Tracks = await GetTracks(episode); return episode; } - private async Task> GetTracks(Episode episode) + private async Task> GetTracks(Episode episode) { - episode.Tracks = (await _transcoder.ExtractInfos(episode.Path)) - .Where(x => x.Type != StreamType.Font) + episode.Tracks = (await _transcoder.ExtractInfos(episode, false)) + .Where(x => x.Type != StreamType.Attachment) .ToArray(); return episode.Tracks; } diff --git a/Kyoo/Tasks/CreateDatabase.cs b/Kyoo/Tasks/CreateDatabase.cs index bd372ae8..2264b116 100644 --- a/Kyoo/Tasks/CreateDatabase.cs +++ b/Kyoo/Tasks/CreateDatabase.cs @@ -28,9 +28,9 @@ namespace Kyoo.Tasks IdentityDatabase identityDatabase = serviceScope.ServiceProvider.GetService(); ConfigurationDbContext identityContext = serviceScope.ServiceProvider.GetService(); - databaseContext.Database.Migrate(); - identityDatabase.Database.Migrate(); - identityContext.Database.Migrate(); + databaseContext!.Database.Migrate(); + identityDatabase!.Database.Migrate(); + identityContext!.Database.Migrate(); if (!identityContext.Clients.Any()) { diff --git a/Kyoo/Tasks/ExtractMetadata.cs b/Kyoo/Tasks/ExtractMetadata.cs index f65d1b42..73ed031b 100644 --- a/Kyoo/Tasks/ExtractMetadata.cs +++ b/Kyoo/Tasks/ExtractMetadata.cs @@ -72,6 +72,7 @@ namespace Kyoo.Tasks { if (thumbs) await _thumbnails!.Validate(show, true); + await _library.Load(show, x => x.Seasons); foreach (Season season in show.Seasons) { if (token.IsCancellationRequested) @@ -84,6 +85,7 @@ namespace Kyoo.Tasks { if (thumbs) await _thumbnails!.Validate(season, true); + await _library.Load(season, x => x.Episodes); foreach (Episode episode in season.Episodes) { if (token.IsCancellationRequested) @@ -98,10 +100,11 @@ namespace Kyoo.Tasks await _thumbnails!.Validate(episode, true); if (subs) { - // TODO this doesn't work. - IEnumerable tracks = (await _transcoder!.ExtractInfos(episode.Path)) - .Where(x => x.Type != StreamType.Font); - episode.Tracks = tracks; + await _library.Load(episode, x => x.Tracks); + episode.Tracks = (await _transcoder!.ExtractInfos(episode, true)) + .Where(x => x.Type != StreamType.Attachment) + .Concat(episode.Tracks.Where(x => x.IsExternal)) + .ToList(); await _library.EditEpisode(episode, false); } } diff --git a/Kyoo/Tasks/MetadataProviderLoader.cs b/Kyoo/Tasks/MetadataProviderLoader.cs index 95539677..5811775e 100644 --- a/Kyoo/Tasks/MetadataProviderLoader.cs +++ b/Kyoo/Tasks/MetadataProviderLoader.cs @@ -21,13 +21,15 @@ namespace Kyoo.Tasks { using IServiceScope serviceScope = serviceProvider.CreateScope(); IProviderRepository providers = serviceScope.ServiceProvider.GetService(); + IThumbnailsManager thumbnails = serviceScope.ServiceProvider.GetService(); IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); - foreach (IMetadataProvider provider in pluginManager.GetPlugins()) + foreach (IMetadataProvider provider in pluginManager!.GetPlugins()) { if (string.IsNullOrEmpty(provider.Provider.Slug)) throw new ArgumentException($"Empty provider slug (name: {provider.Provider.Name})."); - await providers.CreateIfNotExists(provider.Provider); + await providers!.CreateIfNotExists(provider.Provider); + await thumbnails!.Validate(provider.Provider); } } diff --git a/Kyoo/Tasks/ReScan.cs b/Kyoo/Tasks/ReScan.cs index 7ab182a7..a4a62786 100644 --- a/Kyoo/Tasks/ReScan.cs +++ b/Kyoo/Tasks/ReScan.cs @@ -1,127 +1,127 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Tasks -{ - public class ReScan: ITask - { - public string Slug => "re-scan"; - public string Name => "ReScan"; - public string Description => "Re download metadata of an item using it's external ids."; - public string HelpMessage => null; - public bool RunOnStartup => false; - public int Priority => 0; - - - private IServiceProvider _serviceProvider; - private IThumbnailsManager _thumbnailsManager; - private IProviderManager _providerManager; - private DatabaseContext _database; - - public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) - { - using IServiceScope serviceScope = serviceProvider.CreateScope(); - _serviceProvider = serviceProvider; - _thumbnailsManager = serviceProvider.GetService(); - _providerManager = serviceProvider.GetService(); - _database = serviceScope.ServiceProvider.GetService(); - - if (arguments == null || !arguments.Contains('/')) - return; - - string slug = arguments.Substring(arguments.IndexOf('/') + 1); - switch (arguments.Substring(0, arguments.IndexOf('/'))) - { - case "show": - await ReScanShow(slug); - break; - case "season": - await ReScanSeason(slug); - break; - } - } - - private async Task ReScanShow(string slug) - { - Show old; - - using (IServiceScope serviceScope = _serviceProvider.CreateScope()) - { - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - old = _database.Shows.FirstOrDefault(x => x.Slug == slug); - if (old == null) - return; - Library library = _database.LibraryLinks.First(x => x.Show == old && x.Library != null).Library; - Show edited = await _providerManager.CompleteShow(old, library); - edited.ID = old.ID; - edited.Slug = old.Slug; - edited.Path = old.Path; - await libraryManager.EditShow(edited, true); - await _thumbnailsManager.Validate(edited, true); - } - if (old.Seasons != null) - await Task.WhenAll(old.Seasons.Select(x => ReScanSeason(old, x))); - IEnumerable orphans = old.Episodes.Where(x => x.Season == null).ToList(); - if (orphans.Any()) - await Task.WhenAll(orphans.Select(x => ReScanEpisode(old, x))); - } - - private async Task ReScanSeason(string seasonSlug) - { - string[] infos = seasonSlug.Split('-'); - if (infos.Length != 2 || int.TryParse(infos[1], out int seasonNumber)) - return; - string slug = infos[0]; - Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug); - if (show == null) - return; - Season old = _database.Seasons.FirstOrDefault(x => x.SeasonNumber == seasonNumber && x.Show.ID == show.ID); - if (old == null) - return; - await ReScanSeason(show, old); - } - - private async Task ReScanSeason(Show show, Season old) - { - using (IServiceScope serviceScope = _serviceProvider.CreateScope()) - { - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; - Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library); - edited.ID = old.ID; - await libraryManager.EditSeason(edited, true); - await _thumbnailsManager.Validate(edited, true); - } - if (old.Episodes != null) - await Task.WhenAll(old.Episodes.Select(x => ReScanEpisode(show, x))); - } - - private async Task ReScanEpisode(Show show, Episode old) - { - using IServiceScope serviceScope = _serviceProvider.CreateScope(); - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - - Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; - Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library); - edited.ID = old.ID; - await libraryManager.EditEpisode(edited, true); - await _thumbnailsManager.Validate(edited, true); - } - - public Task> GetPossibleParameters() - { - return Task.FromResult>(null); - } - - public int? Progress() - { - return null; - } - } -} \ No newline at end of file +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; +// using Kyoo.Controllers; +// using Kyoo.Models; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kyoo.Tasks +// { +// public class ReScan: ITask +// { +// public string Slug => "re-scan"; +// public string Name => "ReScan"; +// public string Description => "Re download metadata of an item using it's external ids."; +// public string HelpMessage => null; +// public bool RunOnStartup => false; +// public int Priority => 0; +// +// +// private IServiceProvider _serviceProvider; +// private IThumbnailsManager _thumbnailsManager; +// private IProviderManager _providerManager; +// private DatabaseContext _database; +// +// public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) +// { +// using IServiceScope serviceScope = serviceProvider.CreateScope(); +// _serviceProvider = serviceProvider; +// _thumbnailsManager = serviceProvider.GetService(); +// _providerManager = serviceProvider.GetService(); +// _database = serviceScope.ServiceProvider.GetService(); +// +// if (arguments == null || !arguments.Contains('/')) +// return; +// +// string slug = arguments.Substring(arguments.IndexOf('/') + 1); +// switch (arguments.Substring(0, arguments.IndexOf('/'))) +// { +// case "show": +// await ReScanShow(slug); +// break; +// case "season": +// await ReScanSeason(slug); +// break; +// } +// } +// +// private async Task ReScanShow(string slug) +// { +// Show old; +// +// using (IServiceScope serviceScope = _serviceProvider.CreateScope()) +// { +// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); +// old = _database.Shows.FirstOrDefault(x => x.Slug == slug); +// if (old == null) +// return; +// Library library = _database.LibraryLinks.First(x => x.Show == old && x.Library != null).Library; +// Show edited = await _providerManager.CompleteShow(old, library); +// edited.ID = old.ID; +// edited.Slug = old.Slug; +// edited.Path = old.Path; +// await libraryManager.EditShow(edited, true); +// await _thumbnailsManager.Validate(edited, true); +// } +// if (old.Seasons != null) +// await Task.WhenAll(old.Seasons.Select(x => ReScanSeason(old, x))); +// IEnumerable orphans = old.Episodes.Where(x => x.Season == null).ToList(); +// if (orphans.Any()) +// await Task.WhenAll(orphans.Select(x => ReScanEpisode(old, x))); +// } +// +// private async Task ReScanSeason(string seasonSlug) +// { +// string[] infos = seasonSlug.Split('-'); +// if (infos.Length != 2 || int.TryParse(infos[1], out int seasonNumber)) +// return; +// string slug = infos[0]; +// Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug); +// if (show == null) +// return; +// Season old = _database.Seasons.FirstOrDefault(x => x.SeasonNumber == seasonNumber && x.Show.ID == show.ID); +// if (old == null) +// return; +// await ReScanSeason(show, old); +// } +// +// private async Task ReScanSeason(Show show, Season old) +// { +// using (IServiceScope serviceScope = _serviceProvider.CreateScope()) +// { +// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); +// Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; +// Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library); +// edited.ID = old.ID; +// await libraryManager.EditSeason(edited, true); +// await _thumbnailsManager.Validate(edited, true); +// } +// if (old.Episodes != null) +// await Task.WhenAll(old.Episodes.Select(x => ReScanEpisode(show, x))); +// } +// +// private async Task ReScanEpisode(Show show, Episode old) +// { +// using IServiceScope serviceScope = _serviceProvider.CreateScope(); +// ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); +// +// Library library = _database.LibraryLinks.First(x => x.Show == show && x.Library != null).Library; +// Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library); +// edited.ID = old.ID; +// await libraryManager.EditEpisode(edited, true); +// await _thumbnailsManager.Validate(edited, true); +// } +// +// public Task> GetPossibleParameters() +// { +// return Task.FromResult>(null); +// } +// +// public int? Progress() +// { +// return null; +// } +// } +// } \ No newline at end of file diff --git a/Kyoo/Views/API/CollectionApi.cs b/Kyoo/Views/API/CollectionApi.cs index 254be6b6..5bba0650 100644 --- a/Kyoo/Views/API/CollectionApi.cs +++ b/Kyoo/Views/API/CollectionApi.cs @@ -33,10 +33,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -63,10 +59,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -93,10 +85,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetLibraries( @@ -123,10 +111,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetLibraries( diff --git a/Kyoo/Views/API/EpisodeApi.cs b/Kyoo/Views/API/EpisodeApi.cs index eff5fe78..c0c01a18 100644 --- a/Kyoo/Views/API/EpisodeApi.cs +++ b/Kyoo/Views/API/EpisodeApi.cs @@ -2,12 +2,10 @@ using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; @@ -19,11 +17,18 @@ namespace Kyoo.Api public class EpisodeApi : CrudApi { private readonly ILibraryManager _libraryManager; + private readonly IThumbnailsManager _thumbnails; + private readonly IFileManager _files; - public EpisodeApi(ILibraryManager libraryManager, IConfiguration configuration) + public EpisodeApi(ILibraryManager libraryManager, + IConfiguration configuration, + IFileManager files, + IThumbnailsManager thumbnails) : base(libraryManager.EpisodeRepository, configuration) { _libraryManager = libraryManager; + _files = files; + _thumbnails = thumbnails; } [HttpGet("{episodeID:int}/show")] @@ -77,10 +82,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetTracks( @@ -109,10 +110,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetTracks( @@ -143,10 +140,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetTracks(ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == showSlug @@ -165,19 +158,24 @@ namespace Kyoo.Api } } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/thumb")] + [HttpGet("{id:int}/thumb")] [Authorize(Policy="Read")] - public async Task GetThumb(string showSlug, int seasonNumber, int episodeNumber) + public async Task GetThumb(int id) { - string path = (await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber))?.Path; - if (path == null) + Episode episode = await _libraryManager.GetEpisode(id); + if (episode == null) return NotFound(); - - string thumb = Path.ChangeExtension(path, "jpg"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); + return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); + } + + [HttpGet("{slug}/thumb")] + [Authorize(Policy="Read")] + public async Task GetThumb(string slug) + { + Episode episode = await _libraryManager.GetEpisode(slug); + if (episode == null) + return NotFound(); + return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); } } } \ No newline at end of file diff --git a/Kyoo/Views/API/GenreApi.cs b/Kyoo/Views/API/GenreApi.cs index 2571f347..29393e97 100644 --- a/Kyoo/Views/API/GenreApi.cs +++ b/Kyoo/Views/API/GenreApi.cs @@ -34,10 +34,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -64,10 +60,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( diff --git a/Kyoo/Views/API/LibraryApi.cs b/Kyoo/Views/API/LibraryApi.cs index c66ae7f1..ba9ad3d0 100644 --- a/Kyoo/Views/API/LibraryApi.cs +++ b/Kyoo/Views/API/LibraryApi.cs @@ -45,10 +45,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -75,10 +71,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -105,10 +97,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetCollections( @@ -135,10 +123,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetCollections( @@ -165,10 +149,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetItemsFromLibrary(id, @@ -195,10 +175,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetItemsFromLibrary(slug, diff --git a/Kyoo/Views/API/LibraryItemApi.cs b/Kyoo/Views/API/LibraryItemApi.cs index 19132fa1..9f97a275 100644 --- a/Kyoo/Views/API/LibraryItemApi.cs +++ b/Kyoo/Views/API/LibraryItemApi.cs @@ -15,6 +15,7 @@ namespace Kyoo.Api [Route("api/item")] [Route("api/items")] [ApiController] + [ResourceView] public class LibraryItemApi : ControllerBase { private readonly ILibraryItemRepository _libraryItems; @@ -34,10 +35,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryItems.GetAll( diff --git a/Kyoo/Views/API/PeopleApi.cs b/Kyoo/Views/API/PeopleApi.cs index 41d16f04..bfcb8a29 100644 --- a/Kyoo/Views/API/PeopleApi.cs +++ b/Kyoo/Views/API/PeopleApi.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; @@ -17,34 +16,34 @@ namespace Kyoo.Api public class PeopleApi : CrudApi { private readonly ILibraryManager _libraryManager; - private readonly string _peoplePath; + private readonly IFileManager _files; + private readonly IThumbnailsManager _thumbs; - public PeopleApi(ILibraryManager libraryManager, IConfiguration configuration) + public PeopleApi(ILibraryManager libraryManager, + IConfiguration configuration, + IFileManager files, + IThumbnailsManager thumbs) : base(libraryManager.PeopleRepository, configuration) { _libraryManager = libraryManager; - _peoplePath = configuration.GetValue("peoplePath"); + _files = files; + _thumbs = thumbs; } [HttpGet("{id:int}/role")] [HttpGet("{id:int}/roles")] [Authorize(Policy = "Read")] - [JsonDetailed] - public async Task>> GetRoles(int id, + public async Task>> GetRoles(int id, [FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { - ICollection resources = await _libraryManager.GetRolesFromPeople(id, - ApiHelper.ParseWhere(where), - new Sort(sortBy), + ICollection resources = await _libraryManager.GetRolesFromPeople(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), new Pagination(limit, afterID)); return Page(resources, limit); @@ -62,22 +61,17 @@ namespace Kyoo.Api [HttpGet("{slug}/role")] [HttpGet("{slug}/roles")] [Authorize(Policy = "Read")] - [JsonDetailed] - public async Task>> GetRoles(string slug, + public async Task>> GetRoles(string slug, [FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { - ICollection resources = await _libraryManager.GetRolesFromPeople(slug, - ApiHelper.ParseWhere(where), - new Sort(sortBy), + ICollection resources = await _libraryManager.GetRolesFromPeople(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), new Pagination(limit, afterID)); return Page(resources, limit); @@ -92,15 +86,20 @@ namespace Kyoo.Api } } + [HttpGet("{id:int}/poster")] + [Authorize(Policy="Read")] + public async Task GetPeopleIcon(int id) + { + People people = await _libraryManager.GetPeople(id); + return _files.FileResult(await _thumbs.GetPeoplePoster(people)); + } + [HttpGet("{slug}/poster")] [Authorize(Policy="Read")] - public IActionResult GetPeopleIcon(string slug) + public async Task GetPeopleIcon(string slug) { - string thumbPath = Path.Combine(_peoplePath, slug + ".jpg"); - if (!System.IO.File.Exists(thumbPath)) - return NotFound(); - - return new PhysicalFileResult(Path.GetFullPath(thumbPath), "image/jpg"); + People people = await _libraryManager.GetPeople(slug); + return _files.FileResult(await _thumbs.GetPeoplePoster(people)); } } } \ No newline at end of file diff --git a/Kyoo/Views/API/ProviderApi.cs b/Kyoo/Views/API/ProviderApi.cs index c2798434..2d3aab3d 100644 --- a/Kyoo/Views/API/ProviderApi.cs +++ b/Kyoo/Views/API/ProviderApi.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; @@ -13,12 +13,35 @@ namespace Kyoo.Api [ApiController] public class ProviderAPI : CrudApi { + private readonly IThumbnailsManager _thumbnails; private readonly ILibraryManager _libraryManager; + private readonly IFileManager _files; - public ProviderAPI(ILibraryManager libraryManager, IConfiguration config) + public ProviderAPI(ILibraryManager libraryManager, + IConfiguration config, + IFileManager files, + IThumbnailsManager thumbnails) : base(libraryManager.ProviderRepository, config) { _libraryManager = libraryManager; + _files = files; + _thumbnails = thumbnails; + } + + [HttpGet("{id:int}/logo")] + [Authorize(Policy="Read")] + public async Task GetLogo(int id) + { + ProviderID provider = await _libraryManager.GetProvider(id); + return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); + } + + [HttpGet("{slug}/logo")] + [Authorize(Policy="Read")] + public async Task GetLogo(string slug) + { + ProviderID provider = await _libraryManager.GetProvider(slug); + return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); } } } \ No newline at end of file diff --git a/Kyoo/Views/API/SeasonApi.cs b/Kyoo/Views/API/SeasonApi.cs index 67206ca1..bc43cad5 100644 --- a/Kyoo/Views/API/SeasonApi.cs +++ b/Kyoo/Views/API/SeasonApi.cs @@ -17,11 +17,18 @@ namespace Kyoo.Api public class SeasonApi : CrudApi { private readonly ILibraryManager _libraryManager; + private readonly IThumbnailsManager _thumbs; + private readonly IFileManager _files; - public SeasonApi(ILibraryManager libraryManager, IConfiguration configuration) + public SeasonApi(ILibraryManager libraryManager, + IConfiguration configuration, + IThumbnailsManager thumbs, + IFileManager files) : base(libraryManager.SeasonRepository, configuration) { _libraryManager = libraryManager; + _thumbs = thumbs; + _files = files; } [HttpGet("{seasonID:int}/episode")] @@ -33,10 +40,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetEpisodes( @@ -64,10 +67,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetEpisodes( @@ -96,10 +95,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetEpisodes( @@ -137,5 +132,23 @@ namespace Kyoo.Api { return await _libraryManager.GetShow(showID); } + + [HttpGet("{id:int}/thumb")] + [Authorize(Policy="Read")] + public async Task GetThumb(int id) + { + Season season = await _libraryManager.GetSeason(id); + await _libraryManager.Load(season, x => x.Show); + return _files.FileResult(await _thumbs.GetSeasonPoster(season)); + } + + [HttpGet("{slug}/thumb")] + [Authorize(Policy="Read")] + public async Task GetThumb(string slug) + { + Season season = await _libraryManager.GetSeason(slug); + await _libraryManager.Load(season, x => x.Show); + return _files.FileResult(await _thumbs.GetSeasonPoster(season)); + } } } \ No newline at end of file diff --git a/Kyoo/Views/API/ShowApi.cs b/Kyoo/Views/API/ShowApi.cs index dcb90a66..703f8c45 100644 --- a/Kyoo/Views/API/ShowApi.cs +++ b/Kyoo/Views/API/ShowApi.cs @@ -9,7 +9,6 @@ using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; namespace Kyoo.Api @@ -20,12 +19,18 @@ namespace Kyoo.Api public class ShowApi : CrudApi { private readonly ILibraryManager _libraryManager; - private FileExtensionContentTypeProvider _provider; + private readonly IFileManager _files; + private readonly IThumbnailsManager _thumbs; - public ShowApi(ILibraryManager libraryManager, IConfiguration configuration) + public ShowApi(ILibraryManager libraryManager, + IFileManager files, + IThumbnailsManager thumbs, + IConfiguration configuration) : base(libraryManager.ShowRepository, configuration) { _libraryManager = libraryManager; + _files = files; + _thumbs = thumbs; } [HttpGet("{showID:int}/season")] @@ -37,10 +42,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetSeasons( @@ -67,10 +68,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetSeasons( @@ -97,10 +94,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetEpisodes( @@ -127,10 +120,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 50) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetEpisodes( @@ -156,10 +145,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetPeopleFromShow(showID, @@ -185,10 +170,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetPeopleFromShow(slug, @@ -215,10 +196,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetGenres( @@ -245,10 +222,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetGenres( @@ -303,10 +276,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetLibraries( @@ -333,10 +302,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetLibraries( @@ -363,10 +328,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetCollections( @@ -393,10 +354,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 30) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetCollections( @@ -419,13 +376,11 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task>> GetFonts(string slug) { - string path = (await _libraryManager.GetShow(slug))?.Path; - if (path == null) + Show show = await _libraryManager.GetShow(slug); + if (show == null) return NotFound(); - path = Path.Combine(path, "Subtitles", "fonts"); - if (!Directory.Exists(path)) - return new Dictionary(); - return Directory.GetFiles(path) + string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments"); + return (await _files.ListFiles(path)) .ToDictionary(Path.GetFileNameWithoutExtension, x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); } @@ -433,64 +388,43 @@ namespace Kyoo.Api [HttpGet("{showSlug}/font/{slug}")] [HttpGet("{showSlug}/fonts/{slug}")] [Authorize(Policy = "Read")] - public async Task GetFont(string showSlug, string slug) + public async Task GetFont(string showSlug, string slug) { - string path = (await _libraryManager.GetShow(showSlug))?.Path; - if (path == null) + Show show = await _libraryManager.GetShow(showSlug); + if (show == null) return NotFound(); - string fontPath = Path.Combine(path, "Subtitles", "fonts", slug); - if (!System.IO.File.Exists(fontPath)) - return NotFound(); - - if (_provider == null) - _provider = new FileExtensionContentTypeProvider(); - _provider.TryGetContentType(path, out string contentType); - return PhysicalFile(fontPath, contentType ?? "application/x-font-ttf"); + string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); + return _files.FileResult(path); } [HttpGet("{slug}/poster")] [Authorize(Policy = "Read")] - public async Task GetPoster(string slug) + public async Task GetPoster(string slug) { - string path = (await _libraryManager.GetShow(slug))?.Path; - if (path == null) + Show show = await _libraryManager.GetShow(slug); + if (show == null) return NotFound(); - - string poster = Path.Combine(path, "poster.jpg"); - - if (System.IO.File.Exists(poster)) - return new PhysicalFileResult(Path.GetFullPath(poster), "image/jpg"); - return NotFound(); + return _files.FileResult(await _thumbs.GetShowPoster(show)); } [HttpGet("{slug}/logo")] [Authorize(Policy="Read")] public async Task GetLogo(string slug) { - string path = (await _libraryManager.GetShow(slug))?.Path; - if (path == null) + Show show = await _libraryManager.GetShow(slug); + if (show == null) return NotFound(); - - string logo = Path.Combine(path, "logo.png"); - - if (System.IO.File.Exists(logo)) - return new PhysicalFileResult(Path.GetFullPath(logo), "image/png"); - return NotFound(); + return _files.FileResult(await _thumbs.GetShowLogo(show)); } [HttpGet("{slug}/backdrop")] [Authorize(Policy="Read")] public async Task GetBackdrop(string slug) { - string path = (await _libraryManager.GetShow(slug))?.Path; - if (path == null) + Show show = await _libraryManager.GetShow(slug); + if (show == null) return NotFound(); - - string thumb = Path.Combine(path, "backdrop.jpg"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); + return _files.FileResult(await _thumbs.GetShowBackdrop(show)); } } } diff --git a/Kyoo/Views/API/StudioApi.cs b/Kyoo/Views/API/StudioApi.cs index 96c91463..a0cf872e 100644 --- a/Kyoo/Views/API/StudioApi.cs +++ b/Kyoo/Views/API/StudioApi.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Kyoo.CommonApi; using Kyoo.Controllers; using Kyoo.Models; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -34,10 +33,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( @@ -64,10 +59,6 @@ namespace Kyoo.Api [FromQuery] Dictionary where, [FromQuery] int limit = 20) { - where.Remove("sortBy"); - where.Remove("limit"); - where.Remove("afterID"); - try { ICollection resources = await _libraryManager.GetShows( diff --git a/Kyoo/Views/API/SubtitleApi.cs b/Kyoo/Views/API/SubtitleApi.cs index 132b3b19..73f151ee 100644 --- a/Kyoo/Views/API/SubtitleApi.cs +++ b/Kyoo/Views/API/SubtitleApi.cs @@ -14,10 +14,12 @@ namespace Kyoo.Api public class SubtitleApi : ControllerBase { private readonly ILibraryManager _libraryManager; + private readonly IFileManager _files; - public SubtitleApi(ILibraryManager libraryManager) + public SubtitleApi(ILibraryManager libraryManager, IFileManager files) { _libraryManager = libraryManager; + _files = files; } @@ -35,13 +37,12 @@ namespace Kyoo.Api return BadRequest(new {error = ex.Message}); } - if (subtitle == null) + if (subtitle == null || subtitle.Type != StreamType.Subtitle) return NotFound(); if (subtitle.Codec == "subrip" && extension == "vtt") - return new ConvertSubripToVtt(subtitle.Path); - string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip"; - return PhysicalFile(subtitle.Path, mime); + return new ConvertSubripToVtt(subtitle.Path, _files); + return _files.FileResult(subtitle.Path); } } @@ -49,27 +50,29 @@ namespace Kyoo.Api public class ConvertSubripToVtt : IActionResult { private readonly string _path; + private readonly IFileManager _files; - public ConvertSubripToVtt(string subtitlePath) + public ConvertSubripToVtt(string subtitlePath, IFileManager files) { _path = subtitlePath; + _files = files; } public async Task ExecuteResultAsync(ActionContext context) { - string line; - List lines = new List(); + List lines = new(); context.HttpContext.Response.StatusCode = 200; context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt"); - await using (StreamWriter writer = new StreamWriter(context.HttpContext.Response.Body)) + await using (StreamWriter writer = new(context.HttpContext.Response.Body)) { await writer.WriteLineAsync("WEBVTT"); await writer.WriteLineAsync(""); await writer.WriteLineAsync(""); - using StreamReader reader = new StreamReader(_path); + using StreamReader reader = _files.GetReader(_path); + string line; while ((line = await reader.ReadLineAsync()) != null) { if (line == "") diff --git a/Kyoo/Views/API/ThumbnailAPI.cs b/Kyoo/Views/API/ThumbnailAPI.cs deleted file mode 100644 index 72c13ae0..00000000 --- a/Kyoo/Views/API/ThumbnailAPI.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using System.IO; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - public class ThumbnailController : ControllerBase - { - private readonly ILibraryManager _libraryManager; - private readonly string _peoplePath; - - - public ThumbnailController(ILibraryManager libraryManager, IConfiguration config) - { - _libraryManager = libraryManager; - _peoplePath = config.GetValue("peoplePath"); - } - - - [HttpGet("poster/{showSlug}")] - [Authorize(Policy="Read")] - public async Task GetShowThumb(string showSlug) - { - string path = (await _libraryManager.GetShow(showSlug))?.Path; - if (path == null) - return NotFound(); - - string thumb = Path.Combine(path, "poster.jpg"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); - } - - [HttpGet("logo/{showSlug}")] - [Authorize(Policy="Read")] - public async Task GetShowLogo(string showSlug) - { - string path = (await _libraryManager.GetShow(showSlug))?.Path; - if (path == null) - return NotFound(); - - string thumb = Path.Combine(path, "logo.png"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); - } - - [HttpGet("backdrop/{showSlug}")] - [Authorize(Policy="Read")] - public async Task GetShowBackdrop(string showSlug) - { - string path = (await _libraryManager.GetShow(showSlug))?.Path; - if (path == null) - return NotFound(); - - string thumb = Path.Combine(path, "backdrop.jpg"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); - } - - [HttpGet("peopleimg/{peopleSlug}")] - [Authorize(Policy="Read")] - public IActionResult GetPeopleIcon(string peopleSlug) - { - string thumbPath = Path.Combine(_peoplePath, peopleSlug + ".jpg"); - if (!System.IO.File.Exists(thumbPath)) - return NotFound(); - - return new PhysicalFileResult(Path.GetFullPath(thumbPath), "image/jpg"); - } - - [HttpGet("thumb/{showSlug}-s{seasonNumber}e{episodeNumber}")] - [Authorize(Policy="Read")] - public async Task GetEpisodeThumb(string showSlug, int seasonNumber, int episodeNumber) - { - string path = (await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber))?.Path; - if (path == null) - return NotFound(); - - string thumb = Path.ChangeExtension(path, "jpg"); - - if (System.IO.File.Exists(thumb)) - return new PhysicalFileResult(Path.GetFullPath(thumb), "image/jpg"); - return NotFound(); - } - } -} diff --git a/Kyoo/Views/API/TrackApi.cs b/Kyoo/Views/API/TrackApi.cs new file mode 100644 index 00000000..689e904b --- /dev/null +++ b/Kyoo/Views/API/TrackApi.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/track")] + [Route("api/tracks")] + [ApiController] + public class TrackApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public TrackApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.TrackRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/episode")] + [Authorize(Policy = "Read")] + public async Task> GetEpisode(int id) + { + try + { + return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.ID == id)); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + + [HttpGet("{slug}/episode")] + [Authorize(Policy = "Read")] + public async Task> GetEpisode(string slug) + { + try + { + // TODO This won't work with the local repository implementation. + // TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor + return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.Slug == slug)); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/VideoApi.cs b/Kyoo/Views/API/VideoApi.cs index f92bdd6a..1e6651b5 100644 --- a/Kyoo/Views/API/VideoApi.cs +++ b/Kyoo/Views/API/VideoApi.cs @@ -1,12 +1,11 @@ -using Kyoo.Controllers; +using System.IO; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Api { @@ -16,14 +15,18 @@ namespace Kyoo.Api { private readonly ILibraryManager _libraryManager; private readonly ITranscoder _transcoder; + private readonly IFileManager _files; private readonly string _transmuxPath; private readonly string _transcodePath; - private FileExtensionContentTypeProvider _provider; - public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config) + public VideoApi(ILibraryManager libraryManager, + ITranscoder transcoder, + IConfiguration config, + IFileManager files) { _libraryManager = libraryManager; _transcoder = transcoder; + _files = files; _transmuxPath = config.GetValue("transmuxTempPath"); _transcodePath = config.GetValue("transcodeTempPath"); } @@ -37,19 +40,6 @@ namespace Kyoo.Api ctx.HttpContext.Response.Headers.Add("Expires", "0"); } - private string _GetContentType(string path) - { - if (_provider == null) - { - _provider = new FileExtensionContentTypeProvider(); - _provider.Mappings[".mkv"] = "video/x-matroska"; - } - - if (_provider.TryGetContentType(path, out string contentType)) - return contentType; - return "video/mp4"; - } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] @@ -60,9 +50,9 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode != null && System.IO.File.Exists(episode.Path)) - return PhysicalFile(episode.Path, _GetContentType(episode.Path), true); - return NotFound(); + if (episode == null) + return NotFound(); + return _files.FileResult(episode.Path, true); } [HttpGet("{movieSlug}")] @@ -72,9 +62,9 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode != null && System.IO.File.Exists(episode.Path)) - return PhysicalFile(episode.Path, _GetContentType(episode.Path), true); - return NotFound(); + if (episode == null) + return NotFound(); + return _files.FileResult(episode.Path, true); } @@ -86,12 +76,12 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transmux(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transmux/{movieSlug}/master.m3u8")] @@ -100,12 +90,12 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transmux(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")] @@ -116,12 +106,12 @@ namespace Kyoo.Api return BadRequest(new {error = "Season number or episode number can not be negative."}); Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transcode(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } [HttpGet("transcode/{movieSlug}/master.m3u8")] @@ -130,12 +120,12 @@ namespace Kyoo.Api { Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - if (episode == null || !System.IO.File.Exists(episode.Path)) + if (episode == null) return NotFound(); string path = await _transcoder.Transcode(episode); if (path == null) return StatusCode(500); - return PhysicalFile(path, "application/x-mpegurl", true); + return _files.FileResult(path, true); } diff --git a/Kyoo/Views/WebClient b/Kyoo/Views/WebClient index 57d382f7..ab52f039 160000 --- a/Kyoo/Views/WebClient +++ b/Kyoo/Views/WebClient @@ -1 +1 @@ -Subproject commit 57d382f7246287a611892359e444355e745d0795 +Subproject commit ab52f039021928cab9f6ed8c17a0488ca198ef74 diff --git a/Kyoo/appsettings.json b/Kyoo/appsettings.json index 7845b158..3cc8f735 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/appsettings.json @@ -18,7 +18,7 @@ "ConnectionStrings": { "Database": "Server=127.0.0.1; Port=5432; Database=kyooDB; User Id=kyoo; Password=kyooPassword; Pooling=true; MaxPoolSize=95; Timeout=30;" }, - "parallelTasks": "40", + "parallelTasks": "1", "scheduledTasks": { "scan": "24:00:00" @@ -29,6 +29,7 @@ "transmuxTempPath": "cached/kyoo/transmux", "transcodeTempPath": "cached/kyoo/transcode", "peoplePath": "people", + "providerPath": "providers", "profilePicturePath": "users/", "plugins": "plugins/", "defaultPermissions": "read,play,write,admin", diff --git a/transcoder b/transcoder index 3885dca7..1902defd 160000 --- a/transcoder +++ b/transcoder @@ -1 +1 @@ -Subproject commit 3885dca743bbde5d83cb3816646455856fc5c316 +Subproject commit 1902defd32fa98227acad02dabe7f90ee546ec5b