Merge pull request #30 from AnonymusRaccoon/sqlite

Implementing SQLite & Adding tests
This commit is contained in:
Zoe Roux 2021-07-13 17:58:34 +02:00 committed by GitHub
commit 28b32c9e49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 11242 additions and 2367 deletions

View File

@ -2,9 +2,10 @@ name: Analysis
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build: analysis:
name: Static Analysis name: Static Analysis
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
@ -28,16 +29,27 @@ jobs:
run: | run: |
mkdir -p ./.sonar/scanner mkdir -p ./.sonar/scanner
dotnet tool update dotnet-sonarscanner --tool-path ./.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 - name: Build and analyze
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: bash shell: bash
run: | run: |
dotnet test \ find . -name 'coverage.opencover.xml'
'-p:CollectCoverage=true;CoverletOutputFormat=opencover' \
'-p:SkipTranscoder=true;SkipWebApp=true' || echo "Test failed. Skipping..."
dotnet build-server shutdown dotnet build-server shutdown
./.sonar/scanner/dotnet-sonarscanner begin \ ./.sonar/scanner/dotnet-sonarscanner begin \

View File

@ -3,17 +3,40 @@ name: Testing
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build: tests:
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - 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 - 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"

View File

@ -77,6 +77,11 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
IProviderRepository ProviderRepository { get; } IProviderRepository ProviderRepository { get; }
/// <summary>
/// The repository that handle users.
/// </summary>
IUserRepository UserRepository { get; }
/// <summary> /// <summary>
/// Get the resource by it's ID /// Get the resource by it's ID
/// </summary> /// </summary>
@ -149,16 +154,6 @@ namespace Kyoo.Controllers
[ItemNotNull] [ItemNotNull]
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); 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> /// <summary>
/// Get the resource by it's ID or null if it is not found. /// Get the resource by it's ID or null if it is not found.
/// </summary> /// </summary>
@ -224,15 +219,6 @@ namespace Kyoo.Controllers
[ItemCanBeNull] [ItemCanBeNull]
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); 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> /// <summary>
/// Load a related resource /// Load a related resource
@ -242,6 +228,9 @@ namespace Kyoo.Controllers
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam> /// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <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) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
where T : class, IResource where T : class, IResource
where T2 : class, IResource, new(); where T2 : class, IResource, new();
@ -254,6 +243,9 @@ namespace Kyoo.Controllers
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam> /// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <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) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource where T : class, IResource
where T2 : class, new(); where T2 : class, new();
@ -265,6 +257,9 @@ namespace Kyoo.Controllers
/// <param name="memberName">The name of the resource to load (case sensitive)</param> /// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <typeparam name="T">The type of the source object</typeparam> /// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns> /// <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) Task<T> Load<T>([NotNull] T obj, string memberName)
where T : class, IResource; where T : class, IResource;
@ -273,6 +268,9 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="obj">The source object.</param> /// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</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); Task Load([NotNull] IResource obj, string memberName);
/// <summary> /// <summary>

View File

@ -16,6 +16,6 @@ namespace Kyoo.Controllers
Task<Season> GetSeason(Show show, int seasonNumber); 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);
} }
} }

View File

@ -11,7 +11,7 @@ namespace Kyoo.Controllers
Task<Show> SearchShow(string showName, bool isMovie, Library library); Task<Show> SearchShow(string showName, bool isMovie, Library library);
Task<IEnumerable<Show>> SearchShows(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<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); Task<ICollection<PeopleRole>> GetPeople(Show show, Library library);
} }
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -244,47 +243,11 @@ namespace Kyoo.Controllers
Task Delete([NotNull] T obj); Task Delete([NotNull] T obj);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete all resources that match the predicate.
/// </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.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <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> /// <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> /// <summary>
@ -412,25 +375,7 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// A repository to handle tracks /// A repository to handle tracks
/// </summary> /// </summary>
public interface ITrackRepository : IRepository<Track> 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);
}
/// <summary> /// <summary>
/// A repository to handle libraries. /// A repository to handle libraries.
@ -631,10 +576,12 @@ namespace Kyoo.Controllers
/// <param name="where">A predicate to add arbitrary filter</param> /// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order & sort by)</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> /// <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> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null, Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
Sort<MetadataID> sort = default, Sort<MetadataID<T>> sort = default,
Pagination limit = default); Pagination limit = default)
where T : class, IResource;
/// <summary> /// <summary>
/// Get a list of external ids that match all filters /// 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="sort">A sort by expression</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param> /// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns> /// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where, Task<ICollection<MetadataID<T>>> GetMetadataID<T>([Optional] Expression<Func<MetadataID<T>, bool>> where,
Expression<Func<MetadataID, object>> sort, Expression<Func<MetadataID<T>, object>> sort,
Pagination limit = default Pagination limit = default
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit); ) where T : class, IResource
=> GetMetadataID(where, new Sort<MetadataID<T>>(sort), limit);
} }
/// <summary> /// <summary>

View File

