mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-09 06:35:21 -05:00
Merge pull request #30 from AnonymusRaccoon/sqlite
Implementing SQLite & Adding tests
This commit is contained in:
commit
28b32c9e49
22
.github/workflows/analysis.yml
vendored
22
.github/workflows/analysis.yml
vendored
@ -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 \
|
||||
|
||||
37
.github/workflows/tests.yml
vendored
37
.github/workflows/tests.yml
vendored
@ -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"
|
||||
|
||||
@ -77,6 +77,11 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
IProviderRepository ProviderRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle users.
|
||||
/// </summary>
|
||||
IUserRepository UserRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the resource by it's ID
|
||||
/// </summary>
|
||||
@ -149,16 +154,6 @@ namespace Kyoo.Controllers
|
||||
[ItemNotNull]
|
||||
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Get a track from it's slug and it's type.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the track</param>
|
||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The track found</returns>
|
||||
[ItemNotNull]
|
||||
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
|
||||
|
||||
/// <summary>
|
||||
/// Get the resource by it's ID or null if it is not found.
|
||||
/// </summary>
|
||||
@ -224,15 +219,6 @@ namespace Kyoo.Controllers
|
||||
[ItemCanBeNull]
|
||||
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Get a track from it's slug and it's type or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the track</param>
|
||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <returns>The track found</returns>
|
||||
[ItemCanBeNull]
|
||||
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Load a related resource
|
||||
@ -242,6 +228,9 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the source object</typeparam>
|
||||
/// <typeparam name="T2">The related resource's type</typeparam>
|
||||
/// <returns>The param <see cref="obj"/></returns>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
|
||||
/// <seealso cref="Load{T}(T, System.String)"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
|
||||
where T : class, IResource
|
||||
where T2 : class, IResource, new();
|
||||
@ -254,6 +243,9 @@ namespace Kyoo.Controllers
|
||||
/// <typeparam name="T">The type of the source object</typeparam>
|
||||
/// <typeparam name="T2">The related resource's type</typeparam>
|
||||
/// <returns>The param <see cref="obj"/></returns>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
|
||||
/// <seealso cref="Load{T}(T, System.String)"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
|
||||
where T : class, IResource
|
||||
where T2 : class, new();
|
||||
@ -265,6 +257,9 @@ namespace Kyoo.Controllers
|
||||
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
|
||||
/// <typeparam name="T">The type of the source object</typeparam>
|
||||
/// <returns>The param <see cref="obj"/></returns>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
|
||||
/// <seealso cref="Load(IResource, string)"/>
|
||||
Task<T> Load<T>([NotNull] T obj, string memberName)
|
||||
where T : class, IResource;
|
||||
|
||||
@ -273,6 +268,9 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="obj">The source object.</param>
|
||||
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
|
||||
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
|
||||
/// <seealso cref="Load{T}(T, System.String)"/>
|
||||
Task Load([NotNull] IResource obj, string memberName);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -16,6 +16,6 @@ namespace Kyoo.Controllers
|
||||
|
||||
Task<Season> GetSeason(Show show, int seasonNumber);
|
||||
|
||||
Task<Episode> GetEpisode(Show show, int seasonNumber, int episodeNumber, int absoluteNumber);
|
||||
Task<Episode> GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ namespace Kyoo.Controllers
|
||||
Task<Show> SearchShow(string showName, bool isMovie, Library library);
|
||||
Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library);
|
||||
Task<Season> GetSeason(Show show, int seasonNumber, Library library);
|
||||
Task<Episode> GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library);
|
||||
Task<Episode> GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, Library library);
|
||||
Task<ICollection<PeopleRole>> GetPeople(Show show, Library library);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
/// <param name="obj">The resource to delete</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task Delete([NotNull] T obj);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="objs">One or multiple resources to delete</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="objs">An enumerable of resources to delete</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(IEnumerable<T> objs);
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="ids">One or multiple resource's id</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="ids">An enumerable of resource's id</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(IEnumerable<int> ids);
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="slugs">One or multiple resource's slug</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// </summary>
|
||||
/// <param name="slugs">An enumerable of resource's slug</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange(IEnumerable<string> slugs);
|
||||
/// <summary>
|
||||
/// Delete a list of resources.
|
||||
/// Delete all resources that match the predicate.
|
||||
/// </summary>
|
||||
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
|
||||
Task DeleteAll([NotNull] Expression<Func<T, bool>> where);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -412,25 +375,7 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// A repository to handle tracks
|
||||
/// </summary>
|
||||
public interface ITrackRepository : IRepository<Track>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a track from it's slug and it's type.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the track</param>
|
||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The track found</returns>
|
||||
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
|
||||
|
||||
/// <summary>
|
||||
/// Get a track from it's slug and it's type or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the track</param>
|
||||
/// <param name="type">The type (Video, Audio or Subtitle)</param>
|
||||
/// <returns>The track found</returns>
|
||||
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
|
||||
}
|
||||
public interface ITrackRepository : IRepository<Track> { }
|
||||
|
||||
/// <summary>
|
||||
/// A repository to handle libraries.
|
||||
@ -631,10 +576,12 @@ namespace Kyoo.Controllers
|
||||
/// <param name="where">A predicate to add arbitrary filter</param>
|
||||
/// <param name="sort">Sort information (sort order & sort by)</param>
|
||||
/// <param name="limit">Pagination information (where to start and how many to get)</param>
|
||||
/// <typeparam name="T">The type of metadata to retrieve</typeparam>
|
||||
/// <returns>A filtered list of external ids.</returns>
|
||||
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
|
||||
Sort<MetadataID> sort = default,
|
||||
Pagination limit = default);
|
||||
Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
|
||||
Sort<MetadataID<T>> sort = default,
|
||||
Pagination limit = default)
|
||||
where T : class, IResource;
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of external ids that match all filters
|
||||
@ -643,10 +590,11 @@ namespace Kyoo.Controllers
|
||||
/// <param name="sort">A sort by expression</param>
|
||||
/// <param name="limit">Pagination information (where to start and how many to get)</param>
|
||||
/// <returns>A filtered list of external ids.</returns>
|
||||
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
|
||||
Expression<Func<MetadataID, object>> sort,
|
||||
Task<ICollection<MetadataID<T>>> GetMetadataID<T>([Optional] Expression<Func<MetadataID<T>, bool>> where,
|
||||
Expression<Func<MetadataID<T>, object>> sort,
|
||||
Pagination limit = default
|
||||
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
|
||||
) where T : class, IResource
|
||||
=> GetMetadataID(where, new Sort<MetadataID<T>>(sort), limit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -37,6 +37,8 @@ namespace Kyoo.Controllers
|
||||
public IGenreRepository GenreRepository { get; }
|
||||
/// <inheritdoc />
|
||||
public IProviderRepository ProviderRepository { get; }
|
||||
/// <inheritdoc />
|
||||
public IUserRepository UserRepository { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -58,6 +60,7 @@ namespace Kyoo.Controllers
|
||||
StudioRepository = GetRepository<Studio>() as IStudioRepository;
|
||||
GenreRepository = GetRepository<Genre>() as IGenreRepository;
|
||||
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
|
||||
UserRepository = GetRepository<User>() as IUserRepository;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -114,12 +117,6 @@ namespace Kyoo.Controllers
|
||||
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
|
||||
{
|
||||
return TrackRepository.Get(slug, type);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> GetOrDefault<T>(int id)
|
||||
where T : class, IResource
|
||||
@ -165,12 +162,6 @@ namespace Kyoo.Controllers
|
||||
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
|
||||
{
|
||||
return await TrackRepository.GetOrDefault(slug, type);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> 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<Show>(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<Season>(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<Episode>(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<People>(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)
|
||||
|
||||
10
Kyoo.Common/Models/Attributes/ComputedAttribute.cs
Normal file
10
Kyoo.Common/Models/Attributes/ComputedAttribute.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Models.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ComputedAttribute : NotMergeableAttribute { }
|
||||
}
|
||||
@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes
|
||||
/// An attribute to inform that the service will be injected automatically by a service provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>
|
||||
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>.
|
||||
/// It can also be used on <see cref="IPlugin"/> and it will be injected before calling <see cref="IPlugin.ConfigureAspNet"/>.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
|
||||
|
||||
13
Kyoo.Common/Models/Attributes/LinkAttribute.cs
Normal file
13
Kyoo.Common/Models/Attributes/LinkAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Common.Models.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute to mark Link properties on resource.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
[MeansImplicitUse]
|
||||
public class LinkAttribute : SerializeIgnoreAttribute { }
|
||||
}
|
||||
@ -1,17 +1,34 @@
|
||||
using System;
|
||||
using Kyoo.Controllers;
|
||||
|
||||
namespace Kyoo.Models.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
/// <summary>
|
||||
/// The targeted relation can be edited via calls to the repository's <see cref="IRepository{T}.Edit"/> method.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class EditableRelationAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// The targeted relation can be loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class LoadableRelationAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the field containing the related resource's ID.
|
||||
/// </summary>
|
||||
public string RelationID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/>.
|
||||
/// </summary>
|
||||
public LoadableRelationAttribute() {}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
|
||||
/// </summary>
|
||||
/// <param name="relationID">The name of the RelationID field.</param>
|
||||
public LoadableRelationAttribute(string relationID)
|
||||
{
|
||||
RelationID = relationID;
|
||||
|
||||
@ -2,17 +2,41 @@ using System;
|
||||
|
||||
namespace Kyoo.Models.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Remove an property from the serialization pipeline. It will simply be skipped.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public class SerializeIgnoreAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a property from the deserialization pipeline. The user can't input value for this property.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public class DeserializeIgnoreAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public class SerializeAsAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The format string to use.
|
||||
/// </summary>
|
||||
public string Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SerializeAsAttribute"/> with the selected format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// The show's poster serialized uses this format string: <code>{HOST}/api/shows/{Slug}/poster</code>
|
||||
/// </example>
|
||||
/// <param name="format">The format to use</param>
|
||||
public SerializeAsAttribute(string format)
|
||||
{
|
||||
Format = format;
|
||||
|
||||
38
Kyoo.Common/Models/Chapter.cs
Normal file
38
Kyoo.Common/Models/Chapter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A chapter to split an episode in multiple parts.
|
||||
/// </summary>
|
||||
public class Chapter
|
||||
{
|
||||
/// <summary>
|
||||
/// The start time of the chapter (in second from the start of the episode).
|
||||
/// </summary>
|
||||
public float StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The end time of the chapter (in second from the start of the episode)&.
|
||||
/// </summary>
|
||||
public float EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Chapter"/>.
|
||||
/// </summary>
|
||||
/// <param name="startTime">The start time of the chapter (in second)</param>
|
||||
/// <param name="endTime">The end time of the chapter (in second)</param>
|
||||
/// <param name="name">The name of this chapter</param>
|
||||
public Chapter(float startTime, float endTime, string name)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of item, ether a show, a movie or a collection.
|
||||
/// </summary>
|
||||
public enum ItemType
|
||||
{
|
||||
Show,
|
||||
@ -12,22 +14,67 @@ namespace Kyoo.Models
|
||||
Collection
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
|
||||
/// This is used to list content put inside a library.
|
||||
/// </summary>
|
||||
public class LibraryItem : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the show or collection.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summary of the show or collection.
|
||||
/// </summary>
|
||||
public string Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished? This is only applicable for shows.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// The date this show or collection started airing. It can be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? StartAir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this show or collection finished airing.
|
||||
/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
|
||||
/// It can also be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? EndAir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this item (ether a collection, a show or a movie).
|
||||
/// </summary>
|
||||
public ItemType Type { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty <see cref="LibraryItem"/>.
|
||||
/// </summary>
|
||||
public LibraryItem() {}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="LibraryItem"/> from a show.
|
||||
/// </summary>
|
||||
/// <param name="show">The show that this library item should represent.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="LibraryItem"/> from a collection
|
||||
/// </summary>
|
||||
/// <param name="collection">The collection that this library item should represent.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An expression to create a <see cref="LibraryItem"/> representing a show.
|
||||
/// </summary>
|
||||
public static Expression<Func<Show, LibraryItem>> 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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// An expression to create a <see cref="LibraryItem"/> representing a collection.
|
||||
/// </summary>
|
||||
public static Expression<Func<Collection, LibraryItem>> 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
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,33 +1,64 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing a link between two resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Links should only be used on the data layer and not on other application code.
|
||||
/// </remarks>
|
||||
public class Link
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the first item of the link.
|
||||
/// The first item of the link should be the one to own the link.
|
||||
/// </summary>
|
||||
public int FirstID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the second item of this link
|
||||
/// The second item of the link should be the owned resource.
|
||||
/// </summary>
|
||||
public int SecondID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typeless <see cref="Link"/>.
|
||||
/// </summary>
|
||||
public Link() {}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typeless <see cref="Link"/> with two IDs.
|
||||
/// </summary>
|
||||
/// <param name="firstID">The ID of the first resource</param>
|
||||
/// <param name="secondID">The ID of the second resource</param>
|
||||
public Link(int firstID, int secondID)
|
||||
{
|
||||
FirstID = firstID;
|
||||
SecondID = secondID;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typeless <see cref="Link"/> between two resources.
|
||||
/// </summary>
|
||||
/// <param name="first">The first resource</param>
|
||||
/// <param name="second">The second resource</param>
|
||||
public Link(IResource first, IResource second)
|
||||
{
|
||||
FirstID = first.ID;
|
||||
SecondID = second.ID;
|
||||
}
|
||||
|
||||
public static Link Create(IResource first, IResource second)
|
||||
{
|
||||
return new(first, second);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typed link between two resources.
|
||||
/// This method can be used instead of the constructor to make use of generic parameters deduction.
|
||||
/// </summary>
|
||||
/// <param name="first">The first resource</param>
|
||||
/// <param name="second">The second resource</param>
|
||||
/// <typeparam name="T">The type of the first resource</typeparam>
|
||||
/// <typeparam name="T2">The type of the second resource</typeparam>
|
||||
/// <returns>A newly created typed link with both resources</returns>
|
||||
public static Link<T, T2> Create<T, T2>(T first, T2 second)
|
||||
where T : class, IResource
|
||||
where T2 : class, IResource
|
||||
@ -35,6 +66,16 @@ namespace Kyoo.Models
|
||||
return new(first, second);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typed link between two resources without storing references to resources.
|
||||
/// This is the same as <see cref="Create{T,T2}"/> but this method does not set <see cref="Link{T1,T2}.First"/>
|
||||
/// and <see cref="Link{T1,T2}.Second"/> fields. Only IDs are stored and not references.
|
||||
/// </summary>
|
||||
/// <param name="first">The first resource</param>
|
||||
/// <param name="second">The second resource</param>
|
||||
/// <typeparam name="T">The type of the first resource</typeparam>
|
||||
/// <typeparam name="T2">The type of the second resource</typeparam>
|
||||
/// <returns>A newly created typed link with both resources</returns>
|
||||
public static Link<T, T2> UCreate<T, T2>(T first, T2 second)
|
||||
where T : class, IResource
|
||||
where T2 : class, IResource
|
||||
@ -42,6 +83,9 @@ namespace Kyoo.Models
|
||||
return new(first, second, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The expression to retrieve the unique ID of a Link. This is an aggregate of the two resources IDs.
|
||||
/// </summary>
|
||||
public static Expression<Func<Link, object>> PrimaryKey
|
||||
{
|
||||
get
|
||||
@ -51,17 +95,41 @@ namespace Kyoo.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A strongly typed link between two resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first resource</typeparam>
|
||||
/// <typeparam name="T2">The type of the second resource</typeparam>
|
||||
public class Link<T1, T2> : Link
|
||||
where T1 : class, IResource
|
||||
where T2 : class, IResource
|
||||
{
|
||||
public virtual T1 First { get; set; }
|
||||
public virtual T2 Second { get; set; }
|
||||
/// <summary>
|
||||
/// A reference of the first resource.
|
||||
/// </summary>
|
||||
public T1 First { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the second resource.
|
||||
/// </summary>
|
||||
public T2 Second { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty, typed <see cref="Link{T1,T2}"/>.
|
||||
/// </summary>
|
||||
public Link() {}
|
||||
|
||||
[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typed link with two resources.
|
||||
/// </summary>
|
||||
/// <param name="first">The first resource</param>
|
||||
/// <param name="second">The second resource</param>
|
||||
/// <param name="privateItems">
|
||||
/// True if no reference to resources should be kept, false otherwise.
|
||||
/// The default is false (references are kept).
|
||||
/// </param>
|
||||
public Link(T1 first, T2 second, bool privateItems = false)
|
||||
: base(first, second)
|
||||
{
|
||||
@ -71,10 +139,18 @@ namespace Kyoo.Models
|
||||
Second = second;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new typed link with IDs only.
|
||||
/// </summary>
|
||||
/// <param name="firstID">The ID of the first resource</param>
|
||||
/// <param name="secondID">The ID of the second resource</param>
|
||||
public Link(int firstID, int secondID)
|
||||
: base(firstID, secondID)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// The expression to retrieve the unique ID of a typed Link. This is an aggregate of the two resources IDs.
|
||||
/// </summary>
|
||||
public new static Expression<Func<Link<T1, T2>, object>> PrimaryKey
|
||||
{
|
||||
get
|
||||
|
||||
@ -1,26 +1,34 @@
|
||||
using Kyoo.Models.Attributes;
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
public class MetadataID
|
||||
/// <summary>
|
||||
/// ID and link of an item on an external provider.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class MetadataID<T> : Link<T, Provider>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the resource on the external provider.
|
||||
/// </summary>
|
||||
public string DataID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the resource on the external provider.
|
||||
/// </summary>
|
||||
public string Link { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
|
||||
/// </summary>
|
||||
public new static Expression<Func<MetadataID<T>, object>> PrimaryKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return x => new {First = x.FirstID, Second = x.SecondID};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,22 +3,45 @@ using System.Linq;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A page of resource that contains information about the pagination of resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource contained in this page.</typeparam>
|
||||
public class Page<T> where T : IResource
|
||||
{
|
||||
public string This { get; set; }
|
||||
public string First { get; set; }
|
||||
public string Next { get; set; }
|
||||
/// <summary>
|
||||
/// The link of the current page.
|
||||
/// </summary>
|
||||
public string This { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the first page.
|
||||
/// </summary>
|
||||
public string First { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the next page.
|
||||
/// </summary>
|
||||
public string Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of items in the current page.
|
||||
/// </summary>
|
||||
public int Count => Items.Count;
|
||||
public ICollection<T> Items { get; set; }
|
||||
|
||||
public Page() { }
|
||||
|
||||
public Page(ICollection<T> items)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The list of items in the page.
|
||||
/// </summary>
|
||||
public ICollection<T> Items { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="this">The link of the current page.</param>
|
||||
/// <param name="next">The link of the next page.</param>
|
||||
/// <param name="first">The link of the first page.</param>
|
||||
public Page(ICollection<T> items, string @this, string next, string first)
|
||||
{
|
||||
Items = items;
|
||||
@ -27,7 +50,14 @@ namespace Kyoo.Models
|
||||
First = first;
|
||||
}
|
||||
|
||||
public Page(ICollection<T> items,
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/> and compute the urls.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="url">The base url of the resources available from this page.</param>
|
||||
/// <param name="query">The list of query strings of the current page</param>
|
||||
/// <param name="limit">The number of items requested for the current page.</param>
|
||||
public Page(ICollection<T> items,
|
||||
string url,
|
||||
Dictionary<string, string> query,
|
||||
int limit)
|
||||
|
||||
@ -1,17 +1,55 @@
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A role a person played for a show. It can be an actor, musician, voice actor, director, writer...
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is not serialized like other classes.
|
||||
/// Based on the <see cref="ForPeople"/> field, it is serialized like
|
||||
/// a show with two extra fields (<see cref="Role"/> and <see cref="Type"/>).
|
||||
/// </remarks>
|
||||
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; }
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug => ForPeople ? Show.Slug : People.Slug;
|
||||
|
||||
/// <summary>
|
||||
/// Should this role be used as a Show substitute (the value is <c>false</c>) or
|
||||
/// as a People substitute (the value is <c>true</c>).
|
||||
/// </summary>
|
||||
public bool ForPeople { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the People playing the role.
|
||||
/// </summary>
|
||||
public int PeopleID { get; set; }
|
||||
/// <summary>
|
||||
/// The people that played this role.
|
||||
/// </summary>
|
||||
public People People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show where the People playing in.
|
||||
/// </summary>
|
||||
public int ShowID { get; set; }
|
||||
/// <summary>
|
||||
/// The show where the People played in.
|
||||
/// </summary>
|
||||
public Show Show { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of work the person has done for the show.
|
||||
/// That can be something like "Actor", "Writer", "Music", "Voice Actor"...
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The role the People played.
|
||||
/// This is mostly used to inform witch character was played for actor and voice actors.
|
||||
/// </summary>
|
||||
public string Role { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing collections of <see cref="Show"/>.
|
||||
/// A collection can also be stored in a <see cref="Library"/>.
|
||||
/// </summary>
|
||||
public class Collection : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this collection.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of this collection.
|
||||
/// </summary>
|
||||
public string Overview { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows contained in this collection.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Show> Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of libraries that contains this collection.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> ShowLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
|
||||
#endif
|
||||
|
||||
public Collection() { }
|
||||
|
||||
public Collection(string slug, string name, string overview, string poster)
|
||||
{
|
||||
Slug = slug;
|
||||
Name = name;
|
||||
Overview = overview;
|
||||
Poster = poster;
|
||||
}
|
||||
/// <summary>
|
||||
/// The internal link between this collection and shows in the <see cref="Shows"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Collection, Show>> ShowLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal link between this collection and libraries in the <see cref="Libraries"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// A class to represent a single show's episode.
|
||||
/// </summary>
|
||||
public class Episode : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
/// <inheritdoc />
|
||||
[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, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\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, @"(?<show>.+)-(?<absolute>\d+)");
|
||||
if (match.Success)
|
||||
{
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
AbsoluteNumber = int.Parse(match.Groups["absolute"].Value);
|
||||
}
|
||||
else
|
||||
ShowSlug = value;
|
||||
SeasonNumber = null;
|
||||
EpisodeNumber = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string ShowSlug { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int ShowID { get; set; }
|
||||
/// <summary>
|
||||
/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Season containing this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int? SeasonID { get; set; }
|
||||
/// <summary>
|
||||
/// The season that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// This can be null if the season is unknown and the episode is only identified by it's <see cref="AbsoluteNumber"/>.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season in witch this episode is in.
|
||||
/// </summary>
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of this episode is it's season.
|
||||
/// </summary>
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||
/// </summary>
|
||||
public int? AbsoluteNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this episode.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The overview of this episode.
|
||||
/// </summary>
|
||||
public string Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The release date of this episode. It can be null if unknown.
|
||||
/// </summary>
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
public int Runtime { get; set; } //This runtime variable should be in minutes
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Episode>> ExternalIDs { get; set; }
|
||||
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
|
||||
/// <summary>
|
||||
/// The list of tracks this episode has. This lists video, audio and subtitles available.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<Track> Tracks { get; set; }
|
||||
|
||||
|
||||
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber)
|
||||
/// <summary>
|
||||
/// Get the slug of an episode.
|
||||
/// </summary>
|
||||
/// <param name="showSlug">The slug of the show. It can't be null.</param>
|
||||
/// <param name="seasonNumber">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <param name="episodeNumber">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <param name="absoluteNumber">
|
||||
/// The absolute number of this show.
|
||||
/// If you don't know it or this is a movie, use null
|
||||
/// </param>
|
||||
/// <returns>The slug corresponding to the given arguments</returns>
|
||||
/// <exception cref="ArgumentNullException">The given show slug was null.</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A genre that allow one to specify categories for shows.
|
||||
/// </summary>
|
||||
public class Genre : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this genre.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
|
||||
/// <summary>
|
||||
/// The list of shows that have this genre.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Show> Shows { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> ShowLinks { get; set; }
|
||||
/// <summary>
|
||||
/// The internal link between this genre and shows in the <see cref="Shows"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Show, Genre>> ShowLinks { get; set; }
|
||||
#endif
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty <see cref="Genre"/>.
|
||||
/// </summary>
|
||||
public Genre() {}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Genre"/> and specify it's <see cref="Name"/>.
|
||||
/// The <see cref="Slug"/> is automatically calculated from it's name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of this genre.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using Kyoo.Controllers;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
@ -8,6 +10,10 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You don't need to specify an ID manually when creating a new resource,
|
||||
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
|
||||
/// </remarks>
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,24 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A library containing <see cref="Show"/> and <see cref="Collection"/>.
|
||||
/// </summary>
|
||||
public class Library : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this library.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of paths that this library is responsible for. This is mainly used by the Scan task.
|
||||
/// </summary>
|
||||
public string[] Paths { get; set; }
|
||||
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<Provider> Providers { get; set; }
|
||||
/// <summary>
|
||||
/// The list of <see cref="Provider"/> used for items in this library.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<Provider> Providers { get; set; }
|
||||
|
||||
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
|
||||
/// <summary>
|
||||
/// The list of shows in this library.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Show> Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of collections in this library.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Collection> Collections { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
|
||||
/// <summary>
|
||||
/// The internal link between this library and provider in the <see cref="Providers"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal link between this library and shows in the <see cref="Shows"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Show>> ShowLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal link between this library and collection in the <see cref="Collections"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,14 +3,37 @@ using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class People : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<PeopleRole> Roles { get; set; }
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this person.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this person has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<People>> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<PeopleRole> Roles { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// This class contains metadata about <see cref="IMetadataProvider"/>.
|
||||
/// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>.
|
||||
/// </summary>
|
||||
public class Provider : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this provider.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The extension of the logo. This is used for http responses.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string LogoExtension { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of libraries that uses this provider.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; }
|
||||
/// <summary>
|
||||
/// The internal link between this provider and libraries in the <see cref="Libraries"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, default, <see cref="Provider"/>
|
||||
/// </summary>
|
||||
public Provider() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Provider"/> and specify it's <see cref="Name"/>.
|
||||
/// The <see cref="Slug"/> is automatically calculated from it's name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of this provider.</param>
|
||||
/// <param name="logo">The logo of this provider.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A season of a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class Season : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
public string Slug => $"{ShowSlug}-s{SeasonNumber}";
|
||||
[SerializeIgnore] public int ShowID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[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 ?? "", @"(?<show>.+)-s(?<season>\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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string ShowSlug { private get; set; }
|
||||
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this season.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int ShowID { get; set; }
|
||||
/// <summary>
|
||||
/// The show that contains this season. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
|
||||
|
||||
public int SeasonNumber { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// The number of this season. This can be set to 0 to indicate specials.
|
||||
/// </summary>
|
||||
public int SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this season.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A quick overview of this season.
|
||||
/// </summary>
|
||||
public string Overview { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The starting air date of this season.
|
||||
/// </summary>
|
||||
public DateTime? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ending date of this season.
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; }
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Season>> ExternalIDs { get; set; }
|
||||
|
||||
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
|
||||
/// <summary>
|
||||
/// The list of episodes that this season contains.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// </summary>
|
||||
public class Show : IResource, IOnMerge
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this show.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of alternative titles of this show.
|
||||
/// </summary>
|
||||
[EditableRelation] public string[] Aliases { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the root directory of this show.
|
||||
/// This can be any kind of path supported by <see cref="IFileManager"/>
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summary of this show.
|
||||
/// </summary>
|
||||
public string Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An URL to a trailer. This could be any path supported by the <see cref="IFileManager"/>.
|
||||
/// </summary>
|
||||
/// 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; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this show started airing. It can be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? StartAir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this show finished airing.
|
||||
/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
|
||||
/// It can also be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? EndAir { get; set; }
|
||||
|
||||
public int? StartYear { get; set; }
|
||||
public int? EndYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if this show represent a movie, false otherwise.
|
||||
/// </summary>
|
||||
public bool IsMovie { get; set; }
|
||||
|
||||
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this show has. See <see cref="MetadataID{T}"/> for more information.
|
||||
/// </summary>
|
||||
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Show>> ExternalIDs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int? StudioID { get; set; }
|
||||
[LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; }
|
||||
[LoadableRelation] [EditableRelation] public virtual ICollection<Genre> Genres { get; set; }
|
||||
[LoadableRelation] [EditableRelation] public virtual ICollection<PeopleRole> People { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Season> Seasons { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
|
||||
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
|
||||
/// <summary>
|
||||
/// The Studio that made this show. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of genres (themes) this show has.
|
||||
/// </summary>
|
||||
[LoadableRelation] [EditableRelation] public ICollection<Genre> Genres { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of people that made this show.
|
||||
/// </summary>
|
||||
[LoadableRelation] [EditableRelation] public ICollection<PeopleRole> People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The different seasons in this show. If this is a movie, this list is always null or empty.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Season> Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of libraries that contains this show.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of collections that contains this show.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Collection> Collections { get; set; }
|
||||
|
||||
#if ENABLE_INTERNAL_LINKS
|
||||
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> LibraryLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
|
||||
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> GenreLinks { get; set; }
|
||||
/// <summary>
|
||||
/// The internal link between this show and libraries in the <see cref="Libraries"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Library, Show>> LibraryLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal link between this show and collections in the <see cref="Collections"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal link between this show and genres in the <see cref="Genres"/> list.
|
||||
/// </summary>
|
||||
[Link] public ICollection<Link<Show, Genre>> GenreLinks { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the internal provider's ID of a show using it's provider slug.
|
||||
/// </summary>
|
||||
/// <remarks>This method will never return anything if the <see cref="ExternalIDs"/> are not loaded.</remarks>
|
||||
/// <param name="provider">The slug of the provider</param>
|
||||
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
|
||||
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)
|
||||
/// <inheritdoc />
|
||||
public void OnMerge(object merged)
|
||||
{
|
||||
if (ExternalIDs != null)
|
||||
foreach (MetadataID id in ExternalIDs)
|
||||
id.Show = this;
|
||||
foreach (MetadataID<Show> id in ExternalIDs)
|
||||
id.First = this;
|
||||
if (People != null)
|
||||
foreach (PeopleRole link in People)
|
||||
link.Show = this;
|
||||
@ -64,5 +180,8 @@ namespace Kyoo.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The enum containing show's status.
|
||||
/// </summary>
|
||||
public enum Status { Finished, Airing, Planned, Unknown }
|
||||
}
|
||||
|
||||
@ -3,31 +3,40 @@ using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A studio that make shows.
|
||||
/// </summary>
|
||||
public class Studio : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this studio.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
|
||||
/// <summary>
|
||||
/// The list of shows that are made by this studio.
|
||||
/// </summary>
|
||||
[LoadableRelation] public ICollection<Show> Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty, <see cref="Studio"/>.
|
||||
/// </summary>
|
||||
public Studio() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the studio.</param>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of available stream types.
|
||||
/// Attachments are only used temporarily by the transcoder but are not stored in a database.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// A video, audio or subtitle track for an episode.
|
||||
/// </summary>
|
||||
public class Track : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ID { get; set; }
|
||||
[SerializeIgnore] public int EpisodeID { get; set; }
|
||||
public int TrackIndex { get; set; }
|
||||
public bool IsDefault
|
||||
|
||||
/// <inheritdoc />
|
||||
[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,
|
||||
@"(?<ep>[^\.]+)\.(?<lang>\w{0,3})(-(?<index>\d+))?(\.(?<forced>forced))?\.(?<type>\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<StreamType>(match.Groups["type"].Value, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string EpisodeSlug { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the stream.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The language of this stream (as a ISO-639-2 language code)
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The codec of this stream.
|
||||
/// </summary>
|
||||
public string Codec { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is this stream the default one of it's type?
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this stream tagged as forced?
|
||||
/// </summary>
|
||||
public bool IsForced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this track extern to the episode's file?
|
||||
/// </summary>
|
||||
public bool IsExternal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of this track.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this stream.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public StreamType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the episode that uses this track.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public int EpisodeID { get; set; }
|
||||
/// <summary>
|
||||
/// The episode that uses this track.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of this track on the episode.
|
||||
/// </summary>
|
||||
public int TrackIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A user-friendly name for this track. It does not include the track type.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored)
|
||||
/// </summary>
|
||||
/// <param name="baseSlug">The slug to edit</param>
|
||||
/// <param name="type">The new type of this </param>
|
||||
/// <param name="language"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="forced"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Common.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
@ -52,7 +53,7 @@ namespace Kyoo.Models
|
||||
/// <summary>
|
||||
/// Links between Users and Shows.
|
||||
/// </summary>
|
||||
public ICollection<Link<User, Show>> ShowLinks { get; set; }
|
||||
[Link] public ICollection<Link<User, Show>> ShowLinks { get; set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -62,7 +63,7 @@ namespace Kyoo.Models
|
||||
public class WatchedEpisode : Link<User, Episode>
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public int WatchedPercentage { get; set; }
|
||||
}
|
||||
|
||||
@ -2,14 +2,44 @@
|
||||
|
||||
namespace Kyoo.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Results of a search request.
|
||||
/// </summary>
|
||||
public class SearchResult
|
||||
{
|
||||
public string Query;
|
||||
public IEnumerable<Collection> Collections;
|
||||
public IEnumerable<Show> Shows;
|
||||
public IEnumerable<Episode> Episodes;
|
||||
public IEnumerable<People> People;
|
||||
public IEnumerable<Genre> Genres;
|
||||
public IEnumerable<Studio> Studios;
|
||||
/// <summary>
|
||||
/// The query of the search request.
|
||||
/// </summary>
|
||||
public string Query { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The collections that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<Collection> Collections { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The shows that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<Show> Shows { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The episodes that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<Episode> Episodes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The people that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<People> People { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The genres that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<Genre> Genres { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The studios that matched the search.
|
||||
/// </summary>
|
||||
public ICollection<Studio> Studios { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Episode"/> with another form.
|
||||
/// </summary>
|
||||
public class WatchItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the episode associated with this item.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// The slug of this episode.
|
||||
/// </summary>
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the show containing this episode.
|
||||
/// </summary>
|
||||
public string ShowTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the show containing this episode
|
||||
/// </summary>
|
||||
public string ShowSlug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season in witch this episode is in.
|
||||
/// </summary>
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of this episode is it's season.
|
||||
/// </summary>
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||
/// </summary>
|
||||
public int? AbsoluteNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this episode.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The release date of this episode. It can be null if unknown.
|
||||
/// </summary>
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Episode PreviousEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Episode NextEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> if this is a movie, <c>false</c> otherwise.
|
||||
/// </summary>
|
||||
public bool IsMovie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The container of the video file of this episode.
|
||||
/// Common containers are mp4, mkv, avi and so on.
|
||||
/// </summary>
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The video track. See <see cref="Track"/> for more information.
|
||||
/// </summary>
|
||||
public Track Video { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of audio tracks. See <see cref="Track"/> for more information.
|
||||
/// </summary>
|
||||
public ICollection<Track> Audios { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of subtitles tracks. See <see cref="Track"/> for more information.
|
||||
/// </summary>
|
||||
public ICollection<Track> Subtitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of chapters. See <see cref="Chapter"/> for more information.
|
||||
/// </summary>
|
||||
public ICollection<Chapter> 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<Track> audios,
|
||||
ICollection<Track> subtitles)
|
||||
: this(episodeID, show, seasonNumber, episodeNumber, absoluteNumber, title, releaseDate, path)
|
||||
{
|
||||
Video = video;
|
||||
Audios = audios;
|
||||
Subtitles = subtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="WatchItem"/> from an <see cref="Episode"/>.
|
||||
/// </summary>
|
||||
/// <param name="ep">The episode to transform.</param>
|
||||
/// <param name="library">
|
||||
/// A library manager to retrieve the next and previous episode and load the show & tracks of the episode.
|
||||
/// </param>
|
||||
/// <returns>A new WatchItem representing the given episode.</returns>
|
||||
public static async Task<WatchItem> 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<Episode>(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<Episode>(x => x.EpisodeNumber, true))
|
||||
).FirstOrDefault();
|
||||
}
|
||||
|
||||
if (ep.EpisodeNumber >= await library.GetCount<Episode>(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<Episode>(x => x.ShowID == ep.ShowID
|
||||
&& x.AbsoluteNumber == ep.EpisodeNumber + 1);
|
||||
next = await library.GetOrDefault<Episode>(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<ICollection<Chapter>> GetChapters(string episodePath)
|
||||
{
|
||||
string path = PathIO.Combine(
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of utility functions that can be used everywhere.
|
||||
/// </summary>
|
||||
public static class Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the lambda expression a member (like x => x.Body).
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression that should be checked</param>
|
||||
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||
public static bool IsPropertyExpression(LambdaExpression ex)
|
||||
{
|
||||
return ex == null ||
|
||||
ex.Body is MemberExpression ||
|
||||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression</param>
|
||||
/// <returns>The name of the expression</returns>
|
||||
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a member (property or field)
|
||||
/// </summary>
|
||||
/// <param name="member">The member value</param>
|
||||
/// <param name="obj">The owner of this member</param>
|
||||
/// <returns>The value boxed as an object</returns>
|
||||
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
|
||||
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}).")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <returns>The slug version of the given string</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge two lists, can keep duplicates or remove them.
|
||||
/// </summary>
|
||||
/// <param name="first">The first enumerable to merge</param>
|
||||
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
|
||||
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
|
||||
/// <returns>The two list merged as an array</returns>
|
||||
public static T[] MergeLists<T>(IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
Func<T, T, bool> isEqual = null)
|
||||
{
|
||||
if (first == null)
|
||||
return second.ToArray();
|
||||
if (second == null)
|
||||
return first.ToArray();
|
||||
if (isEqual == null)
|
||||
return first.Concat(second).ToArray();
|
||||
List<T> list = first.ToList();
|
||||
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <param name="first">The object to assign</param>
|
||||
/// <param name="second">The object containing new values</param>
|
||||
/// <typeparam name="T">Fields of T will be used</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
public static T Assign<T>(T first, T second)
|
||||
{
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <param name="first">The object to complete</param>
|
||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||
/// <param name="where">Filter fields that will be merged</param>
|
||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
/// <exception cref="ArgumentNullException">If first is null</exception>
|
||||
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
|
||||
{
|
||||
if (first == null)
|
||||
throw new ArgumentNullException(nameof(first));
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An advanced <see cref="Complete{T}"/> function.
|
||||
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
|
||||
/// Enumerable will be merged (concatenated).
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
|
||||
/// </summary>
|
||||
/// <param name="first">The object to complete</param>
|
||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
public static T Merge<T>(T first, T second)
|
||||
{
|
||||
if (first == null)
|
||||
return second;
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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<object>(
|
||||
typeof(Utility),
|
||||
nameof(MergeLists),
|
||||
GetEnumerableType(property.PropertyType),
|
||||
oldValue, newValue, null));
|
||||
}
|
||||
}
|
||||
|
||||
if (first is IOnMerge merge)
|
||||
merge.OnMerge(second);
|
||||
return first;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every fields of <see cref="obj"/> to the default value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to nullify</param>
|
||||
/// <typeparam name="T">Fields of T will be nullified</typeparam>
|
||||
/// <returns><see cref="obj"/></returns>
|
||||
public static T Nullify<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||
/// </summary>
|
||||
/// <param name="type">The starting type</param>
|
||||
/// <returns>A list of types</returns>
|
||||
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
|
||||
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
for (; type != null; type = type.BaseType)
|
||||
yield return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
|
||||
{
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
return IsOfGenericType(obj.GetType(), genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||
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<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generic definition of <see cref="genericType"/>.
|
||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
|
||||
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
|
||||
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<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Select where the index of the item can be used.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
||||
/// <param name="mapper">The function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped.</returns>
|
||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
||||
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, int, T2> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
yield break;
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
int index = 0;
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return mapper(enumerator.Current, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A map where the mapping function is asynchronous.
|
||||
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
||||
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, int, Task<T2>> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
yield break;
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
int index = 0;
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return await mapper(enumerator.Current, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An asynchronous version of Select.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map</param>
|
||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
||||
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, Task<T2>> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
yield break;
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
yield return await mapper(enumerator.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an AsyncEnumerable to a List by waiting for every item.
|
||||
/// </summary>
|
||||
/// <param name="self">The async list</param>
|
||||
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
|
||||
/// <returns>A task that will return a simple list</returns>
|
||||
/// <exception cref="ArgumentNullException">The list can't be null</exception>
|
||||
public static async Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
|
||||
List<T> ret = new();
|
||||
|
||||
await foreach(T i in self)
|
||||
ret.Add(i);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the enumerable is empty, execute an action.
|
||||
/// </summary>
|
||||
/// <param name="self">The enumerable to check</param>
|
||||
/// <param name="action">The action to execute is the list is empty</param>
|
||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
action();
|
||||
yield break;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (object i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
await action(i);
|
||||
}
|
||||
|
||||
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
await foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> 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<T> 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<T>(
|
||||
[NotNull] Type owner,
|
||||
[NotNull] string methodName,
|
||||
[NotNull] Type type,
|
||||
params object[] args)
|
||||
{
|
||||
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
||||
}
|
||||
|
||||
public static T RunGenericMethod<T>(
|
||||
[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<T>(
|
||||
[NotNull] object instance,
|
||||
[NotNull] string methodName,
|
||||
[NotNull] Type type,
|
||||
params object[] args)
|
||||
{
|
||||
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
||||
}
|
||||
|
||||
public static T RunGenericMethod<T>(
|
||||
[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<List<T>> BatchBy<T>(this List<T> 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<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
|
||||
{
|
||||
T[] ret = new T[countPerList];
|
||||
int i = 0;
|
||||
|
||||
using IEnumerator<T> 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<string, string> 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<T> Then<T>(this Task<T> task, Action<T> 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<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> 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<T> Cast<T>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a friendly type name (supporting generics)
|
||||
/// For example a list of string will be displayed as List<string> and not as List`1.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to use</param>
|
||||
/// <returns>The friendly name of the type</returns>
|
||||
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}>";
|
||||
}
|
||||
}
|
||||
}
|
||||
279
Kyoo.Common/Utility/EnumerableExtensions.cs
Normal file
279
Kyoo.Common/Utility/EnumerableExtensions.cs
Normal file
@ -0,0 +1,279 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of extensions class for enumerable.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A Select where the index of the item can be used.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
||||
/// <param name="mapper">The function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped.</returns>
|
||||
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||
[LinqTunnel]
|
||||
public static IEnumerable<T2> Map<T, T2>([NotNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, int, T2> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
static IEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, T2> mapper)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
int index = 0;
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return mapper(enumerator.Current, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return Generator(self, mapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A map where the mapping function is asynchronous.
|
||||
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map.</param>
|
||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||
[LinqTunnel]
|
||||
public static IAsyncEnumerable<T2> MapAsync<T, T2>([NotNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, int, Task<T2>> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
int index = 0;
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return await mapper(enumerator.Current, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return Generator(self, mapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An asynchronous version of Select.
|
||||
/// </summary>
|
||||
/// <param name="self">The IEnumerable to map</param>
|
||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||
[LinqTunnel]
|
||||
public static IAsyncEnumerable<T2> SelectAsync<T, T2>([NotNull] this IEnumerable<T> self,
|
||||
[NotNull] Func<T, Task<T2>> mapper)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
if (mapper == null)
|
||||
throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
yield return await mapper(enumerator.Current);
|
||||
}
|
||||
|
||||
return Generator(self, mapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an AsyncEnumerable to a List by waiting for every item.
|
||||
/// </summary>
|
||||
/// <param name="self">The async list</param>
|
||||
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
|
||||
/// <returns>A task that will return a simple list</returns>
|
||||
/// <exception cref="ArgumentNullException">The list can't be null</exception>
|
||||
[LinqTunnel]
|
||||
public static Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
|
||||
static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
|
||||
{
|
||||
List<T> ret = new();
|
||||
await foreach (T i in self)
|
||||
ret.Add(i);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ToList(self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the enumerable is empty, execute an action.
|
||||
/// </summary>
|
||||
/// <param name="self">The enumerable to check</param>
|
||||
/// <param name="action">The action to execute is the list is empty</param>
|
||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||
/// <exception cref="ArgumentNullException">The iterable and the action can't be null.</exception>
|
||||
/// <returns>The iterator proxied, there is no dual iterations.</returns>
|
||||
[LinqTunnel]
|
||||
public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action)
|
||||
{
|
||||
if (self == null)
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
action();
|
||||
yield break;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
|
||||
return Generator(self, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (object i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (object i in self)
|
||||
await action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The asynchronous action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list.</typeparam>
|
||||
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
await action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The async list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list.</typeparam>
|
||||
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
await foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split a list in a small chunk of data.
|
||||
/// </summary>
|
||||
/// <param name="list">The list to split</param>
|
||||
/// <param name="countPerList">The number of items in each chunk</param>
|
||||
/// <typeparam name="T">The type of data in the initial list.</typeparam>
|
||||
/// <returns>A list of chunks</returns>
|
||||
[LinqTunnel]
|
||||
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
|
||||
{
|
||||
for (int i = 0; i < list.Count; i += countPerList)
|
||||
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split a list in a small chunk of data.
|
||||
/// </summary>
|
||||
/// <param name="list">The list to split</param>
|
||||
/// <param name="countPerList">The number of items in each chunk</param>
|
||||
/// <typeparam name="T">The type of data in the initial list.</typeparam>
|
||||
/// <returns>A list of chunks</returns>
|
||||
[LinqTunnel]
|
||||
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
|
||||
{
|
||||
T[] ret = new T[countPerList];
|
||||
int i = 0;
|
||||
|
||||
using IEnumerator<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Kyoo.Common/Utility/Merger.cs
Normal file
174
Kyoo.Common/Utility/Merger.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A class containing helper methods to merge objects.
|
||||
/// </summary>
|
||||
public static class Merger
|
||||
{
|
||||
/// <summary>
|
||||
/// Merge two lists, can keep duplicates or remove them.
|
||||
/// </summary>
|
||||
/// <param name="first">The first enumerable to merge</param>
|
||||
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
|
||||
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
|
||||
/// <returns>The two list merged as an array</returns>
|
||||
public static T[] MergeLists<T>(IEnumerable<T> first,
|
||||
IEnumerable<T> second,
|
||||
Func<T, T, bool> isEqual = null)
|
||||
{
|
||||
if (first == null)
|
||||
return second.ToArray();
|
||||
if (second == null)
|
||||
return first.ToArray();
|
||||
if (isEqual == null)
|
||||
return first.Concat(second).ToArray();
|
||||
List<T> list = first.ToList();
|
||||
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <param name="first">The object to assign</param>
|
||||
/// <param name="second">The object containing new values</param>
|
||||
/// <typeparam name="T">Fields of T will be used</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
public static T Assign<T>(T first, T second)
|
||||
{
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <param name="first">The object to complete</param>
|
||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||
/// <param name="where">Filter fields that will be merged</param>
|
||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
/// <exception cref="ArgumentNullException">If first is null</exception>
|
||||
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
|
||||
{
|
||||
if (first == null)
|
||||
throw new ArgumentNullException(nameof(first));
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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<DefaultValueAttribute>()?.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An advanced <see cref="Complete{T}"/> function.
|
||||
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
|
||||
/// Enumerable will be merged (concatenated).
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
|
||||
/// </summary>
|
||||
/// <param name="first">The object to complete</param>
|
||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
||||
/// <returns><see cref="first"/></returns>
|
||||
public static T Merge<T>(T first, T second)
|
||||
{
|
||||
if (first == null)
|
||||
return second;
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> 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<object>(
|
||||
typeof(Utility),
|
||||
nameof(MergeLists),
|
||||
enumerableType,
|
||||
oldValue, newValue, null));
|
||||
}
|
||||
}
|
||||
|
||||
if (first is IOnMerge merge)
|
||||
merge.OnMerge(second);
|
||||
return first;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every fields of <see cref="obj"/> to the default value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to nullify</param>
|
||||
/// <typeparam name="T">Fields of T will be nullified</typeparam>
|
||||
/// <returns><see cref="obj"/></returns>
|
||||
public static T Nullify<T>(T obj)
|
||||
{
|
||||
Type type = typeof(T);
|
||||
foreach (PropertyInfo property in type.GetProperties())
|
||||
{
|
||||
if (!property.CanWrite || property.GetCustomAttribute<ComputedAttribute>() != null)
|
||||
continue;
|
||||
property.SetValue(obj, property.PropertyType.GetClrDefault());
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Kyoo.Common/Utility/TaskUtils.cs
Normal file
69
Kyoo.Common/Utility/TaskUtils.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo
|
||||
{
|
||||
/// <summary>
|
||||
/// A class containing helper method for tasks.
|
||||
/// </summary>
|
||||
public static class TaskUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Run a method after the execution of the task.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to wait.</param>
|
||||
/// <param name="then">
|
||||
/// The method to run after the task finish. This will only be run if the task finished successfully.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item in the task.</typeparam>
|
||||
/// <returns>A continuation task wrapping the initial task and adding a continuation method.</returns>
|
||||
/// <exception cref="TaskCanceledException"></exception>
|
||||
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
|
||||
public static Task<T> Then<T>(this Task<T> task, Action<T> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map the result of a task to another result.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to map.</param>
|
||||
/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
|
||||
/// <typeparam name="T">The type of returns of the given task</typeparam>
|
||||
/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
|
||||
/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
|
||||
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
|
||||
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
|
||||
{
|
||||
return task.ContinueWith(x =>
|
||||
{
|
||||
if (x.IsFaulted)
|
||||
x.Exception!.InnerException!.ReThrow();
|
||||
if (x.IsCanceled)
|
||||
throw new TaskCanceledException();
|
||||
return map(x.Result);
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method to return the a default value from a task if the initial task is null.
|
||||
/// </summary>
|
||||
/// <param name="value">The initial task</param>
|
||||
/// <typeparam name="T">The type that the task will return</typeparam>
|
||||
/// <returns>A non-null task.</returns>
|
||||
[NotNull]
|
||||
public static Task<T> DefaultIfNull<T>([CanBeNull] Task<T> value)
|
||||
{
|
||||
return value ?? Task.FromResult<T>(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
298
Kyoo.Common/Utility/Utility.cs
Normal file
298
Kyoo.Common/Utility/Utility.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of utility functions that can be used everywhere.
|
||||
/// </summary>
|
||||
public static class Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the lambda expression a member (like x => x.Body).
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression that should be checked</param>
|
||||
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression</param>
|
||||
/// <returns>The name of the expression</returns>
|
||||
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a member (property or field)
|
||||
/// </summary>
|
||||
/// <param name="member">The member value</param>
|
||||
/// <param name="obj">The owner of this member</param>
|
||||
/// <returns>The value boxed as an object</returns>
|
||||
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
|
||||
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}).")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <returns>The slug version of the given string</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default value of a type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the default value</param>
|
||||
/// <returns>The default value of the given type.</returns>
|
||||
public static object GetClrDefault(this Type type)
|
||||
{
|
||||
return type.IsValueType
|
||||
? Activator.CreateInstance(type)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||
/// </summary>
|
||||
/// <param name="type">The starting type</param>
|
||||
/// <returns>A list of types</returns>
|
||||
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
|
||||
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
for (; type != null; type = type.BaseType)
|
||||
yield return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
|
||||
{
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
return IsOfGenericType(obj.GetType(), genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||
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<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generic definition of <see cref="genericType"/>.
|
||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
|
||||
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
|
||||
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<Type> 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<T> 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<T>(
|
||||
[NotNull] Type owner,
|
||||
[NotNull] string methodName,
|
||||
[NotNull] Type type,
|
||||
params object[] args)
|
||||
{
|
||||
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
||||
}
|
||||
|
||||
public static T RunGenericMethod<T>(
|
||||
[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<T>(
|
||||
[NotNull] object instance,
|
||||
[NotNull] string methodName,
|
||||
[NotNull] Type type,
|
||||
params object[] args)
|
||||
{
|
||||
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
||||
}
|
||||
|
||||
public static T RunGenericMethod<T>(
|
||||
[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<string, string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a friendly type name (supporting generics)
|
||||
/// For example a list of string will be displayed as List<string> and not as List`1.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to use</param>
|
||||
/// <returns>The friendly name of the type</returns>
|
||||
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}>";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,7 +194,7 @@ namespace Kyoo.CommonApi
|
||||
{
|
||||
try
|
||||
{
|
||||
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where));
|
||||
await _repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
|
||||
@ -61,10 +61,6 @@ namespace Kyoo
|
||||
/// </summary>
|
||||
public DbSet<Provider> Providers { get; set; }
|
||||
/// <summary>
|
||||
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
|
||||
/// </summary>
|
||||
public DbSet<MetadataID> MetadataIds { get; set; }
|
||||
/// <summary>
|
||||
/// The list of registered users.
|
||||
/// </summary>
|
||||
public DbSet<User> Users { get; set; }
|
||||
@ -78,7 +74,26 @@ namespace Kyoo
|
||||
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
|
||||
/// </summary>
|
||||
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of library items (shows and collections that are part of a library - or the global one)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This set is ready only, on most database this will be a view.
|
||||
/// </remarks>
|
||||
public DbSet<LibraryItem> LibraryItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The metadata of this type will be returned.</typeparam>
|
||||
/// <returns>A queryable of metadata ids for a type.</returns>
|
||||
public DbSet<MetadataID<T>> MetadataIds<T>()
|
||||
where T : class, IResource
|
||||
{
|
||||
return Set<MetadataID<T>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a generic link between two resource types.
|
||||
/// </summary>
|
||||
@ -125,13 +140,27 @@ namespace Kyoo
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Track>()
|
||||
.Property(t => t.IsDefault)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
modelBuilder.Entity<Track>()
|
||||
.Property(t => t.IsForced)
|
||||
.ValueGeneratedNever();
|
||||
modelBuilder.Entity<Show>()
|
||||
.HasMany(x => x.Seasons)
|
||||
.WithOne(x => x.Show)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Show>()
|
||||
.HasMany(x => x.Episodes)
|
||||
.WithOne(x => x.Show)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Season>()
|
||||
.HasMany(x => x.Episodes)
|
||||
.WithOne(x => x.Season)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Episode>()
|
||||
.HasMany(x => x.Tracks)
|
||||
.WithOne(x => x.Episode)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Show>()
|
||||
.HasOne(x => x.Studio)
|
||||
.WithMany(x => x.Shows)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<Provider>()
|
||||
.HasMany(x => x.Libraries)
|
||||
@ -205,25 +234,41 @@ namespace Kyoo
|
||||
.WithMany(x => x.ShowLinks),
|
||||
y => y.HasKey(Link<User, Show>.PrimaryKey));
|
||||
|
||||
modelBuilder.Entity<MetadataID>()
|
||||
.HasOne(x => x.Show)
|
||||
modelBuilder.Entity<MetadataID<Show>>()
|
||||
.HasKey(MetadataID<Show>.PrimaryKey);
|
||||
modelBuilder.Entity<MetadataID<Show>>()
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ExternalIDs)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID>()
|
||||
.HasOne(x => x.Season)
|
||||
modelBuilder.Entity<MetadataID<Season>>()
|
||||
.HasKey(MetadataID<Season>.PrimaryKey);
|
||||
modelBuilder.Entity<MetadataID<Season>>()
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ExternalIDs)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID>()
|
||||
.HasOne(x => x.Episode)
|
||||
modelBuilder.Entity<MetadataID<Episode>>()
|
||||
.HasKey(MetadataID<Episode>.PrimaryKey);
|
||||
modelBuilder.Entity<MetadataID<Episode>>()
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ExternalIDs)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID>()
|
||||
.HasOne(x => x.People)
|
||||
modelBuilder.Entity<MetadataID<People>>()
|
||||
.HasKey(MetadataID<People>.PrimaryKey);
|
||||
modelBuilder.Entity<MetadataID<People>>()
|
||||
.HasOne(x => x.First)
|
||||
.WithMany(x => x.ExternalIDs)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID>()
|
||||
.HasOne(x => x.Provider)
|
||||
.WithMany(x => x.MetadataLinks)
|
||||
|
||||
|
||||
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID<Season>>().HasOne(x => x.Second).WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID<Episode>>().HasOne(x => x.Second).WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID<People>>().HasOne(x => x.Second).WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<WatchedEpisode>()
|
||||
@ -237,7 +282,7 @@ namespace Kyoo
|
||||
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
|
||||
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
|
||||
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
|
||||
|
||||
|
||||
modelBuilder.Entity<Collection>()
|
||||
.HasIndex(x => x.Slug)
|
||||
.IsUnique();
|
||||
@ -262,15 +307,34 @@ namespace Kyoo
|
||||
modelBuilder.Entity<Season>()
|
||||
.HasIndex(x => new {x.ShowID, x.SeasonNumber})
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<Season>()
|
||||
.HasIndex(x => x.Slug)
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<Episode>()
|
||||
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<Episode>()
|
||||
.HasIndex(x => x.Slug)
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<Track>()
|
||||
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<Track>()
|
||||
.HasIndex(x => x.Slug)
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<User>()
|
||||
.HasIndex(x => x.Slug)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Season>()
|
||||
.Property(x => x.Slug)
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
modelBuilder.Entity<Episode>()
|
||||
.Property(x => x.Slug)
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
modelBuilder.Entity<Track>()
|
||||
.Property(x => x.Slug)
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -441,52 +505,6 @@ namespace Kyoo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save items or retry with a custom method if a duplicate is found.
|
||||
/// </summary>
|
||||
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
|
||||
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
|
||||
/// The second parameter is the current retry number.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <typeparam name="T">The type of the item to save</typeparam>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
|
||||
{
|
||||
return SaveOrRetry(obj, onFail, 0, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save items or retry with a custom method if a duplicate is found.
|
||||
/// </summary>
|
||||
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
|
||||
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
|
||||
/// The second parameter is the current retry number.</param>
|
||||
/// <param name="recurse">The current retry number.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
|
||||
/// <typeparam name="T">The type of the item to save</typeparam>
|
||||
/// <returns>The number of state entries written to the database.</returns>
|
||||
private async Task<T> SaveOrRetry<T>(T obj,
|
||||
Func<T, int, T> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the exception is a duplicated exception.
|
||||
/// </summary>
|
||||
|
||||
@ -12,8 +12,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -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<LoadableRelationAttribute>() == null);
|
||||
old = Merger.Nullify(old);
|
||||
Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
|
||||
await EditRelations(old, edited, resetOld);
|
||||
await Database.SaveChangesAsync();
|
||||
return old;
|
||||
@ -257,6 +257,8 @@ namespace Kyoo.Controllers
|
||||
/// <exception cref="ArgumentException">You can throw this if the resource is illegal and should not be saved.</exception>
|
||||
protected virtual Task Validate(T resource)
|
||||
{
|
||||
if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != 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);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task DeleteRange(IEnumerable<T> objs)
|
||||
public async Task DeleteAll(Expression<Func<T, bool>> where)
|
||||
{
|
||||
foreach (T obj in objs)
|
||||
await Delete(obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task DeleteRange(IEnumerable<int> ids)
|
||||
{
|
||||
foreach (int id in ids)
|
||||
await Delete(id);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task DeleteRange(IEnumerable<string> slugs)
|
||||
{
|
||||
foreach (string slug in slugs)
|
||||
await Delete(slug);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteRange(Expression<Func<T, bool>> where)
|
||||
{
|
||||
ICollection<T> resources = await GetAll(where);
|
||||
await DeleteRange(resources);
|
||||
foreach (T resource in await GetAll(where))
|
||||
await Delete(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
@ -9,33 +8,34 @@
|
||||
<LangVersion>default</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<GenerateDependencyFile>false</GenerateDependencyFile>
|
||||
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<!-- <PropertyGroup>-->
|
||||
<!-- <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>-->
|
||||
<!-- <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>-->
|
||||
<!-- <ProduceReferenceAssembly>false</ProduceReferenceAssembly>-->
|
||||
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
|
||||
<!-- <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>-->
|
||||
<!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
|
||||
<!-- </PropertyGroup>-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5">
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.5.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||
<!-- <Private>false</Private>-->
|
||||
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||
<!-- <Private>false</Private>-->
|
||||
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
1197
Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
generated
Normal file
1197
Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
782
Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
Normal file
782
Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
Normal file
@ -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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true),
|
||||
poster = table.Column<string>(type: "text", nullable: true),
|
||||
overview = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_collections", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "genres",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_genres", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "libraries",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true),
|
||||
paths = table.Column<string[]>(type: "text[]", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_libraries", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "people",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true),
|
||||
poster = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_people", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "providers",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true),
|
||||
logo = table.Column<string>(type: "text", nullable: true),
|
||||
logo_extension = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_providers", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "studios",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_studios", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
username = table.Column<string>(type: "text", nullable: true),
|
||||
email = table.Column<string>(type: "text", nullable: true),
|
||||
password = table.Column<string>(type: "text", nullable: true),
|
||||
permissions = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_users", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "link_library_collection",
|
||||
columns: table => new
|
||||
{
|
||||
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
data_id = table.Column<string>(type: "text", nullable: true),
|
||||
link = table.Column<string>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
title = table.Column<string>(type: "text", nullable: true),
|
||||
aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
path = table.Column<string>(type: "text", nullable: true),
|
||||
overview = table.Column<string>(type: "text", nullable: true),
|
||||
status = table.Column<Status>(type: "status", nullable: true),
|
||||
trailer_url = table.Column<string>(type: "text", nullable: true),
|
||||
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
poster = table.Column<string>(type: "text", nullable: true),
|
||||
logo = table.Column<string>(type: "text", nullable: true),
|
||||
backdrop = table.Column<string>(type: "text", nullable: true),
|
||||
is_movie = table.Column<bool>(type: "boolean", nullable: false),
|
||||
studio_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
data_id = table.Column<string>(type: "text", nullable: true),
|
||||
link = table.Column<string>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
for_people = table.Column<bool>(type: "boolean", nullable: false),
|
||||
people_id = table.Column<int>(type: "integer", nullable: false),
|
||||
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||
type = table.Column<string>(type: "text", nullable: true),
|
||||
role = table.Column<string>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: true),
|
||||
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||
season_number = table.Column<int>(type: "integer", nullable: false),
|
||||
title = table.Column<string>(type: "text", nullable: true),
|
||||
overview = table.Column<string>(type: "text", nullable: true),
|
||||
start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
poster = table.Column<string>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: true),
|
||||
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||
season_id = table.Column<int>(type: "integer", nullable: true),
|
||||
season_number = table.Column<int>(type: "integer", nullable: true),
|
||||
episode_number = table.Column<int>(type: "integer", nullable: true),
|
||||
absolute_number = table.Column<int>(type: "integer", nullable: true),
|
||||
path = table.Column<string>(type: "text", nullable: true),
|
||||
thumb = table.Column<string>(type: "text", nullable: true),
|
||||
title = table.Column<string>(type: "text", nullable: true),
|
||||
overview = table.Column<string>(type: "text", nullable: true),
|
||||
release_date = table.Column<DateTime>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
data_id = table.Column<string>(type: "text", nullable: true),
|
||||
link = table.Column<string>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
data_id = table.Column<string>(type: "text", nullable: true),
|
||||
link = table.Column<string>(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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
slug = table.Column<string>(type: "text", nullable: true),
|
||||
title = table.Column<string>(type: "text", nullable: true),
|
||||
language = table.Column<string>(type: "text", nullable: true),
|
||||
codec = table.Column<string>(type: "text", nullable: true),
|
||||
is_default = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_forced = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_external = table.Column<bool>(type: "boolean", nullable: false),
|
||||
path = table.Column<string>(type: "text", nullable: true),
|
||||
type = table.Column<StreamType>(type: "stream_type", nullable: false),
|
||||
episode_id = table.Column<int>(type: "integer", nullable: false),
|
||||
track_index = table.Column<int>(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<int>(type: "integer", nullable: false),
|
||||
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||
watched_percentage = table.Column<int>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1197
Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
generated
Normal file
1197
Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
186
Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
Normal file
186
Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
Normal file
@ -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;");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -26,16 +26,19 @@ namespace Kyoo.Postgresql
|
||||
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
|
||||
/// </summary>
|
||||
private readonly bool _skipConfigure;
|
||||
|
||||
/// <summary>
|
||||
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||
/// </summary>
|
||||
public PostgresContext()
|
||||
|
||||
|
||||
static PostgresContext()
|
||||
{
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||
/// </summary>
|
||||
public PostgresContext() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="PostgresContext"/> using specific options
|
||||
@ -44,9 +47,6 @@ namespace Kyoo.Postgresql
|
||||
public PostgresContext(DbContextOptions options)
|
||||
: base(options)
|
||||
{
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
|
||||
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
|
||||
_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<ItemType>();
|
||||
modelBuilder.HasPostgresEnum<StreamType>();
|
||||
|
||||
modelBuilder.Entity<LibraryItem>()
|
||||
.ToView("library_items")
|
||||
.HasKey(x => x.ID);
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.ExtraData)
|
||||
.HasColumnType("jsonb");
|
||||
@ -107,7 +112,7 @@ namespace Kyoo.Postgresql
|
||||
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
|
||||
{
|
||||
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(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<Func<T, bool>>(call, query.Parameters);
|
||||
}
|
||||
|
||||
41
Kyoo.SqLite/Kyoo.SqLite.csproj
Normal file
41
Kyoo.SqLite/Kyoo.SqLite.csproj
Normal file
@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<Company>SDG</Company>
|
||||
<Authors>Zoe Roux</Authors>
|
||||
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
|
||||
<LangVersion>default</LangVersion>
|
||||
<RootNamespace>Kyoo.SqLite</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- <PropertyGroup>-->
|
||||
<!-- <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/sqlite</OutputPath>-->
|
||||
<!-- <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>-->
|
||||
<!-- <ProduceReferenceAssembly>false</ProduceReferenceAssembly>-->
|
||||
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
|
||||
<!-- <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>-->
|
||||
<!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
|
||||
<!-- </PropertyGroup>-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
|
||||
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||
<!-- <Private>false</Private>-->
|
||||
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
|
||||
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||
<!-- <Private>false</Private>-->
|
||||
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,50 +1,41 @@
|
||||
// <auto-generated />
|
||||
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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -58,46 +49,49 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AbsoluteNumber")
|
||||
.HasColumnType("integer");
|
||||
b.Property<int?>("AbsoluteNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EpisodeNumber")
|
||||
.HasColumnType("integer");
|
||||
b.Property<int?>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Runtime")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SeasonID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("integer");
|
||||
b.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Thumb")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -130,18 +123,17 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string[]>("Paths")
|
||||
.HasColumnType("text[]");
|
||||
b.Property<string>("Paths")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -154,10 +146,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -169,10 +161,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -184,10 +176,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -199,10 +191,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -214,10 +206,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -229,10 +221,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
@ -241,65 +233,105 @@ namespace Kyoo.Postgresql.Migrations
|
||||
b.ToTable("Link<User, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID", b =>
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("EpisodeID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("PeopleID")
|
||||
.HasColumnType("integer");
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.Property<int>("ProviderID")
|
||||
.HasColumnType("integer");
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.Property<int?>("SeasonID")
|
||||
.HasColumnType("integer");
|
||||
b.ToTable("MetadataID<Episode>");
|
||||
});
|
||||
|
||||
b.Property<int?>("ShowID")
|
||||
.HasColumnType("integer");
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ID");
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasIndex("EpisodeID");
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("PeopleID");
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("ProviderID");
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SeasonID");
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.HasIndex("ShowID");
|
||||
b.ToTable("MetadataID<People>");
|
||||
});
|
||||
|
||||
b.ToTable("MetadataIds");
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Season>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -313,20 +345,22 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ForPeople")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PeopleID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -341,21 +375,20 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LogoExtension")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -369,29 +402,38 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string[]>("Aliases")
|
||||
.HasColumnType("text[]");
|
||||
b.Property<string>("Aliases")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Backdrop")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("EndYear")
|
||||
.HasColumnType("integer");
|
||||
b.Property<DateTime?>("EndAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsMovie")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("StartYear")
|
||||
.HasColumnType("integer");
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Status?>("Status")
|
||||
.HasColumnType("status");
|
||||
b.Property<int?>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TrailerUrl")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -462,15 +503,14 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -484,41 +524,47 @@ namespace Kyoo.Postgresql.Migrations
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Codec")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EpisodeID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsForced")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TrackIndex")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<StreamType>("Type")
|
||||
.HasColumnType("stream_type");
|
||||
b.Property<int>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Dictionary<string, string>>("ExtraData")
|
||||
.HasColumnType("jsonb");
|
||||
b.Property<string>("ExtraData")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string[]>("Permissions")
|
||||
.HasColumnType("text[]");
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
@ -562,13 +607,13 @@ namespace Kyoo.Postgresql.Migrations
|
||||
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("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<Kyoo.Models.Episode>", 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<Kyoo.Models.People>", 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<Kyoo.Models.Season>", 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<Kyoo.Models.Show>", 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 =>
|
||||
@ -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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Poster = table.Column<string>(type: "text", nullable: true),
|
||||
Overview = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Poster = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -35,10 +27,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Genres",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -49,11 +41,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Libraries",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Paths = table.Column<string[]>(type: "text[]", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Paths = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -64,11 +56,11 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "People",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Poster = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Poster = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -79,12 +71,12 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Providers",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Logo = table.Column<string>(type: "text", nullable: true),
|
||||
LogoExtension = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Logo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
LogoExtension = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -95,10 +87,10 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Studios",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -109,14 +101,14 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Username = table.Column<string>(type: "text", nullable: true),
|
||||
Email = table.Column<string>(type: "text", nullable: true),
|
||||
Password = table.Column<string>(type: "text", nullable: true),
|
||||
Permissions = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
ExtraData = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Password = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Permissions = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExtraData = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -127,8 +119,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Link<Library, Collection>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -151,8 +143,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Link<Library, Provider>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -171,26 +163,52 @@ namespace Kyoo.Postgresql.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<People>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataID = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Link = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MetadataID<People>", x => new { x.FirstID, x.SecondID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<People>_People_FirstID",
|
||||
column: x => x.FirstID,
|
||||
principalTable: "People",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<People>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Shows",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
Path = table.Column<string>(type: "text", nullable: true),
|
||||
Overview = table.Column<string>(type: "text", nullable: true),
|
||||
Status = table.Column<Status>(type: "status", nullable: true),
|
||||
TrailerUrl = table.Column<string>(type: "text", nullable: true),
|
||||
StartYear = table.Column<int>(type: "integer", nullable: true),
|
||||
EndYear = table.Column<int>(type: "integer", nullable: true),
|
||||
Poster = table.Column<string>(type: "text", nullable: true),
|
||||
Logo = table.Column<string>(type: "text", nullable: true),
|
||||
Backdrop = table.Column<string>(type: "text", nullable: true),
|
||||
IsMovie = table.Column<bool>(type: "boolean", nullable: false),
|
||||
StudioID = table.Column<int>(type: "integer", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Aliases = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Poster = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Logo = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Backdrop = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
StudioID = table.Column<int>(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<Collection, Show>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -231,8 +249,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Link<Library, Show>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -255,8 +273,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Link<Show, Genre>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -279,8 +297,8 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Link<User, Show>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -299,16 +317,43 @@ namespace Kyoo.Postgresql.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<Show>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataID = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Link = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MetadataID<Show>", x => new { x.FirstID, x.SecondID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Show>_Providers_SecondID",
|
||||
column: x => x.SecondID,
|
||||
principalTable: "Providers",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Show>_Shows_FirstID",
|
||||
column: x => x.FirstID,
|
||||
principalTable: "Shows",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PeopleRoles",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
PeopleID = table.Column<int>(type: "integer", nullable: false),
|
||||
ShowID = table.Column<int>(type: "integer", nullable: false),
|
||||
Role = table.Column<string>(type: "text", nullable: true),
|
||||
Type = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ForPeople = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
PeopleID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ShowID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -331,14 +376,16 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Seasons",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ShowID = table.Column<int>(type: "integer", nullable: false),
|
||||
SeasonNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Overview = table.Column<string>(type: "text", nullable: true),
|
||||
Year = table.Column<int>(type: "integer", nullable: true),
|
||||
Poster = table.Column<string>(type: "text", nullable: true)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ShowID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeasonNumber = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartDate = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
EndDate = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Poster = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -355,19 +402,19 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "Episodes",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ShowID = table.Column<int>(type: "integer", nullable: false),
|
||||
SeasonID = table.Column<int>(type: "integer", nullable: true),
|
||||
SeasonNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
EpisodeNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
AbsoluteNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
Path = table.Column<string>(type: "text", nullable: true),
|
||||
Thumb = table.Column<string>(type: "text", nullable: true),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Overview = table.Column<string>(type: "text", nullable: true),
|
||||
ReleaseDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||
Runtime = table.Column<int>(type: "integer", nullable: false)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ShowID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeasonID = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
SeasonNumber = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AbsoluteNumber = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Thumb = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Overview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ReleaseDate = table.Column<DateTime>(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<Season>",
|
||||
columns: table => new
|
||||
{
|
||||
ID = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ProviderID = table.Column<int>(type: "integer", nullable: false),
|
||||
ShowID = table.Column<int>(type: "integer", nullable: true),
|
||||
EpisodeID = table.Column<int>(type: "integer", nullable: true),
|
||||
SeasonID = table.Column<int>(type: "integer", nullable: true),
|
||||
PeopleID = table.Column<int>(type: "integer", nullable: true),
|
||||
DataID = table.Column<string>(type: "text", nullable: true),
|
||||
Link = table.Column<string>(type: "text", nullable: true)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataID = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Link = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MetadataIds", x => x.ID);
|
||||
table.PrimaryKey("PK_MetadataID<Season>", 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<Season>_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<Season>_Seasons_FirstID",
|
||||
column: x => x.FirstID,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataID<Episode>",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DataID = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Link = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MetadataID<Episode>", x => new { x.FirstID, x.SecondID });
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataIds_Shows_ShowID",
|
||||
column: x => x.ShowID,
|
||||
principalTable: "Shows",
|
||||
name: "FK_MetadataID<Episode>_Episodes_FirstID",
|
||||
column: x => x.FirstID,
|
||||
principalTable: "Episodes",
|
||||
principalColumn: "ID",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MetadataID<Episode>_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<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
EpisodeID = table.Column<int>(type: "integer", nullable: false),
|
||||
TrackIndex = table.Column<int>(type: "integer", nullable: false),
|
||||
IsDefault = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsForced = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsExternal = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
Language = table.Column<string>(type: "text", nullable: true),
|
||||
Codec = table.Column<string>(type: "text", nullable: true),
|
||||
Path = table.Column<string>(type: "text", nullable: true),
|
||||
Type = table.Column<StreamType>(type: "stream_type", nullable: false)
|
||||
ID = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Language = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Codec = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IsForced = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IsExternal = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Path = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
EpisodeID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TrackIndex = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -467,9 +518,9 @@ namespace Kyoo.Postgresql.Migrations
|
||||
name: "WatchedEpisodes",
|
||||
columns: table => new
|
||||
{
|
||||
FirstID = table.Column<int>(type: "integer", nullable: false),
|
||||
SecondID = table.Column<int>(type: "integer", nullable: false),
|
||||
WatchedPercentage = table.Column<int>(type: "integer", nullable: false)
|
||||
FirstID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SecondID = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
WatchedPercentage = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -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<Episode>_SecondID",
|
||||
table: "MetadataID<Episode>",
|
||||
column: "SecondID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MetadataIds_PeopleID",
|
||||
table: "MetadataIds",
|
||||
column: "PeopleID");
|
||||
name: "IX_MetadataID<People>_SecondID",
|
||||
table: "MetadataID<People>",
|
||||
column: "SecondID");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MetadataIds_ProviderID",
|
||||
table: "MetadataIds",
|
||||
column: "ProviderID");
|
||||
name: "IX_MetadataID<Season>_SecondID",
|
||||
table: "MetadataID<Season>",
|
||||
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<Show>_SecondID",
|
||||
table: "MetadataID<Show>",
|
||||
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<User, Show>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataIds");
|
||||
name: "MetadataID<Episode>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<People>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<Season>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataID<Show>");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PeopleRoles");
|
||||
980
Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
generated
Normal file
980
Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
generated
Normal file
@ -0,0 +1,980 @@
|
||||
// <auto-generated />
|
||||
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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Collections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("AbsoluteNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SeasonID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Thumb")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Paths")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Collection, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Collection>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Provider>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Show, Genre>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<User, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Episode>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<People>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Season>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ForPeople")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PeopleID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("PeopleID");
|
||||
|
||||
b.HasIndex("ShowID");
|
||||
|
||||
b.ToTable("PeopleRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LogoExtension")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Providers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Aliases")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Backdrop")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsMovie")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TrailerUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("StudioID");
|
||||
|
||||
b.ToTable("Shows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Studios");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Codec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EpisodeID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsForced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TrackIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExtraData")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("WatchedPercentage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("WatchedEpisodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Kyoo.Models.Season", "Season")
|
||||
.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<Kyoo.Models.Collection, Kyoo.Models.Show>", 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<Kyoo.Models.Library, Kyoo.Models.Collection>", 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<Kyoo.Models.Library, Kyoo.Models.Provider>", 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<Kyoo.Models.Library, Kyoo.Models.Show>", 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<Kyoo.Models.Show, Kyoo.Models.Genre>", 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<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.HasOne("Kyoo.Models.User", "First")
|
||||
.WithMany("ShowLinks")
|
||||
.HasForeignKey("FirstID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Kyoo.Models.Show", "Second")
|
||||
.WithMany()
|
||||
.HasForeignKey("SecondID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("First");
|
||||
|
||||
b.Navigation("Second");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", 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<Kyoo.Models.People>", 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<Kyoo.Models.Season>", 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<Kyoo.Models.Show>", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
187
Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
Normal file
187
Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
Normal file
@ -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<Collection, Show>' 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;");
|
||||
}
|
||||
}
|
||||
}
|
||||
978
Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
Normal file
978
Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
Normal file
@ -0,0 +1,978 @@
|
||||
// <auto-generated />
|
||||
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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Collections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("AbsoluteNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SeasonID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Thumb")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Paths")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Libraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Collection, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Collection>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Provider>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Library, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<Show, Genre>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("Link<User, Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Episode>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<People>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Season>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DataID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("MetadataID<Show>");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ForPeople")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PeopleID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("PeopleID");
|
||||
|
||||
b.HasIndex("ShowID");
|
||||
|
||||
b.ToTable("PeopleRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LogoExtension")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Providers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ShowID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Aliases")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Backdrop")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsMovie")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("StartAir")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StudioID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TrailerUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("StudioID");
|
||||
|
||||
b.ToTable("Shows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Studios");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Codec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EpisodeID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsForced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TrackIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("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<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExtraData")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||
{
|
||||
b.Property<int>("FirstID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecondID")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("WatchedPercentage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FirstID", "SecondID");
|
||||
|
||||
b.HasIndex("SecondID");
|
||||
|
||||
b.ToTable("WatchedEpisodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Kyoo.Models.Season", "Season")
|
||||
.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<Kyoo.Models.Collection, Kyoo.Models.Show>", 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<Kyoo.Models.Library, Kyoo.Models.Collection>", 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<Kyoo.Models.Library, Kyoo.Models.Provider>", 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<Kyoo.Models.Library, Kyoo.Models.Show>", 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<Kyoo.Models.Show, Kyoo.Models.Genre>", 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<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||
{
|
||||
b.HasOne("Kyoo.Models.User", "First")
|
||||
.WithMany("ShowLinks")
|
||||
.HasForeignKey("FirstID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Kyoo.Models.Show", "Second")
|
||||
.WithMany()
|
||||
.HasForeignKey("SecondID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("First");
|
||||
|
||||
b.Navigation("Second");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", 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<Kyoo.Models.People>", 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<Kyoo.Models.Season>", 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<Kyoo.Models.Show>", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Kyoo.SqLite/SqLiteContext.cs
Normal file
134
Kyoo.SqLite/SqLiteContext.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A sqlite implementation of <see cref="DatabaseContext"/>.
|
||||
/// </summary>
|
||||
public class SqLiteContext : DatabaseContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection string to use.
|
||||
/// </summary>
|
||||
private readonly string _connection;
|
||||
|
||||
/// <summary>
|
||||
/// Is this instance in debug mode?
|
||||
/// </summary>
|
||||
private readonly bool _debugMode;
|
||||
|
||||
/// <summary>
|
||||
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
|
||||
/// </summary>
|
||||
private readonly bool _skipConfigure;
|
||||
|
||||
/// <summary>
|
||||
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||
/// </summary>
|
||||
public SqLiteContext()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SqLiteContext"/> using specific options
|
||||
/// </summary>
|
||||
/// <param name="options">The options to use.</param>
|
||||
public SqLiteContext(DbContextOptions options)
|
||||
: base(options)
|
||||
{
|
||||
_skipConfigure = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection string to use</param>
|
||||
/// <param name="debugMode">Is this instance in debug mode?</param>
|
||||
public SqLiteContext(string connection, bool debugMode)
|
||||
{
|
||||
_connection = connection;
|
||||
_debugMode = debugMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set connection information for this database context
|
||||
/// </summary>
|
||||
/// <param name="optionsBuilder">An option builder to fill.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set database parameters to support every types of Kyoo.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The database's model builder.</param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
ValueConverter<string[], string> arrayConvertor = new(
|
||||
x => string.Join(";", x),
|
||||
x => x.Split(';', StringSplitOptions.None));
|
||||
modelBuilder.Entity<Library>()
|
||||
.Property(x => x.Paths)
|
||||
.HasConversion(arrayConvertor);
|
||||
modelBuilder.Entity<Show>()
|
||||
.Property(x => x.Aliases)
|
||||
.HasConversion(arrayConvertor);
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.Permissions)
|
||||
.HasConversion(arrayConvertor);
|
||||
|
||||
modelBuilder.Entity<Show>()
|
||||
.Property(x => x.Status)
|
||||
.HasConversion<int>();
|
||||
modelBuilder.Entity<Track>()
|
||||
.Property(x => x.Type)
|
||||
.HasConversion<int>();
|
||||
|
||||
ValueConverter<Dictionary<string, string>, string> jsonConvertor = new(
|
||||
x => JsonConvert.SerializeObject(x),
|
||||
x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x));
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.ExtraData)
|
||||
.HasConversion(jsonConvertor);
|
||||
|
||||
modelBuilder.Entity<LibraryItem>()
|
||||
.ToView("LibraryItems")
|
||||
.HasKey(x => x.ID);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsDuplicateException(Exception ex)
|
||||
{
|
||||
return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/}
|
||||
or SqliteException { SqliteExtendedErrorCode: 1555 /*SQLITE_CONSTRAINT_PRIMARYKEY*/};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
|
||||
{
|
||||
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(EF.Functions.Like);
|
||||
MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format));
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Kyoo.SqLite/SqLiteModule.cs
Normal file
79
Kyoo.SqLite/SqLiteModule.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A module to add sqlite capacity to the app.
|
||||
/// </summary>
|
||||
public class SqLiteModule : IPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Slug => "sqlite";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "SqLite";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "A database context for sqlite.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<Type> Provides => new[]
|
||||
{
|
||||
typeof(DatabaseContext)
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The configuration to use. The database connection string is pulled from it.
|
||||
/// </summary>
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The host environment to check if the app is in debug mode.
|
||||
/// </summary>
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new postgres module instance and use the given configuration and environment.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to use</param>
|
||||
/// <param name="env">The environment that will be used (if the env is in development mode, more information will be displayed on errors.</param>
|
||||
public SqLiteModule(IConfiguration configuration, IWebHostEnvironment env)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_environment = env;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||
{
|
||||
services.AddDbContext<DatabaseContext, SqLiteContext>(x =>
|
||||
{
|
||||
x.UseSqlite(_configuration.GetDatabaseConnection("sqlite"));
|
||||
if (_environment.IsDevelopment())
|
||||
x.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize(IServiceProvider provider)
|
||||
{
|
||||
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Kyoo.Tests/KAssert.cs
Normal file
45
Kyoo.Tests/KAssert.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom assertions used by Kyoo's tests.
|
||||
/// </summary>
|
||||
public static class KAssert
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if every property of the item is equal to the other's object.
|
||||
/// </summary>
|
||||
/// <param name="expected">The value to check against</param>
|
||||
/// <param name="value">The value to check</param>
|
||||
/// <typeparam name="T">The type to check</typeparam>
|
||||
[AssertionMethod]
|
||||
public static void DeepEqual<T>(T expected, T value)
|
||||
{
|
||||
foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance))
|
||||
Assert.Equal(property.GetValue(expected), property.GetValue(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly fail a test.
|
||||
/// </summary>
|
||||
[AssertionMethod]
|
||||
public static void Fail()
|
||||
{
|
||||
throw new XunitException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly fail a test.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that will be seen in the test report</param>
|
||||
[AssertionMethod]
|
||||
public static void Fail(string message)
|
||||
{
|
||||
throw new XunitException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,9 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Divergic.Logging.Xunit" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
67
Kyoo.Tests/Library/RepositoryActivator.cs
Normal file
67
Kyoo.Tests/Library/RepositoryActivator.cs
Normal file
@ -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<IShowRepository>(() => LibraryManager.ShowRepository));
|
||||
ShowRepository show = new(_database, studio, people, genre, provider);
|
||||
SeasonRepository season = new(_database, provider);
|
||||
LibraryItemRepository libraryItem = new(_database,
|
||||
new Lazy<ILibraryRepository>(() => 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
191
Kyoo.Tests/Library/RepositoryTests.cs
Normal file
191
Kyoo.Tests/Library/RepositoryTests.cs
Normal file
@ -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<T> : IDisposable, IAsyncDisposable
|
||||
where T : class, IResource, new()
|
||||
{
|
||||
protected readonly RepositoryActivator Repositories;
|
||||
private readonly IRepository<T> _repository;
|
||||
|
||||
protected RepositoryTests(RepositoryActivator repositories)
|
||||
{
|
||||
Repositories = repositories;
|
||||
_repository = Repositories.LibraryManager.GetRepository<T>();
|
||||
}
|
||||
|
||||
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<T>().ID);
|
||||
KAssert.DeepEqual(TestSample.Get<T>(), value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBySlugTest()
|
||||
{
|
||||
T value = await _repository.Get(TestSample.Get<T>().Slug);
|
||||
KAssert.DeepEqual(TestSample.Get<T>(), value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByFakeIdTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByFakeSlugTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get("non-existent"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteByIdTest()
|
||||
{
|
||||
await _repository.Delete(TestSample.Get<T>().ID);
|
||||
Assert.Equal(0, await _repository.GetCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteBySlugTest()
|
||||
{
|
||||
await _repository.Delete(TestSample.Get<T>().Slug);
|
||||
Assert.Equal(0, await _repository.GetCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteByValueTest()
|
||||
{
|
||||
await _repository.Delete(TestSample.Get<T>());
|
||||
Assert.Equal(0, await _repository.GetCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<DuplicatedItemException>(() => _repository.Create(TestSample.Get<T>()));
|
||||
await _repository.Delete(TestSample.Get<T>());
|
||||
|
||||
T expected = TestSample.Get<T>();
|
||||
expected.ID = 0;
|
||||
await _repository.Create(expected);
|
||||
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateNullTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Create(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateIfNotExistNullTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.CreateIfNotExists(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateIfNotExistTest()
|
||||
{
|
||||
T expected = TestSample.Get<T>();
|
||||
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
|
||||
await _repository.Delete(TestSample.Get<T>());
|
||||
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditNullTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditNonExistingTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T {ID = 56}, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExpressionIDTest()
|
||||
{
|
||||
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.ID == TestSample.Get<T>().ID));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExpressionSlugTest()
|
||||
{
|
||||
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.Slug == TestSample.Get<T>().Slug));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExpressionNotFoundTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExpressionNullTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Get((Expression<Func<T, bool>>)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<T>().Slug[2..4];
|
||||
Assert.Equal(1, await _repository.GetCount(x => x.Slug.Contains(slug)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllTest()
|
||||
{
|
||||
string slug = TestSample.Get<T>().Slug[2..4];
|
||||
ICollection<T> ret = await _repository.GetAll(x => x.Slug.Contains(slug));
|
||||
Assert.Equal(1, ret.Count);
|
||||
KAssert.DeepEqual(TestSample.Get<T>(), ret.First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAllTest()
|
||||
{
|
||||
string slug = TestSample.Get<T>().Slug[2..4];
|
||||
await _repository.DeleteAll(x => x.Slug.Contains(slug));
|
||||
Assert.Equal(0, await _repository.GetCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
// }
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
Normal file
@ -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<Collection>
|
||||
{
|
||||
private readonly ICollectionRepository _repository;
|
||||
|
||||
protected ACollectionTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.CollectionRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
192
Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
Normal file
192
Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
Normal file
@ -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<Episode>
|
||||
{
|
||||
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<Show>().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<Show>().Slug}-s1e1", episode.Slug);
|
||||
episode = await _repository.Edit(new Episode
|
||||
{
|
||||
ID = 1,
|
||||
SeasonNumber = 2
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
|
||||
episode = await _repository.Get(1);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeNumberEditTest()
|
||||
{
|
||||
Episode episode = await _repository.Get(1);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
|
||||
episode = await _repository.Edit(new Episode
|
||||
{
|
||||
ID = 1,
|
||||
EpisodeNumber = 2
|
||||
}, false);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||
episode = await _repository.Get(1);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeCreationSlugTest()
|
||||
{
|
||||
Episode episode = await _repository.Create(new Episode
|
||||
{
|
||||
ShowID = TestSample.Get<Show>().ID,
|
||||
SeasonNumber = 2,
|
||||
EpisodeNumber = 4
|
||||
});
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
|
||||
}
|
||||
|
||||
|
||||
// TODO absolute numbering tests
|
||||
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteSlugTest()
|
||||
{
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
|
||||
TestSample.GetAbsoluteEpisode().Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeCreationAbsoluteSlugTest()
|
||||
{
|
||||
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
|
||||
Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-56", episode.Slug);
|
||||
episode = await _repository.Get(2);
|
||||
Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-s1e2", episode.Slug);
|
||||
episode = await _repository.Get(2);
|
||||
Assert.Equal($"{TestSample.Get<Show>().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<Show>().Slug}-12", episode.Slug);
|
||||
episode = await _repository.Get(1);
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MovieEpisodeTest()
|
||||
{
|
||||
Episode episode = await _repository.Create(TestSample.GetMovieEpisode());
|
||||
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
|
||||
episode = await _repository.Get(3);
|
||||
Assert.Equal(TestSample.Get<Show>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/GenreTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/GenreTests.cs
Normal file
@ -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<Genre>
|
||||
{
|
||||
private readonly IGenreRepository _repository;
|
||||
|
||||
protected AGenreTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.GenreRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
Normal file
89
Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
Normal file
@ -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<Show>());
|
||||
LibraryItem actual = await _repository.Get(1);
|
||||
KAssert.DeepEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCollectionTests()
|
||||
{
|
||||
LibraryItem expected = new(TestSample.Get<Show>());
|
||||
LibraryItem actual = await _repository.Get(-1);
|
||||
KAssert.DeepEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetShowSlugTests()
|
||||
{
|
||||
LibraryItem expected = new(TestSample.Get<Show>());
|
||||
LibraryItem actual = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||
KAssert.DeepEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCollectionSlugTests()
|
||||
{
|
||||
LibraryItem expected = new(TestSample.Get<Collection>());
|
||||
LibraryItem actual = await _repository.Get(TestSample.Get<Collection>().Slug);
|
||||
KAssert.DeepEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDuplicatedSlugTests()
|
||||
{
|
||||
await _repositories.LibraryManager.Create(new Collection()
|
||||
{
|
||||
Slug = TestSample.Get<Show>().Slug
|
||||
});
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
Normal file
36
Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
Normal file
@ -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<Models.Library>
|
||||
{
|
||||
private readonly ILibraryRepository _repository;
|
||||
|
||||
protected ALibraryTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.LibraryRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
Normal file
@ -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<People>
|
||||
{
|
||||
private readonly IPeopleRepository _repository;
|
||||
|
||||
protected APeopleTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.PeopleRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
Normal file
@ -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<Provider>
|
||||
{
|
||||
private readonly IProviderRepository _repository;
|
||||
|
||||
protected AProviderTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.ProviderRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/SanityTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/SanityTests.cs
Normal file
@ -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<Show>(), TestSample.Get<Show>()));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_repositories.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return _repositories.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
Normal file
79
Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
Normal file
@ -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<Season>
|
||||
{
|
||||
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<Show>().ID,
|
||||
SeasonNumber = 2
|
||||
});
|
||||
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
292
Kyoo.Tests/Library/SpecificTests/ShowTests.cs
Normal file
292
Kyoo.Tests/Library/SpecificTests/ShowTests.cs
Normal file
@ -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<Show>
|
||||
{
|
||||
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<Show>().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<Show>().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<Show>().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<Show>().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<Show>().Slug);
|
||||
value.People = new[]
|
||||
{
|
||||
new PeopleRole
|
||||
{
|
||||
Show = value,
|
||||
People = TestSample.Get<People>(),
|
||||
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<Show>().Slug);
|
||||
value.ExternalIDs = new[]
|
||||
{
|
||||
new MetadataID<Show>()
|
||||
{
|
||||
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<Show>().Slug);
|
||||
Show newValue = new()
|
||||
{
|
||||
ID = value.ID,
|
||||
Title = "Reset"
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => _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<Show>();
|
||||
expected.ID = 0;
|
||||
expected.Slug = "created-relation-test";
|
||||
expected.ExternalIDs = new[]
|
||||
{
|
||||
new MetadataID<Show>
|
||||
{
|
||||
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<People>(),
|
||||
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<Show>();
|
||||
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<Show>();
|
||||
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<Show> ret = await _repository.Search(query);
|
||||
KAssert.DeepEqual(value, ret.First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteShowWithEpisodeAndSeason()
|
||||
{
|
||||
Show show = TestSample.Get<Show>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/StudioTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/StudioTests.cs
Normal file
@ -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<Studio>
|
||||
{
|
||||
private readonly IStudioRepository _repository;
|
||||
|
||||
protected AStudioTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.StudioRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Kyoo.Tests/Library/SpecificTests/TrackTests.cs
Normal file
51
Kyoo.Tests/Library/SpecificTests/TrackTests.cs
Normal file
@ -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<Track>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Kyoo.Tests/Library/SpecificTests/UserTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/UserTests.cs
Normal file
@ -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<User>
|
||||
{
|
||||
private readonly IUserRepository _repository;
|
||||
|
||||
protected AUserTests(RepositoryActivator repositories)
|
||||
: base(repositories)
|
||||
{
|
||||
_repository = Repositories.LibraryManager.UserRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,79 +1,203 @@
|
||||
// using Kyoo.Models;
|
||||
// using Microsoft.Data.Sqlite;
|
||||
// using Microsoft.EntityFrameworkCore;
|
||||
//
|
||||
// namespace Kyoo.Tests
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Class responsible to fill and create in memory databases for unit tests.
|
||||
// /// </summary>
|
||||
// public class TestContext
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// The context's options that specify to use an in memory Sqlite database.
|
||||
// /// </summary>
|
||||
// private readonly DbContextOptions<DatabaseContext> _context;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Create a new database and fill it with information.
|
||||
// /// </summary>
|
||||
// public TestContext()
|
||||
// {
|
||||
// SqliteConnection connection = new("DataSource=:memory:");
|
||||
// connection.Open();
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// _context = new DbContextOptionsBuilder<DatabaseContext>()
|
||||
// .UseSqlite(connection)
|
||||
// .Options;
|
||||
// FillDatabase();
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// connection.Close();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Fill the database with pre defined values using a clean context.
|
||||
// /// </summary>
|
||||
// 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
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Get a new database context connected to a in memory Sqlite database.
|
||||
// /// </summary>
|
||||
// /// <returns>A valid DatabaseContext</returns>
|
||||
// public DatabaseContext New()
|
||||
// {
|
||||
// return new(_context);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal sqlite connection used by all context returned by this class.
|
||||
/// </summary>
|
||||
private readonly SqliteConnection _connection;
|
||||
|
||||
/// <summary>
|
||||
/// The context's options that specify to use an in memory Sqlite database.
|
||||
/// </summary>
|
||||
private readonly DbContextOptions<DatabaseContext> _context;
|
||||
|
||||
public SqLiteTestContext(ITestOutputHelper output)
|
||||
{
|
||||
_connection = new SqliteConnection("DataSource=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
_context = new DbContextOptionsBuilder<DatabaseContext>()
|
||||
.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<PostgresFixture>
|
||||
{}
|
||||
|
||||
public sealed class PostgresFixture : IDisposable
|
||||
{
|
||||
private readonly DbContextOptions<DatabaseContext> _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<DatabaseContext>()
|
||||
.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<DatabaseContext> _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<DatabaseContext>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible to fill and create in memory databases for unit tests.
|
||||
/// </summary>
|
||||
public abstract class TestContext : IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an arbitrary data to the test context.
|
||||
/// </summary>
|
||||
public void Add<T>(T obj)
|
||||
where T : class
|
||||
{
|
||||
using DatabaseContext context = New();
|
||||
context.Set<T>().Add(obj);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an arbitrary data to the test context.
|
||||
/// </summary>
|
||||
public async Task AddAsync<T>(T obj)
|
||||
where T : class
|
||||
{
|
||||
await using DatabaseContext context = New();
|
||||
await context.Set<T>().AddAsync(obj);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a new database context connected to a in memory Sqlite database.
|
||||
/// </summary>
|
||||
/// <returns>A valid DatabaseContext</returns>
|
||||
public abstract DatabaseContext New();
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract ValueTask DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
276
Kyoo.Tests/Library/TestSample.cs
Normal file
276
Kyoo.Tests/Library/TestSample.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Models;
|
||||
|
||||
namespace Kyoo.Tests
|
||||
{
|
||||
public static class TestSample
|
||||
{
|
||||
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
|
||||
{
|
||||
{
|
||||
typeof(Show),
|
||||
() => new Show()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static readonly Dictionary<Type, Func<object>> 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<Episode>().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<T>()
|
||||
{
|
||||
return (T)Samples[typeof(T)]();
|
||||
}
|
||||
|
||||
public static T GetNew<T>()
|
||||
{
|
||||
return (T)NewSamples[typeof(T)]();
|
||||
}
|
||||
|
||||
public static void FillDatabase(DatabaseContext context)
|
||||
{
|
||||
Collection collection = Get<Collection>();
|
||||
collection.ID = 0;
|
||||
context.Collections.Add(collection);
|
||||
|
||||
Show show = Get<Show>();
|
||||
show.ID = 0;
|
||||
context.Shows.Add(show);
|
||||
|
||||
Season season = Get<Season>();
|
||||
season.ID = 0;
|
||||
season.ShowID = 0;
|
||||
season.Show = show;
|
||||
context.Seasons.Add(season);
|
||||
|
||||
Episode episode = Get<Episode>();
|
||||
episode.ID = 0;
|
||||
episode.ShowID = 0;
|
||||
episode.Show = show;
|
||||
episode.SeasonID = 0;
|
||||
episode.Season = season;
|
||||
context.Episodes.Add(episode);
|
||||
|
||||
Track track = Get<Track>();
|
||||
track.ID = 0;
|
||||
track.EpisodeID = 0;
|
||||
track.Episode = episode;
|
||||
context.Tracks.Add(track);
|
||||
|
||||
Studio studio = Get<Studio>();
|
||||
studio.ID = 0;
|
||||
studio.Shows = new List<Show> {show};
|
||||
context.Studios.Add(studio);
|
||||
|
||||
Genre genre = Get<Genre>();
|
||||
genre.ID = 0;
|
||||
genre.Shows = new List<Show> {show};
|
||||
context.Genres.Add(genre);
|
||||
|
||||
People people = Get<People>();
|
||||
people.ID = 0;
|
||||
context.People.Add(people);
|
||||
|
||||
Provider provider = Get<Provider>();
|
||||
provider.ID = 0;
|
||||
context.Providers.Add(provider);
|
||||
|
||||
Models.Library library = Get<Models.Library>();
|
||||
library.ID = 0;
|
||||
library.Collections = new List<Collection> {collection};
|
||||
library.Providers = new List<Provider> {provider};
|
||||
context.Libraries.Add(library);
|
||||
|
||||
User user = Get<User>();
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Kyoo.Tests/Utility/EnumerableTests.cs
Normal file
71
Kyoo.Tests/Utility/EnumerableTests.cs
Normal file
@ -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<ArgumentNullException>(() => list.Map(((Func<int, int, int>)null)!));
|
||||
list = null;
|
||||
Assert.Throws<ArgumentNullException>(() => 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<ArgumentNullException>(() => list.MapAsync(((Func<int, int, Task<int>>)null)!));
|
||||
list = null;
|
||||
Assert.Throws<ArgumentNullException>(() => 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<ArgumentNullException>(() => list.SelectAsync(((Func<int, Task<int>>)null)!));
|
||||
list = null;
|
||||
Assert.Throws<ArgumentNullException>(() => list!.SelectAsync(x => Task.FromResult(x + 1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToListAsyncTest()
|
||||
{
|
||||
int[] expected = {1, 2, 3, 4};
|
||||
IAsyncEnumerable<int> list = expected.SelectAsync(Task.FromResult);
|
||||
Assert.Equal(expected, await list.ToListAsync());
|
||||
list = null;
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => 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<ArgumentNullException>(() => list.IfEmpty(null!).ToList());
|
||||
list = null;
|
||||
Assert.Throws<ArgumentNullException>(() => list!.IfEmpty(() => {}).ToList());
|
||||
list = Array.Empty<int>();
|
||||
Assert.Throws<ArgumentException>(() => list.IfEmpty(() => throw new ArgumentException()).ToList());
|
||||
Assert.Empty(list.IfEmpty(() => {}));
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Kyoo.Tests/Utility/MergerTests.cs
Normal file
21
Kyoo.Tests/Utility/MergerTests.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Kyoo.Tests/Utility/TaskTests.cs
Normal file
76
Kyoo.Tests/Utility/TaskTests.cs
Normal file
@ -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<int>(null));
|
||||
Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThenTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
|
||||
.Then(_ => throw new ArgumentException()));
|
||||
Assert.Equal(1, await Task.FromResult(1)
|
||||
.Then(_ => {}));
|
||||
|
||||
static async Task<int> Faulted()
|
||||
{
|
||||
await Task.Delay(1);
|
||||
throw new ArgumentException();
|
||||
}
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Faulted().Then(_ => KAssert.Fail()));
|
||||
|
||||
static async Task<int> Infinite()
|
||||
{
|
||||
await Task.Delay(100000);
|
||||
return 1;
|
||||
}
|
||||
|
||||
CancellationTokenSource token = new();
|
||||
token.Cancel();
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
|
||||
.Then(_ => {}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
|
||||
.Map<int, int>(_ => throw new ArgumentException()));
|
||||
Assert.Equal(2, await Task.FromResult(1)
|
||||
.Map(x => x + 1));
|
||||
|
||||
static async Task<int> Faulted()
|
||||
{
|
||||
await Task.Delay(1);
|
||||
throw new ArgumentException();
|
||||
}
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Faulted()
|
||||
.Map(x =>
|
||||
{
|
||||
KAssert.Fail();
|
||||
return x;
|
||||
}));
|
||||
|
||||
static async Task<int> Infinite()
|
||||
{
|
||||
await Task.Delay(100000);
|
||||
return 1;
|
||||
}
|
||||
|
||||
CancellationTokenSource token = new();
|
||||
token.Cancel();
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
|
||||
.Map(x => x));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,23 @@ namespace Kyoo.Tests
|
||||
Expression<Func<Show, int>> member = x => x.ID;
|
||||
Expression<Func<Show, object>> 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<Func<Show, object>> call = x => x.GetID("test");
|
||||
Assert.False(Utility.IsPropertyExpression(call));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPropertyName_Test()
|
||||
{
|
||||
Expression<Func<Show, int>> member = x => x.ID;
|
||||
Expression<Func<Show, object>> memberCast = x => x.ID;
|
||||
|
||||
Assert.Equal("ID", Utility.GetPropertyName(member));
|
||||
Assert.Equal("ID", Utility.GetPropertyName(memberCast));
|
||||
Assert.Throws<ArgumentException>(() => Utility.GetPropertyName(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Kyoo.sln
6
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
|
||||
|
||||
@ -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<Episode> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<Episode>, IEpisodeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The databse handle
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
/// <summary>
|
||||
@ -24,10 +23,6 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
private readonly IProviderRepository _providers;
|
||||
/// <summary>
|
||||
/// A show repository to get show's slug from their ID and keep the slug in each episode.
|
||||
/// </summary>
|
||||
private readonly IShowRepository _shows;
|
||||
/// <summary>
|
||||
/// A track repository to handle creation and deletion of tracks related to the current episode.
|
||||
/// </summary>
|
||||
private readonly ITrackRepository _tracks;
|
||||
@ -41,66 +36,31 @@ namespace Kyoo.Controllers
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle to use.</param>
|
||||
/// <param name="providers">A provider repository</param>
|
||||
/// <param name="shows">A show repository</param>
|
||||
/// <param name="tracks">A track repository</param>
|
||||
public EpisodeRepository(DatabaseContext database,
|
||||
IProviderRepository providers,
|
||||
IShowRepository shows,
|
||||
ITrackRepository tracks)
|
||||
: base(database)
|
||||
{
|
||||
_database = database;
|
||||
_providers = providers;
|
||||
_shows = shows;
|
||||
_tracks = tracks;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Episode> GetOrDefault(int id)
|
||||
public Task<Episode> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Episode> GetOrDefault(string slug)
|
||||
public Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
|
||||
{
|
||||
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Episode> GetOrDefault(Expression<Func<Episode, bool>> where)
|
||||
{
|
||||
Episode ret = await base.GetOrDefault(where);
|
||||
if (ret != null)
|
||||
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Episode> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -122,61 +82,30 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
|
||||
public Task<Episode> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
|
||||
public Task<Episode> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Episode> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Episode>> Search(string query)
|
||||
{
|
||||
List<Episode> episodes = await _database.Episodes
|
||||
.Where(x => x.EpisodeNumber != -1)
|
||||
return await _database.Episodes
|
||||
.Where(x => x.EpisodeNumber != null)
|
||||
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
.ToListAsync();
|
||||
foreach (Episode episode in episodes)
|
||||
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
|
||||
return episodes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null,
|
||||
Sort<Episode> sort = default,
|
||||
Pagination limit = default)
|
||||
{
|
||||
ICollection<Episode> episodes = await base.GetAll(where, sort, limit);
|
||||
foreach (Episode episode in episodes)
|
||||
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
|
||||
return episodes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Episode> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -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.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource to fix.</param>
|
||||
/// <returns>The <see cref="resource"/> parameter is returnned.</returns>
|
||||
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
|
||||
private async Task<Episode> 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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -22,15 +22,7 @@ namespace Kyoo.Controllers
|
||||
/// A lazy loaded library repository to validate queries (check if a library does exist)
|
||||
/// </summary>
|
||||
private readonly Lazy<ILibraryRepository> _libraries;
|
||||
/// <summary>
|
||||
/// A lazy loaded show repository to get a show from it's id.
|
||||
/// </summary>
|
||||
private readonly Lazy<IShowRepository> _shows;
|
||||
/// <summary>
|
||||
/// A lazy loaded collection repository to get a collection from it's id.
|
||||
/// </summary>
|
||||
private readonly Lazy<ICollectionRepository> _collections;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title;
|
||||
|
||||
@ -38,60 +30,41 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LibraryItemRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The databse instance</param>
|
||||
/// <param name="database">The database instance</param>
|
||||
/// <param name="libraries">A lazy loaded library repository</param>
|
||||
/// <param name="shows">A lazy loaded show repository</param>
|
||||
/// <param name="collections">A lazy loaded collection repository</param>
|
||||
public LibraryItemRepository(DatabaseContext database,
|
||||
Lazy<ILibraryRepository> libraries,
|
||||
Lazy<IShowRepository> shows,
|
||||
Lazy<ICollectionRepository> collections)
|
||||
Lazy<ILibraryRepository> libraries)
|
||||
: base(database)
|
||||
{
|
||||
_database = database;
|
||||
_libraries = libraries;
|
||||
_shows = shows;
|
||||
_collections = collections;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<LibraryItem> GetOrDefault(int id)
|
||||
public override Task<LibraryItem> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<LibraryItem> GetOrDefault(string slug)
|
||||
{
|
||||
throw new InvalidOperationException("You can't get a library item by a slug.");
|
||||
return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a basic queryable with the right mapping from shows & collections.
|
||||
/// Shows contained in a collection are excluded.
|
||||
/// </summary>
|
||||
private IQueryable<LibraryItem> ItemsQuery
|
||||
=> _database.Shows
|
||||
.Where(x => !x.Collections.Any())
|
||||
.Select(LibraryItem.FromShow)
|
||||
.Concat(_database.Collections
|
||||
.Select(LibraryItem.FromCollection));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
|
||||
Sort<LibraryItem> sort = default,
|
||||
Pagination limit = default)
|
||||
{
|
||||
return ApplyFilters(ItemsQuery, where, sort, limit);
|
||||
return ApplyFilters(_database.LibraryItems, where, sort, limit);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
|
||||
{
|
||||
IQueryable<LibraryItem> query = ItemsQuery;
|
||||
IQueryable<LibraryItem> query = _database.LibraryItems;
|
||||
if (where != null)
|
||||
query = query.Where(where);
|
||||
return query.CountAsync();
|
||||
@ -100,7 +73,7 @@ namespace Kyoo.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task<ICollection<LibraryItem>> Search(string query)
|
||||
{
|
||||
return await ItemsQuery
|
||||
return await _database.LibraryItems
|
||||
.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
@ -109,7 +82,6 @@ namespace Kyoo.Controllers
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -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 =>
|
||||
{
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
|
||||
Sort<MetadataID> sort = default,
|
||||
public Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
|
||||
Sort<MetadataID<T>> 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<T>().Include(y => y.Second),
|
||||
x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.FirstID == x),
|
||||
x => x.FirstID,
|
||||
where,
|
||||
sort,
|
||||
limit);
|
||||
|
||||
@ -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
|
||||
/// </summary>
|
||||
private readonly IProviderRepository _providers;
|
||||
/// <summary>
|
||||
/// A show repository to get show's slug from their ID and keep the slug in each episode.
|
||||
/// </summary>
|
||||
private readonly IShowRepository _shows;
|
||||
/// <summary>
|
||||
/// A lazilly loaded episode repository to handle deletion of episodes with the season.
|
||||
/// </summary>
|
||||
private readonly Lazy<IEpisodeRepository> _episodes;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="SeasonRepository"/> using the provided handle, a provider & a show repository and
|
||||
/// a service provider to lazilly request an episode repository.
|
||||
/// Create a new <see cref="SeasonRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The database handle that will be used</param>
|
||||
/// <param name="providers">A provider repository</param>
|
||||
/// <param name="shows">A show repository</param>
|
||||
/// <param name="episodes">A lazy loaded episode repository.</param>
|
||||
public SeasonRepository(DatabaseContext database,
|
||||
IProviderRepository providers,
|
||||
IShowRepository shows,
|
||||
Lazy<IEpisodeRepository> episodes)
|
||||
IProviderRepository providers)
|
||||
: base(database)
|
||||
{
|
||||
_database = database;
|
||||
_providers = providers;
|
||||
_shows = shows;
|
||||
_episodes = episodes;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<Season> Get(int id)
|
||||
{
|
||||
Season ret = await base.Get(id);
|
||||
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<Season> Get(Expression<Func<Season, bool>> where)
|
||||
{
|
||||
Season ret = await base.Get(where);
|
||||
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<Season> Get(string slug)
|
||||
{
|
||||
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Season> 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
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ICollection<Season>> Search(string query)
|
||||
{
|
||||
List<Season> seasons = await _database.Seasons
|
||||
return await _database.Seasons
|
||||
.Where(_database.Like<Season>(x => x.Title, $"%{query}%"))
|
||||
.OrderBy(DefaultSort)
|
||||
.Take(20)
|
||||
.ToListAsync();
|
||||
foreach (Season season in seasons)
|
||||
season.ShowSlug = await _shows.GetSlug(season.ShowID);
|
||||
return seasons;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null,
|
||||
Sort<Season> sort = default,
|
||||
Pagination limit = default)
|
||||
{
|
||||
ICollection<Season> seasons = await base.GetAll(where, sort, limit);
|
||||
foreach (Season season in seasons)
|
||||
season.ShowSlug = await _shows.GetSlug(season.ShowID);
|
||||
return seasons;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<Season> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,14 +33,6 @@ namespace Kyoo.Controllers
|
||||
/// A provider repository to handle externalID creation and deletion
|
||||
/// </summary>
|
||||
private readonly IProviderRepository _providers;
|
||||
/// <summary>
|
||||
/// A lazy loaded season repository to handle cascade deletion (seasons deletion whith it's show)
|
||||
/// </summary>
|
||||
private readonly Lazy<ISeasonRepository> _seasons;
|
||||
/// <summary>
|
||||
/// A lazy loaded episode repository to handle cascade deletion (episode deletion whith it's show)
|
||||
/// </summary>
|
||||
private readonly Lazy<IEpisodeRepository> _episodes;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Expression<Func<Show, object>> DefaultSort => x => x.Title;
|
||||
@ -53,15 +45,11 @@ namespace Kyoo.Controllers
|
||||
/// <param name="people">A people repository</param>
|
||||
/// <param name="genres">A genres repository</param>
|
||||
/// <param name="providers">A provider repository</param>
|
||||
/// <param name="seasons">A lazy loaded season repository</param>
|
||||
/// <param name="episodes">A lazy loaded episode repository</param>
|
||||
public ShowRepository(DatabaseContext database,
|
||||
IStudioRepository studios,
|
||||
IPeopleRepository people,
|
||||
IGenreRepository genres,
|
||||
IProviderRepository providers,
|
||||
Lazy<ISeasonRepository> seasons,
|
||||
Lazy<IEpisodeRepository> 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
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Track>, ITrackRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The databse handle
|
||||
/// The database handle
|
||||
/// </summary>
|
||||
private readonly DatabaseContext _database;
|
||||
|
||||
@ -27,62 +24,12 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TrackRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="database">The datatabse handle</param>
|
||||
/// <param name="database">The database handle</param>
|
||||
public TrackRepository(DatabaseContext database)
|
||||
: base(database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
Task<Track> IRepository<Track>.Get(string slug)
|
||||
{
|
||||
return Get(slug);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Track> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
|
||||
{
|
||||
Match match = Regex.Match(slug,
|
||||
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
if (int.TryParse(slug, out int id))
|
||||
return GetOrDefault(id);
|
||||
match = Regex.Match(slug, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-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<StreamType>(match.Groups["type"].Value, true);
|
||||
|
||||
IQueryable<Track> 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ICollection<Track>> Search(string query)
|
||||
@ -93,23 +40,19 @@ namespace Kyoo.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task<Track> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ namespace Kyoo.Controllers
|
||||
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
|
||||
if (stream!.Type != StreamType.Unknown)
|
||||
{
|
||||
tracks[j] = new Track(stream);
|
||||
tracks[j] = stream.ToTrack();
|
||||
j++;
|
||||
}
|
||||
streamsPtr += size;
|
||||
|
||||
@ -35,9 +35,9 @@
|
||||
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
|
||||
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.17" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -46,6 +46,9 @@
|
||||
<!-- <ExcludeAssets>all</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj">
|
||||
<!-- <ExcludeAssets>all</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="../Kyoo.SqLite/Kyoo.SqLite.csproj">
|
||||
<!-- <ExcludeAssets>all</ExcludeAssets>-->
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
67
Kyoo/Models/Stream.cs
Normal file
67
Kyoo/Models/Stream.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Kyoo.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Models.Watch
|
||||
{
|
||||
/// <summary>
|
||||
/// The unmanaged stream that the transcoder will return.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public class Stream
|
||||
{
|
||||
/// <summary>
|
||||
/// The title of the stream.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The language of this stream (as a ISO-639-2 language code)
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The codec of this stream.
|
||||
/// </summary>
|
||||
public string Codec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this stream the default one of it's type?
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)] public bool IsDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Is this stream tagged as forced?
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.I1)] public bool IsForced;
|
||||
|
||||
/// <summary>
|
||||
/// The path of this track.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this stream.
|
||||
/// </summary>
|
||||
[SerializeIgnore] public StreamType Type { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a track from this stream.
|
||||
/// </summary>
|
||||
/// <returns>A new track that represent this stream.</returns>
|
||||
public Track ToTrack()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Title = Title,
|
||||
Language = Language,
|
||||
Codec = Codec,
|
||||
IsDefault = IsDefault,
|
||||
IsForced = IsForced,
|
||||
Path = Path,
|
||||
Type = Type,
|
||||
IsExternal = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>(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<Episode> 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;
|
||||
|
||||
@ -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<Show>
|
||||
|
||||
@ -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<IActionResult> 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>(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);
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
},
|
||||
|
||||
"database": {
|
||||
"sqlite": {
|
||||
"data Source": "kyoo.db",
|
||||
"cache": "Shared"
|
||||
},
|
||||
"postgres": {
|
||||
"server": "127.0.0.1",
|
||||
"port": "5432",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user