diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 01ef1a22..fc1679cb 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -2,9 +2,10 @@ name: Analysis on: [push, pull_request] jobs: - build: + analysis: name: Static Analysis runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - uses: actions/checkout@v2 with: @@ -28,16 +29,27 @@ jobs: run: | mkdir -p ./.sonar/scanner dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + - name: Wait for tests to run + uses: lewagon/wait-on-check-action@master + with: + ref: ${{github.ref}} + check-name: tests + repo-token: ${{secrets.GITHUB_TOKEN}} + running-workflow-name: analysis + allowed-conclusions: success,skipped,cancelled,neutral,failure + - name: Download coverage report + uses: dawidd6/action-download-artifact@v2 + with: + commit: ${{env.COMMIT_SHA}} + workflow: tests.yml + github_token: ${{secrets.GITHUB_TOKEN}} - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: bash run: | - dotnet test \ - '-p:CollectCoverage=true;CoverletOutputFormat=opencover' \ - '-p:SkipTranscoder=true;SkipWebApp=true' || echo "Test failed. Skipping..." - + find . -name 'coverage.opencover.xml' dotnet build-server shutdown ./.sonar/scanner/dotnet-sonarscanner begin \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67ab67dd..65a3646f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,17 +3,40 @@ name: Testing on: [push, pull_request] jobs: - build: + tests: runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + container: mcr.microsoft.com/dotnet/sdk:5.0 + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.x - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' + run: | + dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true + cp ./Kyoo.Common/bin/Debug/net5.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./Kyoo.Tests/bin/Debug/net5.0/ - name: Test - run: dotnet test --no-build + run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover' + env: + POSTGRES_HOST: postgres + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres + - name: Sanitize coverage output + if: ${{ always() }} + run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: coverage.xml + path: "**/coverage.opencover.xml" diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 2cd0c909..938d3029 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -77,6 +77,11 @@ namespace Kyoo.Controllers /// IProviderRepository ProviderRepository { get; } + /// + /// The repository that handle users. + /// + IUserRepository UserRepository { get; } + /// /// Get the resource by it's ID /// @@ -149,16 +154,6 @@ namespace Kyoo.Controllers [ItemNotNull] Task Get(string showSlug, int seasonNumber, int episodeNumber); - /// - /// Get a track from it's slug and it's type. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The track found - [ItemNotNull] - Task Get(string slug, StreamType type = StreamType.Unknown); - /// /// Get the resource by it's ID or null if it is not found. /// @@ -224,15 +219,6 @@ namespace Kyoo.Controllers [ItemCanBeNull] Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); - /// - /// Get a track from it's slug and it's type or null if it is not found. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// The track found - [ItemCanBeNull] - Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); - /// /// Load a related resource @@ -242,6 +228,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression> member) where T : class, IResource where T2 : class, IResource, new(); @@ -254,6 +243,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression>> member) where T : class, IResource where T2 : class, new(); @@ -265,6 +257,9 @@ namespace Kyoo.Controllers /// The name of the resource to load (case sensitive) /// The type of the source object /// The param + /// + /// + /// Task Load([NotNull] T obj, string memberName) where T : class, IResource; @@ -273,6 +268,9 @@ namespace Kyoo.Controllers /// /// The source object. /// The name of the resource to load (case sensitive) + /// + /// + /// Task Load([NotNull] IResource obj, string memberName); /// diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs index ce9592f9..a0c30cbb 100644 --- a/Kyoo.Common/Controllers/IMetadataProvider.cs +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -16,6 +16,6 @@ namespace Kyoo.Controllers Task GetSeason(Show show, int seasonNumber); - Task GetEpisode(Show show, int seasonNumber, int episodeNumber, int absoluteNumber); + Task GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber); } } diff --git a/Kyoo.Common/Controllers/IProviderManager.cs b/Kyoo.Common/Controllers/IProviderManager.cs index d1136052..dd83a283 100644 --- a/Kyoo.Common/Controllers/IProviderManager.cs +++ b/Kyoo.Common/Controllers/IProviderManager.cs @@ -11,7 +11,7 @@ namespace Kyoo.Controllers Task SearchShow(string showName, bool isMovie, Library library); 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 GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, 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 dad2d5e3..f7be69ad 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -242,49 +241,13 @@ namespace Kyoo.Controllers /// The resource to delete /// If the item is not found Task Delete([NotNull] T obj); - + /// - /// Delete a list of resources. - /// - /// One or multiple resources to delete - /// If the item is not found - Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resources to delete - /// If the item is not found - Task DeleteRange(IEnumerable objs); - /// - /// Delete a list of resources. - /// - /// One or multiple resource's id - /// If the item is not found - Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resource's id - /// If the item is not found - Task DeleteRange(IEnumerable ids); - /// - /// Delete a list of resources. - /// - /// One or multiple resource's slug - /// If the item is not found - Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); - /// - /// Delete a list of resources. - /// - /// An enumerable of resource's slug - /// If the item is not found - Task DeleteRange(IEnumerable slugs); - /// - /// Delete a list of resources. + /// Delete all resources that match the predicate. /// /// A predicate to filter resources to delete. Every resource that match this will be deleted. /// If the item is not found - Task DeleteRange([NotNull] Expression> where); + Task DeleteAll([NotNull] Expression> where); } /// @@ -412,25 +375,7 @@ namespace Kyoo.Controllers /// /// A repository to handle tracks /// - public interface ITrackRepository : IRepository - { - /// - /// Get a track from it's slug and it's type. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// If the item is not found - /// The track found - Task Get(string slug, StreamType type = StreamType.Unknown); - - /// - /// Get a track from it's slug and it's type or null if it is not found. - /// - /// The slug of the track - /// The type (Video, Audio or Subtitle) - /// The track found - Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); - } + public interface ITrackRepository : IRepository { } /// /// A repository to handle libraries. @@ -631,10 +576,12 @@ namespace Kyoo.Controllers /// A predicate to add arbitrary filter /// Sort information (sort order & sort by) /// Pagination information (where to start and how many to get) + /// The type of metadata to retrieve /// A filtered list of external ids. - Task> GetMetadataID(Expression> where = null, - Sort sort = default, - Pagination limit = default); + Task>> GetMetadataID(Expression, bool>> where = null, + Sort> sort = default, + Pagination limit = default) + where T : class, IResource; /// /// Get a list of external ids that match all filters @@ -643,10 +590,11 @@ namespace Kyoo.Controllers /// A sort by expression /// Pagination information (where to start and how many to get) /// A filtered list of external ids. - Task> GetMetadataID([Optional] Expression> where, - Expression> sort, + Task>> GetMetadataID([Optional] Expression, bool>> where, + Expression, object>> sort, Pagination limit = default - ) => GetMetadataID(where, new Sort(sort), limit); + ) where T : class, IResource + => GetMetadataID(where, new Sort>(sort), limit); } /// diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index ce34f267..12ea0b2a 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -37,6 +37,8 @@ namespace Kyoo.Controllers public IGenreRepository GenreRepository { get; } /// public IProviderRepository ProviderRepository { get; } + /// + public IUserRepository UserRepository { get; } /// @@ -58,6 +60,7 @@ namespace Kyoo.Controllers StudioRepository = GetRepository() as IStudioRepository; GenreRepository = GetRepository() as IGenreRepository; ProviderRepository = GetRepository() as IProviderRepository; + UserRepository = GetRepository() as IUserRepository; } /// @@ -114,12 +117,6 @@ namespace Kyoo.Controllers return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); } - /// - public Task Get(string slug, StreamType type = StreamType.Unknown) - { - return TrackRepository.Get(slug, type); - } - /// public async Task GetOrDefault(int id) where T : class, IResource @@ -165,12 +162,6 @@ namespace Kyoo.Controllers return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber); } - /// - public async Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) - { - return await TrackRepository.GetOrDefault(slug, type); - } - /// public Task Load(T obj, Expression> member) where T : class, IResource @@ -250,9 +241,9 @@ namespace Kyoo.Controllers (Show s, nameof(Show.ExternalIDs)) => SetRelation(s, - ProviderRepository.GetMetadataID(x => x.ShowID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Show = y; x.ShowID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Show s, nameof(Show.Genres)) => GenreRepository .GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) @@ -290,9 +281,9 @@ namespace Kyoo.Controllers (Season s, nameof(Season.ExternalIDs)) => SetRelation(s, - ProviderRepository.GetMetadataID(x => x.SeasonID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Season = y; x.SeasonID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Season s, nameof(Season.Episodes)) => SetRelation(s, EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), @@ -309,9 +300,9 @@ namespace Kyoo.Controllers (Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e, - ProviderRepository.GetMetadataID(x => x.EpisodeID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (Episode e, nameof(Episode.Tracks)) => SetRelation(e, TrackRepository.GetAll(x => x.Episode.ID == obj.ID), @@ -355,9 +346,9 @@ namespace Kyoo.Controllers (People p, nameof(People.ExternalIDs)) => SetRelation(p, - ProviderRepository.GetMetadataID(x => x.PeopleID == obj.ID), + ProviderRepository.GetMetadataID(x => x.FirstID == obj.ID), (x, y) => x.ExternalIDs = y, - (x, y) => { x.People = y; x.PeopleID = y.ID; }), + (x, y) => { x.First = y; x.FirstID = y.ID; }), (People p, nameof(People.Roles)) => PeopleRepository .GetFromPeople(obj.ID) diff --git a/Kyoo.Common/Models/Attributes/ComputedAttribute.cs b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs new file mode 100644 index 00000000..b7f07048 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/ComputedAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Kyoo.Models.Attributes +{ + /// + /// An attribute to inform that the property is computed automatically and can't be assigned manually. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ComputedAttribute : NotMergeableAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs index 1e9a8ece..b036acdf 100644 --- a/Kyoo.Common/Models/Attributes/InjectedAttribute.cs +++ b/Kyoo.Common/Models/Attributes/InjectedAttribute.cs @@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes /// An attribute to inform that the service will be injected automatically by a service provider. /// /// - /// It should only be used on and will be injected before calling + /// It should only be used on and will be injected before calling . + /// It can also be used on and it will be injected before calling . /// [AttributeUsage(AttributeTargets.Property)] [MeansImplicitUse(ImplicitUseKindFlags.Assign)] diff --git a/Kyoo.Common/Models/Attributes/LinkAttribute.cs b/Kyoo.Common/Models/Attributes/LinkAttribute.cs new file mode 100644 index 00000000..d98ad90a --- /dev/null +++ b/Kyoo.Common/Models/Attributes/LinkAttribute.cs @@ -0,0 +1,13 @@ +using System; +using JetBrains.Annotations; +using Kyoo.Models.Attributes; + +namespace Kyoo.Common.Models.Attributes +{ + /// + /// An attribute to mark Link properties on resource. + /// + [AttributeUsage(AttributeTargets.Property)] + [MeansImplicitUse] + public class LinkAttribute : SerializeIgnoreAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/RelationAttributes.cs b/Kyoo.Common/Models/Attributes/RelationAttributes.cs index aac0e633..ef84f5e5 100644 --- a/Kyoo.Common/Models/Attributes/RelationAttributes.cs +++ b/Kyoo.Common/Models/Attributes/RelationAttributes.cs @@ -1,17 +1,34 @@ using System; +using Kyoo.Controllers; namespace Kyoo.Models.Attributes { - [AttributeUsage(AttributeTargets.Property, Inherited = false)] + /// + /// The targeted relation can be edited via calls to the repository's method. + /// + [AttributeUsage(AttributeTargets.Property)] public class EditableRelationAttribute : Attribute { } + /// + /// The targeted relation can be loaded via a call to . + /// [AttributeUsage(AttributeTargets.Property)] public class LoadableRelationAttribute : Attribute { + /// + /// The name of the field containing the related resource's ID. + /// public string RelationID { get; } + /// + /// Create a new . + /// public LoadableRelationAttribute() {} + /// + /// Create a new with a baking relationID field. + /// + /// The name of the RelationID field. public LoadableRelationAttribute(string relationID) { RelationID = relationID; diff --git a/Kyoo.Common/Models/Attributes/SerializeAttribute.cs b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs index 3eafb90c..a7958c91 100644 --- a/Kyoo.Common/Models/Attributes/SerializeAttribute.cs +++ b/Kyoo.Common/Models/Attributes/SerializeAttribute.cs @@ -2,17 +2,41 @@ using System; namespace Kyoo.Models.Attributes { + /// + /// Remove an property from the serialization pipeline. It will simply be skipped. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class SerializeIgnoreAttribute : Attribute {} + /// + /// Remove a property from the deserialization pipeline. The user can't input value for this property. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class DeserializeIgnoreAttribute : Attribute {} + /// + /// Change the way the field is serialized. It allow one to use a string format like formatting instead of the default value. + /// This can be disabled for a request by setting the "internal" query string parameter to true. + /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class SerializeAsAttribute : Attribute { + /// + /// The format string to use. + /// public string Format { get; } + /// + /// Create a new with the selected format. + /// + /// + /// The format string can contains any property within {}. It will be replaced by the actual value of the property. + /// You can also use the special value {HOST} that will put the webhost address. + /// + /// + /// The show's poster serialized uses this format string: {HOST}/api/shows/{Slug}/poster + /// + /// The format to use public SerializeAsAttribute(string format) { Format = format; diff --git a/Kyoo.Common/Models/Chapter.cs b/Kyoo.Common/Models/Chapter.cs new file mode 100644 index 00000000..51ccc231 --- /dev/null +++ b/Kyoo.Common/Models/Chapter.cs @@ -0,0 +1,38 @@ +namespace Kyoo.Models +{ + /// + /// A chapter to split an episode in multiple parts. + /// + public class Chapter + { + /// + /// The start time of the chapter (in second from the start of the episode). + /// + public float StartTime { get; set; } + + /// + /// The end time of the chapter (in second from the start of the episode)&. + /// + public float EndTime { get; set; } + + /// + /// The name of this chapter. This should be a human-readable name that could be presented to the user. + /// There should be well-known chapters name for commonly used chapters. + /// For example, use "Opening" for the introduction-song and "Credits" for the end chapter with credits. + /// + public string Name { get; set; } + + /// + /// Create a new . + /// + /// The start time of the chapter (in second) + /// The end time of the chapter (in second) + /// The name of this chapter + public Chapter(float startTime, float endTime, string name) + { + StartTime = startTime; + EndTime = endTime; + Name = name; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs index 78f604f2..4df07770 100644 --- a/Kyoo.Common/Models/LibraryItem.cs +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -1,10 +1,12 @@ using System; using System.Linq.Expressions; -using JetBrains.Annotations; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// The type of item, ether a show, a movie or a collection. + /// public enum ItemType { Show, @@ -12,22 +14,67 @@ namespace Kyoo.Models Collection } + /// + /// A type union between and . + /// This is used to list content put inside a library. + /// public class LibraryItem : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The title of the show or collection. + /// public string Title { get; set; } + + /// + /// The summary of the show or collection. + /// public string Overview { get; set; } + + /// + /// Is this show airing, not aired yet or finished? This is only applicable for shows. + /// public Status? Status { get; set; } - public string TrailerUrl { get; set; } - public int? StartYear { get; set; } - public int? EndYear { get; set; } - [SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } - [UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show"; + + /// + /// The date this show or collection started airing. It can be null if this is unknown. + /// + public DateTime? StartAir { get; set; } + + /// + /// The date this show or collection finished airing. + /// It must be after the but can be the same (example: for movies). + /// It can also be null if this is unknown. + /// + public DateTime? EndAir { get; set; } + + /// + /// The path of this item's poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// + [SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The type of this item (ether a collection, a show or a movie). + /// public ItemType Type { get; set; } + + /// + /// Create a new, empty . + /// public LibraryItem() {} + /// + /// Create a from a show. + /// + /// The show that this library item should represent. public LibraryItem(Show show) { ID = show.ID; @@ -35,13 +82,16 @@ namespace Kyoo.Models Title = show.Title; Overview = show.Overview; Status = show.Status; - TrailerUrl = show.TrailerUrl; - StartYear = show.StartYear; - EndYear = show.EndYear; + StartAir = show.StartAir; + EndAir = show.EndAir; Poster = show.Poster; Type = show.IsMovie ? ItemType.Movie : ItemType.Show; } + /// + /// Create a from a collection + /// + /// The collection that this library item should represent. public LibraryItem(Collection collection) { ID = -collection.ID; @@ -49,13 +99,15 @@ namespace Kyoo.Models Title = collection.Name; Overview = collection.Overview; Status = Models.Status.Unknown; - TrailerUrl = null; - StartYear = null; - EndYear = null; + StartAir = null; + EndAir = null; Poster = collection.Poster; Type = ItemType.Collection; } + /// + /// An expression to create a representing a show. + /// public static Expression> FromShow => x => new LibraryItem { ID = x.ID, @@ -63,13 +115,15 @@ namespace Kyoo.Models Title = x.Title, Overview = x.Overview, Status = x.Status, - TrailerUrl = x.TrailerUrl, - StartYear = x.StartYear, - EndYear = x.EndYear, + StartAir = x.StartAir, + EndAir = x.EndAir, Poster= x.Poster, Type = x.IsMovie ? ItemType.Movie : ItemType.Show }; + /// + /// An expression to create a representing a collection. + /// public static Expression> FromCollection => x => new LibraryItem { ID = -x.ID, @@ -77,10 +131,9 @@ namespace Kyoo.Models Title = x.Name, Overview = x.Overview, Status = Models.Status.Unknown, - TrailerUrl = null, - StartYear = null, - EndYear = null, - Poster= x.Poster, + StartAir = null, + EndAir = null, + Poster = x.Poster, Type = ItemType.Collection }; } diff --git a/Kyoo.Common/Models/Link.cs b/Kyoo.Common/Models/Link.cs index 2df85f1f..6d815af9 100644 --- a/Kyoo.Common/Models/Link.cs +++ b/Kyoo.Common/Models/Link.cs @@ -1,33 +1,64 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; namespace Kyoo.Models { + /// + /// A class representing a link between two resources. + /// + /// + /// Links should only be used on the data layer and not on other application code. + /// public class Link { + /// + /// The ID of the first item of the link. + /// The first item of the link should be the one to own the link. + /// public int FirstID { get; set; } + + /// + /// The ID of the second item of this link + /// The second item of the link should be the owned resource. + /// public int SecondID { get; set; } + /// + /// Create a new typeless . + /// public Link() {} + /// + /// Create a new typeless with two IDs. + /// + /// The ID of the first resource + /// The ID of the second resource public Link(int firstID, int secondID) { FirstID = firstID; SecondID = secondID; } + /// + /// Create a new typeless between two resources. + /// + /// The first resource + /// The second resource public Link(IResource first, IResource second) { FirstID = first.ID; SecondID = second.ID; } - public static Link Create(IResource first, IResource second) - { - return new(first, second); - } - + /// + /// Create a new typed link between two resources. + /// This method can be used instead of the constructor to make use of generic parameters deduction. + /// + /// The first resource + /// The second resource + /// The type of the first resource + /// The type of the second resource + /// A newly created typed link with both resources public static Link Create(T first, T2 second) where T : class, IResource where T2 : class, IResource @@ -35,6 +66,16 @@ namespace Kyoo.Models return new(first, second); } + /// + /// Create a new typed link between two resources without storing references to resources. + /// This is the same as but this method does not set + /// and fields. Only IDs are stored and not references. + /// + /// The first resource + /// The second resource + /// The type of the first resource + /// The type of the second resource + /// A newly created typed link with both resources public static Link UCreate(T first, T2 second) where T : class, IResource where T2 : class, IResource @@ -42,6 +83,9 @@ namespace Kyoo.Models return new(first, second, true); } + /// + /// The expression to retrieve the unique ID of a Link. This is an aggregate of the two resources IDs. + /// public static Expression> PrimaryKey { get @@ -51,17 +95,41 @@ namespace Kyoo.Models } } + /// + /// A strongly typed link between two resources. + /// + /// The type of the first resource + /// The type of the second resource public class Link : Link where T1 : class, IResource where T2 : class, IResource { - public virtual T1 First { get; set; } - public virtual T2 Second { get; set; } + /// + /// A reference of the first resource. + /// + public T1 First { get; set; } + + /// + /// A reference to the second resource. + /// + public T2 Second { get; set; } + /// + /// Create a new, empty, typed . + /// public Link() {} - [SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")] + + /// + /// Create a new typed link with two resources. + /// + /// The first resource + /// The second resource + /// + /// True if no reference to resources should be kept, false otherwise. + /// The default is false (references are kept). + /// public Link(T1 first, T2 second, bool privateItems = false) : base(first, second) { @@ -71,10 +139,18 @@ namespace Kyoo.Models Second = second; } + /// + /// Create a new typed link with IDs only. + /// + /// The ID of the first resource + /// The ID of the second resource public Link(int firstID, int secondID) : base(firstID, secondID) { } + /// + /// The expression to retrieve the unique ID of a typed Link. This is an aggregate of the two resources IDs. + /// public new static Expression, object>> PrimaryKey { get diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index d1752d50..34872314 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -1,26 +1,34 @@ -using Kyoo.Models.Attributes; +using System; +using System.Linq.Expressions; namespace Kyoo.Models { - public class MetadataID + /// + /// ID and link of an item on an external provider. + /// + /// + public class MetadataID : Link + where T : class, IResource { - [SerializeIgnore] public int ID { get; set; } - [SerializeIgnore] public int ProviderID { get; set; } - public virtual Provider Provider {get; set; } - - [SerializeIgnore] public int? ShowID { get; set; } - [SerializeIgnore] public virtual Show Show { get; set; } - - [SerializeIgnore] public int? EpisodeID { get; set; } - [SerializeIgnore] public virtual Episode Episode { get; set; } - - [SerializeIgnore] public int? SeasonID { get; set; } - [SerializeIgnore] public virtual Season Season { get; set; } - - [SerializeIgnore] public int? PeopleID { get; set; } - [SerializeIgnore] public virtual People People { get; set; } - + /// + /// The ID of the resource on the external provider. + /// public string DataID { get; set; } + + /// + /// The URL of the resource on the external provider. + /// public string Link { get; set; } + + /// + /// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs. + /// + 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/Page.cs b/Kyoo.Common/Models/Page.cs index 71f9c7cd..023aac3e 100644 --- a/Kyoo.Common/Models/Page.cs +++ b/Kyoo.Common/Models/Page.cs @@ -3,22 +3,45 @@ using System.Linq; namespace Kyoo.Models { + /// + /// A page of resource that contains information about the pagination of resources. + /// + /// The type of resource contained in this page. public class Page where T : IResource { - public string This { get; set; } - public string First { get; set; } - public string Next { get; set; } + /// + /// The link of the current page. + /// + public string This { get; } + + /// + /// The link of the first page. + /// + public string First { get; } + + /// + /// The link of the next page. + /// + public string Next { get; } + /// + /// The number of items in the current page. + /// public int Count => Items.Count; - public ICollection Items { get; set; } - - public Page() { } - - public Page(ICollection items) - { - Items = items; - } - + + /// + /// The list of items in the page. + /// + public ICollection Items { get; } + + + /// + /// Create a new . + /// + /// The list of items in the page. + /// The link of the current page. + /// The link of the next page. + /// The link of the first page. public Page(ICollection items, string @this, string next, string first) { Items = items; @@ -27,7 +50,14 @@ namespace Kyoo.Models First = first; } - public Page(ICollection items, + /// + /// Create a new and compute the urls. + /// + /// The list of items in the page. + /// The base url of the resources available from this page. + /// The list of query strings of the current page + /// The number of items requested for the current page. + public Page(ICollection items, string url, Dictionary query, int limit) diff --git a/Kyoo.Common/Models/PeopleRole.cs b/Kyoo.Common/Models/PeopleRole.cs index be48abd1..062bb3e8 100644 --- a/Kyoo.Common/Models/PeopleRole.cs +++ b/Kyoo.Common/Models/PeopleRole.cs @@ -1,17 +1,55 @@ -using Kyoo.Models.Attributes; - namespace Kyoo.Models { + /// + /// A role a person played for a show. It can be an actor, musician, voice actor, director, writer... + /// + /// + /// This class is not serialized like other classes. + /// Based on the field, it is serialized like + /// a show with two extra fields ( and ). + /// public class PeopleRole : IResource { - [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 int ID { get; set; } + + /// + public string Slug => ForPeople ? Show.Slug : People.Slug; + + /// + /// Should this role be used as a Show substitute (the value is false) or + /// as a People substitute (the value is true). + /// + public bool ForPeople { get; set; } + + /// + /// The ID of the People playing the role. + /// + public int PeopleID { get; set; } + /// + /// The people that played this role. + /// + public People People { get; set; } + + /// + /// The ID of the Show where the People playing in. + /// + public int ShowID { get; set; } + /// + /// The show where the People played in. + /// + public Show Show { get; set; } + + /// + /// The type of work the person has done for the show. + /// That can be something like "Actor", "Writer", "Music", "Voice Actor"... + /// public string Type { get; set; } + + /// + /// The role the People played. + /// This is mostly used to inform witch character was played for actor and voice actors. + /// + public string Role { get; set; } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Collection.cs b/Kyoo.Common/Models/Resources/Collection.cs index 3c7bed25..8162ff16 100644 --- a/Kyoo.Common/Models/Resources/Collection.cs +++ b/Kyoo.Common/Models/Resources/Collection.cs @@ -1,31 +1,59 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A class representing collections of . + /// A collection can also be stored in a . + /// public class Collection : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this collection. + /// public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The description of this collection. + /// public string Overview { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of shows contained in this collection. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of libraries that contains this collection. + /// + [LoadableRelation] public 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) - { - Slug = slug; - Name = name; - Overview = overview; - Poster = poster; - } + /// + /// The internal link between this collection and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this collection and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } +#endif } } diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 29aeab06..cdb7fb8f 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,57 +1,166 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { - public class Episode : IResource, IOnMerge + /// + /// A class to represent a single show's episode. + /// + public class Episode : IResource { + /// public int ID { get; set; } - public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); - [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; + /// + [Computed] public string Slug + { + get + { + if (ShowSlug == null && Show == null) + return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); + return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + } + [UsedImplicitly] [NotNull] private set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); + + if (match.Success) + { + ShowSlug = match.Groups["show"].Value; + SeasonNumber = int.Parse(match.Groups["season"].Value); + EpisodeNumber = int.Parse(match.Groups["episode"].Value); + } + else + { + match = Regex.Match(value, @"(?.+)-(?\d+)"); + if (match.Success) + { + ShowSlug = match.Groups["show"].Value; + AbsoluteNumber = int.Parse(match.Groups["absolute"].Value); + } + else + ShowSlug = value; + SeasonNumber = null; + EpisodeNumber = null; + } + } + } + + /// + /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. + /// + [SerializeIgnore] public string ShowSlug { private get; set; } + + /// + /// The ID of the Show containing this episode. + /// + [SerializeIgnore] public int ShowID { get; set; } + /// + /// The show that contains this episode. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } + + /// + /// The ID of the Season containing this episode. + /// + [SerializeIgnore] public int? SeasonID { get; set; } + /// + /// The season that contains this episode. This must be explicitly loaded via a call to . + /// This can be null if the season is unknown and the episode is only identified by it's . + /// + [LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } + + /// + /// The season in witch this episode is in. + /// + public int? SeasonNumber { get; set; } + + /// + /// The number of this episode is it's season. + /// + public int? EpisodeNumber { get; set; } + + /// + /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + /// + public int? AbsoluteNumber { get; set; } + + /// + /// The path of the video file for this episode. Any format supported by a is allowed. + /// [SerializeIgnore] public string Path { get; set; } + /// + /// The path of this episode's thumbnail. + /// By default, the http path for the thumbnail is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; } + + /// + /// The title of this episode. + /// public string Title { get; set; } + + /// + /// The overview of this episode. + /// public string Overview { get; set; } + + /// + /// The release date of this episode. It can be null if unknown. + /// public DateTime? ReleaseDate { get; set; } - public int Runtime { get; set; } //This runtime variable should be in minutes + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - - [EditableRelation] [LoadableRelation] public virtual ICollection Tracks { get; set; } + /// + /// The list of tracks this episode has. This lists video, audio and subtitles available. + /// + [EditableRelation] [LoadableRelation] public ICollection Tracks { get; set; } - public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber) + /// + /// Get the slug of an episode. + /// + /// The slug of the show. It can't be null. + /// + /// The season in which the episode is. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. + /// + /// + /// The number of the episode in it's season. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. + /// + /// + /// The absolute number of this show. + /// If you don't know it or this is a movie, use null + /// + /// The slug corresponding to the given arguments + /// The given show slug was null. + public static string GetSlug([NotNull] string showSlug, + int? seasonNumber, + int? episodeNumber, + int? absoluteNumber = null) { if (showSlug == null) - throw new ArgumentException("Show's slug is null. Can't find episode's slug."); + throw new ArgumentNullException(nameof(showSlug)); return seasonNumber switch { - -1 when absoluteNumber == -1 => showSlug, - -1 => $"{showSlug}-{absoluteNumber}", + null when absoluteNumber == null => showSlug, + null => $"{showSlug}-{absoluteNumber}", _ => $"{showSlug}-s{seasonNumber}e{episodeNumber}" }; } - - public void OnMerge(object merged) - { - Episode other = (Episode)merged; - if (SeasonNumber == -1 && other.SeasonNumber != -1) - SeasonNumber = other.SeasonNumber; - if (EpisodeNumber == -1 && other.EpisodeNumber != -1) - EpisodeNumber = other.EpisodeNumber; - if (AbsoluteNumber == -1 && other.AbsoluteNumber != -1) - AbsoluteNumber = other.AbsoluteNumber; - } } } diff --git a/Kyoo.Common/Models/Resources/Genre.cs b/Kyoo.Common/Models/Resources/Genre.cs index cd6086df..c7aaa76f 100644 --- a/Kyoo.Common/Models/Resources/Genre.cs +++ b/Kyoo.Common/Models/Resources/Genre.cs @@ -1,40 +1,51 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A genre that allow one to specify categories for shows. + /// public class Genre : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this genre. + /// public string Name { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } + /// + /// The list of shows that have this genre. + /// + [LoadableRelation] public ICollection Shows { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } + /// + /// The internal link between this genre and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } #endif - + /// + /// Create a new, empty . + /// public Genre() {} + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this genre. public Genre(string name) { Slug = Utility.ToSlug(name); Name = name; } - - public Genre(string slug, string name) - { - Slug = slug; - Name = name; - } - - public Genre(int id, string slug, string name) - { - ID = id; - Slug = slug; - Name = name; - } } } diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs index c4c4231b..4867bc0a 100644 --- a/Kyoo.Common/Models/Resources/IResource.cs +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -1,3 +1,5 @@ +using Kyoo.Controllers; + namespace Kyoo.Models { /// @@ -8,6 +10,10 @@ namespace Kyoo.Models /// /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. /// + /// + /// You don't need to specify an ID manually when creating a new resource, + /// this field is automatically assigned by the . + /// public int ID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index c8148544..a72d6a37 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -1,24 +1,60 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A library containing and . + /// public class Library : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this library. + /// public string Name { get; set; } + + /// + /// The list of paths that this library is responsible for. This is mainly used by the Scan task. + /// public string[] Paths { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } + /// + /// The list of used for items in this library. + /// + [EditableRelation] [LoadableRelation] public ICollection Providers { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Collections { get; set; } + /// + /// The list of shows in this library. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of collections in this library. + /// + [LoadableRelation] public 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; } + /// + /// The internal link between this library and provider in the list. + /// + [Link] public ICollection> ProviderLinks { get; set; } + + /// + /// The internal link between this library and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this library and collection in the list. + /// + [Link] public ICollection> CollectionLinks { get; set; } #endif } } diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 46b86143..7ae04613 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -3,14 +3,37 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// An actor, voice actor, writer, animator, somebody who worked on a . + /// public class People : IResource { + /// public int ID { get; set; } - public string Slug { get; set; } - public string Name { get; set; } - [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Roles { get; set; } + /// + public string Slug { get; set; } + + /// + /// The name of this person. + /// + public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// + [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The link to metadata providers that this person has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } + + /// + /// The list of roles this person has played in. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection Roles { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/Provider.cs b/Kyoo.Common/Models/Resources/Provider.cs index 6a19f27c..e13a9be4 100644 --- a/Kyoo.Common/Models/Resources/Provider.cs +++ b/Kyoo.Common/Models/Resources/Provider.cs @@ -1,37 +1,67 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// This class contains metadata about . + /// You can have providers even if you don't have the corresponding . + /// public class Provider : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this provider. + /// public string Name { get; set; } + + /// + /// The path of this provider's logo. + /// By default, the http path for this logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; } + + /// + /// The extension of the logo. This is used for http responses. + /// [SerializeIgnore] public string LogoExtension { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of libraries that uses this provider. + /// + [LoadableRelation] public ICollection Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } - [SerializeIgnore] public virtual ICollection MetadataLinks { get; set; } + /// + /// The internal link between this provider and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } #endif + /// + /// Create a new, default, + /// public Provider() { } + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this provider. + /// The logo of this provider. public Provider(string name, string logo) { Slug = Utility.ToSlug(name); Name = name; Logo = logo; } - - public Provider(int id, string name, string logo) - { - ID = id; - Slug = Utility.ToSlug(name); - Name = name; - Logo = logo; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index b3f7ab27..9b292020 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,25 +1,94 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A season of a . + /// public class Season : IResource { + /// public int ID { get; set; } - public string Slug => $"{ShowSlug}-s{SeasonNumber}"; - [SerializeIgnore] public int ShowID { get; set; } + + /// + [Computed] public string Slug + { + get + { + if (ShowSlug == null && Show == null) + return $"{ShowID}-s{SeasonNumber}"; + return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; + } + [UsedImplicitly] [NotNull] private set + { + Match match = Regex.Match(value ?? "", @"(?.+)-s(?\d+)"); + + if (!match.Success) + throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); + ShowSlug = match.Groups["show"].Value; + SeasonNumber = int.Parse(match.Groups["season"].Value); + } + } + + /// + /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. + /// [SerializeIgnore] public string ShowSlug { private get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + + /// + /// The ID of the Show containing this season. + /// + [SerializeIgnore] public int ShowID { get; set; } + /// + /// The show that contains this season. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } - public int SeasonNumber { get; set; } = -1; + /// + /// The number of this season. This can be set to 0 to indicate specials. + /// + public int SeasonNumber { get; set; } + /// + /// The title of this season. + /// public string Title { get; set; } + + /// + /// A quick overview of this season. + /// public string Overview { get; set; } - public int? Year { get; set; } + + /// + /// The starting air date of this season. + /// + public DateTime? StartDate { get; set; } + + /// + /// The ending date of this season. + /// + public DateTime? EndDate { get; set; } + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } + + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } - [LoadableRelation] public virtual ICollection Episodes { get; set; } + /// + /// The list of episodes that this season contains. + /// + [LoadableRelation] public ICollection Episodes { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index e7d14d79..ffb2ae49 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -1,57 +1,173 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Kyoo.Common.Models.Attributes; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A series or a movie. + /// public class Show : IResource, IOnMerge { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The title of this show. + /// public string Title { get; set; } + + /// + /// The list of alternative titles of this show. + /// [EditableRelation] public string[] Aliases { get; set; } + + /// + /// The path of the root directory of this show. + /// This can be any kind of path supported by + /// [SerializeIgnore] public string Path { get; set; } + + /// + /// The summary of this show. + /// public string Overview { get; set; } + + /// + /// Is this show airing, not aired yet or finished? + /// public Status? Status { get; set; } + + /// + /// An URL to a trailer. This could be any path supported by the . + /// + /// TODO for now, this is set to a youtube url. It should be cached and converted to a local file. public string TrailerUrl { get; set; } + + /// + /// The date this show started airing. It can be null if this is unknown. + /// + public DateTime? StartAir { get; set; } + + /// + /// The date this show finished airing. + /// It must be after the but can be the same (example: for movies). + /// It can also be null if this is unknown. + /// + public DateTime? EndAir { get; set; } - public int? StartYear { get; set; } - public int? EndYear { get; set; } - + /// + /// The path of this show's poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The path of this show's logo. + /// By default, the http path for this logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; } + + /// + /// The path of this show's backdrop. + /// By default, the http path for this backdrop is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; } + /// + /// True if this show represent a movie, false otherwise. + /// public bool IsMovie { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - + /// + /// The link to metadata providers that this show has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection> ExternalIDs { get; set; } + /// + /// The ID of the Studio that made this show. + /// [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; } + /// + /// The Studio that made this show. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; } + + /// + /// The list of genres (themes) this show has. + /// + [LoadableRelation] [EditableRelation] public ICollection Genres { get; set; } + + /// + /// The list of people that made this show. + /// + [LoadableRelation] [EditableRelation] public ICollection People { get; set; } + + /// + /// The different seasons in this show. If this is a movie, this list is always null or empty. + /// + [LoadableRelation] public ICollection Seasons { get; set; } + + /// + /// The list of episodes in this show. + /// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null). + /// Having an episode is necessary to store metadata and tracks. + /// + [LoadableRelation] public ICollection Episodes { get; set; } + + /// + /// The list of libraries that contains this show. + /// + [LoadableRelation] public ICollection Libraries { get; set; } + + /// + /// The list of collections that contains this show. + /// + [LoadableRelation] public 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; } + /// + /// The internal link between this show and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } + + /// + /// The internal link between this show and collections in the list. + /// + [Link] public ICollection> CollectionLinks { get; set; } + + /// + /// The internal link between this show and genres in the list. + /// + [Link] public ICollection> GenreLinks { get; set; } #endif + /// + /// Retrieve the internal provider's ID of a show using it's provider slug. + /// + /// This method will never return anything if the are not loaded. + /// The slug of the provider + /// The field of the asked provider. public string GetID(string provider) { - return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID; + return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID; } - public virtual void OnMerge(object merged) + /// + public void OnMerge(object merged) { if (ExternalIDs != null) - foreach (MetadataID id in ExternalIDs) - id.Show = this; + foreach (MetadataID id in ExternalIDs) + id.First = this; if (People != null) foreach (PeopleRole link in People) link.Show = this; @@ -64,5 +180,8 @@ namespace Kyoo.Models } } + /// + /// The enum containing show's status. + /// public enum Status { Finished, Airing, Planned, Unknown } } diff --git a/Kyoo.Common/Models/Resources/Studio.cs b/Kyoo.Common/Models/Resources/Studio.cs index 9eea3a7b..ebc3c4c1 100644 --- a/Kyoo.Common/Models/Resources/Studio.cs +++ b/Kyoo.Common/Models/Resources/Studio.cs @@ -3,31 +3,40 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A studio that make shows. + /// public class Studio : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this studio. + /// public string Name { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } + /// + /// The list of shows that are made by this studio. + /// + [LoadableRelation] public ICollection Shows { get; set; } + /// + /// Create a new, empty, . + /// public Studio() { } + /// + /// Create a new with a specific name, the slug is calculated automatically. + /// + /// The name of the studio. public Studio(string name) { Slug = Utility.ToSlug(name); Name = name; } - - public Studio(string slug, string name) - { - Slug = slug; - Name = name; - } - - public static Studio Default() - { - return new Studio("unknown", "Unknown Studio"); - } } } diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 92438e0a..f093699b 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -1,11 +1,16 @@ -using Kyoo.Models.Watch; +using System; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using JetBrains.Annotations; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// The list of available stream types. + /// Attachments are only used temporarily by the transcoder but are not stored in a database. + /// public enum StreamType { Unknown = 0, @@ -15,61 +20,106 @@ namespace Kyoo.Models Attachment = 4 } - namespace Watch - { - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - public class Stream - { - public string Title { get; set; } - public string Language { get; set; } - public string Codec { get; set; } - [MarshalAs(UnmanagedType.I1)] public bool isDefault; - [MarshalAs(UnmanagedType.I1)] public bool isForced; - [SerializeIgnore] public string Path { get; set; } - [SerializeIgnore] public StreamType Type { get; set; } - - public Stream() {} - - public Stream(string title, string language, string codec, bool isDefault, bool isForced, string path, StreamType type) - { - Title = title; - Language = language; - Codec = codec; - this.isDefault = isDefault; - this.isForced = isForced; - Path = path; - Type = type; - } - - public Stream(Stream stream) - { - Title = stream.Title; - Language = stream.Language; - isDefault = stream.isDefault; - isForced = stream.isForced; - Codec = stream.Codec; - Path = stream.Path; - Type = stream.Type; - } - } - } - - public class Track : Stream, IResource + /// + /// A video, audio or subtitle track for an episode. + /// + public class Track : IResource { + /// public int ID { get; set; } - [SerializeIgnore] public int EpisodeID { get; set; } - public int TrackIndex { get; set; } - public bool IsDefault + + /// + [Computed] public string Slug { - get => isDefault; - set => isDefault = value; - } - public bool IsForced - { - get => isForced; - set => isForced = value; - } + get + { + string type = Type.ToString().ToLower(); + string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; + string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString(); + return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}"; + } + [UsedImplicitly] private set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + Match match = Regex.Match(value, + @"(?[^\.]+)\.(?\w{0,3})(-(?\d+))?(\.(?forced))?\.(?\w+)(\.\w*)?"); + if (!match.Success) + throw new ArgumentException("Invalid track slug. " + + "Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]"); + + EpisodeSlug = match.Groups["ep"].Value; + Language = match.Groups["lang"].Value; + TrackIndex = int.Parse(match.Groups["index"].Value); + IsForced = match.Groups["forced"].Success; + Type = Enum.Parse(match.Groups["type"].Value, true); + } + } + + /// + /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. + /// + [SerializeIgnore] public string EpisodeSlug { private get; set; } + + /// + /// The title of the stream. + /// + public string Title { get; set; } + + /// + /// The language of this stream (as a ISO-639-2 language code) + /// + public string Language { get; set; } + + /// + /// The codec of this stream. + /// + public string Codec { get; set; } + + + /// + /// Is this stream the default one of it's type? + /// + public bool IsDefault { get; set; } + + /// + /// Is this stream tagged as forced? + /// + public bool IsForced { get; set; } + + /// + /// Is this track extern to the episode's file? + /// + public bool IsExternal { get; set; } + + /// + /// The path of this track. + /// + [SerializeIgnore] public string Path { get; set; } + + /// + /// The type of this stream. + /// + [SerializeIgnore] public StreamType Type { get; set; } + + /// + /// The ID of the episode that uses this track. + /// + [SerializeIgnore] public int EpisodeID { get; set; } + /// + /// The episode that uses this track. + /// + [LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; } + + /// + /// The index of this track on the episode. + /// + public int TrackIndex { get; set; } + + /// + /// A user-friendly name for this track. It does not include the track type. + /// public string DisplayName { get @@ -85,61 +135,16 @@ namespace Kyoo.Models name += " Forced"; if (IsExternal) name += " (External)"; - if (Title != null && Title.Length > 1) + if (Title is {Length: > 1}) name += " - " + Title; return name; } } - public string Slug - { - get - { - string type = Type switch - { - 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}"; - } - } - - 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) - : base(title, language, codec, isDefault, isForced, path, type) - { - IsExternal = isExternal; - } - - public Track(Stream stream) - : base(stream) - { - IsExternal = false; - } - //Converting mkv track language to c# system language tag. private static string GetLanguage(string mkvLanguage) { + // TODO delete this and have a real way to get the language string from the ISO-639-2. return mkvLanguage switch { "fre" => "fra", @@ -147,5 +152,32 @@ namespace Kyoo.Models _ => mkvLanguage }; } + + /// + /// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored) + /// + /// The slug to edit + /// The new type of this + /// + /// + /// + /// + public static string EditSlug(string baseSlug, + StreamType type = StreamType.Unknown, + string language = null, + int? index = null, + bool? forced = null) + { + Track track = new() {Slug = baseSlug}; + if (type != StreamType.Unknown) + track.Type = type; + if (language != null) + track.Language = language; + if (index != null) + track.TrackIndex = index.Value; + if (forced != null) + track.IsForced = forced.Value; + return track.Slug; + } } } diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index 94afa240..05f56534 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; namespace Kyoo.Models { @@ -52,7 +53,7 @@ namespace Kyoo.Models /// /// Links between Users and Shows. /// - public ICollection> ShowLinks { get; set; } + [Link] public ICollection> ShowLinks { get; set; } #endif } @@ -62,7 +63,7 @@ namespace Kyoo.Models public class WatchedEpisode : Link { /// - /// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100). + /// Where the player has stopped watching the episode (between 0 and 100). /// public int WatchedPercentage { get; set; } } diff --git a/Kyoo.Common/Models/SearchResult.cs b/Kyoo.Common/Models/SearchResult.cs index 42a25bcf..9ec2bc37 100644 --- a/Kyoo.Common/Models/SearchResult.cs +++ b/Kyoo.Common/Models/SearchResult.cs @@ -2,14 +2,44 @@ namespace Kyoo.Models { + /// + /// Results of a search request. + /// public class SearchResult { - public string Query; - public IEnumerable Collections; - public IEnumerable Shows; - public IEnumerable Episodes; - public IEnumerable People; - public IEnumerable Genres; - public IEnumerable Studios; + /// + /// The query of the search request. + /// + public string Query { get; init; } + + /// + /// The collections that matched the search. + /// + public ICollection Collections { get; init; } + + /// + /// The shows that matched the search. + /// + public ICollection Shows { get; init; } + + /// + /// The episodes that matched the search. + /// + public ICollection Episodes { get; init; } + + /// + /// The people that matched the search. + /// + public ICollection People { get; init; } + + /// + /// The genres that matched the search. + /// + public ICollection Genres { get; init; } + + /// + /// The studios that matched the search. + /// + public ICollection Studios { get; init; } } } diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index 71ec1993..3b64d54e 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -9,95 +9,136 @@ using PathIO = System.IO.Path; namespace Kyoo.Models { - public class Chapter - { - public float StartTime; - public float EndTime; - public string Name; - - public Chapter(float startTime, float endTime, string name) - { - StartTime = startTime; - EndTime = endTime; - Name = name; - } - } - + /// + /// A watch item give information useful for playback. + /// Information about tracks and display information that could be used by the player. + /// This contains mostly data from an with another form. + /// public class WatchItem { + /// + /// The ID of the episode associated with this item. + /// public int EpisodeID { get; set; } - - public string ShowTitle { get; set; } - public string ShowSlug { get; set; } - public int SeasonNumber { get; set; } - public int EpisodeNumber { get; set; } - public int AbsoluteNumber { get; set; } - public string Title { get; set; } + + /// + /// The slug of this episode. + /// public string Slug { get; set; } + + /// + /// The title of the show containing this episode. + /// + public string ShowTitle { get; set; } + + /// + /// The slug of the show containing this episode + /// + public string ShowSlug { get; set; } + + /// + /// The season in witch this episode is in. + /// + public int? SeasonNumber { get; set; } + + /// + /// The number of this episode is it's season. + /// + public int? EpisodeNumber { get; set; } + + /// + /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + /// + public int? AbsoluteNumber { get; set; } + + /// + /// The title of this episode. + /// + public string Title { get; set; } + + /// + /// The release date of this episode. It can be null if unknown. + /// public DateTime? ReleaseDate { get; set; } + + /// + /// The path of the video file for this episode. Any format supported by a is allowed. + /// [SerializeIgnore] public string Path { get; set; } + + /// + /// The episode that come before this one if you follow usual watch orders. + /// If this is the first episode or this is a movie, it will be null. + /// public Episode PreviousEpisode { get; set; } + + /// + /// The episode that come after this one if you follow usual watch orders. + /// If this is the last aired episode or this is a movie, it will be null. + /// public Episode NextEpisode { get; set; } + + /// + /// true if this is a movie, false otherwise. + /// public bool IsMovie { get; set; } + /// + /// The path of this item's poster. + /// By default, the http path for the poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; } + + /// + /// The path of this item's logo. + /// By default, the http path for the logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; } + + /// + /// The path of this item's backdrop. + /// By default, the http path for the backdrop is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; } + /// + /// The container of the video file of this episode. + /// Common containers are mp4, mkv, avi and so on. + /// public string Container { get; set; } + + /// + /// The video track. See for more information. + /// public Track Video { get; set; } + + /// + /// The list of audio tracks. See for more information. + /// public ICollection Audios { get; set; } + + /// + /// The list of subtitles tracks. See for more information. + /// public ICollection Subtitles { get; set; } + + /// + /// The list of chapters. See for more information. + /// public ICollection Chapters { get; set; } + - public WatchItem() { } - - private WatchItem(int episodeID, - Show show, - int seasonNumber, - int episodeNumber, - int absoluteNumber, - string title, - DateTime? releaseDate, - string path) - { - EpisodeID = episodeID; - ShowTitle = show.Title; - ShowSlug = show.Slug; - SeasonNumber = seasonNumber; - EpisodeNumber = episodeNumber; - AbsoluteNumber = absoluteNumber; - 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, absoluteNumber); - } - - private WatchItem(int episodeID, - Show show, - int seasonNumber, - int episodeNumber, - int absoluteNumber, - string title, - DateTime? releaseDate, - string path, - Track video, - ICollection audios, - ICollection subtitles) - : this(episodeID, show, seasonNumber, episodeNumber, absoluteNumber, title, releaseDate, path) - { - Video = video; - Audios = audios; - Subtitles = subtitles; - } - + /// + /// Create a from an . + /// + /// The episode to transform. + /// + /// A library manager to retrieve the next and previous episode and load the show & tracks of the episode. + /// + /// A new WatchItem representing the given episode. public static async Task FromEpisode(Episode ep, ILibraryManager library) { Episode previous = null; @@ -106,41 +147,53 @@ namespace Kyoo.Models await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Tracks); - if (!ep.Show.IsMovie) + if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null) { if (ep.EpisodeNumber > 1) - previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); + previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1); else if (ep.SeasonNumber > 1) { - int count = await library.GetCount(x => x.ShowID == ep.ShowID - && x.SeasonNumber == ep.SeasonNumber - 1); - previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count); + previous = (await library.GetAll(x => x.ShowID == ep.ShowID + && x.SeasonNumber == ep.SeasonNumber.Value - 1, + limit: 1, + sort: new Sort(x => x.EpisodeNumber, true)) + ).FirstOrDefault(); } if (ep.EpisodeNumber >= await library.GetCount(x => x.SeasonID == ep.SeasonID)) - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1); else - next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); + next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1); + } + else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null) + { + previous = await library.GetOrDefault(x => x.ShowID == ep.ShowID + && x.AbsoluteNumber == ep.EpisodeNumber + 1); + next = await library.GetOrDefault(x => x.ShowID == ep.ShowID + && x.AbsoluteNumber == ep.AbsoluteNumber + 1); } - return new WatchItem(ep.ID, - ep.Show, - ep.SeasonNumber, - ep.EpisodeNumber, - ep.AbsoluteNumber, - ep.Title, - ep.ReleaseDate, - ep.Path, - 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()) + return new WatchItem { + EpisodeID = ep.ID, + ShowSlug = ep.Show.Slug, + SeasonNumber = ep.SeasonNumber, + EpisodeNumber = ep.EpisodeNumber, + AbsoluteNumber = ep.AbsoluteNumber, + Title = ep.Title, + ReleaseDate = ep.ReleaseDate, + Path = ep.Path, + Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), + Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), + Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(), PreviousEpisode = previous, NextEpisode = next, Chapters = await GetChapters(ep.Path) }; } + // TODO move this method in a controller to support abstraction. + // TODO use a IFileManager to retrieve and read files. private static async Task> GetChapters(string episodePath) { string path = PathIO.Combine( diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs deleted file mode 100644 index 743cf545..00000000 --- a/Kyoo.Common/Utility.cs +++ /dev/null @@ -1,703 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Kyoo.Models.Attributes; - -namespace Kyoo -{ - /// - /// A set of utility functions that can be used everywhere. - /// - public static class Utility - { - /// - /// Is the lambda expression a member (like x => x.Body). - /// - /// The expression that should be checked - /// True if the expression is a member, false otherwise - public static bool IsPropertyExpression(LambdaExpression ex) - { - return ex == null || - ex.Body is MemberExpression || - ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; - } - - /// - /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) - /// - /// The expression - /// The name of the expression - /// If the expression is not a property, ArgumentException is thrown. - 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; - } - - /// - /// Get the value of a member (property or field) - /// - /// The member value - /// The owner of this member - /// The value boxed as an object - /// if or is null. - /// The member is not a field or a property. - 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}).") - }; - } - - /// - /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) - /// - /// The string to slugify - /// The slug version of the given string - public static string ToSlug(string str) - { - if (str == null) - return null; - - str = str.ToLowerInvariant(); - - string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new(); - foreach (char c in normalizedString) - { - UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - if (unicodeCategory != UnicodeCategory.NonSpacingMark) - stringBuilder.Append(c); - } - str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); - - str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); - str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); - str = str.Trim('-', '_'); - str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); - return str; - } - - /// - /// Merge two lists, can keep duplicates or remove them. - /// - /// The first enumerable to merge - /// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept - /// Equality function to compare items. If this is null, duplicated elements are kept - /// The two list merged as an array - public static T[] MergeLists(IEnumerable first, - IEnumerable second, - Func isEqual = null) - { - if (first == null) - return second.ToArray(); - if (second == null) - return first.ToArray(); - if (isEqual == null) - return first.Concat(second).ToArray(); - List list = first.ToList(); - return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); - } - - /// - /// Set every fields of first to those of second. Ignore fields marked with the attribute - /// At the end, the OnMerge method of first will be called if first is a - /// - /// The object to assign - /// The object containing new values - /// Fields of T will be used - /// - public static T Assign(T first, T second) - { - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - foreach (PropertyInfo property in properties) - { - object value = property.GetValue(second); - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. - /// At the end, the OnMerge method of first will be called if first is a - /// - /// The object to complete - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// Filter fields that will be merged - /// Fields of T will be completed - /// - /// If first is null - public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null) - { - if (first == null) - throw new ArgumentNullException(nameof(first)); - if (second == null) - return first; - - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - if (where != null) - properties = properties.Where(where); - - foreach (PropertyInfo property in properties) - { - object value = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// An advanced function. - /// This will set missing values of to the corresponding values of . - /// Enumerable will be merged (concatenated). - /// At the end, the OnMerge method of first will be called if first is a . - /// - /// The object to complete - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// Fields of T will be merged - /// - public static T Merge(T first, T second) - { - if (first == null) - return second; - if (second == null) - return first; - - Type type = typeof(T); - IEnumerable properties = type.GetProperties() - .Where(x => x.CanRead && x.CanWrite - && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); - - foreach (PropertyInfo property in properties) - { - object oldValue = property.GetValue(first); - object newValue = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (oldValue?.Equals(defaultValue) != false) - property.SetValue(first, newValue); - else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) - && property.PropertyType != typeof(string)) - { - property.SetValue(first, RunGenericMethod( - typeof(Utility), - nameof(MergeLists), - GetEnumerableType(property.PropertyType), - oldValue, newValue, null)); - } - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// Set every fields of to the default value. - /// - /// The object to nullify - /// Fields of T will be nullified - /// - public static T Nullify(T obj) - { - Type type = typeof(T); - foreach (PropertyInfo property in type.GetProperties()) - { - if (!property.CanWrite) - continue; - - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - property.SetValue(obj, defaultValue); - } - - return obj; - } - - /// - /// Return every in the inheritance tree of the parameter (interfaces are not returned) - /// - /// The starting type - /// A list of types - /// can't be null - public static IEnumerable GetInheritanceTree([NotNull] this Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - for (; type != null; type = type.BaseType) - yield return type; - } - - /// - /// Check if inherit from a generic type . - /// - /// Does this object's type is a - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - return IsOfGenericType(obj.GetType(), genericType); - } - - /// - /// Check if inherit from a generic type . - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// Get the generic definition of . - /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// The generic definition of genericType that type inherit or null if type does not implement the generic type. - /// and can't be null - /// must be a generic type - public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// A Select where the index of the item can be used. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped. - /// mapper can't be null - 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++; - } - } - - /// - /// A map where the mapping function is asynchronous. - /// Note: might interest you. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - 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++; - } - } - - /// - /// An asynchronous version of Select. - /// - /// The IEnumerable to map - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - 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); - } - - /// - /// Convert an AsyncEnumerable to a List by waiting for every item. - /// - /// The async list - /// The type of items in the async list and in the returned list. - /// A task that will return a simple list - /// The list can't be null - 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; - } - - /// - /// If the enumerable is empty, execute an action. - /// - /// The enumerable to check - /// The action to execute is the list is empty - /// The type of items inside the list - /// - 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()); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - /// The type of items in the list - public static void ForEach([CanBeNull] this IEnumerable self, Action action) - { - if (self == null) - return; - foreach (T i in self) - action(i); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - 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); - } - - public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) - { - MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) - .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}.")) - // TODO this won't work but I don't know why. - // .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.")) - - // TODO this won't work for Type because T is specified in arguments but not in the parameters type. - // .Where(x => - // { - // int i = 0; - // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); - // }) - // .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, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(owner, methodName, new[] {type}, args); - } - - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) - { - if (owner == null) - throw new ArgumentNullException(nameof(owner)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - if (types == null) - 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 = 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[] types, - params object[] args) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - 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] - public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list) - { - if (list == null) - throw new ArgumentNullException(nameof(list)); - Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? list.GetType(); - return type.GetGenericArguments().First(); - } - - public static Type GetEnumerableType([NotNull] Type listType) - { - if (listType == null) - throw new ArgumentNullException(nameof(listType)); - if (!typeof(IEnumerable).IsAssignableFrom(listType)) - throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable."); - Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? listType; - return type.GetGenericArguments().First(); - } - - public static IEnumerable> BatchBy(this List list, int countPerList) - { - for (int i = 0; i < list.Count; i += countPerList) - yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); - } - - public static IEnumerable BatchBy(this IEnumerable list, int countPerList) - { - T[] ret = new T[countPerList]; - int i = 0; - - using IEnumerator enumerator = list.GetEnumerator(); - while (enumerator.MoveNext()) - { - ret[i] = enumerator.Current; - i++; - if (i < countPerList) - continue; - i = 0; - yield return ret; - } - - Array.Resize(ref ret, i); - yield return ret; - } - - public static string ToQueryString(this Dictionary query) - { - if (!query.Any()) - return string.Empty; - return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); - } - - [System.Diagnostics.CodeAnalysis.DoesNotReturn] - public static void ReThrow([NotNull] this Exception ex) - { - if (ex == null) - throw new ArgumentNullException(nameof(ex)); - ExceptionDispatchInfo.Capture(ex).Throw(); - } - - public static Task Then(this Task task, Action map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - map(x.Result); - return x.Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Map(this Task task, Func map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return map(x.Result); - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Cast(this Task task) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return (T)((dynamic)x).Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - /// - /// Get a friendly type name (supporting generics) - /// For example a list of string will be displayed as List<string> and not as List`1. - /// - /// The type to use - /// The friendly name of the type - public static string FriendlyName(this Type type) - { - if (!type.IsGenericType) - return type.Name; - string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); - return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Utility/EnumerableExtensions.cs b/Kyoo.Common/Utility/EnumerableExtensions.cs new file mode 100644 index 00000000..93f1b52e --- /dev/null +++ b/Kyoo.Common/Utility/EnumerableExtensions.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of extensions class for enumerable. + /// + public static class EnumerableExtensions + { + /// + /// A Select where the index of the item can be used. + /// + /// The IEnumerable to map. If self is null, an empty list is returned + /// The function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped. + /// The list or the mapper can't be null + [LinqTunnel] + public static IEnumerable Map([NotNull] this IEnumerable self, + [NotNull] Func mapper) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + static IEnumerable Generator(IEnumerable self, Func mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return mapper(enumerator.Current, index); + index++; + } + } + return Generator(self, mapper); + } + + /// + /// A map where the mapping function is asynchronous. + /// Note: might interest you. + /// + /// The IEnumerable to map. + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// The list or the mapper can't be null + [LinqTunnel] + public static IAsyncEnumerable MapAsync([NotNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + static async IAsyncEnumerable Generator(IEnumerable self, Func> mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } + } + + return Generator(self, mapper); + } + + /// + /// An asynchronous version of Select. + /// + /// The IEnumerable to map + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// The list or the mapper can't be null + [LinqTunnel] + public static IAsyncEnumerable SelectAsync([NotNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + static async IAsyncEnumerable Generator(IEnumerable self, Func> mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + + while (enumerator.MoveNext()) + yield return await mapper(enumerator.Current); + } + + return Generator(self, mapper); + } + + /// + /// Convert an AsyncEnumerable to a List by waiting for every item. + /// + /// The async list + /// The type of items in the async list and in the returned list. + /// A task that will return a simple list + /// The list can't be null + [LinqTunnel] + public static Task> ToListAsync([NotNull] this IAsyncEnumerable self) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + + static async Task> ToList(IAsyncEnumerable self) + { + List ret = new(); + await foreach (T i in self) + ret.Add(i); + return ret; + } + + return ToList(self); + } + + /// + /// If the enumerable is empty, execute an action. + /// + /// The enumerable to check + /// The action to execute is the list is empty + /// The type of items inside the list + /// The iterable and the action can't be null. + /// The iterator proxied, there is no dual iterations. + [LinqTunnel] + public static IEnumerable IfEmpty([NotNull] this IEnumerable self, [NotNull] Action action) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (action == null) + throw new ArgumentNullException(nameof(action)); + + static IEnumerable Generator(IEnumerable self, Action action) + { + using IEnumerator enumerator = self.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + action(); + yield break; + } + + do + { + yield return enumerator.Current; + } + while (enumerator.MoveNext()); + } + + return Generator(self, action); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (T i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (object i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (object i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The asynchronous action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (T i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The async list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) + { + if (self == null) + return; + await foreach (T i in self) + action(i); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + [LinqTunnel] + public static IEnumerable> BatchBy(this List list, int countPerList) + { + for (int i = 0; i < list.Count; i += countPerList) + yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + [LinqTunnel] + public static IEnumerable BatchBy(this IEnumerable list, int countPerList) + { + T[] ret = new T[countPerList]; + int i = 0; + + using IEnumerator enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + ret[i] = enumerator.Current; + i++; + if (i < countPerList) + continue; + i = 0; + yield return ret; + } + + Array.Resize(ref ret, i); + yield return ret; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs new file mode 100644 index 00000000..55cc17e3 --- /dev/null +++ b/Kyoo.Common/Utility/Merger.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Kyoo.Models.Attributes; + +namespace Kyoo +{ + /// + /// A class containing helper methods to merge objects. + /// + public static class Merger + { + /// + /// Merge two lists, can keep duplicates or remove them. + /// + /// The first enumerable to merge + /// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept + /// Equality function to compare items. If this is null, duplicated elements are kept + /// The two list merged as an array + public static T[] MergeLists(IEnumerable first, + IEnumerable second, + Func isEqual = null) + { + if (first == null) + return second.ToArray(); + if (second == null) + return first.ToArray(); + if (isEqual == null) + return first.Concat(second).ToArray(); + List list = first.ToList(); + return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); + } + + /// + /// Set every fields of first to those of second. Ignore fields marked with the attribute + /// At the end, the OnMerge method of first will be called if first is a + /// + /// The object to assign + /// The object containing new values + /// Fields of T will be used + /// + public static T Assign(T first, T second) + { + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + foreach (PropertyInfo property in properties) + { + object value = property.GetValue(second); + property.SetValue(first, value); + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. + /// At the end, the OnMerge method of first will be called if first is a + /// + /// The object to complete + /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. + /// Filter fields that will be merged + /// Fields of T will be completed + /// + /// If first is null + public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null) + { + if (first == null) + throw new ArgumentNullException(nameof(first)); + if (second == null) + return first; + + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + if (where != null) + properties = properties.Where(where); + + foreach (PropertyInfo property in properties) + { + object value = property.GetValue(second); + object defaultValue = property.GetCustomAttribute()?.Value + ?? property.PropertyType.GetClrDefault(); + + if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) + property.SetValue(first, value); + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// An advanced function. + /// This will set missing values of to the corresponding values of . + /// Enumerable will be merged (concatenated). + /// At the end, the OnMerge method of first will be called if first is a . + /// + /// The object to complete + /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. + /// Fields of T will be merged + /// + public static T Merge(T first, T second) + { + if (first == null) + return second; + if (second == null) + return first; + + Type type = typeof(T); + IEnumerable properties = type.GetProperties() + .Where(x => x.CanRead && x.CanWrite + && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); + + foreach (PropertyInfo property in properties) + { + object oldValue = property.GetValue(first); + object newValue = property.GetValue(second); + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + + if (oldValue?.Equals(defaultValue) != false) + property.SetValue(first, newValue); + else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) + && property.PropertyType != typeof(string)) + { + Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>)) + .GenericTypeArguments + .First(); + property.SetValue(first, Utility.RunGenericMethod( + typeof(Utility), + nameof(MergeLists), + enumerableType, + oldValue, newValue, null)); + } + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// Set every fields of to the default value. + /// + /// The object to nullify + /// Fields of T will be nullified + /// + public static T Nullify(T obj) + { + Type type = typeof(T); + foreach (PropertyInfo property in type.GetProperties()) + { + if (!property.CanWrite || property.GetCustomAttribute() != null) + continue; + property.SetValue(obj, property.PropertyType.GetClrDefault()); + } + + return obj; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/TaskUtils.cs b/Kyoo.Common/Utility/TaskUtils.cs new file mode 100644 index 00000000..78332cc9 --- /dev/null +++ b/Kyoo.Common/Utility/TaskUtils.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A class containing helper method for tasks. + /// + public static class TaskUtils + { + /// + /// Run a method after the execution of the task. + /// + /// The task to wait. + /// + /// The method to run after the task finish. This will only be run if the task finished successfully. + /// + /// The type of the item in the task. + /// A continuation task wrapping the initial task and adding a continuation method. + /// + /// The source task has been canceled. + public static Task Then(this Task task, Action then) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + then(x.Result); + return x.Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Map the result of a task to another result. + /// + /// The task to map. + /// The mapper method, it take the task's result as a parameter and should return the new result. + /// The type of returns of the given task + /// The resulting task after the mapping method + /// A task wrapping the initial task and mapping the initial result. + /// The source task has been canceled. + public static Task Map(this Task task, Func map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return map(x.Result); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// A method to return the a default value from a task if the initial task is null. + /// + /// The initial task + /// The type that the task will return + /// A non-null task. + [NotNull] + public static Task DefaultIfNull([CanBeNull] Task value) + { + return value ?? Task.FromResult(default); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs new file mode 100644 index 00000000..f7041c51 --- /dev/null +++ b/Kyoo.Common/Utility/Utility.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of utility functions that can be used everywhere. + /// + public static class Utility + { + /// + /// Is the lambda expression a member (like x => x.Body). + /// + /// The expression that should be checked + /// True if the expression is a member, false otherwise + public static bool IsPropertyExpression(LambdaExpression ex) + { + if (ex == null) + return false; + return ex.Body is MemberExpression || + ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; + } + + /// + /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) + /// + /// The expression + /// The name of the expression + /// If the expression is not a property, ArgumentException is thrown. + 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; + } + + /// + /// Get the value of a member (property or field) + /// + /// The member value + /// The owner of this member + /// The value boxed as an object + /// if or is null. + /// The member is not a field or a property. + 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}).") + }; + } + + /// + /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) + /// + /// The string to slugify + /// The slug version of the given string + public static string ToSlug(string str) + { + if (str == null) + return null; + + str = str.ToLowerInvariant(); + + string normalizedString = str.Normalize(NormalizationForm.FormD); + StringBuilder stringBuilder = new(); + foreach (char c in normalizedString) + { + UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + stringBuilder.Append(c); + } + str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); + + str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); + str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); + str = str.Trim('-', '_'); + str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); + return str; + } + + /// + /// Get the default value of a type. + /// + /// The type to get the default value + /// The default value of the given type. + public static object GetClrDefault(this Type type) + { + return type.IsValueType + ? Activator.CreateInstance(type) + : null; + } + + /// + /// Return every in the inheritance tree of the parameter (interfaces are not returned) + /// + /// The starting type + /// A list of types + /// can't be null + public static IEnumerable GetInheritanceTree([NotNull] this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + for (; type != null; type = type.BaseType) + yield return type; + } + + /// + /// Check if inherit from a generic type . + /// + /// Does this object's type is a + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return IsOfGenericType(obj.GetType(), genericType); + } + + /// + /// Check if inherit from a generic type . + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + /// + /// Get the generic definition of . + /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// The generic definition of genericType that type inherit or null if type does not implement the generic type. + /// and can't be null + /// must be a generic type + public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args) + { + MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) + .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}.")) + // TODO this won't work but I don't know why. + // .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.")) + + // TODO this won't work for Type because T is specified in arguments but not in the parameters type. + // .Where(x => + // { + // int i = 0; + // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); + // }) + // .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, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(owner, methodName, new[] {type}, args); + } + + public static T RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type[] types, + params object[] args) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + if (types == null) + 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 = 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[] types, + params object[] args) + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + 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()); + } + + public static string ToQueryString(this Dictionary query) + { + if (!query.Any()) + return string.Empty; + return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); + } + + [System.Diagnostics.CodeAnalysis.DoesNotReturn] + public static void ReThrow([NotNull] this Exception ex) + { + if (ex == null) + throw new ArgumentNullException(nameof(ex)); + ExceptionDispatchInfo.Capture(ex).Throw(); + } + + /// + /// Get a friendly type name (supporting generics) + /// For example a list of string will be displayed as List<string> and not as List`1. + /// + /// The type to use + /// The friendly name of the type + public static string FriendlyName(this Type type) + { + if (!type.IsGenericType) + return type.Name; + string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); + return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; + } + } +} \ No newline at end of file diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index b6d03580..336d3226 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -194,7 +194,7 @@ namespace Kyoo.CommonApi { try { - await _repository.DeleteRange(ApiHelper.ParseWhere(where)); + await _repository.DeleteAll(ApiHelper.ParseWhere(where)); } catch (ItemNotFoundException) { diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 584230b9..36cdc47b 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -61,10 +61,6 @@ namespace Kyoo /// public DbSet Providers { get; set; } /// - /// All metadataIDs (ExternalIDs) of Kyoo. See . - /// - public DbSet MetadataIds { get; set; } - /// /// The list of registered users. /// public DbSet Users { get; set; } @@ -78,7 +74,26 @@ namespace Kyoo /// Episodes with a watch percentage. See /// public DbSet WatchedEpisodes { get; set; } + + /// + /// The list of library items (shows and collections that are part of a library - or the global one) + /// + /// + /// This set is ready only, on most database this will be a view. + /// + public DbSet LibraryItems { get; set; } + /// + /// Get all metadataIDs (ExternalIDs) of a given resource. See . + /// + /// The metadata of this type will be returned. + /// A queryable of metadata ids for a type. + public DbSet> MetadataIds() + where T : class, IResource + { + return Set>(); + } + /// /// Get a generic link between two resource types. /// @@ -125,13 +140,27 @@ namespace Kyoo { base.OnModelCreating(modelBuilder); - modelBuilder.Entity() - .Property(t => t.IsDefault) - .ValueGeneratedNever(); - - modelBuilder.Entity() - .Property(t => t.IsForced) - .ValueGeneratedNever(); + modelBuilder.Entity() + .HasMany(x => x.Seasons) + .WithOne(x => x.Show) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasMany(x => x.Episodes) + .WithOne(x => x.Show) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasMany(x => x.Episodes) + .WithOne(x => x.Season) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasMany(x => x.Tracks) + .WithOne(x => x.Episode) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(x => x.Studio) + .WithMany(x => x.Shows) + .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .HasMany(x => x.Libraries) @@ -205,25 +234,41 @@ namespace Kyoo .WithMany(x => x.ShowLinks), y => y.HasKey(Link.PrimaryKey)); - modelBuilder.Entity() - .HasOne(x => x.Show) + modelBuilder.Entity>() + .HasKey(MetadataID.PrimaryKey); + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Season) + modelBuilder.Entity>() + .HasKey(MetadataID.PrimaryKey); + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Episode) + modelBuilder.Entity>() + .HasKey(MetadataID.PrimaryKey); + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.People) + modelBuilder.Entity>() + .HasKey(MetadataID.PrimaryKey); + modelBuilder.Entity>() + .HasOne(x => x.First) .WithMany(x => x.ExternalIDs) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Provider) - .WithMany(x => x.MetadataLinks) + + + modelBuilder.Entity>().HasOne(x => x.Second).WithMany() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>().HasOne(x => x.Second).WithMany() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>().HasOne(x => x.Second).WithMany() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>().HasOne(x => x.Second).WithMany() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>().HasOne(x => x.Second).WithMany() .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() @@ -237,7 +282,7 @@ namespace Kyoo modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); @@ -262,15 +307,34 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => x.Slug) + .IsUnique(); modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); + + modelBuilder.Entity() + .Property(x => x.Slug) + .ValueGeneratedOnAddOrUpdate(); + modelBuilder.Entity() + .Property(x => x.Slug) + .ValueGeneratedOnAddOrUpdate(); + modelBuilder.Entity() + .Property(x => x.Slug) + .ValueGeneratedOnAddOrUpdate(); } /// @@ -441,52 +505,6 @@ namespace Kyoo } } - /// - /// Save items or retry with a custom method if a duplicate is found. - /// - /// The item to save (other changes of this context will also be saved) - /// A function to run on fail, the param wil be mapped. - /// The second parameter is the current retry number. - /// A to observe while waiting for the task to complete - /// The type of the item to save - /// The number of state entries written to the database. - public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) - { - return SaveOrRetry(obj, onFail, 0, cancellationToken); - } - - /// - /// Save items or retry with a custom method if a duplicate is found. - /// - /// The item to save (other changes of this context will also be saved) - /// A function to run on fail, the param wil be mapped. - /// The second parameter is the current retry number. - /// The current retry number. - /// A to observe while waiting for the task to complete - /// The type of the item to save - /// The number of state entries written to the database. - private 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; - } - } - /// /// Check if the exception is a duplicated exception. /// diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj index 5288b814..451c387d 100644 --- a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -12,8 +12,9 @@ - - + + + diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 7498fc14..8ab23443 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -225,8 +225,8 @@ namespace Kyoo.Controllers T old = await GetWithTracking(edited.ID); if (resetOld) - Utility.Nullify(old); - Utility.Complete(old, edited, x => x.GetCustomAttribute() == null); + old = Merger.Nullify(old); + Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); await EditRelations(old, edited, resetOld); await Database.SaveChangesAsync(); return old; @@ -257,6 +257,8 @@ namespace Kyoo.Controllers /// You can throw this if the resource is illegal and should not be saved. protected virtual Task Validate(T resource) { + if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute() != null) + return Task.CompletedTask; if (string.IsNullOrEmpty(resource.Slug)) throw new ArgumentException("Resource can't have null as a slug."); if (int.TryParse(resource.Slug, out int _)) @@ -295,31 +297,10 @@ namespace Kyoo.Controllers public abstract Task Delete(T obj); /// - public virtual async Task DeleteRange(IEnumerable objs) + public async Task DeleteAll(Expression> where) { - foreach (T obj in objs) - await Delete(obj); - } - - /// - public virtual async Task DeleteRange(IEnumerable ids) - { - foreach (int id in ids) - await Delete(id); - } - - /// - public virtual async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); - } - - /// - public async Task DeleteRange(Expression> where) - { - ICollection resources = await GetAll(where); - await DeleteRange(resources); + foreach (T resource in await GetAll(where)) + await Delete(resource); } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 52c9041b..408e10ad 100644 --- a/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -1,5 +1,4 @@ - net5.0 @@ -9,33 +8,34 @@ default - - ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql - false - false - false - false - true - + + + + + + + + - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - all - false - runtime + + + - all - false - runtime + + + diff --git a/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs b/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs new file mode 100644 index 00000000..f1733a77 --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs @@ -0,0 +1,1197 @@ +// +using System; +using System.Collections.Generic; +using Kyoo.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20210627141933_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .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", "attachment" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); + + b.Property("SeasonID") + .HasColumnType("integer") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Thumb") + .HasColumnType("text") + .HasColumnName("thumb"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("ID") + .HasName("pk_episodes"); + + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_genres"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_genres_slug"); + + b.ToTable("genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Paths") + .HasColumnType("text[]") + .HasColumnName("paths"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_libraries"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); + + b.ToTable("libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); + + b.ToTable("link_collection_show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); + + b.ToTable("link_library_collection"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); + + b.ToTable("link_library_provider"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); + + b.ToTable("link_library_show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); + + b.ToTable("link_show_genre"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); + + b.ToTable("link_user_show"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); + + b.ToTable("metadata_id_episode"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); + + b.ToTable("metadata_id_people"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); + + b.ToTable("metadata_id_season"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); + + b.ToTable("metadata_id_show"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_people"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_people_slug"); + + b.ToTable("people"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ForPeople") + .HasColumnType("boolean") + .HasColumnName("for_people"); + + b.Property("PeopleID") + .HasColumnType("integer") + .HasColumnName("people_id"); + + b.Property("Role") + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Type") + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("ID") + .HasName("pk_people_roles"); + + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); + + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); + + b.ToTable("people_roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Logo") + .HasColumnType("text") + .HasColumnName("logo"); + + b.Property("LogoExtension") + .HasColumnType("text") + .HasColumnName("logo_extension"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_providers"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_providers_slug"); + + b.ToTable("providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("ID") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Aliases") + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("Backdrop") + .HasColumnType("text") + .HasColumnName("backdrop"); + + b.Property("EndAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); + + b.Property("IsMovie") + .HasColumnType("boolean") + .HasColumnName("is_movie"); + + b.Property("Logo") + .HasColumnType("text") + .HasColumnName("logo"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("TrailerUrl") + .HasColumnType("text") + .HasColumnName("trailer_url"); + + b.HasKey("ID") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Codec") + .HasColumnType("text") + .HasColumnName("codec"); + + b.Property("EpisodeID") + .HasColumnType("integer") + .HasColumnName("episode_id"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("IsExternal") + .HasColumnType("boolean") + .HasColumnName("is_external"); + + b.Property("IsForced") + .HasColumnType("boolean") + .HasColumnName("is_forced"); + + b.Property("Language") + .HasColumnType("text") + .HasColumnName("language"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("TrackIndex") + .HasColumnType("integer") + .HasColumnName("track_index"); + + b.Property("Type") + .HasColumnType("stream_type") + .HasColumnName("type"); + + b.HasKey("ID") + .HasName("pk_tracks"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); + + b.ToTable("tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property>("ExtraData") + .HasColumnType("jsonb") + .HasColumnName("extra_data"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("ID") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("WatchedPercentage") + .HasColumnType("integer") + .HasColumnName("watched_percentage"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); + + b.ToTable("watched_episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") + .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") + .HasConstraintName("fk_link_library_collection_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") + .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") + .HasConstraintName("fk_link_library_provider_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") + .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") + .HasConstraintName("fk_link_library_show_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") + .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("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + 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.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + 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"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs new file mode 100644 index 00000000..29b51490 --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210627141933_Initial.cs @@ -0,0 +1,782 @@ +using System; +using System.Collections.Generic; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:item_type", "show,movie,collection") + .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); + + migrationBuilder.CreateTable( + name: "collections", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + 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 => + { + table.PrimaryKey("pk_collections", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "genres", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_genres", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "libraries", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + paths = table.Column(type: "text[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_libraries", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "people", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_people", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "providers", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true), + logo = table.Column(type: "text", nullable: true), + logo_extension = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_providers", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "studios", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_studios", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + username = table.Column(type: "text", nullable: true), + email = table.Column(type: "text", nullable: true), + password = table.Column(type: "text", nullable: true), + permissions = table.Column(type: "text[]", nullable: true), + extra_data = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "link_library_collection", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_collection", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_collection_collections_second_id", + column: x => x.second_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_collection_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_library_provider", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_provider", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_provider_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_provider_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_people", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_people", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_people_people_first_id", + column: x => x.first_id, + principalTable: "people", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_people_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "shows", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: false), + title = table.Column(type: "text", nullable: true), + aliases = table.Column(type: "text[]", nullable: true), + path = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + status = table.Column(type: "status", nullable: true), + trailer_url = table.Column(type: "text", nullable: true), + start_air = table.Column(type: "timestamp without time zone", nullable: true), + end_air = table.Column(type: "timestamp without time zone", nullable: true), + poster = table.Column(type: "text", nullable: true), + logo = table.Column(type: "text", nullable: true), + backdrop = table.Column(type: "text", nullable: true), + is_movie = table.Column(type: "boolean", nullable: false), + studio_id = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_shows", x => x.id); + table.ForeignKey( + name: "fk_shows_studios_studio_id", + column: x => x.studio_id, + principalTable: "studios", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "link_collection_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_collection_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_collection_show_collections_first_id", + column: x => x.first_id, + principalTable: "collections", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_collection_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_library_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_library_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_library_show_libraries_first_id", + column: x => x.first_id, + principalTable: "libraries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_library_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_show_genre", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_show_genre", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_show_genre_genres_second_id", + column: x => x.second_id, + principalTable: "genres", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_show_genre_shows_first_id", + column: x => x.first_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "link_user_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_link_user_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_link_user_show_shows_second_id", + column: x => x.second_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_link_user_show_users_first_id", + column: x => x.first_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_show", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_show", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_show_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_show_shows_first_id", + column: x => x.first_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "people_roles", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + for_people = table.Column(type: "boolean", nullable: false), + people_id = table.Column(type: "integer", nullable: false), + show_id = table.Column(type: "integer", nullable: false), + type = table.Column(type: "text", nullable: true), + role = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_people_roles", x => x.id); + table.ForeignKey( + name: "fk_people_roles_people_people_id", + column: x => x.people_id, + principalTable: "people", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_people_roles_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "seasons", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + show_id = table.Column(type: "integer", nullable: false), + season_number = table.Column(type: "integer", nullable: false), + title = table.Column(type: "text", nullable: true), + overview = table.Column(type: "text", nullable: true), + start_date = table.Column(type: "timestamp without time zone", nullable: true), + end_date = table.Column(type: "timestamp without time zone", nullable: true), + poster = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_seasons", x => x.id); + table.ForeignKey( + name: "fk_seasons_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "episodes", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + show_id = table.Column(type: "integer", nullable: false), + season_id = table.Column(type: "integer", nullable: true), + season_number = table.Column(type: "integer", nullable: true), + episode_number = table.Column(type: "integer", nullable: true), + absolute_number = table.Column(type: "integer", nullable: true), + 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), + release_date = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_episodes", x => x.id); + table.ForeignKey( + name: "fk_episodes_seasons_season_id", + column: x => x.season_id, + principalTable: "seasons", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_episodes_shows_show_id", + column: x => x.show_id, + principalTable: "shows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_season", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_season", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_season_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_season_seasons_first_id", + column: x => x.first_id, + principalTable: "seasons", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "metadata_id_episode", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + data_id = table.Column(type: "text", nullable: true), + link = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_metadata_id_episode", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_metadata_id_episode_episodes_first_id", + column: x => x.first_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_metadata_id_episode_providers_second_id", + column: x => x.second_id, + principalTable: "providers", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tracks", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + slug = table.Column(type: "text", nullable: true), + title = table.Column(type: "text", nullable: true), + language = table.Column(type: "text", nullable: true), + codec = table.Column(type: "text", nullable: true), + is_default = table.Column(type: "boolean", nullable: false), + is_forced = table.Column(type: "boolean", nullable: false), + is_external = table.Column(type: "boolean", nullable: false), + path = table.Column(type: "text", nullable: true), + type = table.Column(type: "stream_type", nullable: false), + episode_id = table.Column(type: "integer", nullable: false), + track_index = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tracks", x => x.id); + table.ForeignKey( + name: "fk_tracks_episodes_episode_id", + column: x => x.episode_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "watched_episodes", + columns: table => new + { + first_id = table.Column(type: "integer", nullable: false), + second_id = table.Column(type: "integer", nullable: false), + watched_percentage = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_watched_episodes", x => new { x.first_id, x.second_id }); + table.ForeignKey( + name: "fk_watched_episodes_episodes_second_id", + column: x => x.second_id, + principalTable: "episodes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_watched_episodes_users_first_id", + column: x => x.first_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_collections_slug", + table: "collections", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_episodes_season_id", + table: "episodes", + column: "season_id"); + + migrationBuilder.CreateIndex( + name: "ix_episodes_show_id_season_number_episode_number_absolute_numb", + table: "episodes", + columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_episodes_slug", + table: "episodes", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_genres_slug", + table: "genres", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_libraries_slug", + table: "libraries", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_link_collection_show_second_id", + table: "link_collection_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_collection_second_id", + table: "link_library_collection", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_provider_second_id", + table: "link_library_provider", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_library_show_second_id", + table: "link_library_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_show_genre_second_id", + table: "link_show_genre", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_link_user_show_second_id", + table: "link_user_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_episode_second_id", + table: "metadata_id_episode", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_people_second_id", + table: "metadata_id_people", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_season_second_id", + table: "metadata_id_season", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_metadata_id_show_second_id", + table: "metadata_id_show", + column: "second_id"); + + migrationBuilder.CreateIndex( + name: "ix_people_slug", + table: "people", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_people_roles_people_id", + table: "people_roles", + column: "people_id"); + + migrationBuilder.CreateIndex( + name: "ix_people_roles_show_id", + table: "people_roles", + column: "show_id"); + + migrationBuilder.CreateIndex( + name: "ix_providers_slug", + table: "providers", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_seasons_show_id_season_number", + table: "seasons", + columns: new[] { "show_id", "season_number" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_seasons_slug", + table: "seasons", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_shows_slug", + table: "shows", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_shows_studio_id", + table: "shows", + column: "studio_id"); + + migrationBuilder.CreateIndex( + name: "ix_studios_slug", + table: "studios", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tracks_episode_id_type_language_track_index_is_forced", + table: "tracks", + columns: new[] { "episode_id", "type", "language", "track_index", "is_forced" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_tracks_slug", + table: "tracks", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_users_slug", + table: "users", + column: "slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_watched_episodes_second_id", + table: "watched_episodes", + column: "second_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "link_collection_show"); + + migrationBuilder.DropTable( + name: "link_library_collection"); + + migrationBuilder.DropTable( + name: "link_library_provider"); + + migrationBuilder.DropTable( + name: "link_library_show"); + + migrationBuilder.DropTable( + name: "link_show_genre"); + + migrationBuilder.DropTable( + name: "link_user_show"); + + migrationBuilder.DropTable( + name: "metadata_id_episode"); + + migrationBuilder.DropTable( + name: "metadata_id_people"); + + migrationBuilder.DropTable( + name: "metadata_id_season"); + + migrationBuilder.DropTable( + name: "metadata_id_show"); + + migrationBuilder.DropTable( + name: "people_roles"); + + migrationBuilder.DropTable( + name: "tracks"); + + migrationBuilder.DropTable( + name: "watched_episodes"); + + migrationBuilder.DropTable( + name: "collections"); + + migrationBuilder.DropTable( + name: "libraries"); + + migrationBuilder.DropTable( + name: "genres"); + + migrationBuilder.DropTable( + name: "providers"); + + migrationBuilder.DropTable( + name: "people"); + + migrationBuilder.DropTable( + name: "episodes"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "seasons"); + + migrationBuilder.DropTable( + name: "shows"); + + migrationBuilder.DropTable( + name: "studios"); + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs new file mode 100644 index 00000000..fc019baf --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs @@ -0,0 +1,1197 @@ +// +using System; +using System.Collections.Generic; +using Kyoo.Models; +using Kyoo.Postgresql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Kyoo.Postgresql.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20210627141941_Triggers")] + partial class Triggers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .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", "attachment" }) + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_collections"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_collections_slug"); + + b.ToTable("collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); + + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); + + b.Property("SeasonID") + .HasColumnType("integer") + .HasColumnName("season_id"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Thumb") + .HasColumnType("text") + .HasColumnName("thumb"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("ID") + .HasName("pk_episodes"); + + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); + + b.ToTable("episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_genres"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_genres_slug"); + + b.ToTable("genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Paths") + .HasColumnType("text[]") + .HasColumnName("paths"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_libraries"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); + + b.ToTable("libraries"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); + + b.ToTable("link_collection_show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); + + b.ToTable("link_library_collection"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); + + b.ToTable("link_library_provider"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); + + b.ToTable("link_library_show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); + + b.ToTable("link_show_genre"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); + + b.ToTable("link_user_show"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); + + b.ToTable("metadata_id_episode"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); + + b.ToTable("metadata_id_people"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); + + b.ToTable("metadata_id_season"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); + + b.ToTable("metadata_id_show"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_people"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_people_slug"); + + b.ToTable("people"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ForPeople") + .HasColumnType("boolean") + .HasColumnName("for_people"); + + b.Property("PeopleID") + .HasColumnType("integer") + .HasColumnName("people_id"); + + b.Property("Role") + .HasColumnType("text") + .HasColumnName("role"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Type") + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("ID") + .HasName("pk_people_roles"); + + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); + + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); + + b.ToTable("people_roles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Logo") + .HasColumnType("text") + .HasColumnName("logo"); + + b.Property("LogoExtension") + .HasColumnType("text") + .HasColumnName("logo_extension"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_providers"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_providers_slug"); + + b.ToTable("providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); + + b.Property("ShowID") + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("ID") + .HasName("pk_seasons"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); + + b.ToTable("seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Aliases") + .HasColumnType("text[]") + .HasColumnName("aliases"); + + b.Property("Backdrop") + .HasColumnType("text") + .HasColumnName("backdrop"); + + b.Property("EndAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); + + b.Property("IsMovie") + .HasColumnType("boolean") + .HasColumnName("is_movie"); + + b.Property("Logo") + .HasColumnType("text") + .HasColumnName("logo"); + + b.Property("Overview") + .HasColumnType("text") + .HasColumnName("overview"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Poster") + .HasColumnType("text") + .HasColumnName("poster"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("StartAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); + + b.Property("Status") + .HasColumnType("status") + .HasColumnName("status"); + + b.Property("StudioID") + .HasColumnType("integer") + .HasColumnName("studio_id"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("TrailerUrl") + .HasColumnType("text") + .HasColumnName("trailer_url"); + + b.HasKey("ID") + .HasName("pk_shows"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_shows_slug"); + + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); + + b.ToTable("shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("ID") + .HasName("pk_studios"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_studios_slug"); + + b.ToTable("studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Codec") + .HasColumnType("text") + .HasColumnName("codec"); + + b.Property("EpisodeID") + .HasColumnType("integer") + .HasColumnName("episode_id"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("IsExternal") + .HasColumnType("boolean") + .HasColumnName("is_external"); + + b.Property("IsForced") + .HasColumnType("boolean") + .HasColumnName("is_forced"); + + b.Property("Language") + .HasColumnType("text") + .HasColumnName("language"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("TrackIndex") + .HasColumnType("integer") + .HasColumnName("track_index"); + + b.Property("Type") + .HasColumnType("stream_type") + .HasColumnName("type"); + + b.HasKey("ID") + .HasName("pk_tracks"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); + + b.ToTable("tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property>("ExtraData") + .HasColumnType("jsonb") + .HasColumnName("extra_data"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Permissions") + .HasColumnType("text[]") + .HasColumnName("permissions"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("ID") + .HasName("pk_users"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_users_slug"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("WatchedPercentage") + .HasColumnType("integer") + .HasColumnName("watched_percentage"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); + + b.ToTable("watched_episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("CollectionLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") + .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") + .HasConstraintName("fk_link_library_collection_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Collection", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") + .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") + .HasConstraintName("fk_link_library_provider_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") + .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") + .HasConstraintName("fk_link_library_show_libraries_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany("LibraryLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") + .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("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + 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.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + 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"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs new file mode 100644 index 00000000..16569748 --- /dev/null +++ b/Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs @@ -0,0 +1,186 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Kyoo.Postgresql.Migrations +{ + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION season_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + NEW.slug := CONCAT( + (SELECT slug FROM shows WHERE id = NEW.show_id), + '-s', + NEW.season_number + ); + RETURN NEW; + END + $$;"); + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons + FOR EACH ROW EXECUTE PROCEDURE season_slug_update();"); + + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION episode_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + NEW.slug := CONCAT( + (SELECT slug FROM shows WHERE id = NEW.show_id), + CASE + WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL + WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number) + ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number) + END + ); + RETURN NEW; + END + $$;"); + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER episode_slug_trigger + BEFORE INSERT OR UPDATE OF absolute_number, episode_number, season_number, show_id ON episodes + FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();"); + + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION show_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id; + UPDATE episodes SET slug = CASE + WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug + WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number) + ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number) + END WHERE show_id = NEW.id; + RETURN NEW; + END + $$;"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows + FOR EACH ROW EXECUTE PROCEDURE show_slug_update();"); + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION episode_update_tracks_slug() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE tracks SET slug = CONCAT( + NEW.slug, + '.', language, + CASE (track_index) + WHEN 0 THEN '' + ELSE CONCAT('-', track_index) + END, + CASE (is_forced) + WHEN false THEN '' + ELSE '-forced' + END, + '.', type + ) WHERE episode_id = NEW.id; + RETURN NEW; + END; + $$;"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER episode_track_slug_trigger AFTER UPDATE OF slug ON episodes + FOR EACH ROW EXECUTE PROCEDURE episode_update_tracks_slug();"); + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE FUNCTION track_slug_update() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + IF NEW.track_index = 0 THEN + NEW.track_index := (SELECT COUNT(*) FROM tracks + WHERE episode_id = NEW.episode_id AND type = NEW.type + AND language = NEW.language AND is_forced = NEW.is_forced); + END IF; + NEW.slug := CONCAT( + (SELECT slug FROM episodes WHERE id = NEW.episode_id), + '.', NEW.language, + CASE (NEW.track_index) + WHEN 0 THEN '' + ELSE CONCAT('-', NEW.track_index) + END, + CASE (NEW.is_forced) + WHEN false THEN '' + ELSE '-forced' + END, + '.', NEW.type + ); + RETURN NEW; + END + $$;"); + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE TRIGGER track_slug_trigger + BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks + FOR EACH ROW EXECUTE PROCEDURE track_slug_update();"); + + + // language=PostgreSQL + migrationBuilder.Sql(@" + CREATE VIEW library_items AS + SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.poster, CASE + WHEN s.is_movie THEN 'movie'::item_type + ELSE 'show'::item_type + END AS type + FROM shows AS s + WHERE NOT (EXISTS ( + SELECT 1 + FROM link_collection_show AS l + INNER JOIN collections AS c ON l.first_id = c.id + WHERE s.id = l.second_id)) + UNION ALL + SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status, + NULL AS start_air, NULL AS end_air, c0.poster, 'collection'::item_type AS type + FROM collections AS c0"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON shows;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger ON seasons;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER episode_slug_trigger ON episodes;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER track_slug_trigger ON tracks;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION track_slug_update;"); + // language=PostgreSQL + migrationBuilder.Sql("DROP TRIGGER episode_track_slug_trigger ON episodes;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP FUNCTION episode_update_tracks_slug;"); + // language=PostgreSQL + migrationBuilder.Sql(@"DROP VIEW library_items;"); + } + } +} \ No newline at end of file diff --git a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 4c6ceac7..e5044c60 100644 --- a/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -21,7 +21,7 @@ namespace Kyoo.Postgresql.Migrations .HasPostgresEnum(null, "status", new[] { "finished", "airing", "planned", "unknown" }) .HasPostgresEnum(null, "stream_type", new[] { "unknown", "video", "audio", "subtitle", "attachment" }) .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("ProductVersion", "5.0.7") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("Kyoo.Models.Collection", b => @@ -29,27 +29,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_collections"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_collections_slug"); - b.ToTable("Collections"); + b.ToTable("collections"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => @@ -57,49 +64,69 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AbsoluteNumber") - .HasColumnType("integer"); + b.Property("AbsoluteNumber") + .HasColumnType("integer") + .HasColumnName("absolute_number"); - b.Property("EpisodeNumber") - .HasColumnType("integer"); + b.Property("EpisodeNumber") + .HasColumnType("integer") + .HasColumnName("episode_number"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); - - b.Property("Runtime") - .HasColumnType("integer"); + .HasColumnType("timestamp without time zone") + .HasColumnName("release_date"); b.Property("SeasonID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_id"); - b.Property("SeasonNumber") - .HasColumnType("integer"); + b.Property("SeasonNumber") + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Thumb") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("thumb"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_episodes"); - b.HasIndex("SeasonID"); + b.HasIndex("SeasonID") + .HasDatabaseName("ix_episodes_season_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_episodes_slug"); b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_episodes_show_id_season_number_episode_number_absolute_numb"); - b.ToTable("Episodes"); + b.ToTable("episodes"); }); modelBuilder.Entity("Kyoo.Models.Genre", b => @@ -107,21 +134,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_genres"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_genres_slug"); - b.ToTable("Genres"); + b.ToTable("genres"); }); modelBuilder.Entity("Kyoo.Models.Library", b => @@ -129,157 +161,252 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Paths") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("paths"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_libraries"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_libraries_slug"); - b.ToTable("Libraries"); + b.ToTable("libraries"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_collection_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_collection_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_collection_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_collection"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_collection_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_collection"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_provider"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_provider_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_provider"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_library_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_library_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_library_show"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_show_genre"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_show_genre_second_id"); - b.ToTable("Link"); + b.ToTable("link_show_genre"); }); modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_link_user_show"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_link_user_show_second_id"); - b.ToTable("Link"); + b.ToTable("link_user_show"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() + b.Property("FirstID") .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("DataID") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("text") + .HasColumnName("data_id"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("link"); - b.Property("PeopleID") - .HasColumnType("integer"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_episode"); - b.Property("ProviderID") - .HasColumnType("integer"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_episode_second_id"); - b.Property("SeasonID") - .HasColumnType("integer"); + b.ToTable("metadata_id_episode"); + }); - b.Property("ShowID") - .HasColumnType("integer"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); - b.HasKey("ID"); + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); - b.HasIndex("EpisodeID"); + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); - b.HasIndex("PeopleID"); + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); - b.HasIndex("ProviderID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_people"); - b.HasIndex("SeasonID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_people_second_id"); - b.HasIndex("ShowID"); + b.ToTable("metadata_id_people"); + }); - b.ToTable("MetadataIds"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_season"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_season_second_id"); + + b.ToTable("metadata_id_season"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("integer") + .HasColumnName("first_id"); + + b.Property("SecondID") + .HasColumnType("integer") + .HasColumnName("second_id"); + + b.Property("DataID") + .HasColumnType("text") + .HasColumnName("data_id"); + + b.Property("Link") + .HasColumnType("text") + .HasColumnName("link"); + + b.HasKey("FirstID", "SecondID") + .HasName("pk_metadata_id_show"); + + b.HasIndex("SecondID") + .HasDatabaseName("ix_metadata_id_show_second_id"); + + b.ToTable("metadata_id_show"); }); modelBuilder.Entity("Kyoo.Models.People", b => @@ -287,24 +414,30 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_people_slug"); - b.ToTable("People"); + b.ToTable("people"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -312,27 +445,39 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("ForPeople") + .HasColumnType("boolean") + .HasColumnName("for_people"); + b.Property("PeopleID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("people_id"); b.Property("Role") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("role"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); b.Property("Type") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_people_roles"); - b.HasIndex("PeopleID"); + b.HasIndex("PeopleID") + .HasDatabaseName("ix_people_roles_people_id"); - b.HasIndex("ShowID"); + b.HasIndex("ShowID") + .HasDatabaseName("ix_people_roles_show_id"); - b.ToTable("PeopleRoles"); + b.ToTable("people_roles"); }); modelBuilder.Entity("Kyoo.Models.Provider", b => @@ -340,27 +485,34 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("LogoExtension") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo_extension"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_providers"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_providers_slug"); - b.ToTable("Providers"); + b.ToTable("providers"); }); modelBuilder.Entity("Kyoo.Models.Season", b => @@ -368,32 +520,54 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("EndDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("season_number"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("show_id"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); - b.Property("Year") - .HasColumnType("integer"); + b.HasKey("ID") + .HasName("pk_seasons"); - b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_seasons_slug"); b.HasIndex("ShowID", "SeasonNumber") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_seasons_show_id_season_number"); - b.ToTable("Seasons"); + b.ToTable("seasons"); }); modelBuilder.Entity("Kyoo.Models.Show", b => @@ -401,59 +575,77 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Aliases") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("aliases"); b.Property("Backdrop") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("backdrop"); - b.Property("EndYear") - .HasColumnType("integer"); + b.Property("EndAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_air"); b.Property("IsMovie") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_movie"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("logo"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("overview"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("poster"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.Property("StartYear") - .HasColumnType("integer"); + b.Property("StartAir") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_air"); b.Property("Status") - .HasColumnType("status"); + .HasColumnType("status") + .HasColumnName("status"); b.Property("StudioID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("studio_id"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrailerUrl") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("trailer_url"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_shows"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_shows_slug"); - b.HasIndex("StudioID"); + b.HasIndex("StudioID") + .HasDatabaseName("ix_shows_studio_id"); - b.ToTable("Shows"); + b.ToTable("shows"); }); modelBuilder.Entity("Kyoo.Models.Studio", b => @@ -461,21 +653,26 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("name"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_studios"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_studios_slug"); - b.ToTable("Studios"); + b.ToTable("studios"); }); modelBuilder.Entity("Kyoo.Models.Track", b => @@ -483,44 +680,66 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Codec") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("codec"); b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("episode_id"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_default"); b.Property("IsExternal") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_external"); b.Property("IsForced") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasColumnName("is_forced"); b.Property("Language") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("language"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("title"); b.Property("TrackIndex") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("track_index"); b.Property("Type") - .HasColumnType("stream_type"); + .HasColumnType("stream_type") + .HasColumnName("type"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_tracks"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_tracks_slug"); b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_tracks_episode_id_type_language_track_index_is_forced"); - b.ToTable("Tracks"); + b.ToTable("tracks"); }); modelBuilder.Entity("Kyoo.Models.User", b => @@ -528,62 +747,79 @@ namespace Kyoo.Postgresql.Migrations b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("integer") + .HasColumnName("id") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Email") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("email"); b.Property>("ExtraData") - .HasColumnType("jsonb"); + .HasColumnType("jsonb") + .HasColumnName("extra_data"); b.Property("Password") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("password"); b.Property("Permissions") - .HasColumnType("text[]"); + .HasColumnType("text[]") + .HasColumnName("permissions"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("slug"); b.Property("Username") - .HasColumnType("text"); + .HasColumnType("text") + .HasColumnName("username"); - b.HasKey("ID"); + b.HasKey("ID") + .HasName("pk_users"); b.HasIndex("Slug") - .IsUnique(); + .IsUnique() + .HasDatabaseName("ix_users_slug"); - b.ToTable("Users"); + b.ToTable("users"); }); modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("first_id"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("second_id"); b.Property("WatchedPercentage") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasColumnName("watched_percentage"); - b.HasKey("FirstID", "SecondID"); + b.HasKey("FirstID", "SecondID") + .HasName("pk_watched_episodes"); - b.HasIndex("SecondID"); + b.HasIndex("SecondID") + .HasDatabaseName("ix_watched_episodes_second_id"); - b.ToTable("WatchedEpisodes"); + b.ToTable("watched_episodes"); }); modelBuilder.Entity("Kyoo.Models.Episode", b => { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .HasConstraintName("fk_episodes_seasons_season_id") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") .HasForeignKey("ShowID") + .HasConstraintName("fk_episodes_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -597,12 +833,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Collection", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_collection_show_collections_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("CollectionLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_collection_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -616,12 +854,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("CollectionLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_collection_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Collection", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_collection_collections_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -635,12 +875,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ProviderLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_provider_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Provider", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_provider_providers_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -654,12 +896,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Library", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_library_show_libraries_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany("LibraryLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_library_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -673,12 +917,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "First") .WithMany("GenreLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_show_genre_shows_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Genre", "Second") .WithMany("ShowLinks") .HasForeignKey("SecondID") + .HasConstraintName("fk_link_show_genre_genres_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -692,12 +938,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("ShowLinks") .HasForeignKey("FirstID") + .HasConstraintName("fk_link_user_show_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_link_user_show_shows_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -706,43 +954,88 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_episode_episodes_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_episode_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_people_people_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_people_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_season_seasons_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_season_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .HasConstraintName("fk_metadata_id_show_shows_first_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .HasConstraintName("fk_metadata_id_show_providers_second_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -750,12 +1043,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.People", "People") .WithMany("Roles") .HasForeignKey("PeopleID") + .HasConstraintName("fk_people_roles_people_people_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("People") .HasForeignKey("ShowID") + .HasConstraintName("fk_people_roles_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -769,6 +1064,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Seasons") .HasForeignKey("ShowID") + .HasConstraintName("fk_seasons_shows_show_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -779,7 +1075,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Studio", "Studio") .WithMany("Shows") - .HasForeignKey("StudioID"); + .HasForeignKey("StudioID") + .HasConstraintName("fk_shows_studios_studio_id"); b.Navigation("Studio"); }); @@ -789,6 +1086,7 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.Episode", "Episode") .WithMany("Tracks") .HasForeignKey("EpisodeID") + .HasConstraintName("fk_tracks_episodes_episode_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -800,12 +1098,14 @@ namespace Kyoo.Postgresql.Migrations b.HasOne("Kyoo.Models.User", "First") .WithMany("CurrentlyWatching") .HasForeignKey("FirstID") + .HasConstraintName("fk_watched_episodes_users_first_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Kyoo.Models.Episode", "Second") .WithMany() .HasForeignKey("SecondID") + .HasConstraintName("fk_watched_episodes_episodes_second_id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -852,8 +1152,6 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.Postgresql/PostgresContext.cs b/Kyoo.Postgresql/PostgresContext.cs index 4836601c..b0e534ed 100644 --- a/Kyoo.Postgresql/PostgresContext.cs +++ b/Kyoo.Postgresql/PostgresContext.cs @@ -26,16 +26,19 @@ namespace Kyoo.Postgresql /// Should the configure step be skipped? This is used when the database is created via DbContextOptions. /// private readonly bool _skipConfigure; - - /// - /// A basic constructor that set default values (query tracker behaviors, mapping enums...) - /// - public PostgresContext() + + + static PostgresContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); } + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + public PostgresContext() { } /// /// Create a new using specific options @@ -44,9 +47,6 @@ namespace Kyoo.Postgresql public PostgresContext(DbContextOptions options) : base(options) { - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); _skipConfigure = true; } @@ -77,6 +77,7 @@ namespace Kyoo.Postgresql optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); } + optionsBuilder.UseSnakeCaseNamingConvention(); base.OnConfiguring(optionsBuilder); } @@ -90,6 +91,10 @@ namespace Kyoo.Postgresql modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); + modelBuilder.Entity() + .ToView("library_items") + .HasKey(x => x.ID); + modelBuilder.Entity() .Property(x => x.ExtraData) .HasColumnType("jsonb"); @@ -107,7 +112,7 @@ namespace Kyoo.Postgresql public override Expression> Like(Expression> query, string format) { MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.ILike); - MethodCallExpression call = Expression.Call(iLike, query.Body, Expression.Constant(format)); + MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format)); return Expression.Lambda>(call, query.Parameters); } diff --git a/Kyoo.SqLite/Kyoo.SqLite.csproj b/Kyoo.SqLite/Kyoo.SqLite.csproj new file mode 100644 index 00000000..357b5ae0 --- /dev/null +++ b/Kyoo.SqLite/Kyoo.SqLite.csproj @@ -0,0 +1,41 @@ + + + net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + Kyoo.SqLite + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs b/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs similarity index 64% rename from Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs rename to Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs index 834321b2..1337ce6b 100644 --- a/Kyoo.Postgresql/Migrations/20210507203809_Initial.Designer.cs +++ b/Kyoo.SqLite/Migrations/20210626141337_Initial.Designer.cs @@ -1,50 +1,41 @@ // using System; -using System.Collections.Generic; -using Kyoo.Models; -using Kyoo.Postgresql; +using Kyoo.SqLite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Postgresql.Migrations +namespace Kyoo.SqLite.Migrations { - [DbContext(typeof(PostgresContext))] - [Migration("20210507203809_Initial")] + [DbContext(typeof(SqLiteContext))] + [Migration("20210626141337_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .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", "attachment" }) - .HasAnnotation("Relational:MaxIdentifierLength", 63) - .HasAnnotation("ProductVersion", "5.0.5") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasAnnotation("ProductVersion", "5.0.7"); modelBuilder.Entity("Kyoo.Models.Collection", b => { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -58,46 +49,49 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); - b.Property("AbsoluteNumber") - .HasColumnType("integer"); + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); - b.Property("EpisodeNumber") - .HasColumnType("integer"); + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("ReleaseDate") - .HasColumnType("timestamp without time zone"); - - b.Property("Runtime") - .HasColumnType("integer"); + .HasColumnType("TEXT"); b.Property("SeasonID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); - b.Property("SeasonNumber") - .HasColumnType("integer"); + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); b.Property("Thumb") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); b.HasIndex("SeasonID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") .IsUnique(); @@ -108,15 +102,14 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -130,18 +123,17 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property("Paths") - .HasColumnType("text[]"); + b.Property("Paths") + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -154,10 +146,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -169,10 +161,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -184,10 +176,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -199,10 +191,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -214,10 +206,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -229,10 +221,10 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -241,65 +233,105 @@ namespace Kyoo.Postgresql.Migrations b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.Property("ID") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); b.Property("DataID") - .HasColumnType("text"); - - b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("TEXT"); b.Property("Link") - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property("PeopleID") - .HasColumnType("integer"); + b.HasKey("FirstID", "SecondID"); - b.Property("ProviderID") - .HasColumnType("integer"); + b.HasIndex("SecondID"); - b.Property("SeasonID") - .HasColumnType("integer"); + b.ToTable("MetadataID"); + }); - b.Property("ShowID") - .HasColumnType("integer"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); - b.HasKey("ID"); + b.Property("SecondID") + .HasColumnType("INTEGER"); - b.HasIndex("EpisodeID"); + b.Property("DataID") + .HasColumnType("TEXT"); - b.HasIndex("PeopleID"); + b.Property("Link") + .HasColumnType("TEXT"); - b.HasIndex("ProviderID"); + b.HasKey("FirstID", "SecondID"); - b.HasIndex("SeasonID"); + b.HasIndex("SecondID"); - b.HasIndex("ShowID"); + b.ToTable("MetadataID"); + }); - b.ToTable("MetadataIds"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); }); modelBuilder.Entity("Kyoo.Models.People", b => { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -313,20 +345,22 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); + + b.Property("ForPeople") + .HasColumnType("INTEGER"); b.Property("PeopleID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("Role") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("Type") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -341,21 +375,20 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("LogoExtension") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -369,29 +402,38 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("SeasonNumber") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("ShowID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); b.Property("Title") - .HasColumnType("text"); - - b.Property("Year") - .HasColumnType("integer"); + .HasColumnType("TEXT"); b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("ShowID", "SeasonNumber") .IsUnique(); @@ -402,51 +444,50 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); - b.Property("Aliases") - .HasColumnType("text[]"); + b.Property("Aliases") + .HasColumnType("TEXT"); b.Property("Backdrop") - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property("EndYear") - .HasColumnType("integer"); + b.Property("EndAir") + .HasColumnType("TEXT"); b.Property("IsMovie") - .HasColumnType("boolean"); + .HasColumnType("INTEGER"); b.Property("Logo") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Overview") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Poster") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property("StartYear") - .HasColumnType("integer"); + b.Property("StartAir") + .HasColumnType("TEXT"); - b.Property("Status") - .HasColumnType("status"); + b.Property("Status") + .HasColumnType("INTEGER"); b.Property("StudioID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("TrailerUrl") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -462,15 +503,14 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Name") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -484,41 +524,47 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Codec") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("EpisodeID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("INTEGER"); b.Property("IsExternal") - .HasColumnType("boolean"); + .HasColumnType("INTEGER"); b.Property("IsForced") - .HasColumnType("boolean"); + .HasColumnType("INTEGER"); b.Property("Language") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Path") - .HasColumnType("text"); + .HasColumnType("TEXT"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); b.Property("Title") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("TrackIndex") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); - b.Property("Type") - .HasColumnType("stream_type"); + b.Property("Type") + .HasColumnType("INTEGER"); b.HasKey("ID"); + b.HasIndex("Slug") + .IsUnique(); + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") .IsUnique(); @@ -529,27 +575,26 @@ namespace Kyoo.Postgresql.Migrations { b.Property("ID") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("INTEGER"); b.Property("Email") - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property>("ExtraData") - .HasColumnType("jsonb"); + b.Property("ExtraData") + .HasColumnType("TEXT"); b.Property("Password") - .HasColumnType("text"); + .HasColumnType("TEXT"); - b.Property("Permissions") - .HasColumnType("text[]"); + b.Property("Permissions") + .HasColumnType("TEXT"); b.Property("Slug") .IsRequired() - .HasColumnType("text"); + .HasColumnType("TEXT"); b.Property("Username") - .HasColumnType("text"); + .HasColumnType("TEXT"); b.HasKey("ID"); @@ -562,13 +607,13 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => { b.Property("FirstID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("SecondID") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.Property("WatchedPercentage") - .HasColumnType("integer"); + .HasColumnType("INTEGER"); b.HasKey("FirstID", "SecondID"); @@ -581,7 +626,8 @@ namespace Kyoo.Postgresql.Migrations { b.HasOne("Kyoo.Models.Season", "Season") .WithMany("Episodes") - .HasForeignKey("SeasonID"); + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("Kyoo.Models.Show", "Show") .WithMany("Episodes") @@ -708,43 +754,80 @@ namespace Kyoo.Postgresql.Migrations b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.MetadataID", b => + modelBuilder.Entity("Kyoo.Models.MetadataID", b => { - b.HasOne("Kyoo.Models.Episode", "Episode") + b.HasOne("Kyoo.Models.Episode", "First") .WithMany("ExternalIDs") - .HasForeignKey("EpisodeID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.People", "People") - .WithMany("ExternalIDs") - .HasForeignKey("PeopleID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Kyoo.Models.Provider", "Provider") - .WithMany("MetadataLinks") - .HasForeignKey("ProviderID") + .HasForeignKey("FirstID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Kyoo.Models.Season", "Season") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") .WithMany("ExternalIDs") - .HasForeignKey("SeasonID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Kyoo.Models.Show", "Show") + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") .WithMany("ExternalIDs") - .HasForeignKey("ShowID") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Episode"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("People"); + b.Navigation("First"); - b.Navigation("Provider"); + b.Navigation("Second"); + }); - b.Navigation("Season"); + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("Show"); + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); }); modelBuilder.Entity("Kyoo.Models.PeopleRole", b => @@ -854,8 +937,6 @@ namespace Kyoo.Postgresql.Migrations modelBuilder.Entity("Kyoo.Models.Provider", b => { b.Navigation("LibraryLinks"); - - b.Navigation("MetadataLinks"); }); modelBuilder.Entity("Kyoo.Models.Season", b => diff --git a/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs similarity index 62% rename from Kyoo.Postgresql/Migrations/20210507203809_Initial.cs rename to Kyoo.SqLite/Migrations/20210626141337_Initial.cs index 678e90ee..88823571 100644 --- a/Kyoo.Postgresql/Migrations/20210507203809_Initial.cs +++ b/Kyoo.SqLite/Migrations/20210626141337_Initial.cs @@ -1,30 +1,22 @@ using System; -using System.Collections.Generic; -using Kyoo.Models; using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace Kyoo.Postgresql.Migrations +namespace Kyoo.SqLite.Migrations { public partial class Initial : Migration { protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:Enum:item_type", "show,movie,collection") - .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") - .Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment"); - migrationBuilder.CreateTable( name: "Collections", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", 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 => { @@ -35,10 +27,10 @@ namespace Kyoo.Postgresql.Migrations name: "Genres", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -49,11 +41,11 @@ namespace Kyoo.Postgresql.Migrations name: "Libraries", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Paths = table.Column(type: "text[]", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Paths = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -64,11 +56,11 @@ namespace Kyoo.Postgresql.Migrations name: "People", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Poster = table.Column(type: "text", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -79,12 +71,12 @@ namespace Kyoo.Postgresql.Migrations name: "Providers", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true), - Logo = table.Column(type: "text", nullable: true), - LogoExtension = table.Column(type: "text", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Logo = table.Column(type: "TEXT", nullable: true), + LogoExtension = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -95,10 +87,10 @@ namespace Kyoo.Postgresql.Migrations name: "Studios", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -109,14 +101,14 @@ namespace Kyoo.Postgresql.Migrations name: "Users", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Username = table.Column(type: "text", nullable: true), - Email = table.Column(type: "text", nullable: true), - Password = table.Column(type: "text", nullable: true), - Permissions = table.Column(type: "text[]", nullable: true), - ExtraData = table.Column>(type: "jsonb", nullable: true) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: false), + Username = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + Password = table.Column(type: "TEXT", nullable: true), + Permissions = table.Column(type: "TEXT", nullable: true), + ExtraData = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -127,8 +119,8 @@ namespace Kyoo.Postgresql.Migrations name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -151,8 +143,8 @@ namespace Kyoo.Postgresql.Migrations name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -171,26 +163,52 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_People_FirstID", + column: x => x.FirstID, + principalTable: "People", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Shows", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Slug = table.Column(type: "text", nullable: false), - Title = table.Column(type: "text", nullable: true), - Aliases = table.Column(type: "text[]", nullable: true), - Path = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - Status = table.Column(type: "status", 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) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", 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(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), + StartAir = table.Column(type: "TEXT", nullable: true), + EndAir = table.Column(type: "TEXT", 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: "INTEGER", nullable: false), + StudioID = table.Column(type: "INTEGER", nullable: true) }, constraints: table => { @@ -200,15 +218,15 @@ namespace Kyoo.Postgresql.Migrations column: x => x.StudioID, principalTable: "Studios", principalColumn: "ID", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.SetNull); }); migrationBuilder.CreateTable( name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -231,8 +249,8 @@ namespace Kyoo.Postgresql.Migrations name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -255,8 +273,8 @@ namespace Kyoo.Postgresql.Migrations name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -279,8 +297,8 @@ namespace Kyoo.Postgresql.Migrations name: "Link", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -299,16 +317,43 @@ namespace Kyoo.Postgresql.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Shows_FirstID", + column: x => x.FirstID, + principalTable: "Shows", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "PeopleRoles", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ForPeople = table.Column(type: "INTEGER", nullable: false), + PeopleID = table.Column(type: "INTEGER", nullable: false), + ShowID = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "TEXT", nullable: true), + Role = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -331,14 +376,16 @@ namespace Kyoo.Postgresql.Migrations name: "Seasons", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", 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), + StartDate = table.Column(type: "TEXT", nullable: true), + EndDate = table.Column(type: "TEXT", nullable: true), + Poster = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -355,19 +402,19 @@ namespace Kyoo.Postgresql.Migrations name: "Episodes", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: true), + ShowID = table.Column(type: "INTEGER", nullable: false), + SeasonID = table.Column(type: "INTEGER", nullable: true), + SeasonNumber = table.Column(type: "INTEGER", nullable: true), + EpisodeNumber = table.Column(type: "INTEGER", nullable: true), + AbsoluteNumber = table.Column(type: "INTEGER", nullable: true), + 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: "TEXT", nullable: true) }, constraints: table => { @@ -377,7 +424,7 @@ namespace Kyoo.Postgresql.Migrations column: x => x.SeasonID, principalTable: "Seasons", principalColumn: "ID", - onDelete: ReferentialAction.Restrict); + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Episodes_Shows_ShowID", column: x => x.ShowID, @@ -387,50 +434,53 @@ namespace Kyoo.Postgresql.Migrations }); migrationBuilder.CreateTable( - name: "MetadataIds", + name: "MetadataID", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_MetadataIds", x => x.ID); + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Episodes_EpisodeID", - column: x => x.EpisodeID, - principalTable: "Episodes", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_People_PeopleID", - column: x => x.PeopleID, - principalTable: "People", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MetadataIds_Providers_ProviderID", - column: x => x.ProviderID, + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_MetadataIds_Seasons_SeasonID", - column: x => x.SeasonID, + name: "FK_MetadataID_Seasons_FirstID", + column: x => x.FirstID, principalTable: "Seasons", principalColumn: "ID", onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MetadataID", + columns: table => new + { + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + DataID = table.Column(type: "TEXT", nullable: true), + Link = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataID", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_MetadataIds_Shows_ShowID", - column: x => x.ShowID, - principalTable: "Shows", + name: "FK_MetadataID_Episodes_FirstID", + column: x => x.FirstID, + principalTable: "Episodes", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataID_Providers_SecondID", + column: x => x.SecondID, + principalTable: "Providers", principalColumn: "ID", onDelete: ReferentialAction.Cascade); }); @@ -439,18 +489,19 @@ namespace Kyoo.Postgresql.Migrations name: "Tracks", columns: table => new { - ID = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - 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: "stream_type", nullable: false) + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Slug = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", nullable: true), + Language = table.Column(type: "TEXT", nullable: true), + Codec = table.Column(type: "TEXT", nullable: true), + IsDefault = table.Column(type: "INTEGER", nullable: false), + IsForced = table.Column(type: "INTEGER", nullable: false), + IsExternal = table.Column(type: "INTEGER", nullable: false), + Path = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + EpisodeID = table.Column(type: "INTEGER", nullable: false), + TrackIndex = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -467,9 +518,9 @@ namespace Kyoo.Postgresql.Migrations name: "WatchedEpisodes", columns: table => new { - FirstID = table.Column(type: "integer", nullable: false), - SecondID = table.Column(type: "integer", nullable: false), - WatchedPercentage = table.Column(type: "integer", nullable: false) + FirstID = table.Column(type: "INTEGER", nullable: false), + SecondID = table.Column(type: "INTEGER", nullable: false), + WatchedPercentage = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -505,6 +556,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Episodes_Slug", + table: "Episodes", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Genres_Slug", table: "Genres", @@ -548,29 +605,24 @@ namespace Kyoo.Postgresql.Migrations column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_EpisodeID", - table: "MetadataIds", - column: "EpisodeID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_PeopleID", - table: "MetadataIds", - column: "PeopleID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ProviderID", - table: "MetadataIds", - column: "ProviderID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( - name: "IX_MetadataIds_SeasonID", - table: "MetadataIds", - column: "SeasonID"); - - migrationBuilder.CreateIndex( - name: "IX_MetadataIds_ShowID", - table: "MetadataIds", - column: "ShowID"); + name: "IX_MetadataID_SecondID", + table: "MetadataID", + column: "SecondID"); migrationBuilder.CreateIndex( name: "IX_People_Slug", @@ -600,6 +652,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "ShowID", "SeasonNumber" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Seasons_Slug", + table: "Seasons", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Shows_Slug", table: "Shows", @@ -623,6 +681,12 @@ namespace Kyoo.Postgresql.Migrations columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_Tracks_Slug", + table: "Tracks", + column: "Slug", + unique: true); + migrationBuilder.CreateIndex( name: "IX_Users_Slug", table: "Users", @@ -656,7 +720,16 @@ namespace Kyoo.Postgresql.Migrations name: "Link"); migrationBuilder.DropTable( - name: "MetadataIds"); + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); + + migrationBuilder.DropTable( + name: "MetadataID"); migrationBuilder.DropTable( name: "PeopleRoles"); diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs new file mode 100644 index 00000000..02045e3f --- /dev/null +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs @@ -0,0 +1,980 @@ +// +using System; +using Kyoo.SqLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Kyoo.SqLite.Migrations +{ + [DbContext(typeof(SqLiteContext))] + [Migration("20210626141347_Triggers")] + partial class Triggers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.7"); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + 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.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.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 => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ForPeople") + .HasColumnType("INTEGER"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("LogoExtension") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aliases") + .HasColumnType("TEXT"); + + b.Property("Backdrop") + .HasColumnType("TEXT"); + + b.Property("EndAir") + .HasColumnType("TEXT"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartAir") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StudioID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrailerUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("ExtraData") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("WatchedPercentage") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Provider", "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("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + 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.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + 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"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs new file mode 100644 index 00000000..f3ae8325 --- /dev/null +++ b/Kyoo.SqLite/Migrations/20210626141347_Triggers.cs @@ -0,0 +1,187 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Kyoo.SqLite.Migrations +{ + public partial class Triggers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber + WHERE ID == new.ID; + END"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber + WHERE ID == new.ID; + END"); + + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END + WHERE ID == new.ID; + END"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID + ON Episodes FOR EACH ROW + BEGIN + UPDATE Episodes + SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END + WHERE ID == new.ID; + END"); + + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER TrackSlugInsert + AFTER INSERT ON Tracks + FOR EACH ROW + BEGIN + UPDATE Tracks SET TrackIndex = ( + SELECT COUNT(*) FROM Tracks + WHERE EpisodeID = new.EpisodeID AND Type = new.Type + AND Language = new.Language AND IsForced = new.IsForced + ) WHERE ID = new.ID AND TrackIndex = 0; + UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || (TrackIndex) + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE ID = new.ID; + END;"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER TrackSlugUpdate + AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks + FOR EACH ROW + BEGIN + UPDATE Tracks SET TrackIndex = ( + SELECT COUNT(*) FROM Tracks + WHERE EpisodeID = new.EpisodeID AND Type = new.Type + AND Language = new.Language AND IsForced = new.IsForced + ) WHERE ID = new.ID AND TrackIndex = 0; + UPDATE Tracks SET Slug = + (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || (TrackIndex) + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE ID = new.ID; + END;"); + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER EpisodeUpdateTracksSlug + AFTER UPDATE OF Slug ON Episodes + FOR EACH ROW + BEGIN + UPDATE Tracks SET Slug = + NEW.Slug || + '.' || Language || + CASE (TrackIndex) + WHEN 0 THEN '' + ELSE '-' || TrackIndex + END || + CASE (IsForced) + WHEN false THEN '' + ELSE '-forced' + END || + CASE (Type) + WHEN 1 THEN '.video' + WHEN 2 THEN '.audio' + WHEN 3 THEN '.subtitle' + ELSE '.' || Type + END + WHERE EpisodeID = NEW.ID; + END;"); + + + // language=SQLite + migrationBuilder.Sql(@" + CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW + BEGIN + UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID; + UPDATE Episodes + SET Slug = new.Slug || + CASE + WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN '' + WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber + ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber + END + WHERE ShowID = new.ID; + END;"); + + + // language=SQLite + migrationBuilder.Sql(@" + CREATE VIEW LibraryItems AS + SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Poster, CASE + WHEN s.IsMovie THEN 1 + ELSE 0 + END AS Type + FROM Shows AS s + WHERE NOT (EXISTS ( + SELECT 1 + FROM 'Link' AS l + INNER JOIN Collections AS c ON l.FirstID = c.ID + WHERE s.ID = l.SecondID)) + UNION ALL + SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status, + NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type + FROM collections AS c0"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // language=SQLite + migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;"); + // language=SQLite + migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;"); + // language=SQLite + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;"); + // language=SQLite + migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;"); + // language=SQLite + migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;"); + } + } +} \ No newline at end of file diff --git a/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs new file mode 100644 index 00000000..58ded130 --- /dev/null +++ b/Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs @@ -0,0 +1,978 @@ +// +using System; +using Kyoo.SqLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Kyoo.SqLite.Migrations +{ + [DbContext(typeof(SqLiteContext))] + partial class SqLiteContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.7"); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteNumber") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonID") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("SeasonID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Genre", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Kyoo.Models.Library", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + 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.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.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 => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("DataID") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("MetadataID"); + }); + + modelBuilder.Entity("Kyoo.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ForPeople") + .HasColumnType("INTEGER"); + + b.Property("PeopleID") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("PeopleID"); + + b.HasIndex("ShowID"); + + b.ToTable("PeopleRoles"); + }); + + modelBuilder.Entity("Kyoo.Models.Provider", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("LogoExtension") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowID") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("ShowID", "SeasonNumber") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aliases") + .HasColumnType("TEXT"); + + b.Property("Backdrop") + .HasColumnType("TEXT"); + + b.Property("EndAir") + .HasColumnType("TEXT"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartAir") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StudioID") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrailerUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("StudioID"); + + b.ToTable("Shows"); + }); + + modelBuilder.Entity("Kyoo.Models.Studio", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Studios"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("EpisodeID") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Slug") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") + .IsUnique(); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("ExtraData") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.Property("FirstID") + .HasColumnType("INTEGER"); + + b.Property("SecondID") + .HasColumnType("INTEGER"); + + b.Property("WatchedPercentage") + .HasColumnType("INTEGER"); + + b.HasKey("FirstID", "SecondID"); + + b.HasIndex("SecondID"); + + b.ToTable("WatchedEpisodes"); + }); + + modelBuilder.Entity("Kyoo.Models.Episode", b => + { + b.HasOne("Kyoo.Models.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Episodes") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.Collection", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Provider", "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("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Genre", "Second") + .WithMany("ShowLinks") + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Link", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("ShowLinks") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Episode", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.People", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Season", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.MetadataID", b => + { + b.HasOne("Kyoo.Models.Show", "First") + .WithMany("ExternalIDs") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Provider", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.PeopleRole", b => + { + b.HasOne("Kyoo.Models.People", "People") + .WithMany("Roles") + .HasForeignKey("PeopleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("People") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("People"); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Season", b => + { + b.HasOne("Kyoo.Models.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("Kyoo.Models.Show", b => + { + b.HasOne("Kyoo.Models.Studio", "Studio") + .WithMany("Shows") + .HasForeignKey("StudioID"); + + b.Navigation("Studio"); + }); + + modelBuilder.Entity("Kyoo.Models.Track", b => + { + b.HasOne("Kyoo.Models.Episode", "Episode") + .WithMany("Tracks") + .HasForeignKey("EpisodeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => + { + b.HasOne("Kyoo.Models.User", "First") + .WithMany("CurrentlyWatching") + .HasForeignKey("FirstID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kyoo.Models.Episode", "Second") + .WithMany() + .HasForeignKey("SecondID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("First"); + + b.Navigation("Second"); + }); + + modelBuilder.Entity("Kyoo.Models.Collection", b => + { + b.Navigation("LibraryLinks"); + + 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.Provider", b => + { + b.Navigation("LibraryLinks"); + }); + + 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"); + }); + + modelBuilder.Entity("Kyoo.Models.User", b => + { + b.Navigation("CurrentlyWatching"); + + b.Navigation("ShowLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kyoo.SqLite/SqLiteContext.cs b/Kyoo.SqLite/SqLiteContext.cs new file mode 100644 index 00000000..23145cf1 --- /dev/null +++ b/Kyoo.SqLite/SqLiteContext.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Kyoo.Models; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; + +namespace Kyoo.SqLite +{ + /// + /// A sqlite implementation of . + /// + public class SqLiteContext : DatabaseContext + { + /// + /// The connection string to use. + /// + private readonly string _connection; + + /// + /// Is this instance in debug mode? + /// + private readonly bool _debugMode; + + /// + /// Should the configure step be skipped? This is used when the database is created via DbContextOptions. + /// + private readonly bool _skipConfigure; + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + public SqLiteContext() + { } + + /// + /// Create a new using specific options + /// + /// The options to use. + public SqLiteContext(DbContextOptions options) + : base(options) + { + _skipConfigure = true; + } + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// + /// The connection string to use + /// Is this instance in debug mode? + public SqLiteContext(string connection, bool debugMode) + { + _connection = connection; + _debugMode = debugMode; + } + + /// + /// Set connection information for this database context + /// + /// An option builder to fill. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!_skipConfigure) + { + if (_connection != null) + optionsBuilder.UseSqlite(_connection); + else + optionsBuilder.UseSqlite(); + if (_debugMode) + optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); + } + + base.OnConfiguring(optionsBuilder); + } + + /// + /// Set database parameters to support every types of Kyoo. + /// + /// The database's model builder. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + ValueConverter arrayConvertor = new( + x => string.Join(";", x), + x => x.Split(';', StringSplitOptions.None)); + modelBuilder.Entity() + .Property(x => x.Paths) + .HasConversion(arrayConvertor); + modelBuilder.Entity() + .Property(x => x.Aliases) + .HasConversion(arrayConvertor); + modelBuilder.Entity() + .Property(x => x.Permissions) + .HasConversion(arrayConvertor); + + modelBuilder.Entity() + .Property(x => x.Status) + .HasConversion(); + modelBuilder.Entity() + .Property(x => x.Type) + .HasConversion(); + + ValueConverter, string> jsonConvertor = new( + x => JsonConvert.SerializeObject(x), + x => JsonConvert.DeserializeObject>(x)); + modelBuilder.Entity() + .Property(x => x.ExtraData) + .HasConversion(jsonConvertor); + + modelBuilder.Entity() + .ToView("LibraryItems") + .HasKey(x => x.ID); + base.OnModelCreating(modelBuilder); + } + + /// + protected override bool IsDuplicateException(Exception ex) + { + return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/} + or SqliteException { SqliteExtendedErrorCode: 1555 /*SQLITE_CONSTRAINT_PRIMARYKEY*/}; + } + + /// + public override Expression> Like(Expression> query, string format) + { + MethodInfo iLike = MethodOfUtils.MethodOf(EF.Functions.Like); + MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format)); + + return Expression.Lambda>(call, query.Parameters); + } + } +} \ No newline at end of file diff --git a/Kyoo.SqLite/SqLiteModule.cs b/Kyoo.SqLite/SqLiteModule.cs new file mode 100644 index 00000000..34802b20 --- /dev/null +++ b/Kyoo.SqLite/SqLiteModule.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Kyoo.Controllers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Kyoo.SqLite +{ + /// + /// A module to add sqlite capacity to the app. + /// + public class SqLiteModule : IPlugin + { + /// + public string Slug => "sqlite"; + + /// + public string Name => "SqLite"; + + /// + public string Description => "A database context for sqlite."; + + /// + public ICollection Provides => new[] + { + typeof(DatabaseContext) + }; + + /// + public ICollection ConditionalProvides => ArraySegment.Empty; + + /// + public ICollection Requires => ArraySegment.Empty; + + + /// + /// The configuration to use. The database connection string is pulled from it. + /// + private readonly IConfiguration _configuration; + + /// + /// The host environment to check if the app is in debug mode. + /// + private readonly IWebHostEnvironment _environment; + + /// + /// Create a new postgres module instance and use the given configuration and environment. + /// + /// The configuration to use + /// The environment that will be used (if the env is in development mode, more information will be displayed on errors. + public SqLiteModule(IConfiguration configuration, IWebHostEnvironment env) + { + _configuration = configuration; + _environment = env; + } + + + /// + public void Configure(IServiceCollection services, ICollection availableTypes) + { + services.AddDbContext(x => + { + x.UseSqlite(_configuration.GetDatabaseConnection("sqlite")); + if (_environment.IsDevelopment()) + x.EnableDetailedErrors().EnableSensitiveDataLogging(); + }); + } + + /// + public void Initialize(IServiceProvider provider) + { + DatabaseContext context = provider.GetRequiredService(); + context.Database.Migrate(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/KAssert.cs b/Kyoo.Tests/KAssert.cs new file mode 100644 index 00000000..80209c98 --- /dev/null +++ b/Kyoo.Tests/KAssert.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using JetBrains.Annotations; +using Xunit; +using Xunit.Sdk; + +namespace Kyoo.Tests +{ + /// + /// Custom assertions used by Kyoo's tests. + /// + public static class KAssert + { + /// + /// Check if every property of the item is equal to the other's object. + /// + /// The value to check against + /// The value to check + /// The type to check + [AssertionMethod] + public static void DeepEqual(T expected, T value) + { + foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance)) + Assert.Equal(property.GetValue(expected), property.GetValue(value)); + } + + /// + /// Explicitly fail a test. + /// + [AssertionMethod] + public static void Fail() + { + throw new XunitException(); + } + + /// + /// Explicitly fail a test. + /// + /// The message that will be seen in the test report + [AssertionMethod] + public static void Fail(string message) + { + throw new XunitException(message); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index b5e3dd82..d198dae6 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -14,8 +14,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Library/RepositoryActivator.cs new file mode 100644 index 00000000..ee6aa4df --- /dev/null +++ b/Kyoo.Tests/Library/RepositoryActivator.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Xunit.Abstractions; + +namespace Kyoo.Tests +{ + public class RepositoryActivator : IDisposable, IAsyncDisposable + { + public TestContext Context { get; } + public ILibraryManager LibraryManager { get; } + + + private readonly DatabaseContext _database; + + public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null) + { + Context = postgres == null + ? new SqLiteTestContext(output) + : new PostgresTestContext(postgres, output); + _database = Context.New(); + + ProviderRepository provider = new(_database); + LibraryRepository library = new(_database, provider); + CollectionRepository collection = new(_database); + GenreRepository genre = new(_database); + StudioRepository studio = new(_database); + PeopleRepository people = new(_database, provider, + new Lazy(() => LibraryManager.ShowRepository)); + ShowRepository show = new(_database, studio, people, genre, provider); + SeasonRepository season = new(_database, provider); + LibraryItemRepository libraryItem = new(_database, + new Lazy(() => LibraryManager.LibraryRepository)); + TrackRepository track = new(_database); + EpisodeRepository episode = new(_database, provider, track); + UserRepository user = new(_database); + + LibraryManager = new LibraryManager(new IBaseRepository[] { + provider, + library, + libraryItem, + collection, + show, + season, + episode, + track, + people, + studio, + genre, + user + }); + } + + public void Dispose() + { + _database.Dispose(); + Context.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await _database.DisposeAsync(); + await Context.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Library/RepositoryTests.cs new file mode 100644 index 00000000..51db3061 --- /dev/null +++ b/Kyoo.Tests/Library/RepositoryTests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Xunit; + +namespace Kyoo.Tests +{ + public abstract class RepositoryTests : IDisposable, IAsyncDisposable + where T : class, IResource, new() + { + protected readonly RepositoryActivator Repositories; + private readonly IRepository _repository; + + protected RepositoryTests(RepositoryActivator repositories) + { + Repositories = repositories; + _repository = Repositories.LibraryManager.GetRepository(); + } + + public void Dispose() + { + Repositories.Dispose(); + } + + public ValueTask DisposeAsync() + { + return Repositories.DisposeAsync(); + } + + [Fact] + public async Task FillTest() + { + await using DatabaseContext database = Repositories.Context.New(); + + Assert.Equal(1, database.Shows.Count()); + } + + [Fact] + public async Task GetByIdTest() + { + T value = await _repository.Get(TestSample.Get().ID); + KAssert.DeepEqual(TestSample.Get(), value); + } + + [Fact] + public async Task GetBySlugTest() + { + T value = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(TestSample.Get(), value); + } + + [Fact] + public async Task GetByFakeIdTest() + { + await Assert.ThrowsAsync(() => _repository.Get(2)); + } + + [Fact] + public async Task GetByFakeSlugTest() + { + await Assert.ThrowsAsync(() => _repository.Get("non-existent")); + } + + [Fact] + public async Task DeleteByIdTest() + { + await _repository.Delete(TestSample.Get().ID); + Assert.Equal(0, await _repository.GetCount()); + } + + [Fact] + public async Task DeleteBySlugTest() + { + await _repository.Delete(TestSample.Get().Slug); + Assert.Equal(0, await _repository.GetCount()); + } + + [Fact] + public async Task DeleteByValueTest() + { + await _repository.Delete(TestSample.Get()); + Assert.Equal(0, await _repository.GetCount()); + } + + [Fact] + public async Task CreateTest() + { + await Assert.ThrowsAsync(() => _repository.Create(TestSample.Get())); + await _repository.Delete(TestSample.Get()); + + T expected = TestSample.Get(); + expected.ID = 0; + await _repository.Create(expected); + KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); + } + + [Fact] + public async Task CreateNullTest() + { + await Assert.ThrowsAsync(() => _repository.Create(null!)); + } + + [Fact] + public async Task CreateIfNotExistNullTest() + { + await Assert.ThrowsAsync(() => _repository.CreateIfNotExists(null!)); + } + + [Fact] + public async Task CreateIfNotExistTest() + { + T expected = TestSample.Get(); + KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); + await _repository.Delete(TestSample.Get()); + KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get())); + } + + [Fact] + public async Task EditNullTest() + { + await Assert.ThrowsAsync(() => _repository.Edit(null!, false)); + } + + [Fact] + public async Task EditNonExistingTest() + { + await Assert.ThrowsAsync(() => _repository.Edit(new T {ID = 56}, false)); + } + + [Fact] + public async Task GetExpressionIDTest() + { + KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.ID == TestSample.Get().ID)); + } + + [Fact] + public async Task GetExpressionSlugTest() + { + KAssert.DeepEqual(TestSample.Get(), await _repository.Get(x => x.Slug == TestSample.Get().Slug)); + } + + [Fact] + public async Task GetExpressionNotFoundTest() + { + await Assert.ThrowsAsync(() => _repository.Get(x => x.Slug == "non-existing")); + } + + [Fact] + public async Task GetExpressionNullTest() + { + await Assert.ThrowsAsync(() => _repository.Get((Expression>)null!)); + } + + [Fact] + public async Task GetOrDefaultTest() + { + Assert.Null(await _repository.GetOrDefault(56)); + Assert.Null(await _repository.GetOrDefault("non-existing")); + Assert.Null(await _repository.GetOrDefault(x => x.Slug == "non-existing")); + } + + [Fact] + public async Task GetCountWithFilterTest() + { + string slug = TestSample.Get().Slug[2..4]; + Assert.Equal(1, await _repository.GetCount(x => x.Slug.Contains(slug))); + } + + [Fact] + public async Task GetAllTest() + { + string slug = TestSample.Get().Slug[2..4]; + ICollection ret = await _repository.GetAll(x => x.Slug.Contains(slug)); + Assert.Equal(1, ret.Count); + KAssert.DeepEqual(TestSample.Get(), ret.First()); + } + + [Fact] + public async Task DeleteAllTest() + { + string slug = TestSample.Get().Slug[2..4]; + await _repository.DeleteAll(x => x.Slug.Contains(slug)); + Assert.Equal(0, await _repository.GetCount()); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SetupTests.cs b/Kyoo.Tests/Library/SetupTests.cs deleted file mode 100644 index ec9ed12a..00000000 --- a/Kyoo.Tests/Library/SetupTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Kyoo.Tests -{ - public class SetupTests - { - // TODO test libraries & repositories via a on-memory SQLite database. - // TODO Requires: Kyoo should be database agonistic and database implementations should be available via a plugin. - - // [Fact] - // public void Get_Test() - // { - // TestContext context = new(); - // using DatabaseContext database = context.New(); - // - // Assert.Equal(1, database.Shows.Count()); - // } - } -} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs b/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs new file mode 100644 index 00000000..7a5976de --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class CollectionTests : ACollectionTests + { + public CollectionTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class CollectionTests : ACollectionTests + { + public CollectionTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ACollectionTests : RepositoryTests + { + private readonly ICollectionRepository _repository; + + protected ACollectionTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.CollectionRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs b/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs new file mode 100644 index 00000000..6b1adf27 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs @@ -0,0 +1,192 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class EpisodeTests : AEpisodeTests + { + public EpisodeTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class EpisodeTests : AEpisodeTests + { + public EpisodeTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AEpisodeTests : RepositoryTests + { + private readonly IEpisodeRepository _repository; + + protected AEpisodeTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = repositories.LibraryManager.EpisodeRepository; + } + + [Fact] + public async Task SlugEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + Show show = new() + { + ID = episode.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + episode = await _repository.Get(1); + Assert.Equal("new-slug-s1e1", episode.Slug); + } + + [Fact] + public async Task SeasonNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + episode = await _repository.Edit(new Episode + { + ID = 1, + SeasonNumber = 2 + }, false); + Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s2e1", episode.Slug); + } + + [Fact] + public async Task EpisodeNumberEditTest() + { + Episode episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e1", episode.Slug); + episode = await _repository.Edit(new Episode + { + ID = 1, + EpisodeNumber = 2 + }, false); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + } + + [Fact] + public async Task EpisodeCreationSlugTest() + { + Episode episode = await _repository.Create(new Episode + { + ShowID = TestSample.Get().ID, + SeasonNumber = 2, + EpisodeNumber = 4 + }); + Assert.Equal($"{TestSample.Get().Slug}-s2e4", episode.Slug); + } + + + // TODO absolute numbering tests + + + [Fact] + public void AbsoluteSlugTest() + { + Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", + TestSample.GetAbsoluteEpisode().Slug); + } + + [Fact] + public async Task EpisodeCreationAbsoluteSlugTest() + { + Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); + Assert.Equal($"{TestSample.Get().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug); + } + + [Fact] + public async Task SlugEditAbsoluteTest() + { + Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); + Show show = new() + { + ID = episode.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + episode = await _repository.Get(2); + Assert.Equal($"new-slug-3", episode.Slug); + } + + + [Fact] + public async Task AbsoluteNumberEditTest() + { + await _repository.Create(TestSample.GetAbsoluteEpisode()); + Episode episode = await _repository.Edit(new Episode + { + ID = 2, + AbsoluteNumber = 56 + }, false); + Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); + episode = await _repository.Get(2); + Assert.Equal($"{TestSample.Get().Slug}-56", episode.Slug); + } + + [Fact] + public async Task AbsoluteToNormalEditTest() + { + await _repository.Create(TestSample.GetAbsoluteEpisode()); + Episode episode = await _repository.Edit(new Episode + { + ID = 2, + SeasonNumber = 1, + EpisodeNumber = 2 + }, false); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + episode = await _repository.Get(2); + Assert.Equal($"{TestSample.Get().Slug}-s1e2", episode.Slug); + } + + [Fact] + public async Task NormalToAbsoluteEditTest() + { + Episode episode = await _repository.Get(1); + episode.SeasonNumber = null; + episode.AbsoluteNumber = 12; + episode = await _repository.Edit(episode, true); + Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); + episode = await _repository.Get(1); + Assert.Equal($"{TestSample.Get().Slug}-12", episode.Slug); + } + + [Fact] + public async Task MovieEpisodeTest() + { + Episode episode = await _repository.Create(TestSample.GetMovieEpisode()); + Assert.Equal(TestSample.Get().Slug, episode.Slug); + episode = await _repository.Get(3); + Assert.Equal(TestSample.Get().Slug, episode.Slug); + } + + [Fact] + public async Task MovieEpisodeEditTest() + { + await _repository.Create(TestSample.GetMovieEpisode()); + await Repositories.LibraryManager.Edit(new Show + { + ID = 1, + Slug = "john-wick" + }, false); + Episode episode = await _repository.Get(3); + Assert.Equal("john-wick", episode.Slug); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/GenreTests.cs b/Kyoo.Tests/Library/SpecificTests/GenreTests.cs new file mode 100644 index 00000000..d79dba5e --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/GenreTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class GenreTests : AGenreTests + { + public GenreTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class GenreTests : AGenreTests + { + public GenreTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AGenreTests : RepositoryTests + { + private readonly IGenreRepository _repository; + + protected AGenreTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.GenreRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs new file mode 100644 index 00000000..b2db4f66 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class LibraryItemTest : ALibraryItemTest + { + public LibraryItemTest(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class LibraryItemTest : ALibraryItemTest + { + public LibraryItemTest(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ALibraryItemTest + { + private readonly ILibraryItemRepository _repository; + private readonly RepositoryActivator _repositories; + + protected ALibraryItemTest(RepositoryActivator repositories) + { + _repositories = repositories; + _repository = repositories.LibraryManager.LibraryItemRepository; + } + + [Fact] + public async Task CountTest() + { + Assert.Equal(2, await _repository.GetCount()); + } + + [Fact] + public async Task GetShowTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(1); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetCollectionTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(-1); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetShowSlugTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetCollectionSlugTests() + { + LibraryItem expected = new(TestSample.Get()); + LibraryItem actual = await _repository.Get(TestSample.Get().Slug); + KAssert.DeepEqual(expected, actual); + } + + [Fact] + public async Task GetDuplicatedSlugTests() + { + await _repositories.LibraryManager.Create(new Collection() + { + Slug = TestSample.Get().Slug + }); + await Assert.ThrowsAsync(() => _repository.Get(TestSample.Get().Slug)); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs b/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs new file mode 100644 index 00000000..fbed1793 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs @@ -0,0 +1,36 @@ +using Kyoo.Controllers; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class LibraryTests : ALibraryTests + { + public LibraryTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class LibraryTests : ALibraryTests + { + public LibraryTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ALibraryTests : RepositoryTests + { + private readonly ILibraryRepository _repository; + + protected ALibraryTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.LibraryRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs b/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs new file mode 100644 index 00000000..fc8b788d --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class PeopleTests : APeopleTests + { + public PeopleTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class PeopleTests : APeopleTests + { + public PeopleTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class APeopleTests : RepositoryTests + { + private readonly IPeopleRepository _repository; + + protected APeopleTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.PeopleRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs b/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs new file mode 100644 index 00000000..853e34a1 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class ProviderTests : AProviderTests + { + public ProviderTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class ProviderTests : AProviderTests + { + public ProviderTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AProviderTests : RepositoryTests + { + private readonly IProviderRepository _repository; + + protected AProviderTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.ProviderRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs new file mode 100644 index 00000000..78637d35 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/SanityTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + public class GlobalTests : IDisposable, IAsyncDisposable + { + private readonly RepositoryActivator _repositories; + + public GlobalTests(ITestOutputHelper output) + { + _repositories = new RepositoryActivator(output); + } + + [Fact] + [SuppressMessage("ReSharper", "EqualExpressionComparison")] + public void SampleTest() + { + Assert.False(ReferenceEquals(TestSample.Get(), TestSample.Get())); + } + + public void Dispose() + { + _repositories.Dispose(); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + return _repositories.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs new file mode 100644 index 00000000..39be8b82 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class SeasonTests : ASeasonTests + { + public SeasonTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class SeasonTests : ASeasonTests + { + public SeasonTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ASeasonTests : RepositoryTests + { + private readonly ISeasonRepository _repository; + + protected ASeasonTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.SeasonRepository; + } + + [Fact] + public async Task SlugEditTest() + { + Season season = await _repository.Get(1); + Assert.Equal("anohana-s1", season.Slug); + Show show = new() + { + ID = season.ShowID, + Slug = "new-slug" + }; + await Repositories.LibraryManager.ShowRepository.Edit(show, false); + season = await _repository.Get(1); + Assert.Equal("new-slug-s1", season.Slug); + } + + [Fact] + public async Task SeasonNumberEditTest() + { + Season season = await _repository.Get(1); + Assert.Equal("anohana-s1", season.Slug); + await _repository.Edit(new Season + { + ID = 1, + SeasonNumber = 2 + }, false); + season = await _repository.Get(1); + Assert.Equal("anohana-s2", season.Slug); + } + + [Fact] + public async Task SeasonCreationSlugTest() + { + Season season = await _repository.Create(new Season + { + ShowID = TestSample.Get().ID, + SeasonNumber = 2 + }); + Assert.Equal($"{TestSample.Get().Slug}-s2", season.Slug); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs new file mode 100644 index 00000000..8940f0c3 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class ShowTests : AShowTests + { + public ShowTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class ShowTests : AShowTests + { + public ShowTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AShowTests : RepositoryTests + { + private readonly IShowRepository _repository; + + protected AShowTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.ShowRepository; + } + + [Fact] + public async Task EditTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Path = "/super"; + value.Title = "New Title"; + Show edited = await _repository.Edit(value, false); + KAssert.DeepEqual(value, edited); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows.FirstAsync(); + + KAssert.DeepEqual(show, value); + } + + [Fact] + public async Task EditGenreTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Genres = new[] {new Genre("test")}; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.Genres) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name})); + } + + [Fact] + public async Task EditStudioTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Studio = new Studio("studio"); + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal("studio", edited.Studio.Slug); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.Genres) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal("studio", edited.Studio.Slug); + } + + [Fact] + public async Task EditAliasesTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Aliases = new[] {"NiceNewAlias", "SecondAlias"}; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(value.Aliases, edited.Aliases); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows.FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal(value.Aliases, edited.Aliases); + } + + [Fact] + public async Task EditPeopleTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.People = new[] + { + new PeopleRole + { + Show = value, + People = TestSample.Get(), + ForPeople = false, + Type = "Actor", + Role = "NiceCharacter" + } + }; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal(edited.People.First().ShowID, value.ID); + Assert.Equal( + value.People.Select(x => new{x.Role, x.Slug, x.People.Name}), + edited.People.Select(x => new{x.Role, x.Slug, x.People.Name})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.People) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal( + value.People.Select(x => new{x.Role, x.Slug, x.People.Name}), + edited.People.Select(x => new{x.Role, x.Slug, x.People.Name})); + } + + [Fact] + public async Task EditExternalIDsTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.ExternalIDs = new[] + { + new MetadataID() + { + First = value, + Second = new Provider("test", "test.png"), + DataID = "1234" + } + }; + Show edited = await _repository.Edit(value, false); + + Assert.Equal(value.Slug, edited.Slug); + Assert.Equal( + value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}), + edited.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.ExternalIDs) + .ThenInclude(x => x.Second) + .FirstAsync(); + + Assert.Equal(value.Slug, show.Slug); + Assert.Equal( + value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}), + show.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug})); + } + + [Fact] + public async Task EditResetOldTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + Show newValue = new() + { + ID = value.ID, + Title = "Reset" + }; + + await Assert.ThrowsAsync(() => _repository.Edit(newValue, true)); + + newValue.Slug = "reset"; + Show edited = await _repository.Edit(newValue, true); + + Assert.Equal(value.ID, edited.ID); + Assert.Null(edited.Overview); + Assert.Equal("reset", edited.Slug); + Assert.Equal("Reset", edited.Title); + Assert.Null(edited.Aliases); + Assert.Null(edited.ExternalIDs); + Assert.Null(edited.People); + Assert.Null(edited.Genres); + Assert.Null(edited.Studio); + } + + [Fact] + public async Task CreateWithRelationsTest() + { + Show expected = TestSample.Get(); + expected.ID = 0; + expected.Slug = "created-relation-test"; + expected.ExternalIDs = new[] + { + new MetadataID + { + First = expected, + Second = new Provider("provider", "provider.png"), + DataID = "ID" + } + }; + expected.Genres = new[] + { + new Genre + { + Name = "Genre", + Slug = "genre" + } + }; + expected.People = new[] + { + new PeopleRole + { + People = TestSample.Get(), + Show = expected, + ForPeople = false, + Role = "actor" + } + }; + expected.Studio = new Studio("studio"); + Show created = await _repository.Create(expected); + KAssert.DeepEqual(expected, created); + } + + [Fact] + public async Task SlugDuplicationTest() + { + Show test = TestSample.Get(); + test.ID = 0; + test.Slug = "300"; + Show created = await _repository.Create(test); + Assert.Equal("300!", created.Slug); + } + + [Fact] + public async Task GetSlugTest() + { + Show reference = TestSample.Get(); + Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID)); + } + + [Theory] + [InlineData("test")] + [InlineData("super")] + [InlineData("title")] + [InlineData("TiTlE")] + [InlineData("SuPeR")] + public async Task SearchTest(string query) + { + Show value = new() + { + Slug = "super-test", + Title = "This is a test title²" + }; + await _repository.Create(value); + ICollection ret = await _repository.Search(query); + KAssert.DeepEqual(value, ret.First()); + } + + [Fact] + public async Task DeleteShowWithEpisodeAndSeason() + { + Show show = TestSample.Get(); + await Repositories.LibraryManager.Load(show, x => x.Seasons); + await Repositories.LibraryManager.Load(show, x => x.Episodes); + Assert.Equal(1, await _repository.GetCount()); + Assert.Equal(1, show.Seasons.Count); + Assert.Equal(1, show.Episodes.Count); + await _repository.Delete(show); + Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount()); + Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount()); + Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount()); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/StudioTests.cs b/Kyoo.Tests/Library/SpecificTests/StudioTests.cs new file mode 100644 index 00000000..f5093b19 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/StudioTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class StudioTests : AStudioTests + { + public StudioTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class StudioTests : AStudioTests + { + public StudioTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AStudioTests : RepositoryTests + { + private readonly IStudioRepository _repository; + + protected AStudioTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.StudioRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs new file mode 100644 index 00000000..3c2e2043 --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/TrackTests.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class TrackTests : ATrackTests + { + public TrackTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class TrackTests : ATrackTests + { + public TrackTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class ATrackTests : RepositoryTests + { + private readonly ITrackRepository _repository; + + protected ATrackTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = repositories.LibraryManager.TrackRepository; + } + + [Fact] + public async Task SlugEditTest() + { + await Repositories.LibraryManager.ShowRepository.Edit(new Show + { + ID = 1, + Slug = "new-slug" + }, false); + Track track = await _repository.Get(1); + Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/SpecificTests/UserTests.cs b/Kyoo.Tests/Library/SpecificTests/UserTests.cs new file mode 100644 index 00000000..be67296d --- /dev/null +++ b/Kyoo.Tests/Library/SpecificTests/UserTests.cs @@ -0,0 +1,37 @@ +using Kyoo.Controllers; +using Kyoo.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests.Library +{ + namespace SqLite + { + public class UserTests : AUserTests + { + public UserTests(ITestOutputHelper output) + : base(new RepositoryActivator(output)) { } + } + } + + namespace PostgreSQL + { + [Collection(nameof(Postgresql))] + public class UserTests : AUserTests + { + public UserTests(PostgresFixture postgres, ITestOutputHelper output) + : base(new RepositoryActivator(output, postgres)) { } + } + } + + public abstract class AUserTests : RepositoryTests + { + private readonly IUserRepository _repository; + + protected AUserTests(RepositoryActivator repositories) + : base(repositories) + { + _repository = Repositories.LibraryManager.UserRepository; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs index e3cabc03..fa2935b8 100644 --- a/Kyoo.Tests/Library/TestContext.cs +++ b/Kyoo.Tests/Library/TestContext.cs @@ -1,79 +1,203 @@ -// using Kyoo.Models; -// using Microsoft.Data.Sqlite; -// using Microsoft.EntityFrameworkCore; -// -// namespace Kyoo.Tests -// { -// /// -// /// Class responsible to fill and create in memory databases for unit tests. -// /// -// public class TestContext -// { -// /// -// /// The context's options that specify to use an in memory Sqlite database. -// /// -// private readonly DbContextOptions _context; -// -// /// -// /// Create a new database and fill it with information. -// /// -// public TestContext() -// { -// SqliteConnection connection = new("DataSource=:memory:"); -// connection.Open(); -// -// try -// { -// _context = new DbContextOptionsBuilder() -// .UseSqlite(connection) -// .Options; -// FillDatabase(); -// } -// finally -// { -// connection.Close(); -// } -// } -// -// /// -// /// Fill the database with pre defined values using a clean context. -// /// -// private void FillDatabase() -// { -// using DatabaseContext context = new(_context); -// context.Shows.Add(new Show -// { -// ID = 67, -// Slug = "anohana", -// Title = "Anohana: The Flower We Saw That Day", -// Aliases = new[] -// { -// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", -// "AnoHana", -// "We Still Don't Know the Name of the Flower We Saw That Day." -// }, -// Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + -// "In time, however, these childhood friends drifted apart, and when they became high " + -// "school students, they had long ceased to think of each other as friends.", -// Status = Status.Finished, -// TrailerUrl = null, -// StartYear = 2011, -// EndYear = 2011, -// Poster = "poster", -// Logo = "logo", -// Backdrop = "backdrop", -// IsMovie = false, -// Studio = null -// }); -// } -// -// /// -// /// Get a new database context connected to a in memory Sqlite database. -// /// -// /// A valid DatabaseContext -// public DatabaseContext New() -// { -// return new(_context); -// } -// } -// } \ No newline at end of file +using System; +using System.Threading.Tasks; +using Kyoo.Postgresql; +using Kyoo.SqLite; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; +using Xunit; +using Xunit.Abstractions; + +namespace Kyoo.Tests +{ + public sealed class SqLiteTestContext : TestContext + { + /// + /// The internal sqlite connection used by all context returned by this class. + /// + private readonly SqliteConnection _connection; + + /// + /// The context's options that specify to use an in memory Sqlite database. + /// + private readonly DbContextOptions _context; + + public SqLiteTestContext(ITestOutputHelper output) + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .UseLoggerFactory(LoggerFactory.Create(x => + { + x.ClearProviders(); + x.AddXunit(output); + })) + .EnableSensitiveDataLogging() + .EnableDetailedErrors() + .Options; + + using DatabaseContext context = New(); + context.Database.Migrate(); + TestSample.FillDatabase(context); + } + + public override void Dispose() + { + _connection.Close(); + } + + public override async ValueTask DisposeAsync() + { + await _connection.CloseAsync(); + } + + public override DatabaseContext New() + { + return new SqLiteContext(_context); + } + } + + [CollectionDefinition(nameof(Postgresql))] + public class PostgresCollection : ICollectionFixture + {} + + public sealed class PostgresFixture : IDisposable + { + private readonly DbContextOptions _options; + + public string Template { get; } + + public string Connection => PostgresTestContext.GetConnectionString(Template); + + public PostgresFixture() + { + // TODO Assert.Skip when postgres is not available. (this needs xunit v3) + + string id = Guid.NewGuid().ToString().Replace('-', '_'); + Template = $"kyoo_template_{id}"; + + _options = new DbContextOptionsBuilder() + .UseNpgsql(Connection) + .Options; + + using PostgresContext context = new(_options); + context.Database.Migrate(); + + using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection(); + conn.Open(); + conn.ReloadTypes(); + + TestSample.FillDatabase(context); + conn.Close(); + } + + public void Dispose() + { + using PostgresContext context = new(_options); + context.Database.EnsureDeleted(); + } + } + + public sealed class PostgresTestContext : TestContext + { + private readonly NpgsqlConnection _connection; + private readonly DbContextOptions _context; + + public PostgresTestContext(PostgresFixture template, ITestOutputHelper output) + { + string id = Guid.NewGuid().ToString().Replace('-', '_'); + string database = $"kyoo_test_{id}"; + + using (NpgsqlConnection connection = new(template.Connection)) + { + connection.Open(); + using NpgsqlCommand cmd = new($"CREATE DATABASE {database} WITH TEMPLATE {template.Template}", connection); + cmd.ExecuteNonQuery(); + } + + _connection = new NpgsqlConnection(GetConnectionString(database)); + _connection.Open(); + + _context = new DbContextOptionsBuilder() + .UseNpgsql(_connection) + .UseLoggerFactory(LoggerFactory.Create(x => + { + x.ClearProviders(); + x.AddXunit(output); + })) + .EnableSensitiveDataLogging() + .EnableDetailedErrors() + .Options; + } + + public static string GetConnectionString(string database) + { + string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1"; + string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432"; + string username = Environment.GetEnvironmentVariable("POSTGRES_USERNAME") ?? "kyoo"; + string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "kyooPassword"; + return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true"; + } + + public override void Dispose() + { + using DatabaseContext db = New(); + db.Database.EnsureDeleted(); + _connection.Close(); + } + + public override async ValueTask DisposeAsync() + { + await using DatabaseContext db = New(); + await db.Database.EnsureDeletedAsync(); + await _connection.CloseAsync(); + } + + public override DatabaseContext New() + { + return new PostgresContext(_context); + } + } + + + /// + /// Class responsible to fill and create in memory databases for unit tests. + /// + public abstract class TestContext : IDisposable, IAsyncDisposable + { + /// + /// Add an arbitrary data to the test context. + /// + public void Add(T obj) + where T : class + { + using DatabaseContext context = New(); + context.Set().Add(obj); + context.SaveChanges(); + } + + /// + /// Add an arbitrary data to the test context. + /// + public async Task AddAsync(T obj) + where T : class + { + await using DatabaseContext context = New(); + await context.Set().AddAsync(obj); + await context.SaveChangesAsync(); + } + + /// + /// Get a new database context connected to a in memory Sqlite database. + /// + /// A valid DatabaseContext + public abstract DatabaseContext New(); + + public abstract void Dispose(); + + public abstract ValueTask DisposeAsync(); + } +} diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Library/TestSample.cs new file mode 100644 index 00000000..adbe7d84 --- /dev/null +++ b/Kyoo.Tests/Library/TestSample.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using Kyoo.Models; + +namespace Kyoo.Tests +{ + public static class TestSample + { + private static readonly Dictionary> NewSamples = new() + { + { + typeof(Show), + () => new Show() + } + }; + + + private static readonly Dictionary> Samples = new() + { + { + typeof(Models.Library), + () => new Models.Library + { + ID = 1, + Slug = "deck", + Name = "Deck", + Paths = new[] {"/path/to/deck"} + } + }, + { + typeof(Collection), + () => new Collection + { + ID = 1, + Slug = "collection", + Name = "Collection", + Overview = "A nice collection for tests", + Poster = "Poster" + } + }, + { + typeof(Show), + () => new Show + { + ID = 1, + Slug = "anohana", + Title = "Anohana: The Flower We Saw That Day", + Aliases = new[] + { + "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.", + "AnoHana", + "We Still Don't Know the Name of the Flower We Saw That Day." + }, + Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " + + "In time, however, these childhood friends drifted apart, and when they became high " + + "school students, they had long ceased to think of each other as friends.", + Status = Status.Finished, + TrailerUrl = null, + StartAir = new DateTime(2011, 1, 1), + EndAir = new DateTime(2011, 1, 1), + Poster = "poster", + Logo = "logo", + Backdrop = "backdrop", + IsMovie = false, + Studio = null + } + }, + { + typeof(Season), + () => new Season + { + ID = 1, + ShowSlug = "anohana", + ShowID = 1, + SeasonNumber = 1, + Title = "Season 1", + Overview = "The first season", + StartDate = new DateTime(2020, 06, 05), + EndDate = new DateTime(2020, 07, 05), + Poster = "poster" + } + }, + { + typeof(Episode), + () => new Episode + { + ID = 1, + ShowSlug = "anohana", + ShowID = 1, + SeasonID = 1, + SeasonNumber = 1, + EpisodeNumber = 1, + AbsoluteNumber = 1, + Path = "/home/kyoo/anohana-s1e1", + Thumb = "thumbnail", + Title = "Episode 1", + Overview = "Summary of the first episode", + ReleaseDate = new DateTime(2020, 06, 05) + } + }, + { + typeof(Track), + () => new Track + { + ID = 1, + EpisodeID = 1, + Codec = "subrip", + Language = "eng", + Path = "/path", + Title = "Subtitle track", + Type = StreamType.Subtitle, + EpisodeSlug = Get().Slug, + IsDefault = true, + IsExternal = false, + IsForced = false, + TrackIndex = 1 + } + }, + { + typeof(People), + () => new People + { + ID = 1, + Slug = "the-actor", + Name = "The Actor", + Poster = "NicePoster" + } + }, + { + typeof(Studio), + () => new Studio + { + ID = 1, + Slug = "hyper-studio", + Name = "Hyper studio" + } + }, + { + typeof(Genre), + () => new Genre + { + ID = 1, + Slug = "action", + Name = "Action" + } + }, + { + typeof(Provider), + () => new Provider + { + ID = 1, + Slug = "tvdb", + Name = "The TVDB", + Logo = "path/tvdb.svg", + LogoExtension = "svg" + } + }, + { + typeof(User), + () => new User + { + ID = 1, + Slug = "user", + Username = "User", + Email = "user@im-a-user.com", + Password = "MD5-encoded", + Permissions = new [] {"overall.read"} + } + } + }; + + public static T Get() + { + return (T)Samples[typeof(T)](); + } + + public static T GetNew() + { + return (T)NewSamples[typeof(T)](); + } + + public static void FillDatabase(DatabaseContext context) + { + Collection collection = Get(); + collection.ID = 0; + context.Collections.Add(collection); + + Show show = Get(); + show.ID = 0; + context.Shows.Add(show); + + Season season = Get(); + season.ID = 0; + season.ShowID = 0; + season.Show = show; + context.Seasons.Add(season); + + Episode episode = Get(); + episode.ID = 0; + episode.ShowID = 0; + episode.Show = show; + episode.SeasonID = 0; + episode.Season = season; + context.Episodes.Add(episode); + + Track track = Get(); + track.ID = 0; + track.EpisodeID = 0; + track.Episode = episode; + context.Tracks.Add(track); + + Studio studio = Get(); + studio.ID = 0; + studio.Shows = new List {show}; + context.Studios.Add(studio); + + Genre genre = Get(); + genre.ID = 0; + genre.Shows = new List {show}; + context.Genres.Add(genre); + + People people = Get(); + people.ID = 0; + context.People.Add(people); + + Provider provider = Get(); + provider.ID = 0; + context.Providers.Add(provider); + + Models.Library library = Get(); + library.ID = 0; + library.Collections = new List {collection}; + library.Providers = new List {provider}; + context.Libraries.Add(library); + + User user = Get(); + user.ID = 0; + context.Users.Add(user); + + context.SaveChanges(); + } + + public static Episode GetAbsoluteEpisode() + { + return new() + { + ID = 2, + ShowSlug = "anohana", + ShowID = 1, + SeasonNumber = null, + EpisodeNumber = null, + AbsoluteNumber = 3, + Path = "/home/kyoo/anohana-3", + Thumb = "thumbnail", + Title = "Episode 3", + Overview = "Summary of the third absolute episode", + ReleaseDate = new DateTime(2020, 06, 05) + }; + } + + public static Episode GetMovieEpisode() + { + return new() + { + ID = 3, + ShowSlug = "anohana", + ShowID = 1, + Path = "/home/kyoo/john-wick", + Thumb = "thumb", + Title = "John wick", + Overview = "A movie episode test", + ReleaseDate = new DateTime(1595, 05, 12) + }; + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Utility/EnumerableTests.cs b/Kyoo.Tests/Utility/EnumerableTests.cs new file mode 100644 index 00000000..9cdd8a00 --- /dev/null +++ b/Kyoo.Tests/Utility/EnumerableTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Kyoo.Tests +{ + public class EnumerableTests + { + [Fact] + public void MapTest() + { + int[] list = {1, 2, 3, 4}; + Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i)); + Assert.Throws(() => list.Map(((Func)null)!)); + list = null; + Assert.Throws(() => list!.Map((x, _) => x + 1)); + } + + [Fact] + public async Task MapAsyncTest() + { + int[] list = {1, 2, 3, 4}; + await foreach((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i)))) + { + Assert.Equal(x - 1, i); + } + Assert.Throws(() => list.MapAsync(((Func>)null)!)); + list = null; + Assert.Throws(() => list!.MapAsync((x, _) => Task.FromResult(x + 1))); + } + + [Fact] + public async Task SelectAsyncTest() + { + int[] list = {1, 2, 3, 4}; + int i = 2; + await foreach(int x in list.SelectAsync(x => Task.FromResult(x + 1))) + { + Assert.Equal(i++, x); + } + Assert.Throws(() => list.SelectAsync(((Func>)null)!)); + list = null; + Assert.Throws(() => list!.SelectAsync(x => Task.FromResult(x + 1))); + } + + [Fact] + public async Task ToListAsyncTest() + { + int[] expected = {1, 2, 3, 4}; + IAsyncEnumerable list = expected.SelectAsync(Task.FromResult); + Assert.Equal(expected, await list.ToListAsync()); + list = null; + await Assert.ThrowsAsync(() => list!.ToListAsync()); + } + + [Fact] + public void IfEmptyTest() + { + int[] list = {1, 2, 3, 4}; + list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray(); + Assert.Throws(() => list.IfEmpty(null!).ToList()); + list = null; + Assert.Throws(() => list!.IfEmpty(() => {}).ToList()); + list = Array.Empty(); + Assert.Throws(() => list.IfEmpty(() => throw new ArgumentException()).ToList()); + Assert.Empty(list.IfEmpty(() => {})); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Utility/MergerTests.cs b/Kyoo.Tests/Utility/MergerTests.cs new file mode 100644 index 00000000..614d328f --- /dev/null +++ b/Kyoo.Tests/Utility/MergerTests.cs @@ -0,0 +1,21 @@ +using Kyoo.Models; +using Xunit; + +namespace Kyoo.Tests +{ + public class MergerTests + { + [Fact] + public void NullifyTest() + { + Genre genre = new("test") + { + ID = 5 + }; + Merger.Nullify(genre); + Assert.Equal(0, genre.ID); + Assert.Null(genre.Name); + Assert.Null(genre.Slug); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Utility/TaskTests.cs b/Kyoo.Tests/Utility/TaskTests.cs new file mode 100644 index 00000000..3a7baa48 --- /dev/null +++ b/Kyoo.Tests/Utility/TaskTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Kyoo.Tests +{ + public class TaskTests + { + [Fact] + public async Task DefaultIfNullTest() + { + Assert.Equal(0, await TaskUtils.DefaultIfNull(null)); + Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1))); + } + + [Fact] + public async Task ThenTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Then(_ => throw new ArgumentException())); + Assert.Equal(1, await Task.FromResult(1) + .Then(_ => {})); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted().Then(_ => KAssert.Fail())); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Then(_ => {})); + } + + [Fact] + public async Task MapTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Map(_ => throw new ArgumentException())); + Assert.Equal(2, await Task.FromResult(1) + .Map(x => x + 1)); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted() + .Map(x => + { + KAssert.Fail(); + return x; + })); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Map(x => x)); + } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/UtilityTests.cs b/Kyoo.Tests/Utility/UtilityTests.cs similarity index 56% rename from Kyoo.Tests/UtilityTests.cs rename to Kyoo.Tests/Utility/UtilityTests.cs index e046cdb9..15469411 100644 --- a/Kyoo.Tests/UtilityTests.cs +++ b/Kyoo.Tests/Utility/UtilityTests.cs @@ -13,12 +13,23 @@ namespace Kyoo.Tests Expression> member = x => x.ID; Expression> memberCast = x => x.ID; - Assert.True(Utility.IsPropertyExpression(null)); + Assert.False(Utility.IsPropertyExpression(null)); Assert.True(Utility.IsPropertyExpression(member)); Assert.True(Utility.IsPropertyExpression(memberCast)); Expression> call = x => x.GetID("test"); Assert.False(Utility.IsPropertyExpression(call)); } + + [Fact] + public void GetPropertyName_Test() + { + Expression> member = x => x.ID; + Expression> memberCast = x => x.ID; + + Assert.Equal("ID", Utility.GetPropertyName(member)); + Assert.Equal("ID", Utility.GetPropertyName(memberCast)); + Assert.Throws(() => Utility.GetPropertyName(null)); + } } } \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index 3f814bd3..60998b55 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Pos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,5 +43,9 @@ Global {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 61f41593..6ed5f796 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -29,7 +29,7 @@ namespace Kyoo.Controllers { try { - ret = Utility.Merge(ret, await providerCall(provider)); + ret = Merger.Merge(ret, await providerCall(provider)); } catch (Exception ex) { await Console.Error.WriteLineAsync( @@ -122,16 +122,15 @@ namespace Kyoo.Controllers 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; } public async Task GetEpisode(Show show, string episodePath, - int seasonNumber, - int episodeNumber, - int absoluteNumber, + int? seasonNumber, + int? episodeNumber, + int? absoluteNumber, Library library) { Episode episode = await GetMetadata( @@ -142,9 +141,9 @@ namespace Kyoo.Controllers 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; - episode.AbsoluteNumber = episode.AbsoluteNumber != -1 ? episode.AbsoluteNumber : absoluteNumber; + episode.SeasonNumber ??= seasonNumber; + episode.EpisodeNumber ??= episodeNumber; + episode.AbsoluteNumber ??= absoluteNumber; return episode; } diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 8452b950..2cf7ff1f 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -16,7 +15,7 @@ namespace Kyoo.Controllers public class EpisodeRepository : LocalRepository, IEpisodeRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; /// @@ -24,10 +23,6 @@ namespace Kyoo.Controllers /// private readonly IProviderRepository _providers; /// - /// A show repository to get show's slug from their ID and keep the slug in each episode. - /// - private readonly IShowRepository _shows; - /// /// A track repository to handle creation and deletion of tracks related to the current episode. /// private readonly ITrackRepository _tracks; @@ -41,66 +36,31 @@ namespace Kyoo.Controllers /// /// The database handle to use. /// A provider repository - /// A show repository /// A track repository public EpisodeRepository(DatabaseContext database, IProviderRepository providers, - IShowRepository shows, ITrackRepository tracks) : base(database) { _database = database; _providers = providers; - _shows = shows; _tracks = tracks; } - /// - public override async Task GetOrDefault(int id) + public Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) { - Episode ret = await base.GetOrDefault(id); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); } /// - public override async Task GetOrDefault(string slug) + public Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber) { - Match match = Regex.Match(slug, @"(?.*)-s(?\d*)e(?\d*)"); - - if (match.Success) - { - return await GetOrDefault(match.Groups["show"].Value, - int.Parse(match.Groups["season"].Value), - int.Parse(match.Groups["episode"].Value)); - } - - Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); - if (episode != null) - episode.ShowSlug = slug; - return episode; - } - - /// - public override async Task GetOrDefault(Expression> where) - { - Episode ret = await base.GetOrDefault(where); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; - } - - /// - public async Task GetOrDefault(string showSlug, int seasonNumber, int 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; + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); } /// @@ -122,61 +82,30 @@ namespace Kyoo.Controllers } /// - public async Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) + public Task GetAbsolute(int showID, int absoluteNumber) { - 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; + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.AbsoluteNumber == absoluteNumber); } /// - public async Task GetAbsolute(int showID, int absoluteNumber) + public Task GetAbsolute(string showSlug, int 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 GetAbsolute(string showSlug, int absoluteNumber) - { - Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.AbsoluteNumber == absoluteNumber); - if (ret != null) - ret.ShowSlug = showSlug; - return ret; + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.AbsoluteNumber == absoluteNumber); } /// public override async Task> Search(string query) { - List episodes = await _database.Episodes - .Where(x => x.EpisodeNumber != -1) + return await _database.Episodes + .Where(x => x.EpisodeNumber != null) .Where(_database.Like(x => x.Title, $"%{query}%")) .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) { @@ -185,6 +114,9 @@ namespace Kyoo.Controllers 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 await ValidateTracks(obj); + // TODO check if this is needed + // obj.Slug = await _database.Entry(obj).Property(x => x.Slug). + // return obj; } /// @@ -195,7 +127,7 @@ namespace Kyoo.Controllers if (changed.Tracks != null || resetOld) { - await _tracks.DeleteRange(x => x.EpisodeID == resource.ID); + await _tracks.DeleteAll(x => x.EpisodeID == resource.ID); resource.Tracks = changed.Tracks; await ValidateTracks(resource); } @@ -213,18 +145,15 @@ namespace Kyoo.Controllers /// Set track's index and ensure that every tracks is well-formed. /// /// The resource to fix. - /// The parameter is returnned. + /// The parameter is returned. private async Task ValidateTracks(Episode resource) { - resource.Tracks = await resource.Tracks.MapAsync((x, i) => + resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x => { 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); + x.EpisodeSlug = resource.Slug; return _tracks.Create(x); - }).ToListAsync(); + }).ToListAsync()); return resource; } @@ -232,13 +161,12 @@ namespace Kyoo.Controllers protected override async Task Validate(Episode resource) { await base.Validate(resource); - resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x => + await resource.ExternalIDs.ForEachAsync(async x => { - x.Provider = await _providers.CreateIfNotExists(x.Provider); - x.ProviderID = x.Provider.ID; - _database.Entry(x.Provider).State = EntityState.Detached; - return x; - }).ToListAsync(); + x.Second = await _providers.CreateIfNotExists(x.Second); + x.SecondID = x.Second.ID; + _database.Entry(x.Second).State = EntityState.Detached; + }); } /// diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 703ece0b..37391c5d 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -22,15 +22,7 @@ namespace Kyoo.Controllers /// A lazy loaded library repository to validate queries (check if a library does exist) /// private readonly Lazy _libraries; - /// - /// A lazy loaded show repository to get a show from it's id. - /// - private readonly Lazy _shows; - /// - /// A lazy loaded collection repository to get a collection from it's id. - /// - private readonly Lazy _collections; - + /// protected override Expression> DefaultSort => x => x.Title; @@ -38,60 +30,41 @@ namespace Kyoo.Controllers /// /// Create a new . /// - /// The databse instance + /// The database instance /// A lazy loaded library repository - /// A lazy loaded show repository - /// A lazy loaded collection repository public LibraryItemRepository(DatabaseContext database, - Lazy libraries, - Lazy shows, - Lazy collections) + Lazy libraries) : base(database) { _database = database; _libraries = libraries; - _shows = shows; - _collections = collections; } /// - public override async Task GetOrDefault(int id) + public override Task GetOrDefault(int id) { - return id > 0 - ? new LibraryItem(await _shows.Value.GetOrDefault(id)) - : new LibraryItem(await _collections.Value.GetOrDefault(-id)); + return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id); } /// public override Task GetOrDefault(string slug) { - throw new InvalidOperationException("You can't get a library item by a slug."); + return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug); } - /// - /// Get a basic queryable with the right mapping from shows & collections. - /// Shows contained in a collection are excluded. - /// - private IQueryable ItemsQuery - => _database.Shows - .Where(x => !x.Collections.Any()) - .Select(LibraryItem.FromShow) - .Concat(_database.Collections - .Select(LibraryItem.FromCollection)); - /// public override Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) { - return ApplyFilters(ItemsQuery, where, sort, limit); + return ApplyFilters(_database.LibraryItems, where, sort, limit); } /// public override Task GetCount(Expression> where = null) { - IQueryable query = ItemsQuery; + IQueryable query = _database.LibraryItems; if (where != null) query = query.Where(where); return query.CountAsync(); @@ -100,7 +73,7 @@ namespace Kyoo.Controllers /// public override async Task> Search(string query) { - return await ItemsQuery + return await _database.LibraryItems .Where(_database.Like(x => x.Title, $"%{query}%")) .OrderBy(DefaultSort) .Take(20) @@ -109,7 +82,6 @@ namespace Kyoo.Controllers /// public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); - /// public override Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); /// diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index d569f6fe..02194b77 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -63,9 +63,12 @@ namespace Kyoo.Controllers protected override async Task Validate(Library resource) { await base.Validate(resource); - resource.Providers = await resource.Providers - .SelectAsync(x => _providers.CreateIfNotExists(x)) - .ToListAsync(); + await resource.ProviderLinks.ForEachAsync(async id => + { + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; + }); } /// diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 452f59eb..9adb37ee 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -73,9 +73,9 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); await resource.Roles.ForEachAsync(async role => { diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index 135e8148..e48d4f50 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -58,18 +58,18 @@ namespace Kyoo.Controllers throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Deleted; - obj.MetadataLinks.ForEach(x => _database.Entry(x).State = EntityState.Deleted); await _database.SaveChangesAsync(); } /// - public Task> GetMetadataID(Expression> where = null, - Sort sort = default, + public Task>> GetMetadataID(Expression, bool>> where = null, + Sort> sort = default, Pagination limit = default) + where T : class, IResource { - return ApplyFilters(_database.MetadataIds.Include(y => y.Provider), - x => _database.MetadataIds.FirstOrDefaultAsync(y => y.ID == x), - x => x.ID, + return ApplyFilters(_database.MetadataIds().Include(y => y.Second), + x => _database.MetadataIds().FirstOrDefaultAsync(y => y.FirstID == x), + x => x.FirstID, where, sort, limit); diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 289ca08f..fe042e66 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -23,73 +22,30 @@ namespace Kyoo.Controllers /// A provider repository to handle externalID creation and deletion /// private readonly IProviderRepository _providers; - /// - /// A show repository to get show's slug from their ID and keep the slug in each episode. - /// - private readonly IShowRepository _shows; - /// - /// A lazilly loaded episode repository to handle deletion of episodes with the season. - /// - private readonly Lazy _episodes; - + /// protected override Expression> DefaultSort => x => x.SeasonNumber; /// - /// Create a new using the provided handle, a provider & a show repository and - /// a service provider to lazilly request an episode repository. + /// Create a new . /// /// The database handle that will be used /// A provider repository - /// A show repository - /// A lazy loaded episode repository. public SeasonRepository(DatabaseContext database, - IProviderRepository providers, - IShowRepository shows, - Lazy episodes) + IProviderRepository providers) : base(database) { _database = database; _providers = providers; - _shows = shows; - _episodes = episodes; - } - - - /// - public override async Task Get(int id) - { - Season ret = await base.Get(id); - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; } - /// - public override async Task Get(Expression> where) - { - Season ret = await base.Get(where); - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); - return ret; - } - - /// - public override Task Get(string slug) - { - Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); - - if (!match.Success) - throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); - return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); - } - /// public async Task Get(int showID, int seasonNumber) { Season ret = await GetOrDefault(showID, seasonNumber); if (ret == null) throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}"); - ret.ShowSlug = await _shows.GetSlug(showID); return ret; } @@ -99,7 +55,6 @@ namespace Kyoo.Controllers Season ret = await GetOrDefault(showSlug, seasonNumber); if (ret == null) throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}"); - ret.ShowSlug = showSlug; return ret; } @@ -120,27 +75,13 @@ namespace Kyoo.Controllers /// public override async Task> Search(string query) { - List seasons = await _database.Seasons + return await _database.Seasons .Where(_database.Like(x => 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) { @@ -160,9 +101,9 @@ namespace Kyoo.Controllers await base.Validate(resource); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); } @@ -182,13 +123,9 @@ namespace Kyoo.Controllers { if (obj == null) throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted); - await _database.SaveChangesAsync(); - if (obj.Episodes != null) - await _episodes.Value.DeleteRange(obj.Episodes); + _database.Remove(obj); + await _database.SaveChangesAsync(); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index eb3f36e4..769cb232 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -33,14 +33,6 @@ namespace Kyoo.Controllers /// A provider repository to handle externalID creation and deletion /// private readonly IProviderRepository _providers; - /// - /// A lazy loaded season repository to handle cascade deletion (seasons deletion whith it's show) - /// - private readonly Lazy _seasons; - /// - /// A lazy loaded episode repository to handle cascade deletion (episode deletion whith it's show) - /// - private readonly Lazy _episodes; /// protected override Expression> DefaultSort => x => x.Title; @@ -53,15 +45,11 @@ namespace Kyoo.Controllers /// A people repository /// A genres repository /// A provider repository - /// A lazy loaded season repository - /// A lazy loaded episode repository public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, IGenreRepository genres, - IProviderRepository providers, - Lazy seasons, - Lazy episodes) + IProviderRepository providers) : base(database) { _database = database; @@ -69,8 +57,6 @@ namespace Kyoo.Controllers _people = people; _genres = genres; _providers = providers; - _seasons = seasons; - _episodes = episodes; } @@ -103,17 +89,21 @@ namespace Kyoo.Controllers await base.Validate(resource); if (resource.Studio != null) resource.Studio = await _studios.CreateIfNotExists(resource.Studio); - resource.Genres = await resource.Genres - .SelectAsync(x => _genres.CreateIfNotExists(x)) - .ToListAsync(); + resource.GenreLinks = resource.Genres? - .Select(x => Link.UCreate(resource, x)) + .Select(x => Link.Create(resource, x)) .ToList(); + await resource.GenreLinks.ForEachAsync(async id => + { + id.Second = await _genres.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; + }); await resource.ExternalIDs.ForEachAsync(async id => { - id.Provider = await _providers.CreateIfNotExists(id.Provider); - id.ProviderID = id.Provider.ID; - _database.Entry(id.Provider).State = EntityState.Detached; + id.Second = await _providers.CreateIfNotExists(id.Second); + id.SecondID = id.Second.ID; + _database.Entry(id.Second).State = EntityState.Detached; }); await resource.People.ForEachAsync(async role => { @@ -131,10 +121,16 @@ namespace Kyoo.Controllers if (changed.Aliases != null || resetOld) resource.Aliases = changed.Aliases; + if (changed.Studio != null || resetOld) + { + await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); + resource.Studio = changed.Studio; + } + 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(); + await Database.Entry(resource).Collection(x => x.Genres).LoadAsync(); + resource.Genres = changed.Genres; } if (changed.People != null || resetOld) @@ -185,27 +181,8 @@ namespace Kyoo.Controllers /// public override async Task Delete(Show obj) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - _database.Entry(obj).State = EntityState.Deleted; - - - if (obj.People != null) - foreach (PeopleRole entry in obj.People) - _database.Entry(entry).State = EntityState.Deleted; - - if (obj.ExternalIDs != null) - foreach (MetadataID entry in obj.ExternalIDs) - _database.Entry(entry).State = EntityState.Deleted; - + _database.Remove(obj); await _database.SaveChangesAsync(); - - if (obj.Seasons != null) - await _seasons.Value.DeleteRange(obj.Seasons); - - if (obj.Episodes != null) - await _episodes.Value.DeleteRange(obj.Episodes); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 4b27a5e4..9b642a93 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; 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 @@ -16,7 +13,7 @@ namespace Kyoo.Controllers public class TrackRepository : LocalRepository, ITrackRepository { /// - /// The databse handle + /// The database handle /// private readonly DatabaseContext _database; @@ -27,62 +24,12 @@ namespace Kyoo.Controllers /// /// Create a new . /// - /// The datatabse handle + /// The database handle public TrackRepository(DatabaseContext database) : base(database) { _database = database; } - - - /// - Task IRepository.Get(string slug) - { - return Get(slug); - } - - /// - public async Task Get(string slug, StreamType type = StreamType.Unknown) - { - Track ret = await GetOrDefault(slug, type); - if (ret == null) - throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}."); - return ret; - } - - /// - public Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) - { - Match match = Regex.Match(slug, - @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); - - if (!match.Success) - { - if (int.TryParse(slug, out int id)) - return GetOrDefault(id); - match = Regex.Match(slug, @"(?.*)\.(?.{0,3})(?-forced)?(\..*)?"); - if (!match.Success) - throw new ArgumentException("Invalid track slug. " + - "Format: {episodeSlug}.{language}[-forced][.{extension}]"); - } - - string showSlug = match.Groups["show"].Value; - int seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1; - 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); - - IQueryable query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber - && x.Language == language - && x.IsForced == forced); - if (type != StreamType.Unknown) - return query.FirstOrDefaultAsync(x => x.Type == type); - return query.FirstOrDefaultAsync(); - } /// public override Task> Search(string query) @@ -93,23 +40,19 @@ namespace Kyoo.Controllers /// public override async Task Create(Track obj) { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + if (obj.EpisodeID <= 0) { 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; - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - 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; - }); + await _database.SaveChangesAsync(); return obj; } diff --git a/Kyoo/Controllers/Transcoder.cs b/Kyoo/Controllers/Transcoder.cs index 823aa9d2..8f6e006e 100644 --- a/Kyoo/Controllers/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder.cs @@ -57,7 +57,7 @@ namespace Kyoo.Controllers Stream stream = Marshal.PtrToStructure(streamsPtr); if (stream!.Type != StreamType.Unknown) { - tracks[j] = new Track(stream); + tracks[j] = stream.ToTrack(); j++; } streamsPtr += size; diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 22fd6e21..438f3e9b 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -35,9 +35,9 @@ - - - + + + @@ -46,6 +46,9 @@ + + + diff --git a/Kyoo/Models/Stream.cs b/Kyoo/Models/Stream.cs new file mode 100644 index 00000000..3639a4a6 --- /dev/null +++ b/Kyoo/Models/Stream.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; +using Kyoo.Models.Attributes; + +namespace Kyoo.Models.Watch +{ + /// + /// The unmanaged stream that the transcoder will return. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public class Stream + { + /// + /// The title of the stream. + /// + public string Title { get; set; } + + /// + /// The language of this stream (as a ISO-639-2 language code) + /// + public string Language { get; set; } + + /// + /// The codec of this stream. + /// + public string Codec { get; set; } + + /// + /// Is this stream the default one of it's type? + /// + [MarshalAs(UnmanagedType.I1)] public bool IsDefault; + + /// + /// Is this stream tagged as forced? + /// + [MarshalAs(UnmanagedType.I1)] public bool IsForced; + + /// + /// The path of this track. + /// + [SerializeIgnore] public string Path { get; set; } + + /// + /// The type of this stream. + /// + [SerializeIgnore] public StreamType Type { get; set; } + + + /// + /// Create a track from this stream. + /// + /// A new track that represent this stream. + public Track ToTrack() + { + return new() + { + Title = Title, + Language = Language, + Codec = Codec, + IsDefault = IsDefault, + IsForced = IsForced, + Path = Path, + Type = Type, + IsExternal = false + }; + } + } +} \ No newline at end of file diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 8bd517cc..4d5995ef 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -47,6 +47,7 @@ namespace Kyoo _plugins.LoadPlugins(new IPlugin[] { new CoreModule(configuration), new PostgresModule(configuration, host), + // new SqLiteModule(configuration, host), new AuthenticationModule(configuration, loggerFactory, host) }); } diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index e6a6cebf..9f1185dd 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -210,18 +210,20 @@ namespace Kyoo.Tasks string showPath = Path.GetDirectoryName(path); string collectionName = match.Groups["Collection"].Value; string showName = match.Groups["Show"].Value; - int seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : -1; - int episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : -1; - int absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : -1; + int? seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : null; + int? episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : null; + int? absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : null; Collection collection = await GetCollection(libraryManager, collectionName, library); - bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1; + bool isMovie = seasonNumber == null && episodeNumber == null && absoluteNumber == null; Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); if (isMovie) await libraryManager!.Create(await GetMovie(show, path)); else { - Season season = await GetSeason(libraryManager, show, seasonNumber, library); + Season season = seasonNumber != null + ? await GetSeason(libraryManager, show, seasonNumber.Value, library) + : null; Episode episode = await GetEpisode(libraryManager, show, season, @@ -292,13 +294,19 @@ namespace Kyoo.Tasks catch (DuplicatedItemException) { old = await libraryManager.GetOrDefault(show.Slug); - if (old.Path == showPath) + if (old != null && old.Path == showPath) { await libraryManager.Load(old, x => x.ExternalIDs); return old; } - show.Slug += $"-{show.StartYear}"; - await libraryManager.Create(show); + + if (show.StartAir != null) + { + show.Slug += $"-{show.StartAir.Value.Year}"; + await libraryManager.Create(show); + } + else + throw; } await ThumbnailsManager.Validate(show); return show; @@ -309,8 +317,6 @@ namespace Kyoo.Tasks int seasonNumber, Library library) { - if (seasonNumber == -1) - return default; try { Season season = await libraryManager.Get(show.Slug, seasonNumber); @@ -337,21 +343,24 @@ namespace Kyoo.Tasks private async Task GetEpisode(ILibraryManager libraryManager, Show show, Season season, - int episodeNumber, - int absoluteNumber, + int? episodeNumber, + int? absoluteNumber, string episodePath, Library library) { Episode episode = await MetadataProvider.GetEpisode(show, episodePath, - season?.SeasonNumber ?? -1, + season?.SeasonNumber, episodeNumber, absoluteNumber, library); - - season ??= await GetSeason(libraryManager, show, episode.SeasonNumber, library); - episode.Season = season; - episode.SeasonID = season?.ID; + + if (episode.SeasonNumber != null) + { + season ??= await GetSeason(libraryManager, show, episode.SeasonNumber.Value, library); + episode.Season = season; + episode.SeasonID = season?.ID; + } await ThumbnailsManager.Validate(episode); await GetTracks(episode); return episode; diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index 4e121d75..966aad05 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -16,6 +16,8 @@ namespace Kyoo.Api { [Route("api/show")] [Route("api/shows")] + [Route("api/movie")] + [Route("api/movies")] [ApiController] [PartialPermission(nameof(ShowApi))] public class ShowApi : CrudApi diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 5e680a73..7f05eedf 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -1,5 +1,4 @@ -using System; -using Kyoo.Models; +using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.IO; @@ -27,19 +26,9 @@ namespace Kyoo.Api [Permission(nameof(SubtitleApi), Kind.Read)] public async Task GetSubtitle(string slug, string extension) { - Track subtitle; - try - { - subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle); - } - catch (ArgumentException ex) - { - return BadRequest(new {error = ex.Message}); - } - - if (subtitle is not {Type: StreamType.Subtitle}) + Track subtitle = await _libraryManager.GetOrDefault(Track.EditSlug(slug, StreamType.Subtitle)); + if (subtitle == null) return NotFound(); - if (subtitle.Codec == "subrip" && extension == "vtt") return new ConvertSubripToVtt(subtitle.Path, _files); return _files.FileResult(subtitle.Path); diff --git a/Kyoo/settings.json b/Kyoo/settings.json index ff7dff63..2ff0e24b 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -10,6 +10,10 @@ }, "database": { + "sqlite": { + "data Source": "kyoo.db", + "cache": "Shared" + }, "postgres": { "server": "127.0.0.1", "port": "5432",