@ -37,6 +37,8 @@ namespace Kyoo.Controllers
public IGenreRepository GenreRepository { get; } public IGenreRepository GenreRepository { get; }
/// <inheritdoc /> /// <inheritdoc />
public IProviderRepository ProviderRepository { get; } public IProviderRepository ProviderRepository { get; }
/// <inheritdoc />
public IUserRepository UserRepository { get; }
/// <summary> /// <summary>
@ -58,6 +60,7 @@ namespace Kyoo.Controllers
StudioRepository = GetRepository<Studio>() as IStudioRepository; StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository; GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository; ProviderRepository = GetRepository<Provider>() as IProviderRepository;
UserRepository = GetRepository<User>() as IUserRepository;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -114,12 +117,6 @@ namespace Kyoo.Controllers
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
} }
/// <inheritdoc />
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
{
return TrackRepository.Get(slug, type);
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<T> GetOrDefault<T>(int id) public async Task<T> GetOrDefault<T>(int id)
where T : class, IResource where T : class, IResource
@ -165,12 +162,6 @@ namespace Kyoo.Controllers
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber); 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 /> /// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member) public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource where T : class, IResource
@ -250,9 +241,9 @@ namespace Kyoo.Controllers
(Show s, nameof(Show.ExternalIDs)) => SetRelation(s, (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.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 (Show s, nameof(Show.Genres)) => GenreRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) .GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
@ -290,9 +281,9 @@ namespace Kyoo.Controllers
(Season s, nameof(Season.ExternalIDs)) => SetRelation(s, (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.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, (Season s, nameof(Season.Episodes)) => SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), EpisodeRepository.GetAll(x => x.Season.ID == obj.ID),
@ -309,9 +300,9 @@ namespace Kyoo.Controllers
(Episode e, nameof(Episode.ExternalIDs)) => SetRelation(e, (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.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, (Episode e, nameof(Episode.Tracks)) => SetRelation(e,
TrackRepository.GetAll(x => x.Episode.ID == obj.ID), TrackRepository.GetAll(x => x.Episode.ID == obj.ID),
@ -355,9 +346,9 @@ namespace Kyoo.Controllers
(People p, nameof(People.ExternalIDs)) => SetRelation(p, (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.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 (People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.ID) .GetFromPeople(obj.ID)

View 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 { }
}

View File

@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes
/// An attribute to inform that the service will be injected automatically by a service provider. /// An attribute to inform that the service will be injected automatically by a service provider.
/// </summary> /// </summary>
/// <remarks> /// <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> /// </remarks>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)] [MeansImplicitUse(ImplicitUseKindFlags.Assign)]

View 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 { }
}

View File

@ -1,17 +1,34 @@
using System; using System;
using Kyoo.Controllers;
namespace Kyoo.Models.Attributes 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 { } public class EditableRelationAttribute : Attribute { }
/// <summary>
/// The targeted relation can be loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class LoadableRelationAttribute : Attribute public class LoadableRelationAttribute : Attribute
{ {
/// <summary>
/// The name of the field containing the related resource's ID.
/// </summary>
public string RelationID { get; } public string RelationID { get; }
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary>
public LoadableRelationAttribute() {} 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) public LoadableRelationAttribute(string relationID)
{ {
RelationID = relationID; RelationID = relationID;

View File

@ -2,17 +2,41 @@ using System;
namespace Kyoo.Models.Attributes namespace Kyoo.Models.Attributes
{ {
/// <summary>
/// Remove an property from the serialization pipeline. It will simply be skipped.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeIgnoreAttribute : Attribute {} 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)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DeserializeIgnoreAttribute : Attribute {} 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)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeAsAttribute : Attribute public class SerializeAsAttribute : Attribute
{ {
/// <summary>
/// The format string to use.
/// </summary>
public string Format { get; } 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) public SerializeAsAttribute(string format)
{ {
Format = format; Format = format;

View 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;
}
}
}

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using JetBrains.Annotations;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// The type of item, ether a show, a movie or a collection.
/// </summary>
public enum ItemType public enum ItemType
{ {
Show, Show,
@ -12,22 +14,67 @@ namespace Kyoo.Models
Collection 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 public class LibraryItem : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The title of the show or collection.
/// </summary>
public string Title { get; set; } public string Title { get; set; }
/// <summary>
/// The summary of the show or collection.
/// </summary>
public string Overview { get; set; } 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 Status? Status { get; set; }
public string TrailerUrl { get; set; }
public int? StartYear { get; set; } /// <summary>
public int? EndYear { get; set; } /// The date this show or collection started airing. It can be null if this is unknown.
[SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; } /// </summary>
[UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show"; 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; } public ItemType Type { get; set; }
/// <summary>
/// Create a new, empty <see cref="LibraryItem"/>.
/// </summary>
public LibraryItem() {} 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) public LibraryItem(Show show)
{ {
ID = show.ID; ID = show.ID;
@ -35,13 +82,16 @@ namespace Kyoo.Models
Title = show.Title; Title = show.Title;
Overview = show.Overview; Overview = show.Overview;
Status = show.Status; Status = show.Status;
TrailerUrl = show.TrailerUrl; StartAir = show.StartAir;
StartYear = show.StartYear; EndAir = show.EndAir;
EndYear = show.EndYear;
Poster = show.Poster; Poster = show.Poster;
Type = show.IsMovie ? ItemType.Movie : ItemType.Show; 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) public LibraryItem(Collection collection)
{ {
ID = -collection.ID; ID = -collection.ID;
@ -49,13 +99,15 @@ namespace Kyoo.Models
Title = collection.Name; Title = collection.Name;
Overview = collection.Overview; Overview = collection.Overview;
Status = Models.Status.Unknown; Status = Models.Status.Unknown;
TrailerUrl = null; StartAir = null;
StartYear = null; EndAir = null;
EndYear = null;
Poster = collection.Poster; Poster = collection.Poster;
Type = ItemType.Collection; 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 public static Expression<Func<Show, LibraryItem>> FromShow => x => new LibraryItem
{ {
ID = x.ID, ID = x.ID,
@ -63,13 +115,15 @@ namespace Kyoo.Models
Title = x.Title, Title = x.Title,
Overview = x.Overview, Overview = x.Overview,
Status = x.Status, Status = x.Status,
TrailerUrl = x.TrailerUrl, StartAir = x.StartAir,
StartYear = x.StartYear, EndAir = x.EndAir,
EndYear = x.EndYear,
Poster= x.Poster, Poster= x.Poster,
Type = x.IsMovie ? ItemType.Movie : ItemType.Show 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 public static Expression<Func<Collection, LibraryItem>> FromCollection => x => new LibraryItem
{ {
ID = -x.ID, ID = -x.ID,
@ -77,10 +131,9 @@ namespace Kyoo.Models
Title = x.Name, Title = x.Name,
Overview = x.Overview, Overview = x.Overview,
Status = Models.Status.Unknown, Status = Models.Status.Unknown,
TrailerUrl = null, StartAir = null,
StartYear = null, EndAir = null,
EndYear = null, Poster = x.Poster,
Poster= x.Poster,
Type = ItemType.Collection Type = ItemType.Collection
}; };
} }

View File

@ -1,33 +1,64 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
namespace Kyoo.Models 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 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; } 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; } public int SecondID { get; set; }
/// <summary>
/// Create a new typeless <see cref="Link"/>.
/// </summary>
public Link() {} 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) public Link(int firstID, int secondID)
{ {
FirstID = firstID; FirstID = firstID;
SecondID = secondID; 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) public Link(IResource first, IResource second)
{ {
FirstID = first.ID; FirstID = first.ID;
SecondID = second.ID; SecondID = second.ID;
} }
public static Link Create(IResource first, IResource second) /// <summary>
{ /// Create a new typed link between two resources.
return new(first, second); /// 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) public static Link<T, T2> Create<T, T2>(T first, T2 second)
where T : class, IResource where T : class, IResource
where T2 : class, IResource where T2 : class, IResource
@ -35,6 +66,16 @@ namespace Kyoo.Models
return new(first, second); 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) public static Link<T, T2> UCreate<T, T2>(T first, T2 second)
where T : class, IResource where T : class, IResource
where T2 : class, IResource where T2 : class, IResource
@ -42,6 +83,9 @@ namespace Kyoo.Models
return new(first, second, true); 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 public static Expression<Func<Link, object>> PrimaryKey
{ {
get 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 public class Link<T1, T2> : Link
where T1 : class, IResource where T1 : class, IResource
where T2 : class, IResource where T2 : class, IResource
{ {
public virtual T1 First { get; set; } /// <summary>
public virtual T2 Second { get; set; } /// 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() {} 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) public Link(T1 first, T2 second, bool privateItems = false)
: base(first, second) : base(first, second)
{ {
@ -71,10 +139,18 @@ namespace Kyoo.Models
Second = second; 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) public Link(int firstID, int secondID)
: base(firstID, 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 public new static Expression<Func<Link<T1, T2>, object>> PrimaryKey
{ {
get get

View File

@ -1,26 +1,34 @@
using Kyoo.Models.Attributes; using System;
using System.Linq.Expressions;
namespace Kyoo.Models 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; } /// <summary>
[SerializeIgnore] public int ProviderID { get; set; } /// The ID of the resource on the external provider.
public virtual Provider Provider {get; set; } /// </summary>
[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; }
public string DataID { get; set; } public string DataID { get; set; }
/// <summary>
/// The URL of the resource on the external provider.
/// </summary>
public string Link { get; set; } 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};
}
}
} }
} }

View File

@ -3,22 +3,45 @@ using System.Linq;
namespace Kyoo.Models 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 class Page<T> where T : IResource
{ {
public string This { get; set; } /// <summary>
public string First { get; set; } /// The link of the current page.
public string Next { get; set; } /// </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 int Count => Items.Count;
public ICollection<T> Items { get; set; }
public Page() { } /// <summary>
/// The list of items in the page.
/// </summary>
public ICollection<T> Items { get; }
public Page(ICollection<T> items)
{
Items = items;
}
/// <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) public Page(ICollection<T> items, string @this, string next, string first)
{ {
Items = items; Items = items;
@ -27,6 +50,13 @@ namespace Kyoo.Models
First = first; First = first;
} }
/// <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, public Page(ICollection<T> items,
string url, string url,
Dictionary<string, string> query, Dictionary<string, string> query,

View File

@ -1,17 +1,55 @@
using Kyoo.Models.Attributes;
namespace Kyoo.Models 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 public class PeopleRole : IResource
{ {
[SerializeIgnore] public int ID { get; set; } /// <inheritdoc />
[SerializeIgnore] public string Slug => ForPeople ? Show.Slug : People.Slug; public int ID { get; set; }
[SerializeIgnore] public bool ForPeople;
[SerializeIgnore] public int PeopleID { get; set; } /// <inheritdoc />
[SerializeIgnore] public virtual People People { get; set; } public string Slug => ForPeople ? Show.Slug : People.Slug;
[SerializeIgnore] public int ShowID { get; set; }
[SerializeIgnore] public virtual Show Show { get; set; } /// <summary>
public string Role { get; set; } /// 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; } 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; }
} }
} }

View File

@ -1,31 +1,59 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models 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 public class Collection : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The name of this collection.
/// </summary>
public string Name { get; set; } 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; } [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The description of this collection.
/// </summary>
public string Overview { get; set; } 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 #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> LibraryLinks { get; set; } /// <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 #endif
public Collection() { }
public Collection(string slug, string name, string overview, string poster)
{
Slug = slug;
Name = name;
Overview = overview;
Poster = poster;
}
} }
} }

View File

@ -1,57 +1,166 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models 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 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; /// <inheritdoc />
public int EpisodeNumber { get; set; } = -1; [Computed] public string Slug
public int AbsoluteNumber { get; set; } = -1; {
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; } [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; } [SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; }
/// <summary>
/// The title of this episode.
/// </summary>
public string Title { get; set; } public string Title { get; set; }
/// <summary>
/// The overview of this episode.
/// </summary>
public string Overview { get; set; } 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 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; } /// <summary>
/// The list of tracks this episode has. This lists video, audio and subtitles available.
[EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; } /// </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) 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 return seasonNumber switch
{ {
-1 when absoluteNumber == -1 => showSlug, null when absoluteNumber == null => showSlug,
-1 => $"{showSlug}-{absoluteNumber}", null => $"{showSlug}-{absoluteNumber}",
_ => $"{showSlug}-s{seasonNumber}e{episodeNumber}" _ => $"{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;
}
} }
} }

View File

@ -1,40 +1,51 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// A genre that allow one to specify categories for shows.
/// </summary>
public class Genre : IResource public class Genre : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The name of this genre.
/// </summary>
public string Name { get; set; } 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 #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 #endif
/// <summary>
/// Create a new, empty <see cref="Genre"/>.
/// </summary>
public Genre() {} 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) public Genre(string name)
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);
Name = 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;
}
} }
} }

View File

@ -1,3 +1,5 @@
using Kyoo.Controllers;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary> /// <summary>
@ -8,6 +10,10 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
/// </summary> /// </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; } public int ID { get; set; }
/// <summary> /// <summary>

View File

@ -1,24 +1,60 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// A library containing <see cref="Show"/> and <see cref="Collection"/>.
/// </summary>
public class Library : IResource public class Library : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The name of this library.
/// </summary>
public string Name { get; set; } 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; } 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; } /// <summary>
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; } /// 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 #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> ProviderLinks { get; set; } /// <summary>
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; } /// The internal link between this library and provider in the <see cref="Providers"/> list.
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; } /// </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 #endif
} }
} }

View File

@ -3,14 +3,37 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
/// </summary>
public class People : IResource public class People : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } 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; }
} }
} }

View File

@ -1,37 +1,67 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models 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 public class Provider : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The name of this provider.
/// </summary>
public string Name { get; set; } 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; } [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; } [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 #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> LibraryLinks { get; set; } /// <summary>
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; } /// 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 #endif
/// <summary>
/// Create a new, default, <see cref="Provider"/>
/// </summary>
public Provider() { } 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) public Provider(string name, string logo)
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);
Name = name; Name = name;
Logo = logo; Logo = logo;
} }
public Provider(int id, string name, string logo)
{
ID = id;
Slug = Utility.ToSlug(name);
Name = name;
Logo = logo;
}
} }
} }

View File

@ -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; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// A season of a <see cref="Show"/>.
/// </summary>
public class Season : IResource public class Season : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } 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; } [SerializeIgnore] public string ShowSlug { private get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
public int SeasonNumber { get; set; } = -1; /// <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; }
/// <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; } public string Title { get; set; }
/// <summary>
/// A quick overview of this season.
/// </summary>
public string Overview { get; set; } 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; } [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { 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; }
/// <summary>
/// The list of episodes that this season contains.
/// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
} }
} }

View File

@ -1,57 +1,173 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// A series or a movie.
/// </summary>
public class Show : IResource, IOnMerge public class Show : IResource, IOnMerge
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The title of this show.
/// </summary>
public string Title { get; set; } public string Title { get; set; }
/// <summary>
/// The list of alternative titles of this show.
/// </summary>
[EditableRelation] public string[] Aliases { get; set; } [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; } [SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The summary of this show.
/// </summary>
public string Overview { get; set; } public string Overview { get; set; }
/// <summary>
/// Is this show airing, not aired yet or finished?
/// </summary>
public Status? Status { get; set; } 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; } public string TrailerUrl { get; set; }
public int? StartYear { get; set; } /// <summary>
public int? EndYear { get; set; } /// 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; }
/// <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; } [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; } [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; } [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; } 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; } [SerializeIgnore] public int? StudioID { get; set; }
[LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; } /// <summary>
[LoadableRelation] [EditableRelation] public virtual ICollection<Genre> Genres { get; set; } /// The Studio that made this show. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
[LoadableRelation] [EditableRelation] public virtual ICollection<PeopleRole> People { get; set; } /// </summary>
[LoadableRelation] public virtual ICollection<Season> Seasons { get; set; } [LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; } /// <summary>
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; } /// 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 #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> LibraryLinks { get; set; } /// <summary>
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> CollectionLinks { get; set; } /// The internal link between this show and libraries in the <see cref="Libraries"/> list.
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> GenreLinks { get; set; } /// </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 #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) 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) if (ExternalIDs != null)
foreach (MetadataID id in ExternalIDs) foreach (MetadataID<Show> id in ExternalIDs)
id.Show = this; id.First = this;
if (People != null) if (People != null)
foreach (PeopleRole link in People) foreach (PeopleRole link in People)
link.Show = this; 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 } public enum Status { Finished, Airing, Planned, Unknown }
} }

View File

@ -3,31 +3,40 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// A studio that make shows.
/// </summary>
public class Studio : IResource public class Studio : IResource
{ {
/// <inheritdoc />
public int ID { get; set; } public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; } public string Slug { get; set; }
/// <summary>
/// The name of this studio.
/// </summary>
public string Name { get; set; } 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() { } 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) public Studio(string name)
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);
Name = name; Name = name;
} }
public Studio(string slug, string name)
{
Slug = slug;
Name = name;
}
public static Studio Default()
{
return new Studio("unknown", "Unknown Studio");
}
} }
} }

View File

@ -1,11 +1,16 @@
using Kyoo.Models.Watch; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
namespace Kyoo.Models 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 public enum StreamType
{ {
Unknown = 0, Unknown = 0,
@ -15,61 +20,106 @@ namespace Kyoo.Models
Attachment = 4 Attachment = 4
} }
namespace Watch /// <summary>
/// A video, audio or subtitle track for an episode.
/// </summary>
public class Track : IResource
{ {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] /// <inheritdoc />
public class Stream public int ID { get; set; }
/// <inheritdoc />
[Computed] public string Slug
{ {
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; } public string Title { get; set; }
/// <summary>
/// The language of this stream (as a ISO-639-2 language code)
/// </summary>
public string Language { get; set; } public string Language { get; set; }
/// <summary>
/// The codec of this stream.
/// </summary>
public string Codec { get; set; } public string Codec { get; set; }
[MarshalAs(UnmanagedType.I1)] public bool isDefault;
[MarshalAs(UnmanagedType.I1)] public bool isForced;
/// <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; } [SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The type of this stream.
/// </summary>
[SerializeIgnore] public StreamType Type { get; set; } [SerializeIgnore] public StreamType Type { get; set; }
public Stream() {} /// <summary>
/// The ID of the episode that uses this track.
public Stream(string title, string language, string codec, bool isDefault, bool isForced, string path, StreamType type) /// </summary>
{
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
{
public int ID { get; set; }
[SerializeIgnore] public int EpisodeID { get; set; } [SerializeIgnore] public int EpisodeID { get; set; }
public int TrackIndex { get; set; } /// <summary>
public bool IsDefault /// The episode that uses this track.
{ /// </summary>
get => isDefault; [LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; }
set => isDefault = value;
}
public bool IsForced
{
get => isForced;
set => isForced = value;
}
/// <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 public string DisplayName
{ {
get get
@ -85,61 +135,16 @@ namespace Kyoo.Models
name += " Forced"; name += " Forced";
if (IsExternal) if (IsExternal)
name += " (External)"; name += " (External)";
if (Title != null && Title.Length > 1) if (Title is {Length: > 1})
name += " - " + Title; name += " - " + Title;
return name; 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. //Converting mkv track language to c# system language tag.
private static string GetLanguage(string mkvLanguage) 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 return mkvLanguage switch
{ {
"fre" => "fra", "fre" => "fra",
@ -147,5 +152,32 @@ namespace Kyoo.Models
_ => mkvLanguage _ => 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;
}
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
@ -52,7 +53,7 @@ namespace Kyoo.Models
/// <summary> /// <summary>
/// Links between Users and Shows. /// Links between Users and Shows.
/// </summary> /// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; } [Link] public ICollection<Link<User, Show>> ShowLinks { get; set; }
#endif #endif
} }
@ -62,7 +63,7 @@ namespace Kyoo.Models
public class WatchedEpisode : Link<User, Episode> public class WatchedEpisode : Link<User, Episode>
{ {
/// <summary> /// <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> /// </summary>
public int WatchedPercentage { get; set; } public int WatchedPercentage { get; set; }
} }

View File

@ -2,14 +2,44 @@
namespace Kyoo.Models namespace Kyoo.Models
{ {
/// <summary>
/// Results of a search request.
/// </summary>
public class SearchResult public class SearchResult
{ {
public string Query; /// <summary>
public IEnumerable<Collection> Collections; /// The query of the search request.
public IEnumerable<Show> Shows; /// </summary>
public IEnumerable<Episode> Episodes; public string Query { get; init; }
public IEnumerable<People> People;
public IEnumerable<Genre> Genres; /// <summary>
public IEnumerable<Studio> Studios; /// 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; }
} }
} }

View File

@ -9,95 +9,136 @@ using PathIO = System.IO.Path;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Chapter /// <summary>
{ /// A watch item give information useful for playback.
public float StartTime; /// Information about tracks and display information that could be used by the player.
public float EndTime; /// This contains mostly data from an <see cref="Episode"/> with another form.
public string Name; /// </summary>
public Chapter(float startTime, float endTime, string name)
{
StartTime = startTime;
EndTime = endTime;
Name = name;
}
}
public class WatchItem public class WatchItem
{ {
/// <summary>
/// The ID of the episode associated with this item.
/// </summary>
public int EpisodeID { get; set; } public int EpisodeID { get; set; }
public string ShowTitle { get; set; } /// <summary>
public string ShowSlug { get; set; } /// The slug of this episode.
public int SeasonNumber { get; set; } /// </summary>
public int EpisodeNumber { get; set; }
public int AbsoluteNumber { get; set; }
public string Title { get; set; }
public string Slug { get; set; } 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; } 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; } [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; } 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; } public Episode NextEpisode { get; set; }
/// <summary>
/// <c>true</c> if this is a movie, <c>false</c> otherwise.
/// </summary>
public bool IsMovie { get; set; } 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; } [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; } [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; } [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; } public string Container { get; set; }
/// <summary>
/// The video track. See <see cref="Track"/> for more information.
/// </summary>
public Track Video { get; set; } 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; } 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; } 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 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) public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library)
{ {
Episode previous = null; Episode previous = null;
@ -106,41 +147,53 @@ namespace Kyoo.Models
await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks); 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) 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) else if (ep.SeasonNumber > 1)
{ {
int count = await library.GetCount<Episode>(x => x.ShowID == ep.ShowID previous = (await library.GetAll(x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber - 1); && x.SeasonNumber == ep.SeasonNumber.Value - 1,
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count); limit: 1,
sort: new Sort<Episode>(x => x.EpisodeNumber, true))
).FirstOrDefault();
} }
if (ep.EpisodeNumber >= await library.GetCount<Episode>(x => x.SeasonID == ep.SeasonID)) 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 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, return new WatchItem
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())
{ {
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, PreviousEpisode = previous,
NextEpisode = next, NextEpisode = next,
Chapters = await GetChapters(ep.Path) 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) private static async Task<ICollection<Chapter>> GetChapters(string episodePath)
{ {
string path = PathIO.Combine( string path = PathIO.Combine(

View File

@ -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&lt;&gt;).</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&lt;&gt;).</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&lt;string&gt; and typeof(IEnumerable&lt;&gt;) will return IEnumerable&lt;string&gt;
/// </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&lt;&gt;).</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&lt;string&gt; 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}>";
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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&lt;&gt;).</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&lt;&gt;).</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&lt;string&gt; and typeof(IEnumerable&lt;&gt;) will return IEnumerable&lt;string&gt;
/// </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&lt;&gt;).</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&lt;string&gt; 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}>";
}
}
}

View File

@ -194,7 +194,7 @@ namespace Kyoo.CommonApi
{ {
try try
{ {
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where)); await _repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {

View File

@ -61,10 +61,6 @@ namespace Kyoo
/// </summary> /// </summary>
public DbSet<Provider> Providers { get; set; } public DbSet<Provider> Providers { get; set; }
/// <summary> /// <summary>
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
/// </summary>
public DbSet<MetadataID> MetadataIds { get; set; }
/// <summary>
/// The list of registered users. /// The list of registered users.
/// </summary> /// </summary>
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
@ -79,6 +75,25 @@ namespace Kyoo
/// </summary> /// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; } 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> /// <summary>
/// Get a generic link between two resource types. /// Get a generic link between two resource types.
/// </summary> /// </summary>
@ -125,13 +140,27 @@ namespace Kyoo
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Track>() modelBuilder.Entity<Show>()
.Property(t => t.IsDefault) .HasMany(x => x.Seasons)
.ValueGeneratedNever(); .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<Track>() modelBuilder.Entity<Show>()
.Property(t => t.IsForced) .HasOne(x => x.Studio)
.ValueGeneratedNever(); .WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Provider>() modelBuilder.Entity<Provider>()
.HasMany(x => x.Libraries) .HasMany(x => x.Libraries)
@ -205,25 +234,41 @@ namespace Kyoo
.WithMany(x => x.ShowLinks), .WithMany(x => x.ShowLinks),
y => y.HasKey(Link<User, Show>.PrimaryKey)); y => y.HasKey(Link<User, Show>.PrimaryKey));
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID<Show>>()
.HasOne(x => x.Show) .HasKey(MetadataID<Show>.PrimaryKey);
modelBuilder.Entity<MetadataID<Show>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID<Season>>()
.HasOne(x => x.Season) .HasKey(MetadataID<Season>.PrimaryKey);
modelBuilder.Entity<MetadataID<Season>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID<Episode>>()
.HasOne(x => x.Episode) .HasKey(MetadataID<Episode>.PrimaryKey);
modelBuilder.Entity<MetadataID<Episode>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>() modelBuilder.Entity<MetadataID<People>>()
.HasOne(x => x.People) .HasKey(MetadataID<People>.PrimaryKey);
modelBuilder.Entity<MetadataID<People>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs) .WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade); .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); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WatchedEpisode>() modelBuilder.Entity<WatchedEpisode>()
@ -262,15 +307,34 @@ namespace Kyoo
modelBuilder.Entity<Season>() modelBuilder.Entity<Season>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber}) .HasIndex(x => new {x.ShowID, x.SeasonNumber})
.IsUnique(); .IsUnique();
modelBuilder.Entity<Season>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Episode>() modelBuilder.Entity<Episode>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
.IsUnique(); .IsUnique();
modelBuilder.Entity<Episode>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Track>() modelBuilder.Entity<Track>()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced}) .HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique(); .IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .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> /// <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> /// <summary>
/// Check if the exception is a duplicated exception. /// Check if the exception is a duplicated exception.
/// </summary> /// </summary>

View File

@ -12,8 +12,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" /> <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" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -225,8 +225,8 @@ namespace Kyoo.Controllers
T old = await GetWithTracking(edited.ID); T old = await GetWithTracking(edited.ID);
if (resetOld) if (resetOld)
Utility.Nullify(old); old = Merger.Nullify(old);
Utility.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null); Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
await EditRelations(old, edited, resetOld); await EditRelations(old, edited, resetOld);
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
return old; 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> /// <exception cref="ArgumentException">You can throw this if the resource is illegal and should not be saved.</exception>
protected virtual Task Validate(T resource) protected virtual Task Validate(T resource)
{ {
if (typeof(T).GetProperty(nameof(resource.Slug))!.GetCustomAttribute<ComputedAttribute>() != null)
return Task.CompletedTask;
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("Resource can't have null as a slug."); throw new ArgumentException("Resource can't have null as a slug.");
if (int.TryParse(resource.Slug, out int _)) if (int.TryParse(resource.Slug, out int _))
@ -295,31 +297,10 @@ namespace Kyoo.Controllers
public abstract Task Delete(T obj); public abstract Task Delete(T obj);
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<T> objs) public async Task DeleteAll(Expression<Func<T, bool>> where)
{ {
foreach (T obj in objs) foreach (T resource in await GetAll(where))
await Delete(obj); await Delete(resource);
}
/// <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);
} }
} }
} }

View File

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
@ -9,33 +8,34 @@
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <!-- <PropertyGroup>-->
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath> <!-- <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>-->
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <!-- <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>-->
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <!-- <ProduceReferenceAssembly>false</ProduceReferenceAssembly>-->
<GenerateDependencyFile>false</GenerateDependencyFile> <!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles> <!-- <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>-->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
</PropertyGroup> <!-- </PropertyGroup>-->
<ItemGroup> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.5.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj"> <ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
<PrivateAssets>all</PrivateAssets> <!-- <PrivateAssets>all</PrivateAssets>-->
<Private>false</Private> <!-- <Private>false</Private>-->
<ExcludeAssets>runtime</ExcludeAssets> <!-- <ExcludeAssets>runtime</ExcludeAssets>-->
</ProjectReference> </ProjectReference>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj"> <ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets> <!-- <PrivateAssets>all</PrivateAssets>-->
<Private>false</Private> <!-- <Private>false</Private>-->
<ExcludeAssets>runtime</ExcludeAssets> <!-- <ExcludeAssets>runtime</ExcludeAssets>-->
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

File diff suppressed because it is too large Load Diff

View 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");
}
}
}

File diff suppressed because it is too large Load Diff

View 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

View File

@ -27,16 +27,19 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
private readonly bool _skipConfigure; private readonly bool _skipConfigure;
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...) static PostgresContext()
/// </summary>
public PostgresContext()
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
} }
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
public PostgresContext() { }
/// <summary> /// <summary>
/// Create a new <see cref="PostgresContext"/> using specific options /// Create a new <see cref="PostgresContext"/> using specific options
/// </summary> /// </summary>
@ -44,9 +47,6 @@ namespace Kyoo.Postgresql
public PostgresContext(DbContextOptions options) public PostgresContext(DbContextOptions options)
: base(options) : base(options)
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<StreamType>();
_skipConfigure = true; _skipConfigure = true;
} }
@ -77,6 +77,7 @@ namespace Kyoo.Postgresql
optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging(); optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging();
} }
optionsBuilder.UseSnakeCaseNamingConvention();
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
} }
@ -90,6 +91,10 @@ namespace Kyoo.Postgresql
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>(); modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<LibraryItem>()
.ToView("library_items")
.HasKey(x => x.ID);
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.Property(x => x.ExtraData) .Property(x => x.ExtraData)
.HasColumnType("jsonb"); .HasColumnType("jsonb");
@ -107,7 +112,7 @@ namespace Kyoo.Postgresql
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format) 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); 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); return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
} }

View 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>

View File

@ -1,50 +1,41 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using Kyoo.SqLite;
using Kyoo.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations namespace Kyoo.SqLite.Migrations
{ {
[DbContext(typeof(PostgresContext))] [DbContext(typeof(SqLiteContext))]
[Migration("20210507203809_Initial")] [Migration("20210626141337_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasPostgresEnum(null, "item_type", new[] { "show", "movie", "collection" }) .HasAnnotation("ProductVersion", "5.0.7");
.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);
modelBuilder.Entity("Kyoo.Models.Collection", b => modelBuilder.Entity("Kyoo.Models.Collection", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Poster") b.Property<string>("Poster")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -58,46 +49,49 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AbsoluteNumber") b.Property<int?>("AbsoluteNumber")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber") b.Property<int?>("EpisodeNumber")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<DateTime?>("ReleaseDate") b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp without time zone"); .HasColumnType("TEXT");
b.Property<int>("Runtime")
.HasColumnType("integer");
b.Property<int?>("SeasonID") b.Property<int?>("SeasonID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SeasonNumber") b.Property<int?>("SeasonNumber")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("ShowID") b.Property<int>("ShowID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Thumb") b.Property<string>("Thumb")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("SeasonID"); b.HasIndex("SeasonID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber") b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber")
.IsUnique(); .IsUnique();
@ -108,15 +102,14 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -130,18 +123,17 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string[]>("Paths") b.Property<string>("Paths")
.HasColumnType("text[]"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -154,10 +146,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -169,10 +161,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -184,10 +176,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -199,10 +191,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -214,10 +206,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -229,10 +221,10 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -241,65 +233,105 @@ namespace Kyoo.Postgresql.Migrations
b.ToTable("Link<User, Show>"); b.ToTable("Link<User, Show>");
}); });
modelBuilder.Entity("Kyoo.Models.MetadataID", b => modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
{ {
b.Property<int>("ID") b.Property<int>("FirstID")
.ValueGeneratedOnAdd() .HasColumnType("INTEGER");
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID") b.Property<string>("DataID")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int?>("EpisodeID")
.HasColumnType("integer");
b.Property<string>("Link") b.Property<string>("Link")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int?>("PeopleID") b.HasKey("FirstID", "SecondID");
.HasColumnType("integer");
b.Property<int>("ProviderID") b.HasIndex("SecondID");
.HasColumnType("integer");
b.Property<int?>("SeasonID") b.ToTable("MetadataID<Episode>");
.HasColumnType("integer"); });
b.Property<int?>("ShowID") modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
.HasColumnType("integer"); {
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 => modelBuilder.Entity("Kyoo.Models.People", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Poster") b.Property<string>("Poster")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -313,20 +345,22 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("ForPeople")
.HasColumnType("INTEGER");
b.Property<int>("PeopleID") b.Property<int>("PeopleID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<string>("Role") b.Property<string>("Role")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int>("ShowID") b.Property<int>("ShowID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<string>("Type") b.Property<string>("Type")
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -341,21 +375,20 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Logo") b.Property<string>("Logo")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("LogoExtension") b.Property<string>("LogoExtension")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -369,29 +402,38 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Poster") b.Property<string>("Poster")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int>("SeasonNumber") b.Property<int>("SeasonNumber")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("ShowID") 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") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int?>("Year")
.HasColumnType("integer");
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber") b.HasIndex("ShowID", "SeasonNumber")
.IsUnique(); .IsUnique();
@ -402,51 +444,50 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string[]>("Aliases") b.Property<string>("Aliases")
.HasColumnType("text[]"); .HasColumnType("TEXT");
b.Property<string>("Backdrop") b.Property<string>("Backdrop")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int?>("EndYear") b.Property<DateTime?>("EndAir")
.HasColumnType("integer"); .HasColumnType("TEXT");
b.Property<bool>("IsMovie") b.Property<bool>("IsMovie")
.HasColumnType("boolean"); .HasColumnType("INTEGER");
b.Property<string>("Logo") b.Property<string>("Logo")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Poster") b.Property<string>("Poster")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int?>("StartYear") b.Property<DateTime?>("StartAir")
.HasColumnType("integer"); .HasColumnType("TEXT");
b.Property<Status?>("Status") b.Property<int?>("Status")
.HasColumnType("status"); .HasColumnType("INTEGER");
b.Property<int?>("StudioID") b.Property<int?>("StudioID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("TrailerUrl") b.Property<string>("TrailerUrl")
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -462,15 +503,14 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -484,41 +524,47 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Codec") b.Property<string>("Codec")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int>("EpisodeID") b.Property<int>("EpisodeID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<bool>("IsDefault") b.Property<bool>("IsDefault")
.HasColumnType("boolean"); .HasColumnType("INTEGER");
b.Property<bool>("IsExternal") b.Property<bool>("IsExternal")
.HasColumnType("boolean"); .HasColumnType("INTEGER");
b.Property<bool>("IsForced") b.Property<bool>("IsForced")
.HasColumnType("boolean"); .HasColumnType("INTEGER");
b.Property<string>("Language") b.Property<string>("Language")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<int>("TrackIndex") b.Property<int>("TrackIndex")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<StreamType>("Type") b.Property<int>("Type")
.HasColumnType("stream_type"); .HasColumnType("INTEGER");
b.HasKey("ID"); b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced") b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique(); .IsUnique();
@ -529,27 +575,26 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("INTEGER");
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Email") b.Property<string>("Email")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<Dictionary<string, string>>("ExtraData") b.Property<string>("ExtraData")
.HasColumnType("jsonb"); .HasColumnType("TEXT");
b.Property<string>("Password") b.Property<string>("Password")
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string[]>("Permissions") b.Property<string>("Permissions")
.HasColumnType("text[]"); .HasColumnType("TEXT");
b.Property<string>("Slug") b.Property<string>("Slug")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("TEXT");
b.Property<string>("Username") b.Property<string>("Username")
.HasColumnType("text"); .HasColumnType("TEXT");
b.HasKey("ID"); b.HasKey("ID");
@ -562,13 +607,13 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b => modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("SecondID") b.Property<int>("SecondID")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.Property<int>("WatchedPercentage") b.Property<int>("WatchedPercentage")
.HasColumnType("integer"); .HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID"); b.HasKey("FirstID", "SecondID");
@ -581,7 +626,8 @@ namespace Kyoo.Postgresql.Migrations
{ {
b.HasOne("Kyoo.Models.Season", "Season") b.HasOne("Kyoo.Models.Season", "Season")
.WithMany("Episodes") .WithMany("Episodes")
.HasForeignKey("SeasonID"); .HasForeignKey("SeasonID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.Show", "Show") b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Episodes") .WithMany("Episodes")
@ -708,43 +754,80 @@ namespace Kyoo.Postgresql.Migrations
b.Navigation("Second"); 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") .WithMany("ExternalIDs")
.HasForeignKey("EpisodeID") .HasForeignKey("FirstID")
.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")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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") .WithMany("ExternalIDs")
.HasForeignKey("SeasonID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade); .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") .WithMany("ExternalIDs")
.HasForeignKey("ShowID") .HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade); .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 => modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
@ -854,8 +937,6 @@ namespace Kyoo.Postgresql.Migrations
modelBuilder.Entity("Kyoo.Models.Provider", b => modelBuilder.Entity("Kyoo.Models.Provider", b =>
{ {
b.Navigation("LibraryLinks"); b.Navigation("LibraryLinks");
b.Navigation("MetadataLinks");
}); });
modelBuilder.Entity("Kyoo.Models.Season", b => modelBuilder.Entity("Kyoo.Models.Season", b =>

View File

@ -1,30 +1,22 @@
using System; using System;
using System.Collections.Generic;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations namespace Kyoo.SqLite.Migrations
{ {
public partial class Initial : Migration public partial class Initial : Migration
{ {
protected override void Up(MigrationBuilder migrationBuilder) 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( migrationBuilder.CreateTable(
name: "Collections", name: "Collections",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "text", nullable: true), Poster = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "text", nullable: true) Overview = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -35,10 +27,10 @@ namespace Kyoo.Postgresql.Migrations
name: "Genres", name: "Genres",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true) Name = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -49,11 +41,11 @@ namespace Kyoo.Postgresql.Migrations
name: "Libraries", name: "Libraries",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Paths = table.Column<string[]>(type: "text[]", nullable: true) Paths = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -64,11 +56,11 @@ namespace Kyoo.Postgresql.Migrations
name: "People", name: "People",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "text", nullable: true) Poster = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -79,12 +71,12 @@ namespace Kyoo.Postgresql.Migrations
name: "Providers", name: "Providers",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true), Name = table.Column<string>(type: "TEXT", nullable: true),
Logo = table.Column<string>(type: "text", nullable: true), Logo = table.Column<string>(type: "TEXT", nullable: true),
LogoExtension = table.Column<string>(type: "text", nullable: true) LogoExtension = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -95,10 +87,10 @@ namespace Kyoo.Postgresql.Migrations
name: "Studios", name: "Studios",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "text", nullable: true) Name = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -109,14 +101,14 @@ namespace Kyoo.Postgresql.Migrations
name: "Users", name: "Users",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Username = table.Column<string>(type: "text", nullable: true), Username = table.Column<string>(type: "TEXT", nullable: true),
Email = table.Column<string>(type: "text", nullable: true), Email = table.Column<string>(type: "TEXT", nullable: true),
Password = table.Column<string>(type: "text", nullable: true), Password = table.Column<string>(type: "TEXT", nullable: true),
Permissions = 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) ExtraData = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -127,8 +119,8 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<Library, Collection>", name: "Link<Library, Collection>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -151,8 +143,8 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<Library, Provider>", name: "Link<Library, Provider>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -171,26 +163,52 @@ namespace Kyoo.Postgresql.Migrations
onDelete: ReferentialAction.Cascade); 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( migrationBuilder.CreateTable(
name: "Shows", name: "Shows",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
Slug = table.Column<string>(type: "text", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: false),
Title = table.Column<string>(type: "text", nullable: true), Title = table.Column<string>(type: "TEXT", nullable: true),
Aliases = table.Column<string[]>(type: "text[]", nullable: true), Aliases = table.Column<string>(type: "TEXT", nullable: true),
Path = table.Column<string>(type: "text", nullable: true), Path = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "text", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<Status>(type: "status", nullable: true), Status = table.Column<int>(type: "INTEGER", nullable: true),
TrailerUrl = table.Column<string>(type: "text", nullable: true), TrailerUrl = table.Column<string>(type: "TEXT", nullable: true),
StartYear = table.Column<int>(type: "integer", nullable: true), StartAir = table.Column<DateTime>(type: "TEXT", nullable: true),
EndYear = table.Column<int>(type: "integer", nullable: true), EndAir = table.Column<DateTime>(type: "TEXT", nullable: true),
Poster = table.Column<string>(type: "text", nullable: true), Poster = table.Column<string>(type: "TEXT", nullable: true),
Logo = table.Column<string>(type: "text", nullable: true), Logo = table.Column<string>(type: "TEXT", nullable: true),
Backdrop = table.Column<string>(type: "text", nullable: true), Backdrop = table.Column<string>(type: "TEXT", nullable: true),
IsMovie = table.Column<bool>(type: "boolean", nullable: false), IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
StudioID = table.Column<int>(type: "integer", nullable: true) StudioID = table.Column<int>(type: "INTEGER", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -200,15 +218,15 @@ namespace Kyoo.Postgresql.Migrations
column: x => x.StudioID, column: x => x.StudioID,
principalTable: "Studios", principalTable: "Studios",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.SetNull);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Collection, Show>", name: "Link<Collection, Show>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -231,8 +249,8 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<Library, Show>", name: "Link<Library, Show>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -255,8 +273,8 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<Show, Genre>", name: "Link<Show, Genre>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -279,8 +297,8 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<User, Show>", name: "Link<User, Show>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false) SecondID = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -299,16 +317,43 @@ namespace Kyoo.Postgresql.Migrations
onDelete: ReferentialAction.Cascade); 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( migrationBuilder.CreateTable(
name: "PeopleRoles", name: "PeopleRoles",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
PeopleID = table.Column<int>(type: "integer", nullable: false), ForPeople = table.Column<bool>(type: "INTEGER", nullable: false),
ShowID = table.Column<int>(type: "integer", nullable: false), PeopleID = table.Column<int>(type: "INTEGER", nullable: false),
Role = table.Column<string>(type: "text", nullable: true), ShowID = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "text", nullable: true) Type = table.Column<string>(type: "TEXT", nullable: true),
Role = table.Column<string>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -331,14 +376,16 @@ namespace Kyoo.Postgresql.Migrations
name: "Seasons", name: "Seasons",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
ShowID = table.Column<int>(type: "integer", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: true),
SeasonNumber = table.Column<int>(type: "integer", nullable: false), ShowID = table.Column<int>(type: "INTEGER", nullable: false),
Title = table.Column<string>(type: "text", nullable: true), SeasonNumber = table.Column<int>(type: "INTEGER", nullable: false),
Overview = table.Column<string>(type: "text", nullable: true), Title = table.Column<string>(type: "TEXT", nullable: true),
Year = table.Column<int>(type: "integer", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
Poster = 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 => constraints: table =>
{ {
@ -355,19 +402,19 @@ namespace Kyoo.Postgresql.Migrations
name: "Episodes", name: "Episodes",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
ShowID = table.Column<int>(type: "integer", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: true),
SeasonID = table.Column<int>(type: "integer", nullable: true), ShowID = table.Column<int>(type: "INTEGER", nullable: false),
SeasonNumber = table.Column<int>(type: "integer", nullable: false), SeasonID = table.Column<int>(type: "INTEGER", nullable: true),
EpisodeNumber = table.Column<int>(type: "integer", nullable: false), SeasonNumber = table.Column<int>(type: "INTEGER", nullable: true),
AbsoluteNumber = table.Column<int>(type: "integer", nullable: false), EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: true),
Path = table.Column<string>(type: "text", nullable: true), AbsoluteNumber = table.Column<int>(type: "INTEGER", nullable: true),
Thumb = table.Column<string>(type: "text", nullable: true), Path = table.Column<string>(type: "TEXT", nullable: true),
Title = table.Column<string>(type: "text", nullable: true), Thumb = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "text", nullable: true), Title = table.Column<string>(type: "TEXT", nullable: true),
ReleaseDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: true), Overview = table.Column<string>(type: "TEXT", nullable: true),
Runtime = table.Column<int>(type: "integer", nullable: false) ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -377,7 +424,7 @@ namespace Kyoo.Postgresql.Migrations
column: x => x.SeasonID, column: x => x.SeasonID,
principalTable: "Seasons", principalTable: "Seasons",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Episodes_Shows_ShowID", name: "FK_Episodes_Shows_ShowID",
column: x => x.ShowID, column: x => x.ShowID,
@ -387,50 +434,53 @@ namespace Kyoo.Postgresql.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MetadataIds", name: "MetadataID<Season>",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) FirstID = table.Column<int>(type: "INTEGER", nullable: false),
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), SecondID = table.Column<int>(type: "INTEGER", nullable: false),
ProviderID = table.Column<int>(type: "integer", nullable: false), DataID = table.Column<string>(type: "TEXT", nullable: true),
ShowID = table.Column<int>(type: "integer", nullable: true), Link = table.Column<string>(type: "TEXT", 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)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MetadataIds", x => x.ID); table.PrimaryKey("PK_MetadataID<Season>", x => new { x.FirstID, x.SecondID });
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataIds_Episodes_EpisodeID", name: "FK_MetadataID<Season>_Providers_SecondID",
column: x => x.EpisodeID, column: x => x.SecondID,
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,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MetadataIds_Seasons_SeasonID", name: "FK_MetadataID<Season>_Seasons_FirstID",
column: x => x.SeasonID, column: x => x.FirstID,
principalTable: "Seasons", principalTable: "Seasons",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); 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( table.ForeignKey(
name: "FK_MetadataIds_Shows_ShowID", name: "FK_MetadataID<Episode>_Episodes_FirstID",
column: x => x.ShowID, column: x => x.FirstID,
principalTable: "Shows", principalTable: "Episodes",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataID<Episode>_Providers_SecondID",
column: x => x.SecondID,
principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -439,18 +489,19 @@ namespace Kyoo.Postgresql.Migrations
name: "Tracks", name: "Tracks",
columns: table => new columns: table => new
{ {
ID = table.Column<int>(type: "integer", nullable: false) ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Sqlite:Autoincrement", true),
EpisodeID = table.Column<int>(type: "integer", nullable: false), Slug = table.Column<string>(type: "TEXT", nullable: true),
TrackIndex = table.Column<int>(type: "integer", nullable: false), Title = table.Column<string>(type: "TEXT", nullable: true),
IsDefault = table.Column<bool>(type: "boolean", nullable: false), Language = table.Column<string>(type: "TEXT", nullable: true),
IsForced = table.Column<bool>(type: "boolean", nullable: false), Codec = table.Column<string>(type: "TEXT", nullable: true),
IsExternal = table.Column<bool>(type: "boolean", nullable: false), IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
Title = table.Column<string>(type: "text", nullable: true), IsForced = table.Column<bool>(type: "INTEGER", nullable: false),
Language = table.Column<string>(type: "text", nullable: true), IsExternal = table.Column<bool>(type: "INTEGER", nullable: false),
Codec = table.Column<string>(type: "text", nullable: true), Path = table.Column<string>(type: "TEXT", nullable: true),
Path = table.Column<string>(type: "text", nullable: true), Type = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<StreamType>(type: "stream_type", nullable: false) EpisodeID = table.Column<int>(type: "INTEGER", nullable: false),
TrackIndex = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -467,9 +518,9 @@ namespace Kyoo.Postgresql.Migrations
name: "WatchedEpisodes", name: "WatchedEpisodes",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "INTEGER", nullable: false),
SecondID = table.Column<int>(type: "integer", nullable: false), SecondID = table.Column<int>(type: "INTEGER", nullable: false),
WatchedPercentage = table.Column<int>(type: "integer", nullable: false) WatchedPercentage = table.Column<int>(type: "INTEGER", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -505,6 +556,12 @@ namespace Kyoo.Postgresql.Migrations
columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" }, columns: new[] { "ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_Episodes_Slug",
table: "Episodes",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Genres_Slug", name: "IX_Genres_Slug",
table: "Genres", table: "Genres",
@ -548,29 +605,24 @@ namespace Kyoo.Postgresql.Migrations
column: "SecondID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_EpisodeID", name: "IX_MetadataID<Episode>_SecondID",
table: "MetadataIds", table: "MetadataID<Episode>",
column: "EpisodeID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_PeopleID", name: "IX_MetadataID<People>_SecondID",
table: "MetadataIds", table: "MetadataID<People>",
column: "PeopleID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_ProviderID", name: "IX_MetadataID<Season>_SecondID",
table: "MetadataIds", table: "MetadataID<Season>",
column: "ProviderID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MetadataIds_SeasonID", name: "IX_MetadataID<Show>_SecondID",
table: "MetadataIds", table: "MetadataID<Show>",
column: "SeasonID"); column: "SecondID");
migrationBuilder.CreateIndex(
name: "IX_MetadataIds_ShowID",
table: "MetadataIds",
column: "ShowID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_People_Slug", name: "IX_People_Slug",
@ -600,6 +652,12 @@ namespace Kyoo.Postgresql.Migrations
columns: new[] { "ShowID", "SeasonNumber" }, columns: new[] { "ShowID", "SeasonNumber" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_Seasons_Slug",
table: "Seasons",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Shows_Slug", name: "IX_Shows_Slug",
table: "Shows", table: "Shows",
@ -623,6 +681,12 @@ namespace Kyoo.Postgresql.Migrations
columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" }, columns: new[] { "EpisodeID", "Type", "Language", "TrackIndex", "IsForced" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_Tracks_Slug",
table: "Tracks",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Users_Slug", name: "IX_Users_Slug",
table: "Users", table: "Users",
@ -656,7 +720,16 @@ namespace Kyoo.Postgresql.Migrations
name: "Link<User, Show>"); name: "Link<User, Show>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MetadataIds"); name: "MetadataID<Episode>");
migrationBuilder.DropTable(
name: "MetadataID<People>");
migrationBuilder.DropTable(
name: "MetadataID<Season>");
migrationBuilder.DropTable(
name: "MetadataID<Show>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PeopleRoles"); name: "PeopleRoles");

View 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
}
}
}

View 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;");
}
}
}

View 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
}
}
}

View 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);
}
}
}

View 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
View 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);
}
}
}

View File

@ -14,8 +14,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" /> <PackageReference Include="Divergic.Logging.Xunit" Version="3.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <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" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View 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();
}
}
}

View 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());
}
}
}

View File

@ -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());
// }
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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));
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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());
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View File

@ -1,79 +1,203 @@
// using Kyoo.Models; using System;
// using Microsoft.Data.Sqlite; using System.Threading.Tasks;
// using Microsoft.EntityFrameworkCore; using Kyoo.Postgresql;
// using Kyoo.SqLite;
// namespace Kyoo.Tests using Microsoft.Data.Sqlite;
// { using Microsoft.EntityFrameworkCore;
// /// <summary> using Microsoft.Extensions.Logging;
// /// Class responsible to fill and create in memory databases for unit tests. using Npgsql;
// /// </summary> using Xunit;
// public class TestContext using Xunit.Abstractions;
// {
// /// <summary> namespace Kyoo.Tests
// /// The context's options that specify to use an in memory Sqlite database. {
// /// </summary> public sealed class SqLiteTestContext : TestContext
// private readonly DbContextOptions<DatabaseContext> _context; {
// /// <summary>
// /// <summary> /// The internal sqlite connection used by all context returned by this class.
// /// Create a new database and fill it with information. /// </summary>
// /// </summary> private readonly SqliteConnection _connection;
// public TestContext()
// { /// <summary>
// SqliteConnection connection = new("DataSource=:memory:"); /// The context's options that specify to use an in memory Sqlite database.
// connection.Open(); /// </summary>
// private readonly DbContextOptions<DatabaseContext> _context;
// try
// { public SqLiteTestContext(ITestOutputHelper output)
// _context = new DbContextOptionsBuilder<DatabaseContext>() {
// .UseSqlite(connection) _connection = new SqliteConnection("DataSource=:memory:");
// .Options; _connection.Open();
// FillDatabase();
// } _context = new DbContextOptionsBuilder<DatabaseContext>()
// finally .UseSqlite(_connection)
// { .UseLoggerFactory(LoggerFactory.Create(x =>
// connection.Close(); {
// } x.ClearProviders();
// } x.AddXunit(output);
// }))
// /// <summary> .EnableSensitiveDataLogging()
// /// Fill the database with pre defined values using a clean context. .EnableDetailedErrors()
// /// </summary> .Options;
// private void FillDatabase()
// { using DatabaseContext context = New();
// using DatabaseContext context = new(_context); context.Database.Migrate();
// context.Shows.Add(new Show TestSample.FillDatabase(context);
// { }
// ID = 67,
// Slug = "anohana", public override void Dispose()
// Title = "Anohana: The Flower We Saw That Day", {
// Aliases = new[] _connection.Close();
// { }
// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
// "AnoHana", public override async ValueTask DisposeAsync()
// "We Still Don't Know the Name of the Flower We Saw That Day." {
// }, await _connection.CloseAsync();
// 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.", public override DatabaseContext New()
// Status = Status.Finished, {
// TrailerUrl = null, return new SqLiteContext(_context);
// StartYear = 2011, }
// EndYear = 2011, }
// Poster = "poster",
// Logo = "logo", [CollectionDefinition(nameof(Postgresql))]
// Backdrop = "backdrop", public class PostgresCollection : ICollectionFixture<PostgresFixture>
// IsMovie = false, {}
// Studio = null
// }); public sealed class PostgresFixture : IDisposable
// } {
// private readonly DbContextOptions<DatabaseContext> _options;
// /// <summary>
// /// Get a new database context connected to a in memory Sqlite database. public string Template { get; }
// /// </summary>
// /// <returns>A valid DatabaseContext</returns> public string Connection => PostgresTestContext.GetConnectionString(Template);
// public DatabaseContext New()
// { public PostgresFixture()
// return new(_context); {
// } // 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();
}
}

View 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)
};
}
}
}

View 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(() => {}));
}
}
}

View 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);
}
}
}

View 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));
}
}
}

View File

@ -13,12 +13,23 @@ namespace Kyoo.Tests
Expression<Func<Show, int>> member = x => x.ID; Expression<Func<Show, int>> member = x => x.ID;
Expression<Func<Show, object>> memberCast = 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(member));
Assert.True(Utility.IsPropertyExpression(memberCast)); Assert.True(Utility.IsPropertyExpression(memberCast));
Expression<Func<Show, object>> call = x => x.GetID("test"); Expression<Func<Show, object>> call = x => x.GetID("test");
Assert.False(Utility.IsPropertyExpression(call)); 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));
}
} }
} }

View File

@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Pos
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\Kyoo.SqLite.csproj", "{6515380E-1E57-42DA-B6E3-E1C8A848818A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@ -29,7 +29,7 @@ namespace Kyoo.Controllers
{ {
try try
{ {
ret = Utility.Merge(ret, await providerCall(provider)); ret = Merger.Merge(ret, await providerCall(provider));
} catch (Exception ex) } catch (Exception ex)
{ {
await Console.Error.WriteLineAsync( await Console.Error.WriteLineAsync(
@ -122,16 +122,15 @@ namespace Kyoo.Controllers
season.Show = show; season.Show = show;
season.ShowID = show.ID; season.ShowID = show.ID;
season.ShowSlug = show.Slug; season.ShowSlug = show.Slug;
season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber;
season.Title ??= $"Season {season.SeasonNumber}"; season.Title ??= $"Season {season.SeasonNumber}";
return season; return season;
} }
public async Task<Episode> GetEpisode(Show show, public async Task<Episode> GetEpisode(Show show,
string episodePath, string episodePath,
int seasonNumber, int? seasonNumber,
int episodeNumber, int? episodeNumber,
int absoluteNumber, int? absoluteNumber,
Library library) Library library)
{ {
Episode episode = await GetMetadata( Episode episode = await GetMetadata(
@ -142,9 +141,9 @@ namespace Kyoo.Controllers
episode.ShowID = show.ID; episode.ShowID = show.ID;
episode.ShowSlug = show.Slug; episode.ShowSlug = show.Slug;
episode.Path = episodePath; episode.Path = episodePath;
episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; episode.SeasonNumber ??= seasonNumber;
episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber; episode.EpisodeNumber ??= episodeNumber;
episode.AbsoluteNumber = episode.AbsoluteNumber != -1 ? episode.AbsoluteNumber : absoluteNumber; episode.AbsoluteNumber ??= absoluteNumber;
return episode; return episode;
} }

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
@ -16,7 +15,7 @@ namespace Kyoo.Controllers
public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository
{ {
/// <summary> /// <summary>
/// The databse handle /// The database handle
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary> /// <summary>
@ -24,10 +23,6 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary> /// <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. /// A track repository to handle creation and deletion of tracks related to the current episode.
/// </summary> /// </summary>
private readonly ITrackRepository _tracks; private readonly ITrackRepository _tracks;
@ -41,66 +36,31 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="database">The database handle to use.</param> /// <param name="database">The database handle to use.</param>
/// <param name="providers">A provider repository</param> /// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="tracks">A track repository</param> /// <param name="tracks">A track repository</param>
public EpisodeRepository(DatabaseContext database, public EpisodeRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers,
IShowRepository shows,
ITrackRepository tracks) ITrackRepository tracks)
: base(database) : base(database)
{ {
_database = database; _database = database;
_providers = providers; _providers = providers;
_shows = shows;
_tracks = tracks; _tracks = tracks;
} }
/// <inheritdoc /> /// <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); return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
if (ret != null) && x.SeasonNumber == seasonNumber
ret.ShowSlug = await _shows.GetSlug(ret.ShowID); && x.EpisodeNumber == episodeNumber);
return ret; }
}
/// <inheritdoc />
/// <inheritdoc /> public Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
public override async Task<Episode> GetOrDefault(string slug) {
{ return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
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.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -122,59 +82,28 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <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 return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber && x.AbsoluteNumber == absoluteNumber);
&& x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
} }
/// <inheritdoc /> /// <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 return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber); && 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;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query) public override async Task<ICollection<Episode>> Search(string query)
{ {
List<Episode> episodes = await _database.Episodes return await _database.Episodes
.Where(x => x.EpisodeNumber != -1) .Where(x => x.EpisodeNumber != null)
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%")) .Where(_database.Like<Episode>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .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 /> /// <inheritdoc />
@ -185,6 +114,9 @@ namespace Kyoo.Controllers
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added); obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
return await ValidateTracks(obj); return await ValidateTracks(obj);
// TODO check if this is needed
// obj.Slug = await _database.Entry(obj).Property(x => x.Slug).
// return obj;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -195,7 +127,7 @@ namespace Kyoo.Controllers
if (changed.Tracks != null || resetOld) 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; resource.Tracks = changed.Tracks;
await ValidateTracks(resource); await ValidateTracks(resource);
} }
@ -213,18 +145,15 @@ namespace Kyoo.Controllers
/// Set track's index and ensure that every tracks is well-formed. /// Set track's index and ensure that every tracks is well-formed.
/// </summary> /// </summary>
/// <param name="resource">The resource to fix.</param> /// <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) 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.Episode = resource;
x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language x.EpisodeSlug = resource.Slug;
&& x.IsForced == y.IsForced
&& x.Codec == y.Codec
&& x.Type == y.Type);
return _tracks.Create(x); return _tracks.Create(x);
}).ToListAsync(); }).ToListAsync());
return resource; return resource;
} }
@ -232,13 +161,12 @@ namespace Kyoo.Controllers
protected override async Task Validate(Episode resource) protected override async Task Validate(Episode resource)
{ {
await base.Validate(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.Second = await _providers.CreateIfNotExists(x.Second);
x.ProviderID = x.Provider.ID; x.SecondID = x.Second.ID;
_database.Entry(x.Provider).State = EntityState.Detached; _database.Entry(x.Second).State = EntityState.Detached;
return x; });
}).ToListAsync();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -22,14 +22,6 @@ namespace Kyoo.Controllers
/// A lazy loaded library repository to validate queries (check if a library does exist) /// A lazy loaded library repository to validate queries (check if a library does exist)
/// </summary> /// </summary>
private readonly Lazy<ILibraryRepository> _libraries; 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 /> /// <inheritdoc />
protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title; protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title;
@ -38,60 +30,41 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// Create a new <see cref="LibraryItemRepository"/>. /// Create a new <see cref="LibraryItemRepository"/>.
/// </summary> /// </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="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, public LibraryItemRepository(DatabaseContext database,
Lazy<ILibraryRepository> libraries, Lazy<ILibraryRepository> libraries)
Lazy<IShowRepository> shows,
Lazy<ICollectionRepository> collections)
: base(database) : base(database)
{ {
_database = database; _database = database;
_libraries = libraries; _libraries = libraries;
_shows = shows;
_collections = collections;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<LibraryItem> GetOrDefault(int id) public override Task<LibraryItem> GetOrDefault(int id)
{ {
return id > 0 return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id);
? new LibraryItem(await _shows.Value.GetOrDefault(id))
: new LibraryItem(await _collections.Value.GetOrDefault(-id));
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> GetOrDefault(string slug) 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 /> /// <inheritdoc />
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null, public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return ApplyFilters(ItemsQuery, where, sort, limit); return ApplyFilters(_database.LibraryItems, where, sort, limit);
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null) public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
{ {
IQueryable<LibraryItem> query = ItemsQuery; IQueryable<LibraryItem> query = _database.LibraryItems;
if (where != null) if (where != null)
query = query.Where(where); query = query.Where(where);
return query.CountAsync(); return query.CountAsync();
@ -100,7 +73,7 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query) public override async Task<ICollection<LibraryItem>> Search(string query)
{ {
return await ItemsQuery return await _database.LibraryItems
.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%")) .Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
@ -109,7 +82,6 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException(); public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />

View File

@ -63,9 +63,12 @@ namespace Kyoo.Controllers
protected override async Task Validate(Library resource) protected override async Task Validate(Library resource)
{ {
await base.Validate(resource); await base.Validate(resource);
resource.Providers = await resource.Providers await resource.ProviderLinks.ForEachAsync(async id =>
.SelectAsync(x => _providers.CreateIfNotExists(x)) {
.ToListAsync(); id.Second = await _providers.CreateIfNotExists(id.Second);
id.SecondID = id.Second.ID;
_database.Entry(id.Second).State = EntityState.Detached;
});
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -73,9 +73,9 @@ namespace Kyoo.Controllers
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id => await resource.ExternalIDs.ForEachAsync(async id =>
{ {
id.Provider = await _providers.CreateIfNotExists(id.Provider); id.Second = await _providers.CreateIfNotExists(id.Second);
id.ProviderID = id.Provider.ID; id.SecondID = id.Second.ID;
_database.Entry(id.Provider).State = EntityState.Detached; _database.Entry(id.Second).State = EntityState.Detached;
}); });
await resource.Roles.ForEachAsync(async role => await resource.Roles.ForEachAsync(async role =>
{ {

View File

@ -58,18 +58,18 @@ namespace Kyoo.Controllers
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
obj.MetadataLinks.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null, public Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
Sort<MetadataID> sort = default, Sort<MetadataID<T>> sort = default,
Pagination limit = default) Pagination limit = default)
where T : class, IResource
{ {
return ApplyFilters(_database.MetadataIds.Include(y => y.Provider), return ApplyFilters(_database.MetadataIds<T>().Include(y => y.Second),
x => _database.MetadataIds.FirstOrDefaultAsync(y => y.ID == x), x => _database.MetadataIds<T>().FirstOrDefaultAsync(y => y.FirstID == x),
x => x.ID, x => x.FirstID,
where, where,
sort, sort,
limit); limit);

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
@ -23,64 +22,22 @@ namespace Kyoo.Controllers
/// A provider repository to handle externalID creation and deletion /// A provider repository to handle externalID creation and deletion
/// </summary> /// </summary>
private readonly IProviderRepository _providers; 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/> /// <inheritdoc/>
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber; protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
/// <summary> /// <summary>
/// Create a new <see cref="SeasonRepository"/> using the provided handle, a provider & a show repository and /// Create a new <see cref="SeasonRepository"/>.
/// a service provider to lazilly request an episode repository.
/// </summary> /// </summary>
/// <param name="database">The database handle that will be used</param> /// <param name="database">The database handle that will be used</param>
/// <param name="providers">A provider repository</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, public SeasonRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers)
IShowRepository shows,
Lazy<IEpisodeRepository> episodes)
: base(database) : base(database)
{ {
_database = database; _database = database;
_providers = providers; _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/> /// <inheritdoc/>
@ -89,7 +46,6 @@ namespace Kyoo.Controllers
Season ret = await GetOrDefault(showID, seasonNumber); Season ret = await GetOrDefault(showID, seasonNumber);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}"); throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID);
return ret; return ret;
} }
@ -99,7 +55,6 @@ namespace Kyoo.Controllers
Season ret = await GetOrDefault(showSlug, seasonNumber); Season ret = await GetOrDefault(showSlug, seasonNumber);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}"); throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug;
return ret; return ret;
} }
@ -120,25 +75,11 @@ namespace Kyoo.Controllers
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query) 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}%")) .Where(_database.Like<Season>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort) .OrderBy(DefaultSort)
.Take(20) .Take(20)
.ToListAsync(); .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/> /// <inheritdoc/>
@ -160,9 +101,9 @@ namespace Kyoo.Controllers
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id => await resource.ExternalIDs.ForEachAsync(async id =>
{ {
id.Provider = await _providers.CreateIfNotExists(id.Provider); id.Second = await _providers.CreateIfNotExists(id.Second);
id.ProviderID = id.Provider.ID; id.SecondID = id.Second.ID;
_database.Entry(id.Provider).State = EntityState.Detached; _database.Entry(id.Second).State = EntityState.Detached;
}); });
} }
@ -183,12 +124,8 @@ namespace Kyoo.Controllers
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Remove(obj);
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
if (obj.Episodes != null)
await _episodes.Value.DeleteRange(obj.Episodes);
} }
} }
} }

View File

@ -33,14 +33,6 @@ namespace Kyoo.Controllers
/// A provider repository to handle externalID creation and deletion /// A provider repository to handle externalID creation and deletion
/// </summary> /// </summary>
private readonly IProviderRepository _providers; 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 /> /// <inheritdoc />
protected override Expression<Func<Show, object>> DefaultSort => x => x.Title; 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="people">A people repository</param>
/// <param name="genres">A genres repository</param> /// <param name="genres">A genres repository</param>
/// <param name="providers">A provider 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, public ShowRepository(DatabaseContext database,
IStudioRepository studios, IStudioRepository studios,
IPeopleRepository people, IPeopleRepository people,
IGenreRepository genres, IGenreRepository genres,
IProviderRepository providers, IProviderRepository providers)
Lazy<ISeasonRepository> seasons,
Lazy<IEpisodeRepository> episodes)
: base(database) : base(database)
{ {
_database = database; _database = database;
@ -69,8 +57,6 @@ namespace Kyoo.Controllers
_people = people; _people = people;
_genres = genres; _genres = genres;
_providers = providers; _providers = providers;
_seasons = seasons;
_episodes = episodes;
} }
@ -103,17 +89,21 @@ namespace Kyoo.Controllers
await base.Validate(resource); await base.Validate(resource);
if (resource.Studio != null) if (resource.Studio != null)
resource.Studio = await _studios.CreateIfNotExists(resource.Studio); resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.Genres = await resource.Genres
.SelectAsync(x => _genres.CreateIfNotExists(x))
.ToListAsync();
resource.GenreLinks = resource.Genres? resource.GenreLinks = resource.Genres?
.Select(x => Link.UCreate(resource, x)) .Select(x => Link.Create(resource, x))
.ToList(); .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 => await resource.ExternalIDs.ForEachAsync(async id =>
{ {
id.Provider = await _providers.CreateIfNotExists(id.Provider); id.Second = await _providers.CreateIfNotExists(id.Second);
id.ProviderID = id.Provider.ID; id.SecondID = id.Second.ID;
_database.Entry(id.Provider).State = EntityState.Detached; _database.Entry(id.Second).State = EntityState.Detached;
}); });
await resource.People.ForEachAsync(async role => await resource.People.ForEachAsync(async role =>
{ {
@ -131,10 +121,16 @@ namespace Kyoo.Controllers
if (changed.Aliases != null || resetOld) if (changed.Aliases != null || resetOld)
resource.Aliases = changed.Aliases; 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) if (changed.Genres != null || resetOld)
{ {
await Database.Entry(resource).Collection(x => x.GenreLinks).LoadAsync(); await Database.Entry(resource).Collection(x => x.Genres).LoadAsync();
resource.GenreLinks = changed.Genres?.Select(x => Link.UCreate(resource, x)).ToList(); resource.Genres = changed.Genres;
} }
if (changed.People != null || resetOld) if (changed.People != null || resetOld)
@ -185,27 +181,8 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(Show obj) public override async Task Delete(Show obj)
{ {
if (obj == null) _database.Remove(obj);
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;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
if (obj.Seasons != null)
await _seasons.Value.DeleteRange(obj.Seasons);
if (obj.Episodes != null)
await _episodes.Value.DeleteRange(obj.Episodes);
} }
} }
} }

View File

@ -1,11 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
@ -16,7 +13,7 @@ namespace Kyoo.Controllers
public class TrackRepository : LocalRepository<Track>, ITrackRepository public class TrackRepository : LocalRepository<Track>, ITrackRepository
{ {
/// <summary> /// <summary>
/// The databse handle /// The database handle
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
@ -27,63 +24,13 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// Create a new <see cref="TrackRepository"/>. /// Create a new <see cref="TrackRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The datatabse handle</param> /// <param name="database">The database handle</param>
public TrackRepository(DatabaseContext database) public TrackRepository(DatabaseContext database)
: base(database) : base(database)
{ {
_database = 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 /> /// <inheritdoc />
public override Task<ICollection<Track>> Search(string query) public override Task<ICollection<Track>> Search(string query)
{ {
@ -93,6 +40,9 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<Track> Create(Track obj) public override async Task<Track> Create(Track obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (obj.EpisodeID <= 0) if (obj.EpisodeID <= 0)
{ {
obj.EpisodeID = obj.Episode?.ID ?? 0; obj.EpisodeID = obj.Episode?.ID ?? 0;
@ -102,14 +52,7 @@ namespace Kyoo.Controllers
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local await _database.SaveChangesAsync();
await _database.SaveOrRetry(obj, (x, i) =>
{
if (i > 10)
throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting...");
x.TrackIndex++;
return x;
});
return obj; return obj;
} }

View File

@ -57,7 +57,7 @@ namespace Kyoo.Controllers
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr); Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
if (stream!.Type != StreamType.Unknown) if (stream!.Type != StreamType.Unknown)
{ {
tracks[j] = new Track(stream); tracks[j] = stream.ToTrack();
j++; j++;
} }
streamsPtr += size; streamsPtr += size;

View File

@ -35,9 +35,9 @@
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" /> <ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj" />
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" /> <ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.14" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.17" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.5" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
@ -46,6 +46,9 @@
<!-- <ExcludeAssets>all</ExcludeAssets>--> <!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference> </ProjectReference>
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj"> <ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj">
<!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference>
<ProjectReference Include="../Kyoo.SqLite/Kyoo.SqLite.csproj">
<!-- <ExcludeAssets>all</ExcludeAssets>--> <!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

67
Kyoo/Models/Stream.cs Normal file
View 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
};
}
}
}

View File

@ -47,6 +47,7 @@ namespace Kyoo
_plugins.LoadPlugins(new IPlugin[] { _plugins.LoadPlugins(new IPlugin[] {
new CoreModule(configuration), new CoreModule(configuration),
new PostgresModule(configuration, host), new PostgresModule(configuration, host),
// new SqLiteModule(configuration, host),
new AuthenticationModule(configuration, loggerFactory, host) new AuthenticationModule(configuration, loggerFactory, host)
}); });
} }

View File

@ -210,18 +210,20 @@ namespace Kyoo.Tasks
string showPath = Path.GetDirectoryName(path); string showPath = Path.GetDirectoryName(path);
string collectionName = match.Groups["Collection"].Value; string collectionName = match.Groups["Collection"].Value;
string showName = match.Groups["Show"].Value; string showName = match.Groups["Show"].Value;
int seasonNumber = int.TryParse(match.Groups["Season"].Value, out int 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 : -1; int? episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : null;
int absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : -1; int? absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : null;
Collection collection = await GetCollection(libraryManager, collectionName, library); 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); Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
if (isMovie) if (isMovie)
await libraryManager!.Create(await GetMovie(show, path)); await libraryManager!.Create(await GetMovie(show, path));
else 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, Episode episode = await GetEpisode(libraryManager,
show, show,
season, season,
@ -292,14 +294,20 @@ namespace Kyoo.Tasks
catch (DuplicatedItemException) catch (DuplicatedItemException)
{ {
old = await libraryManager.GetOrDefault<Show>(show.Slug); old = await libraryManager.GetOrDefault<Show>(show.Slug);
if (old.Path == showPath) if (old != null && old.Path == showPath)
{ {
await libraryManager.Load(old, x => x.ExternalIDs); await libraryManager.Load(old, x => x.ExternalIDs);
return old; return old;
} }
show.Slug += $"-{show.StartYear}";
if (show.StartAir != null)
{
show.Slug += $"-{show.StartAir.Value.Year}";
await libraryManager.Create(show); await libraryManager.Create(show);
} }
else
throw;
}
await ThumbnailsManager.Validate(show); await ThumbnailsManager.Validate(show);
return show; return show;
} }
@ -309,8 +317,6 @@ namespace Kyoo.Tasks
int seasonNumber, int seasonNumber,
Library library) Library library)
{ {
if (seasonNumber == -1)
return default;
try try
{ {
Season season = await libraryManager.Get(show.Slug, seasonNumber); Season season = await libraryManager.Get(show.Slug, seasonNumber);
@ -337,21 +343,24 @@ namespace Kyoo.Tasks
private async Task<Episode> GetEpisode(ILibraryManager libraryManager, private async Task<Episode> GetEpisode(ILibraryManager libraryManager,
Show show, Show show,
Season season, Season season,
int episodeNumber, int? episodeNumber,
int absoluteNumber, int? absoluteNumber,
string episodePath, string episodePath,
Library library) Library library)
{ {
Episode episode = await MetadataProvider.GetEpisode(show, Episode episode = await MetadataProvider.GetEpisode(show,
episodePath, episodePath,
season?.SeasonNumber ?? -1, season?.SeasonNumber,
episodeNumber, episodeNumber,
absoluteNumber, absoluteNumber,
library); library);
season ??= await GetSeason(libraryManager, show, episode.SeasonNumber, library); if (episode.SeasonNumber != null)
{
season ??= await GetSeason(libraryManager, show, episode.SeasonNumber.Value, library);
episode.Season = season; episode.Season = season;
episode.SeasonID = season?.ID; episode.SeasonID = season?.ID;
}
await ThumbnailsManager.Validate(episode); await ThumbnailsManager.Validate(episode);
await GetTracks(episode); await GetTracks(episode);
return episode; return episode;

View File

@ -16,6 +16,8 @@ namespace Kyoo.Api
{ {
[Route("api/show")] [Route("api/show")]
[Route("api/shows")] [Route("api/shows")]
[Route("api/movie")]
[Route("api/movies")]
[ApiController] [ApiController]
[PartialPermission(nameof(ShowApi))] [PartialPermission(nameof(ShowApi))]
public class ShowApi : CrudApi<Show> public class ShowApi : CrudApi<Show>

View File

@ -1,5 +1,4 @@
using System; using Kyoo.Models;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -27,19 +26,9 @@ namespace Kyoo.Api
[Permission(nameof(SubtitleApi), Kind.Read)] [Permission(nameof(SubtitleApi), Kind.Read)]
public async Task<IActionResult> GetSubtitle(string slug, string extension) public async Task<IActionResult> GetSubtitle(string slug, string extension)
{ {
Track subtitle; Track subtitle = await _libraryManager.GetOrDefault<Track>(Track.EditSlug(slug, StreamType.Subtitle));
try if (subtitle == null)
{
subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle);
}
catch (ArgumentException ex)
{
return BadRequest(new {error = ex.Message});
}
if (subtitle is not {Type: StreamType.Subtitle})
return NotFound(); return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt") if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path, _files); return new ConvertSubripToVtt(subtitle.Path, _files);
return _files.FileResult(subtitle.Path); return _files.FileResult(subtitle.Path);

View File

@ -10,6 +10,10 @@
}, },
"database": { "database": {
"sqlite": {
"data Source": "kyoo.db",
"cache": "Shared"
},
"postgres": { "postgres": {
"server": "127.0.0.1", "server": "127.0.0.1",
"port": "5432", "port": "5432",