mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-11 23:55:54 -05:00
Merge pull request #30 from AnonymusRaccoon/sqlite
Implementing SQLite & Adding tests
This commit is contained in:
commit
28b32c9e49
22
.github/workflows/analysis.yml
vendored
22
.github/workflows/analysis.yml
vendored
@ -2,9 +2,10 @@ name: Analysis
|
|||||||
on: [push, pull_request]
|
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 \
|
||||||
|
|||||||
37
.github/workflows/tests.yml
vendored
37
.github/workflows/tests.yml
vendored
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
@ -242,49 +241,13 @@ namespace Kyoo.Controllers
|
|||||||
/// <param name="obj">The resource to delete</param>
|
/// <param name="obj">The resource to delete</param>
|
||||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
10
Kyoo.Common/Models/Attributes/ComputedAttribute.cs
Normal file
10
Kyoo.Common/Models/Attributes/ComputedAttribute.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Models.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ComputedAttribute : NotMergeableAttribute { }
|
||||||
|
}
|
||||||
@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes
|
|||||||
/// An attribute to inform that the service will be injected automatically by a service provider.
|
/// 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)]
|
||||||
|
|||||||
13
Kyoo.Common/Models/Attributes/LinkAttribute.cs
Normal file
13
Kyoo.Common/Models/Attributes/LinkAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Common.Models.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to mark Link properties on resource.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
[MeansImplicitUse]
|
||||||
|
public class LinkAttribute : SerializeIgnoreAttribute { }
|
||||||
|
}
|
||||||
@ -1,17 +1,34 @@
|
|||||||
using System;
|
using 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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
38
Kyoo.Common/Models/Chapter.cs
Normal file
38
Kyoo.Common/Models/Chapter.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
namespace Kyoo.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A chapter to split an episode in multiple parts.
|
||||||
|
/// </summary>
|
||||||
|
public class Chapter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The start time of the chapter (in second from the start of the episode).
|
||||||
|
/// </summary>
|
||||||
|
public float StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The end time of the chapter (in second from the start of the episode)&.
|
||||||
|
/// </summary>
|
||||||
|
public float EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of this chapter. This should be a human-readable name that could be presented to the user.
|
||||||
|
/// There should be well-known chapters name for commonly used chapters.
|
||||||
|
/// For example, use "Opening" for the introduction-song and "Credits" for the end chapter with credits.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Chapter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime">The start time of the chapter (in second)</param>
|
||||||
|
/// <param name="endTime">The end time of the chapter (in second)</param>
|
||||||
|
/// <param name="name">The name of this chapter</param>
|
||||||
|
public Chapter(float startTime, float endTime, string name)
|
||||||
|
{
|
||||||
|
StartTime = startTime;
|
||||||
|
EndTime = endTime;
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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; }
|
|
||||||
|
/// <summary>
|
||||||
public Page() { }
|
/// The list of items in the page.
|
||||||
|
/// </summary>
|
||||||
public Page(ICollection<T> items)
|
public ICollection<T> Items { get; }
|
||||||
{
|
|
||||||
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,7 +50,14 @@ namespace Kyoo.Models
|
|||||||
First = first;
|
First = first;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page(ICollection<T> items,
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Page{T}"/> and compute the urls.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The list of items in the page.</param>
|
||||||
|
/// <param name="url">The base url of the resources available from this page.</param>
|
||||||
|
/// <param name="query">The list of query strings of the current page</param>
|
||||||
|
/// <param name="limit">The number of items requested for the current page.</param>
|
||||||
|
public Page(ICollection<T> items,
|
||||||
string url,
|
string url,
|
||||||
Dictionary<string, string> query,
|
Dictionary<string, string> query,
|
||||||
int limit)
|
int limit)
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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; }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public Collection() { }
|
/// <summary>
|
||||||
|
/// The internal link between this collection and shows in the <see cref="Shows"/> list.
|
||||||
public Collection(string slug, string name, string overview, string poster)
|
/// </summary>
|
||||||
{
|
[Link] public ICollection<Link<Collection, Show>> ShowLinks { get; set; }
|
||||||
Slug = slug;
|
|
||||||
Name = name;
|
/// <summary>
|
||||||
Overview = overview;
|
/// The internal link between this collection and libraries in the <see cref="Libraries"/> list.
|
||||||
Poster = poster;
|
/// </summary>
|
||||||
}
|
[Link] public ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Show containing this season.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public int ShowID { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The show that contains this season. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
|
||||||
|
|
||||||
public int SeasonNumber { get; set; } = -1;
|
/// <summary>
|
||||||
|
/// The number of this season. This can be set to 0 to indicate specials.
|
||||||
|
/// </summary>
|
||||||
|
public int SeasonNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of this season.
|
||||||
|
/// </summary>
|
||||||
public string Title { get; set; }
|
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; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
|
||||||
|
/// </summary>
|
||||||
|
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Season>> ExternalIDs { get; set; }
|
||||||
|
|
||||||
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
|
/// <summary>
|
||||||
|
/// The list of episodes that this season contains.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +1,173 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using 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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date this show started airing. It can be null if this is unknown.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAir { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date this show finished airing.
|
||||||
|
/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
|
||||||
|
/// It can also be null if this is unknown.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAir { get; set; }
|
||||||
|
|
||||||
public int? StartYear { get; set; }
|
/// <summary>
|
||||||
public int? EndYear { get; set; }
|
/// The path of this show's poster.
|
||||||
|
/// By default, the http path for this poster is returned from the public API.
|
||||||
|
/// This can be disabled using the internal query flag.
|
||||||
|
/// </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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
/// </summary>
|
||||||
public class Stream
|
public class Track : IResource
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Language { get; set; }
|
|
||||||
public string Codec { get; set; }
|
|
||||||
[MarshalAs(UnmanagedType.I1)] public bool isDefault;
|
|
||||||
[MarshalAs(UnmanagedType.I1)] public bool isForced;
|
|
||||||
[SerializeIgnore] public string Path { get; set; }
|
|
||||||
[SerializeIgnore] public StreamType Type { get; set; }
|
|
||||||
|
|
||||||
public Stream() {}
|
|
||||||
|
|
||||||
public Stream(string title, string language, string codec, bool isDefault, bool isForced, string path, StreamType type)
|
|
||||||
{
|
|
||||||
Title = title;
|
|
||||||
Language = language;
|
|
||||||
Codec = codec;
|
|
||||||
this.isDefault = isDefault;
|
|
||||||
this.isForced = isForced;
|
|
||||||
Path = path;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream(Stream stream)
|
|
||||||
{
|
|
||||||
Title = stream.Title;
|
|
||||||
Language = stream.Language;
|
|
||||||
isDefault = stream.isDefault;
|
|
||||||
isForced = stream.isForced;
|
|
||||||
Codec = stream.Codec;
|
|
||||||
Path = stream.Path;
|
|
||||||
Type = stream.Type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Track : Stream, IResource
|
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public int ID { get; set; }
|
public int ID { get; set; }
|
||||||
[SerializeIgnore] public int EpisodeID { get; set; }
|
|
||||||
public int TrackIndex { get; set; }
|
/// <inheritdoc />
|
||||||
public bool IsDefault
|
[Computed] public string Slug
|
||||||
{
|
{
|
||||||
get => isDefault;
|
get
|
||||||
set => isDefault = value;
|
{
|
||||||
}
|
string type = Type.ToString().ToLower();
|
||||||
public bool IsForced
|
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
|
||||||
{
|
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
|
||||||
get => isForced;
|
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
|
||||||
set => isForced = value;
|
}
|
||||||
}
|
[UsedImplicitly] private set
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
Match match = Regex.Match(value,
|
||||||
|
@"(?<ep>[^\.]+)\.(?<lang>\w{0,3})(-(?<index>\d+))?(\.(?<forced>forced))?\.(?<type>\w+)(\.\w*)?");
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
throw new ArgumentException("Invalid track slug. " +
|
||||||
|
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
|
||||||
|
|
||||||
|
EpisodeSlug = match.Groups["ep"].Value;
|
||||||
|
Language = match.Groups["lang"].Value;
|
||||||
|
TrackIndex = int.Parse(match.Groups["index"].Value);
|
||||||
|
IsForced = match.Groups["forced"].Success;
|
||||||
|
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public string EpisodeSlug { private get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of the stream.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The language of this stream (as a ISO-639-2 language code)
|
||||||
|
/// </summary>
|
||||||
|
public string Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The codec of this stream.
|
||||||
|
/// </summary>
|
||||||
|
public string Codec { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this stream the default one of it's type?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this stream tagged as forced?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsForced { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this track extern to the episode's file?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsExternal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of this track.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of this stream.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public StreamType Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the episode that uses this track.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public int EpisodeID { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The episode that uses this track.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of this track on the episode.
|
||||||
|
/// </summary>
|
||||||
|
public int TrackIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A user-friendly name for this track. It does not include the track type.
|
||||||
|
/// </summary>
|
||||||
public string DisplayName
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() { }
|
/// <summary>
|
||||||
|
/// Create a <see cref="WatchItem"/> from an <see cref="Episode"/>.
|
||||||
private WatchItem(int episodeID,
|
/// </summary>
|
||||||
Show show,
|
/// <param name="ep">The episode to transform.</param>
|
||||||
int seasonNumber,
|
/// <param name="library">
|
||||||
int episodeNumber,
|
/// A library manager to retrieve the next and previous episode and load the show & tracks of the episode.
|
||||||
int absoluteNumber,
|
/// </param>
|
||||||
string title,
|
/// <returns>A new WatchItem representing the given episode.</returns>
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
@ -1,703 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Kyoo.Models.Attributes;
|
|
||||||
|
|
||||||
namespace Kyoo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A set of utility functions that can be used everywhere.
|
|
||||||
/// </summary>
|
|
||||||
public static class Utility
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Is the lambda expression a member (like x => x.Body).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ex">The expression that should be checked</param>
|
|
||||||
/// <returns>True if the expression is a member, false otherwise</returns>
|
|
||||||
public static bool IsPropertyExpression(LambdaExpression ex)
|
|
||||||
{
|
|
||||||
return ex == null ||
|
|
||||||
ex.Body is MemberExpression ||
|
|
||||||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ex">The expression</param>
|
|
||||||
/// <returns>The name of the expression</returns>
|
|
||||||
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
|
||||||
public static string GetPropertyName(LambdaExpression ex)
|
|
||||||
{
|
|
||||||
if (!IsPropertyExpression(ex))
|
|
||||||
throw new ArgumentException($"{ex} is not a property expression.");
|
|
||||||
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
|
|
||||||
? ((UnaryExpression)ex.Body).Operand as MemberExpression
|
|
||||||
: ex.Body as MemberExpression;
|
|
||||||
return member!.Member.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the value of a member (property or field)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="member">The member value</param>
|
|
||||||
/// <param name="obj">The owner of this member</param>
|
|
||||||
/// <returns>The value boxed as an object</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
|
|
||||||
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
|
|
||||||
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
|
|
||||||
{
|
|
||||||
if (member == null)
|
|
||||||
throw new ArgumentNullException(nameof(member));
|
|
||||||
if (obj == null)
|
|
||||||
throw new ArgumentNullException(nameof(obj));
|
|
||||||
return member switch
|
|
||||||
{
|
|
||||||
PropertyInfo property => property.GetValue(obj),
|
|
||||||
FieldInfo field => field.GetValue(obj),
|
|
||||||
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="str">The string to slugify</param>
|
|
||||||
/// <returns>The slug version of the given string</returns>
|
|
||||||
public static string ToSlug(string str)
|
|
||||||
{
|
|
||||||
if (str == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
str = str.ToLowerInvariant();
|
|
||||||
|
|
||||||
string normalizedString = str.Normalize(NormalizationForm.FormD);
|
|
||||||
StringBuilder stringBuilder = new();
|
|
||||||
foreach (char c in normalizedString)
|
|
||||||
{
|
|
||||||
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
|
||||||
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
|
||||||
stringBuilder.Append(c);
|
|
||||||
}
|
|
||||||
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
|
||||||
|
|
||||||
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
|
|
||||||
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled);
|
|
||||||
str = str.Trim('-', '_');
|
|
||||||
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Merge two lists, can keep duplicates or remove them.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">The first enumerable to merge</param>
|
|
||||||
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
|
|
||||||
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
|
|
||||||
/// <returns>The two list merged as an array</returns>
|
|
||||||
public static T[] MergeLists<T>(IEnumerable<T> first,
|
|
||||||
IEnumerable<T> second,
|
|
||||||
Func<T, T, bool> isEqual = null)
|
|
||||||
{
|
|
||||||
if (first == null)
|
|
||||||
return second.ToArray();
|
|
||||||
if (second == null)
|
|
||||||
return first.ToArray();
|
|
||||||
if (isEqual == null)
|
|
||||||
return first.Concat(second).ToArray();
|
|
||||||
List<T> list = first.ToList();
|
|
||||||
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
|
|
||||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">The object to assign</param>
|
|
||||||
/// <param name="second">The object containing new values</param>
|
|
||||||
/// <typeparam name="T">Fields of T will be used</typeparam>
|
|
||||||
/// <returns><see cref="first"/></returns>
|
|
||||||
public static T Assign<T>(T first, T second)
|
|
||||||
{
|
|
||||||
Type type = typeof(T);
|
|
||||||
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
|
||||||
.Where(x => x.CanRead && x.CanWrite
|
|
||||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in properties)
|
|
||||||
{
|
|
||||||
object value = property.GetValue(second);
|
|
||||||
property.SetValue(first, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first is IOnMerge merge)
|
|
||||||
merge.OnMerge(second);
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
|
|
||||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">The object to complete</param>
|
|
||||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
|
||||||
/// <param name="where">Filter fields that will be merged</param>
|
|
||||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
|
||||||
/// <returns><see cref="first"/></returns>
|
|
||||||
/// <exception cref="ArgumentNullException">If first is null</exception>
|
|
||||||
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
|
|
||||||
{
|
|
||||||
if (first == null)
|
|
||||||
throw new ArgumentNullException(nameof(first));
|
|
||||||
if (second == null)
|
|
||||||
return first;
|
|
||||||
|
|
||||||
Type type = typeof(T);
|
|
||||||
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
|
||||||
.Where(x => x.CanRead && x.CanWrite
|
|
||||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
|
||||||
|
|
||||||
if (where != null)
|
|
||||||
properties = properties.Where(where);
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in properties)
|
|
||||||
{
|
|
||||||
object value = property.GetValue(second);
|
|
||||||
object defaultValue = property.PropertyType.IsValueType
|
|
||||||
? Activator.CreateInstance(property.PropertyType)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
|
|
||||||
property.SetValue(first, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first is IOnMerge merge)
|
|
||||||
merge.OnMerge(second);
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An advanced <see cref="Complete{T}"/> function.
|
|
||||||
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
|
|
||||||
/// Enumerable will be merged (concatenated).
|
|
||||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">The object to complete</param>
|
|
||||||
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
|
||||||
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
|
||||||
/// <returns><see cref="first"/></returns>
|
|
||||||
public static T Merge<T>(T first, T second)
|
|
||||||
{
|
|
||||||
if (first == null)
|
|
||||||
return second;
|
|
||||||
if (second == null)
|
|
||||||
return first;
|
|
||||||
|
|
||||||
Type type = typeof(T);
|
|
||||||
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
|
||||||
.Where(x => x.CanRead && x.CanWrite
|
|
||||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in properties)
|
|
||||||
{
|
|
||||||
object oldValue = property.GetValue(first);
|
|
||||||
object newValue = property.GetValue(second);
|
|
||||||
object defaultValue = property.PropertyType.IsValueType
|
|
||||||
? Activator.CreateInstance(property.PropertyType)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (oldValue?.Equals(defaultValue) != false)
|
|
||||||
property.SetValue(first, newValue);
|
|
||||||
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
|
|
||||||
&& property.PropertyType != typeof(string))
|
|
||||||
{
|
|
||||||
property.SetValue(first, RunGenericMethod<object>(
|
|
||||||
typeof(Utility),
|
|
||||||
nameof(MergeLists),
|
|
||||||
GetEnumerableType(property.PropertyType),
|
|
||||||
oldValue, newValue, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first is IOnMerge merge)
|
|
||||||
merge.OnMerge(second);
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set every fields of <see cref="obj"/> to the default value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">The object to nullify</param>
|
|
||||||
/// <typeparam name="T">Fields of T will be nullified</typeparam>
|
|
||||||
/// <returns><see cref="obj"/></returns>
|
|
||||||
public static T Nullify<T>(T obj)
|
|
||||||
{
|
|
||||||
Type type = typeof(T);
|
|
||||||
foreach (PropertyInfo property in type.GetProperties())
|
|
||||||
{
|
|
||||||
if (!property.CanWrite)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
object defaultValue = property.PropertyType.IsValueType
|
|
||||||
? Activator.CreateInstance(property.PropertyType)
|
|
||||||
: null;
|
|
||||||
property.SetValue(obj, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type">The starting type</param>
|
|
||||||
/// <returns>A list of types</returns>
|
|
||||||
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
|
|
||||||
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
|
|
||||||
{
|
|
||||||
if (type == null)
|
|
||||||
throw new ArgumentNullException(nameof(type));
|
|
||||||
for (; type != null; type = type.BaseType)
|
|
||||||
yield return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
|
|
||||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
|
||||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
|
||||||
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
|
|
||||||
{
|
|
||||||
if (obj == null)
|
|
||||||
throw new ArgumentNullException(nameof(obj));
|
|
||||||
return IsOfGenericType(obj.GetType(), genericType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type">The type to check</param>
|
|
||||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
|
||||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
|
||||||
public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType)
|
|
||||||
{
|
|
||||||
if (type == null)
|
|
||||||
throw new ArgumentNullException(nameof(type));
|
|
||||||
if (genericType == null)
|
|
||||||
throw new ArgumentNullException(nameof(genericType));
|
|
||||||
if (!genericType.IsGenericType)
|
|
||||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
|
||||||
|
|
||||||
IEnumerable<Type> types = genericType.IsInterface
|
|
||||||
? type.GetInterfaces()
|
|
||||||
: type.GetInheritanceTree();
|
|
||||||
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the generic definition of <see cref="genericType"/>.
|
|
||||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type">The type to check</param>
|
|
||||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
|
||||||
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
|
|
||||||
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
|
|
||||||
public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType)
|
|
||||||
{
|
|
||||||
if (type == null)
|
|
||||||
throw new ArgumentNullException(nameof(type));
|
|
||||||
if (genericType == null)
|
|
||||||
throw new ArgumentNullException(nameof(genericType));
|
|
||||||
if (!genericType.IsGenericType)
|
|
||||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
|
||||||
|
|
||||||
IEnumerable<Type> types = genericType.IsInterface
|
|
||||||
? type.GetInterfaces()
|
|
||||||
: type.GetInheritanceTree();
|
|
||||||
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A Select where the index of the item can be used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
|
||||||
/// <param name="mapper">The function that will map each items</param>
|
|
||||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
|
||||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
|
||||||
/// <returns>The list mapped.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
|
||||||
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self,
|
|
||||||
[NotNull] Func<T, int, T2> mapper)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
yield break;
|
|
||||||
if (mapper == null)
|
|
||||||
throw new ArgumentNullException(nameof(mapper));
|
|
||||||
|
|
||||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
|
||||||
int index = 0;
|
|
||||||
|
|
||||||
while (enumerator.MoveNext())
|
|
||||||
{
|
|
||||||
yield return mapper(enumerator.Current, index);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A map where the mapping function is asynchronous.
|
|
||||||
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
|
||||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
|
||||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
|
||||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
|
||||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
|
||||||
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
|
|
||||||
[NotNull] Func<T, int, Task<T2>> mapper)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
yield break;
|
|
||||||
if (mapper == null)
|
|
||||||
throw new ArgumentNullException(nameof(mapper));
|
|
||||||
|
|
||||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
|
||||||
int index = 0;
|
|
||||||
|
|
||||||
while (enumerator.MoveNext())
|
|
||||||
{
|
|
||||||
yield return await mapper(enumerator.Current, index);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An asynchronous version of Select.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The IEnumerable to map</param>
|
|
||||||
/// <param name="mapper">The asynchronous function that will map each items</param>
|
|
||||||
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
|
||||||
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
|
||||||
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
|
|
||||||
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
|
|
||||||
[NotNull] Func<T, Task<T2>> mapper)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
yield break;
|
|
||||||
if (mapper == null)
|
|
||||||
throw new ArgumentNullException(nameof(mapper));
|
|
||||||
|
|
||||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
|
||||||
|
|
||||||
while (enumerator.MoveNext())
|
|
||||||
yield return await mapper(enumerator.Current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert an AsyncEnumerable to a List by waiting for every item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The async list</param>
|
|
||||||
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
|
|
||||||
/// <returns>A task that will return a simple list</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">The list can't be null</exception>
|
|
||||||
public static async Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
throw new ArgumentNullException(nameof(self));
|
|
||||||
|
|
||||||
List<T> ret = new();
|
|
||||||
|
|
||||||
await foreach(T i in self)
|
|
||||||
ret.Add(i);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If the enumerable is empty, execute an action.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The enumerable to check</param>
|
|
||||||
/// <param name="action">The action to execute is the list is empty</param>
|
|
||||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
|
|
||||||
{
|
|
||||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
|
||||||
|
|
||||||
if (!enumerator.MoveNext())
|
|
||||||
{
|
|
||||||
action();
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
yield return enumerator.Current;
|
|
||||||
}
|
|
||||||
while (enumerator.MoveNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A foreach used as a function with a little specificity: the list can be null.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
|
||||||
/// <param name="action">The action to execute for each arguments</param>
|
|
||||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
|
||||||
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
return;
|
|
||||||
foreach (T i in self)
|
|
||||||
action(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A foreach used as a function with a little specificity: the list can be null.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
|
||||||
/// <param name="action">The action to execute for each arguments</param>
|
|
||||||
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
return;
|
|
||||||
foreach (object i in self)
|
|
||||||
action(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
return;
|
|
||||||
foreach (T i in self)
|
|
||||||
await action(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
return;
|
|
||||||
await foreach (T i in self)
|
|
||||||
action(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
|
|
||||||
{
|
|
||||||
if (self == null)
|
|
||||||
return;
|
|
||||||
foreach (object i in self)
|
|
||||||
await action(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
|
|
||||||
{
|
|
||||||
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
|
|
||||||
.Where(x => x.Name == name)
|
|
||||||
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
|
||||||
.Where(x => x.GetParameters().Length == args.Length)
|
|
||||||
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
|
|
||||||
$"{args.Length} arguments and {generics.Length} generic " +
|
|
||||||
$"types could not be found on {type.Name}."))
|
|
||||||
// TODO this won't work but I don't know why.
|
|
||||||
// .Where(x =>
|
|
||||||
// {
|
|
||||||
// int i = 0;
|
|
||||||
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
|
|
||||||
// })
|
|
||||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
|
|
||||||
|
|
||||||
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
|
|
||||||
// .Where(x =>
|
|
||||||
// {
|
|
||||||
// int i = 0;
|
|
||||||
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
|
|
||||||
// })
|
|
||||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
|
|
||||||
.Take(2)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (methods.Length == 1)
|
|
||||||
return methods[0];
|
|
||||||
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T RunGenericMethod<T>(
|
|
||||||
[NotNull] Type owner,
|
|
||||||
[NotNull] string methodName,
|
|
||||||
[NotNull] Type type,
|
|
||||||
params object[] args)
|
|
||||||
{
|
|
||||||
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T RunGenericMethod<T>(
|
|
||||||
[NotNull] Type owner,
|
|
||||||
[NotNull] string methodName,
|
|
||||||
[NotNull] Type[] types,
|
|
||||||
params object[] args)
|
|
||||||
{
|
|
||||||
if (owner == null)
|
|
||||||
throw new ArgumentNullException(nameof(owner));
|
|
||||||
if (methodName == null)
|
|
||||||
throw new ArgumentNullException(nameof(methodName));
|
|
||||||
if (types == null)
|
|
||||||
throw new ArgumentNullException(nameof(types));
|
|
||||||
if (types.Length < 1)
|
|
||||||
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
|
||||||
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
|
||||||
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T RunGenericMethod<T>(
|
|
||||||
[NotNull] object instance,
|
|
||||||
[NotNull] string methodName,
|
|
||||||
[NotNull] Type type,
|
|
||||||
params object[] args)
|
|
||||||
{
|
|
||||||
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T RunGenericMethod<T>(
|
|
||||||
[NotNull] object instance,
|
|
||||||
[NotNull] string methodName,
|
|
||||||
[NotNull] Type[] types,
|
|
||||||
params object[] args)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
throw new ArgumentNullException(nameof(instance));
|
|
||||||
if (methodName == null)
|
|
||||||
throw new ArgumentNullException(nameof(methodName));
|
|
||||||
if (types == null || types.Length == 0)
|
|
||||||
throw new ArgumentNullException(nameof(types));
|
|
||||||
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
|
|
||||||
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
[NotNull]
|
|
||||||
public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list)
|
|
||||||
{
|
|
||||||
if (list == null)
|
|
||||||
throw new ArgumentNullException(nameof(list));
|
|
||||||
Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t)
|
|
||||||
&& t.GetGenericArguments().Any()) ?? list.GetType();
|
|
||||||
return type.GetGenericArguments().First();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Type GetEnumerableType([NotNull] Type listType)
|
|
||||||
{
|
|
||||||
if (listType == null)
|
|
||||||
throw new ArgumentNullException(nameof(listType));
|
|
||||||
if (!typeof(IEnumerable).IsAssignableFrom(listType))
|
|
||||||
throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable.");
|
|
||||||
Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t)
|
|
||||||
&& t.GetGenericArguments().Any()) ?? listType;
|
|
||||||
return type.GetGenericArguments().First();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < list.Count; i += countPerList)
|
|
||||||
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
|
|
||||||
{
|
|
||||||
T[] ret = new T[countPerList];
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
using IEnumerator<T> enumerator = list.GetEnumerator();
|
|
||||||
while (enumerator.MoveNext())
|
|
||||||
{
|
|
||||||
ret[i] = enumerator.Current;
|
|
||||||
i++;
|
|
||||||
if (i < countPerList)
|
|
||||||
continue;
|
|
||||||
i = 0;
|
|
||||||
yield return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.Resize(ref ret, i);
|
|
||||||
yield return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToQueryString(this Dictionary<string, string> query)
|
|
||||||
{
|
|
||||||
if (!query.Any())
|
|
||||||
return string.Empty;
|
|
||||||
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
|
|
||||||
public static void ReThrow([NotNull] this Exception ex)
|
|
||||||
{
|
|
||||||
if (ex == null)
|
|
||||||
throw new ArgumentNullException(nameof(ex));
|
|
||||||
ExceptionDispatchInfo.Capture(ex).Throw();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> Then<T>(this Task<T> task, Action<T> map)
|
|
||||||
{
|
|
||||||
return task.ContinueWith(x =>
|
|
||||||
{
|
|
||||||
if (x.IsFaulted)
|
|
||||||
x.Exception!.InnerException!.ReThrow();
|
|
||||||
if (x.IsCanceled)
|
|
||||||
throw new TaskCanceledException();
|
|
||||||
map(x.Result);
|
|
||||||
return x.Result;
|
|
||||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
|
|
||||||
{
|
|
||||||
return task.ContinueWith(x =>
|
|
||||||
{
|
|
||||||
if (x.IsFaulted)
|
|
||||||
x.Exception!.InnerException!.ReThrow();
|
|
||||||
if (x.IsCanceled)
|
|
||||||
throw new TaskCanceledException();
|
|
||||||
return map(x.Result);
|
|
||||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<T> Cast<T>(this Task task)
|
|
||||||
{
|
|
||||||
return task.ContinueWith(x =>
|
|
||||||
{
|
|
||||||
if (x.IsFaulted)
|
|
||||||
x.Exception!.InnerException!.ReThrow();
|
|
||||||
if (x.IsCanceled)
|
|
||||||
throw new TaskCanceledException();
|
|
||||||
return (T)((dynamic)x).Result;
|
|
||||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a friendly type name (supporting generics)
|
|
||||||
/// For example a list of string will be displayed as List<string> and not as List`1.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type">The type to use</param>
|
|
||||||
/// <returns>The friendly name of the type</returns>
|
|
||||||
public static string FriendlyName(this Type type)
|
|
||||||
{
|
|
||||||
if (!type.IsGenericType)
|
|
||||||
return type.Name;
|
|
||||||
string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName()));
|
|
||||||
return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
279
Kyoo.Common/Utility/EnumerableExtensions.cs
Normal file
279
Kyoo.Common/Utility/EnumerableExtensions.cs
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Kyoo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A set of extensions class for enumerable.
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A Select where the index of the item can be used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
|
||||||
|
/// <param name="mapper">The function that will map each items</param>
|
||||||
|
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||||
|
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||||
|
/// <returns>The list mapped.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IEnumerable<T2> Map<T, T2>([NotNull] this IEnumerable<T> self,
|
||||||
|
[NotNull] Func<T, int, T2> mapper)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
if (mapper == null)
|
||||||
|
throw new ArgumentNullException(nameof(mapper));
|
||||||
|
|
||||||
|
static IEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, T2> mapper)
|
||||||
|
{
|
||||||
|
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
yield return mapper(enumerator.Current, index);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Generator(self, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A map where the mapping function is asynchronous.
|
||||||
|
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The IEnumerable to map.</param>
|
||||||
|
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||||
|
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||||
|
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||||
|
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IAsyncEnumerable<T2> MapAsync<T, T2>([NotNull] this IEnumerable<T> self,
|
||||||
|
[NotNull] Func<T, int, Task<T2>> mapper)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
if (mapper == null)
|
||||||
|
throw new ArgumentNullException(nameof(mapper));
|
||||||
|
|
||||||
|
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
|
||||||
|
{
|
||||||
|
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
yield return await mapper(enumerator.Current, index);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Generator(self, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An asynchronous version of Select.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The IEnumerable to map</param>
|
||||||
|
/// <param name="mapper">The asynchronous function that will map each items</param>
|
||||||
|
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
|
||||||
|
/// <typeparam name="T2">The type of items in the returned list</typeparam>
|
||||||
|
/// <returns>The list mapped as an AsyncEnumerable</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IAsyncEnumerable<T2> SelectAsync<T, T2>([NotNull] this IEnumerable<T> self,
|
||||||
|
[NotNull] Func<T, Task<T2>> mapper)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
if (mapper == null)
|
||||||
|
throw new ArgumentNullException(nameof(mapper));
|
||||||
|
|
||||||
|
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
|
||||||
|
{
|
||||||
|
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
yield return await mapper(enumerator.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Generator(self, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert an AsyncEnumerable to a List by waiting for every item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The async list</param>
|
||||||
|
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
|
||||||
|
/// <returns>A task that will return a simple list</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">The list can't be null</exception>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
|
||||||
|
static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
|
||||||
|
{
|
||||||
|
List<T> ret = new();
|
||||||
|
await foreach (T i in self)
|
||||||
|
ret.Add(i);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToList(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the enumerable is empty, execute an action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The enumerable to check</param>
|
||||||
|
/// <param name="action">The action to execute is the list is empty</param>
|
||||||
|
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||||
|
/// <exception cref="ArgumentNullException">The iterable and the action can't be null.</exception>
|
||||||
|
/// <returns>The iterator proxied, there is no dual iterations.</returns>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
if (action == null)
|
||||||
|
throw new ArgumentNullException(nameof(action));
|
||||||
|
|
||||||
|
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
|
||||||
|
{
|
||||||
|
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||||
|
|
||||||
|
if (!enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
yield return enumerator.Current;
|
||||||
|
}
|
||||||
|
while (enumerator.MoveNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Generator(self, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A foreach used as a function with a little specificity: the list can be null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||||
|
/// <param name="action">The action to execute for each arguments</param>
|
||||||
|
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||||
|
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
return;
|
||||||
|
foreach (T i in self)
|
||||||
|
action(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A foreach used as a function with a little specificity: the list can be null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||||
|
/// <param name="action">The action to execute for each arguments</param>
|
||||||
|
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
return;
|
||||||
|
foreach (object i in self)
|
||||||
|
action(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A foreach used as a function with a little specificity: the list can be null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||||
|
/// <param name="action">The action to execute for each arguments</param>
|
||||||
|
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
return;
|
||||||
|
foreach (object i in self)
|
||||||
|
await action(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A foreach used as a function with a little specificity: the list can be null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||||
|
/// <param name="action">The asynchronous action to execute for each arguments</param>
|
||||||
|
/// <typeparam name="T">The type of items in the list.</typeparam>
|
||||||
|
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
return;
|
||||||
|
foreach (T i in self)
|
||||||
|
await action(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A foreach used as a function with a little specificity: the list can be null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The async list to enumerate. If this is null, the function result in a no-op</param>
|
||||||
|
/// <param name="action">The action to execute for each arguments</param>
|
||||||
|
/// <typeparam name="T">The type of items in the list.</typeparam>
|
||||||
|
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
|
||||||
|
{
|
||||||
|
if (self == null)
|
||||||
|
return;
|
||||||
|
await foreach (T i in self)
|
||||||
|
action(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a list in a small chunk of data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="list">The list to split</param>
|
||||||
|
/// <param name="countPerList">The number of items in each chunk</param>
|
||||||
|
/// <typeparam name="T">The type of data in the initial list.</typeparam>
|
||||||
|
/// <returns>A list of chunks</returns>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < list.Count; i += countPerList)
|
||||||
|
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a list in a small chunk of data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="list">The list to split</param>
|
||||||
|
/// <param name="countPerList">The number of items in each chunk</param>
|
||||||
|
/// <typeparam name="T">The type of data in the initial list.</typeparam>
|
||||||
|
/// <returns>A list of chunks</returns>
|
||||||
|
[LinqTunnel]
|
||||||
|
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
|
||||||
|
{
|
||||||
|
T[] ret = new T[countPerList];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
using IEnumerator<T> enumerator = list.GetEnumerator();
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
ret[i] = enumerator.Current;
|
||||||
|
i++;
|
||||||
|
if (i < countPerList)
|
||||||
|
continue;
|
||||||
|
i = 0;
|
||||||
|
yield return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Resize(ref ret, i);
|
||||||
|
yield return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Kyoo.Common/Utility/Merger.cs
Normal file
174
Kyoo.Common/Utility/Merger.cs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class containing helper methods to merge objects.
|
||||||
|
/// </summary>
|
||||||
|
public static class Merger
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Merge two lists, can keep duplicates or remove them.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="first">The first enumerable to merge</param>
|
||||||
|
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
|
||||||
|
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
|
||||||
|
/// <returns>The two list merged as an array</returns>
|
||||||
|
public static T[] MergeLists<T>(IEnumerable<T> first,
|
||||||
|
IEnumerable<T> second,
|
||||||
|
Func<T, T, bool> isEqual = null)
|
||||||
|
{
|
||||||
|
if (first == null)
|
||||||
|
return second.ToArray();
|
||||||
|
if (second == null)
|
||||||
|
return first.ToArray();
|
||||||
|
if (isEqual == null)
|
||||||
|
return first.Concat(second).ToArray();
|
||||||
|
List<T> list = first.ToList();
|
||||||
|
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
|
||||||
|
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="first">The object to assign</param>
|
||||||
|
/// <param name="second">The object containing new values</param>
|
||||||
|
/// <typeparam name="T">Fields of T will be used</typeparam>
|
||||||
|
/// <returns><see cref="first"/></returns>
|
||||||
|
public static T Assign<T>(T first, T second)
|
||||||
|
{
|
||||||
|
Type type = typeof(T);
|
||||||
|
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
||||||
|
.Where(x => x.CanRead && x.CanWrite
|
||||||
|
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
||||||
|
|
||||||
|
foreach (PropertyInfo property in properties)
|
||||||
|
{
|
||||||
|
object value = property.GetValue(second);
|
||||||
|
property.SetValue(first, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first is IOnMerge merge)
|
||||||
|
merge.OnMerge(second);
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
|
||||||
|
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="first">The object to complete</param>
|
||||||
|
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||||
|
/// <param name="where">Filter fields that will be merged</param>
|
||||||
|
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||||
|
/// <returns><see cref="first"/></returns>
|
||||||
|
/// <exception cref="ArgumentNullException">If first is null</exception>
|
||||||
|
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
|
||||||
|
{
|
||||||
|
if (first == null)
|
||||||
|
throw new ArgumentNullException(nameof(first));
|
||||||
|
if (second == null)
|
||||||
|
return first;
|
||||||
|
|
||||||
|
Type type = typeof(T);
|
||||||
|
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
||||||
|
.Where(x => x.CanRead && x.CanWrite
|
||||||
|
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
||||||
|
|
||||||
|
if (where != null)
|
||||||
|
properties = properties.Where(where);
|
||||||
|
|
||||||
|
foreach (PropertyInfo property in properties)
|
||||||
|
{
|
||||||
|
object value = property.GetValue(second);
|
||||||
|
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
|
||||||
|
?? property.PropertyType.GetClrDefault();
|
||||||
|
|
||||||
|
if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
|
||||||
|
property.SetValue(first, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first is IOnMerge merge)
|
||||||
|
merge.OnMerge(second);
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An advanced <see cref="Complete{T}"/> function.
|
||||||
|
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
|
||||||
|
/// Enumerable will be merged (concatenated).
|
||||||
|
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="first">The object to complete</param>
|
||||||
|
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
|
||||||
|
/// <typeparam name="T">Fields of T will be merged</typeparam>
|
||||||
|
/// <returns><see cref="first"/></returns>
|
||||||
|
public static T Merge<T>(T first, T second)
|
||||||
|
{
|
||||||
|
if (first == null)
|
||||||
|
return second;
|
||||||
|
if (second == null)
|
||||||
|
return first;
|
||||||
|
|
||||||
|
Type type = typeof(T);
|
||||||
|
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
||||||
|
.Where(x => x.CanRead && x.CanWrite
|
||||||
|
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
|
||||||
|
|
||||||
|
foreach (PropertyInfo property in properties)
|
||||||
|
{
|
||||||
|
object oldValue = property.GetValue(first);
|
||||||
|
object newValue = property.GetValue(second);
|
||||||
|
object defaultValue = property.PropertyType.IsValueType
|
||||||
|
? Activator.CreateInstance(property.PropertyType)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (oldValue?.Equals(defaultValue) != false)
|
||||||
|
property.SetValue(first, newValue);
|
||||||
|
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
|
||||||
|
&& property.PropertyType != typeof(string))
|
||||||
|
{
|
||||||
|
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
|
||||||
|
.GenericTypeArguments
|
||||||
|
.First();
|
||||||
|
property.SetValue(first, Utility.RunGenericMethod<object>(
|
||||||
|
typeof(Utility),
|
||||||
|
nameof(MergeLists),
|
||||||
|
enumerableType,
|
||||||
|
oldValue, newValue, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first is IOnMerge merge)
|
||||||
|
merge.OnMerge(second);
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set every fields of <see cref="obj"/> to the default value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The object to nullify</param>
|
||||||
|
/// <typeparam name="T">Fields of T will be nullified</typeparam>
|
||||||
|
/// <returns><see cref="obj"/></returns>
|
||||||
|
public static T Nullify<T>(T obj)
|
||||||
|
{
|
||||||
|
Type type = typeof(T);
|
||||||
|
foreach (PropertyInfo property in type.GetProperties())
|
||||||
|
{
|
||||||
|
if (!property.CanWrite || property.GetCustomAttribute<ComputedAttribute>() != null)
|
||||||
|
continue;
|
||||||
|
property.SetValue(obj, property.PropertyType.GetClrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
Kyoo.Common/Utility/TaskUtils.cs
Normal file
69
Kyoo.Common/Utility/TaskUtils.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Kyoo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class containing helper method for tasks.
|
||||||
|
/// </summary>
|
||||||
|
public static class TaskUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Run a method after the execution of the task.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task">The task to wait.</param>
|
||||||
|
/// <param name="then">
|
||||||
|
/// The method to run after the task finish. This will only be run if the task finished successfully.
|
||||||
|
/// </param>
|
||||||
|
/// <typeparam name="T">The type of the item in the task.</typeparam>
|
||||||
|
/// <returns>A continuation task wrapping the initial task and adding a continuation method.</returns>
|
||||||
|
/// <exception cref="TaskCanceledException"></exception>
|
||||||
|
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
|
||||||
|
public static Task<T> Then<T>(this Task<T> task, Action<T> then)
|
||||||
|
{
|
||||||
|
return task.ContinueWith(x =>
|
||||||
|
{
|
||||||
|
if (x.IsFaulted)
|
||||||
|
x.Exception!.InnerException!.ReThrow();
|
||||||
|
if (x.IsCanceled)
|
||||||
|
throw new TaskCanceledException();
|
||||||
|
then(x.Result);
|
||||||
|
return x.Result;
|
||||||
|
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map the result of a task to another result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task">The task to map.</param>
|
||||||
|
/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
|
||||||
|
/// <typeparam name="T">The type of returns of the given task</typeparam>
|
||||||
|
/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
|
||||||
|
/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
|
||||||
|
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
|
||||||
|
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
|
||||||
|
{
|
||||||
|
return task.ContinueWith(x =>
|
||||||
|
{
|
||||||
|
if (x.IsFaulted)
|
||||||
|
x.Exception!.InnerException!.ReThrow();
|
||||||
|
if (x.IsCanceled)
|
||||||
|
throw new TaskCanceledException();
|
||||||
|
return map(x.Result);
|
||||||
|
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A method to return the a default value from a task if the initial task is null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The initial task</param>
|
||||||
|
/// <typeparam name="T">The type that the task will return</typeparam>
|
||||||
|
/// <returns>A non-null task.</returns>
|
||||||
|
[NotNull]
|
||||||
|
public static Task<T> DefaultIfNull<T>([CanBeNull] Task<T> value)
|
||||||
|
{
|
||||||
|
return value ?? Task.FromResult<T>(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
298
Kyoo.Common/Utility/Utility.cs
Normal file
298
Kyoo.Common/Utility/Utility.cs
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Kyoo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A set of utility functions that can be used everywhere.
|
||||||
|
/// </summary>
|
||||||
|
public static class Utility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Is the lambda expression a member (like x => x.Body).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">The expression that should be checked</param>
|
||||||
|
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||||
|
public static bool IsPropertyExpression(LambdaExpression ex)
|
||||||
|
{
|
||||||
|
if (ex == null)
|
||||||
|
return false;
|
||||||
|
return ex.Body is MemberExpression ||
|
||||||
|
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">The expression</param>
|
||||||
|
/// <returns>The name of the expression</returns>
|
||||||
|
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||||
|
public static string GetPropertyName(LambdaExpression ex)
|
||||||
|
{
|
||||||
|
if (!IsPropertyExpression(ex))
|
||||||
|
throw new ArgumentException($"{ex} is not a property expression.");
|
||||||
|
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
|
||||||
|
? ((UnaryExpression)ex.Body).Operand as MemberExpression
|
||||||
|
: ex.Body as MemberExpression;
|
||||||
|
return member!.Member.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the value of a member (property or field)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="member">The member value</param>
|
||||||
|
/// <param name="obj">The owner of this member</param>
|
||||||
|
/// <returns>The value boxed as an object</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
|
||||||
|
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
|
||||||
|
{
|
||||||
|
if (member == null)
|
||||||
|
throw new ArgumentNullException(nameof(member));
|
||||||
|
if (obj == null)
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
return member switch
|
||||||
|
{
|
||||||
|
PropertyInfo property => property.GetValue(obj),
|
||||||
|
FieldInfo field => field.GetValue(obj),
|
||||||
|
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to slugify</param>
|
||||||
|
/// <returns>The slug version of the given string</returns>
|
||||||
|
public static string ToSlug(string str)
|
||||||
|
{
|
||||||
|
if (str == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
str = str.ToLowerInvariant();
|
||||||
|
|
||||||
|
string normalizedString = str.Normalize(NormalizationForm.FormD);
|
||||||
|
StringBuilder stringBuilder = new();
|
||||||
|
foreach (char c in normalizedString)
|
||||||
|
{
|
||||||
|
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||||
|
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||||
|
stringBuilder.Append(c);
|
||||||
|
}
|
||||||
|
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
|
||||||
|
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
|
||||||
|
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled);
|
||||||
|
str = str.Trim('-', '_');
|
||||||
|
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the default value of a type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to get the default value</param>
|
||||||
|
/// <returns>The default value of the given type.</returns>
|
||||||
|
public static object GetClrDefault(this Type type)
|
||||||
|
{
|
||||||
|
return type.IsValueType
|
||||||
|
? Activator.CreateInstance(type)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The starting type</param>
|
||||||
|
/// <returns>A list of types</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
|
||||||
|
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
for (; type != null; type = type.BaseType)
|
||||||
|
yield return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
|
||||||
|
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||||
|
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||||
|
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
return IsOfGenericType(obj.GetType(), genericType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to check</param>
|
||||||
|
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||||
|
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
|
||||||
|
public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (genericType == null)
|
||||||
|
throw new ArgumentNullException(nameof(genericType));
|
||||||
|
if (!genericType.IsGenericType)
|
||||||
|
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||||
|
|
||||||
|
IEnumerable<Type> types = genericType.IsInterface
|
||||||
|
? type.GetInterfaces()
|
||||||
|
: type.GetInheritanceTree();
|
||||||
|
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the generic definition of <see cref="genericType"/>.
|
||||||
|
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to check</param>
|
||||||
|
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||||
|
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
|
||||||
|
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
|
||||||
|
public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (genericType == null)
|
||||||
|
throw new ArgumentNullException(nameof(genericType));
|
||||||
|
if (!genericType.IsGenericType)
|
||||||
|
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||||
|
|
||||||
|
IEnumerable<Type> types = genericType.IsInterface
|
||||||
|
? type.GetInterfaces()
|
||||||
|
: type.GetInheritanceTree();
|
||||||
|
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
|
||||||
|
{
|
||||||
|
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
|
||||||
|
.Where(x => x.Name == name)
|
||||||
|
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
||||||
|
.Where(x => x.GetParameters().Length == args.Length)
|
||||||
|
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
|
||||||
|
$"{args.Length} arguments and {generics.Length} generic " +
|
||||||
|
$"types could not be found on {type.Name}."))
|
||||||
|
// TODO this won't work but I don't know why.
|
||||||
|
// .Where(x =>
|
||||||
|
// {
|
||||||
|
// int i = 0;
|
||||||
|
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
|
||||||
|
// })
|
||||||
|
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
|
||||||
|
|
||||||
|
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
|
||||||
|
// .Where(x =>
|
||||||
|
// {
|
||||||
|
// int i = 0;
|
||||||
|
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
|
||||||
|
// })
|
||||||
|
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
|
||||||
|
.Take(2)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (methods.Length == 1)
|
||||||
|
return methods[0];
|
||||||
|
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T RunGenericMethod<T>(
|
||||||
|
[NotNull] Type owner,
|
||||||
|
[NotNull] string methodName,
|
||||||
|
[NotNull] Type type,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T RunGenericMethod<T>(
|
||||||
|
[NotNull] Type owner,
|
||||||
|
[NotNull] string methodName,
|
||||||
|
[NotNull] Type[] types,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
if (owner == null)
|
||||||
|
throw new ArgumentNullException(nameof(owner));
|
||||||
|
if (methodName == null)
|
||||||
|
throw new ArgumentNullException(nameof(methodName));
|
||||||
|
if (types == null)
|
||||||
|
throw new ArgumentNullException(nameof(types));
|
||||||
|
if (types.Length < 1)
|
||||||
|
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
|
||||||
|
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
||||||
|
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T RunGenericMethod<T>(
|
||||||
|
[NotNull] object instance,
|
||||||
|
[NotNull] string methodName,
|
||||||
|
[NotNull] Type type,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T RunGenericMethod<T>(
|
||||||
|
[NotNull] object instance,
|
||||||
|
[NotNull] string methodName,
|
||||||
|
[NotNull] Type[] types,
|
||||||
|
params object[] args)
|
||||||
|
{
|
||||||
|
if (instance == null)
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
if (methodName == null)
|
||||||
|
throw new ArgumentNullException(nameof(methodName));
|
||||||
|
if (types == null || types.Length == 0)
|
||||||
|
throw new ArgumentNullException(nameof(types));
|
||||||
|
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
|
||||||
|
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToQueryString(this Dictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (!query.Any())
|
||||||
|
return string.Empty;
|
||||||
|
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
|
||||||
|
public static void ReThrow([NotNull] this Exception ex)
|
||||||
|
{
|
||||||
|
if (ex == null)
|
||||||
|
throw new ArgumentNullException(nameof(ex));
|
||||||
|
ExceptionDispatchInfo.Capture(ex).Throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a friendly type name (supporting generics)
|
||||||
|
/// For example a list of string will be displayed as List<string> and not as List`1.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to use</param>
|
||||||
|
/// <returns>The friendly name of the type</returns>
|
||||||
|
public static string FriendlyName(this Type type)
|
||||||
|
{
|
||||||
|
if (!type.IsGenericType)
|
||||||
|
return type.Name;
|
||||||
|
string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName()));
|
||||||
|
return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -194,7 +194,7 @@ namespace Kyoo.CommonApi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _repository.DeleteRange(ApiHelper.ParseWhere<T>(where));
|
await _repository.DeleteAll(ApiHelper.ParseWhere<T>(where));
|
||||||
}
|
}
|
||||||
catch (ItemNotFoundException)
|
catch (ItemNotFoundException)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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; }
|
||||||
@ -78,7 +74,26 @@ namespace Kyoo
|
|||||||
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
|
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
|
||||||
/// </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<Track>()
|
modelBuilder.Entity<Show>()
|
||||||
.Property(t => t.IsForced)
|
.HasMany(x => x.Episodes)
|
||||||
.ValueGeneratedNever();
|
.WithOne(x => x.Show)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Season>()
|
||||||
|
.HasMany(x => x.Episodes)
|
||||||
|
.WithOne(x => x.Season)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Episode>()
|
||||||
|
.HasMany(x => x.Tracks)
|
||||||
|
.WithOne(x => x.Episode)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Show>()
|
||||||
|
.HasOne(x => x.Studio)
|
||||||
|
.WithMany(x => x.Shows)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
modelBuilder.Entity<Provider>()
|
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>()
|
||||||
@ -237,7 +282,7 @@ namespace Kyoo
|
|||||||
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
|
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
|
||||||
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
|
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
|
||||||
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
|
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
|
||||||
|
|
||||||
modelBuilder.Entity<Collection>()
|
modelBuilder.Entity<Collection>()
|
||||||
.HasIndex(x => x.Slug)
|
.HasIndex(x => x.Slug)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
1197
Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
generated
Normal file
1197
Kyoo.Postgresql/Migrations/20210627141933_Initial.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
782
Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
Normal file
782
Kyoo.Postgresql/Migrations/20210627141933_Initial.cs
Normal file
@ -0,0 +1,782 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
namespace Kyoo.Postgresql.Migrations
|
||||||
|
{
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
|
||||||
|
.Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown")
|
||||||
|
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "collections",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
poster = table.Column<string>(type: "text", nullable: true),
|
||||||
|
overview = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_collections", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "genres",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_genres", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "libraries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
paths = table.Column<string[]>(type: "text[]", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_libraries", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "people",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
poster = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_people", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "providers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
logo = table.Column<string>(type: "text", nullable: true),
|
||||||
|
logo_extension = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_providers", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "studios",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_studios", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
username = table.Column<string>(type: "text", nullable: true),
|
||||||
|
email = table.Column<string>(type: "text", nullable: true),
|
||||||
|
password = table.Column<string>(type: "text", nullable: true),
|
||||||
|
permissions = table.Column<string[]>(type: "text[]", nullable: true),
|
||||||
|
extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_users", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_library_collection",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_library_collection", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_collection_collections_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "collections",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_collection_libraries_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "libraries",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_library_provider",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_library_provider", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_provider_libraries_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "libraries",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_provider_providers_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "providers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "metadata_id_people",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
data_id = table.Column<string>(type: "text", nullable: true),
|
||||||
|
link = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_metadata_id_people", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_people_people_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "people",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_people_providers_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "providers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "shows",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: false),
|
||||||
|
title = table.Column<string>(type: "text", nullable: true),
|
||||||
|
aliases = table.Column<string[]>(type: "text[]", nullable: true),
|
||||||
|
path = table.Column<string>(type: "text", nullable: true),
|
||||||
|
overview = table.Column<string>(type: "text", nullable: true),
|
||||||
|
status = table.Column<Status>(type: "status", nullable: true),
|
||||||
|
trailer_url = table.Column<string>(type: "text", nullable: true),
|
||||||
|
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||||
|
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||||
|
poster = table.Column<string>(type: "text", nullable: true),
|
||||||
|
logo = table.Column<string>(type: "text", nullable: true),
|
||||||
|
backdrop = table.Column<string>(type: "text", nullable: true),
|
||||||
|
is_movie = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
studio_id = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_shows", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_shows_studios_studio_id",
|
||||||
|
column: x => x.studio_id,
|
||||||
|
principalTable: "studios",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_collection_show",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_collection_show", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_collection_show_collections_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "collections",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_collection_show_shows_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_library_show",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_library_show", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_show_libraries_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "libraries",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_library_show_shows_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_show_genre",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_show_genre", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_show_genre_genres_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "genres",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_show_genre_shows_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "link_user_show",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_link_user_show", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_user_show_shows_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_link_user_show_users_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "metadata_id_show",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
data_id = table.Column<string>(type: "text", nullable: true),
|
||||||
|
link = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_metadata_id_show", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_show_providers_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "providers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_show_shows_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "people_roles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
for_people = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
people_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
type = table.Column<string>(type: "text", nullable: true),
|
||||||
|
role = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_people_roles", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_people_roles_people_people_id",
|
||||||
|
column: x => x.people_id,
|
||||||
|
principalTable: "people",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_people_roles_shows_show_id",
|
||||||
|
column: x => x.show_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "seasons",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: true),
|
||||||
|
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
season_number = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
title = table.Column<string>(type: "text", nullable: true),
|
||||||
|
overview = table.Column<string>(type: "text", nullable: true),
|
||||||
|
start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||||
|
end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
|
||||||
|
poster = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_seasons", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_seasons_shows_show_id",
|
||||||
|
column: x => x.show_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "episodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: true),
|
||||||
|
show_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
season_id = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
season_number = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
episode_number = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
absolute_number = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
path = table.Column<string>(type: "text", nullable: true),
|
||||||
|
thumb = table.Column<string>(type: "text", nullable: true),
|
||||||
|
title = table.Column<string>(type: "text", nullable: true),
|
||||||
|
overview = table.Column<string>(type: "text", nullable: true),
|
||||||
|
release_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_episodes", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_episodes_seasons_season_id",
|
||||||
|
column: x => x.season_id,
|
||||||
|
principalTable: "seasons",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_episodes_shows_show_id",
|
||||||
|
column: x => x.show_id,
|
||||||
|
principalTable: "shows",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "metadata_id_season",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
data_id = table.Column<string>(type: "text", nullable: true),
|
||||||
|
link = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_metadata_id_season", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_season_providers_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "providers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_season_seasons_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "seasons",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "metadata_id_episode",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
data_id = table.Column<string>(type: "text", nullable: true),
|
||||||
|
link = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_metadata_id_episode", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_episode_episodes_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "episodes",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_metadata_id_episode_providers_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "providers",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "tracks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
slug = table.Column<string>(type: "text", nullable: true),
|
||||||
|
title = table.Column<string>(type: "text", nullable: true),
|
||||||
|
language = table.Column<string>(type: "text", nullable: true),
|
||||||
|
codec = table.Column<string>(type: "text", nullable: true),
|
||||||
|
is_default = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_forced = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_external = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
path = table.Column<string>(type: "text", nullable: true),
|
||||||
|
type = table.Column<StreamType>(type: "stream_type", nullable: false),
|
||||||
|
episode_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
track_index = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_tracks", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_tracks_episodes_episode_id",
|
||||||
|
column: x => x.episode_id,
|
||||||
|
principalTable: "episodes",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "watched_episodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
first_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
second_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
watched_percentage = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_watched_episodes", x => new { x.first_id, x.second_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_watched_episodes_episodes_second_id",
|
||||||
|
column: x => x.second_id,
|
||||||
|
principalTable: "episodes",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_watched_episodes_users_first_id",
|
||||||
|
column: x => x.first_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_collections_slug",
|
||||||
|
table: "collections",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_episodes_season_id",
|
||||||
|
table: "episodes",
|
||||||
|
column: "season_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_episodes_show_id_season_number_episode_number_absolute_numb",
|
||||||
|
table: "episodes",
|
||||||
|
columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_episodes_slug",
|
||||||
|
table: "episodes",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_genres_slug",
|
||||||
|
table: "genres",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_libraries_slug",
|
||||||
|
table: "libraries",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_collection_show_second_id",
|
||||||
|
table: "link_collection_show",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_library_collection_second_id",
|
||||||
|
table: "link_library_collection",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_library_provider_second_id",
|
||||||
|
table: "link_library_provider",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_library_show_second_id",
|
||||||
|
table: "link_library_show",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_show_genre_second_id",
|
||||||
|
table: "link_show_genre",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_link_user_show_second_id",
|
||||||
|
table: "link_user_show",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_metadata_id_episode_second_id",
|
||||||
|
table: "metadata_id_episode",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_metadata_id_people_second_id",
|
||||||
|
table: "metadata_id_people",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_metadata_id_season_second_id",
|
||||||
|
table: "metadata_id_season",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_metadata_id_show_second_id",
|
||||||
|
table: "metadata_id_show",
|
||||||
|
column: "second_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_people_slug",
|
||||||
|
table: "people",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_people_roles_people_id",
|
||||||
|
table: "people_roles",
|
||||||
|
column: "people_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_people_roles_show_id",
|
||||||
|
table: "people_roles",
|
||||||
|
column: "show_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_providers_slug",
|
||||||
|
table: "providers",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_seasons_show_id_season_number",
|
||||||
|
table: "seasons",
|
||||||
|
columns: new[] { "show_id", "season_number" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_seasons_slug",
|
||||||
|
table: "seasons",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_shows_slug",
|
||||||
|
table: "shows",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_shows_studio_id",
|
||||||
|
table: "shows",
|
||||||
|
column: "studio_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_studios_slug",
|
||||||
|
table: "studios",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_tracks_episode_id_type_language_track_index_is_forced",
|
||||||
|
table: "tracks",
|
||||||
|
columns: new[] { "episode_id", "type", "language", "track_index", "is_forced" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_tracks_slug",
|
||||||
|
table: "tracks",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_users_slug",
|
||||||
|
table: "users",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_watched_episodes_second_id",
|
||||||
|
table: "watched_episodes",
|
||||||
|
column: "second_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_collection_show");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_library_collection");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_library_provider");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_library_show");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_show_genre");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "link_user_show");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "metadata_id_episode");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "metadata_id_people");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "metadata_id_season");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "metadata_id_show");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "people_roles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "tracks");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "watched_episodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "collections");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "libraries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "genres");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "providers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "people");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "episodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "users");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "seasons");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "shows");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "studios");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1197
Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
generated
Normal file
1197
Kyoo.Postgresql/Migrations/20210627141941_Triggers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
186
Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
Normal file
186
Kyoo.Postgresql/Migrations/20210627141941_Triggers.cs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Kyoo.Postgresql.Migrations
|
||||||
|
{
|
||||||
|
public partial class Triggers : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION season_slug_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.slug := CONCAT(
|
||||||
|
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||||
|
'-s',
|
||||||
|
NEW.season_number
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE season_slug_update();");
|
||||||
|
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION episode_slug_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.slug := CONCAT(
|
||||||
|
(SELECT slug FROM shows WHERE id = NEW.show_id),
|
||||||
|
CASE
|
||||||
|
WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL
|
||||||
|
WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number)
|
||||||
|
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
|
||||||
|
END
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER episode_slug_trigger
|
||||||
|
BEFORE INSERT OR UPDATE OF absolute_number, episode_number, season_number, show_id ON episodes
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();");
|
||||||
|
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION show_slug_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id;
|
||||||
|
UPDATE episodes SET slug = CASE
|
||||||
|
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
|
||||||
|
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
|
||||||
|
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
|
||||||
|
END WHERE show_id = NEW.id;
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE show_slug_update();");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION episode_update_tracks_slug()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE tracks SET slug = CONCAT(
|
||||||
|
NEW.slug,
|
||||||
|
'.', language,
|
||||||
|
CASE (track_index)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE CONCAT('-', track_index)
|
||||||
|
END,
|
||||||
|
CASE (is_forced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END,
|
||||||
|
'.', type
|
||||||
|
) WHERE episode_id = NEW.id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER episode_track_slug_trigger AFTER UPDATE OF slug ON episodes
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE episode_update_tracks_slug();");
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE FUNCTION track_slug_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.track_index = 0 THEN
|
||||||
|
NEW.track_index := (SELECT COUNT(*) FROM tracks
|
||||||
|
WHERE episode_id = NEW.episode_id AND type = NEW.type
|
||||||
|
AND language = NEW.language AND is_forced = NEW.is_forced);
|
||||||
|
END IF;
|
||||||
|
NEW.slug := CONCAT(
|
||||||
|
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
|
||||||
|
'.', NEW.language,
|
||||||
|
CASE (NEW.track_index)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE CONCAT('-', NEW.track_index)
|
||||||
|
END,
|
||||||
|
CASE (NEW.is_forced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END,
|
||||||
|
'.', NEW.type
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER track_slug_trigger
|
||||||
|
BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE track_slug_update();");
|
||||||
|
|
||||||
|
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE VIEW library_items AS
|
||||||
|
SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.poster, CASE
|
||||||
|
WHEN s.is_movie THEN 'movie'::item_type
|
||||||
|
ELSE 'show'::item_type
|
||||||
|
END AS type
|
||||||
|
FROM shows AS s
|
||||||
|
WHERE NOT (EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM link_collection_show AS l
|
||||||
|
INNER JOIN collections AS c ON l.first_id = c.id
|
||||||
|
WHERE s.id = l.second_id))
|
||||||
|
UNION ALL
|
||||||
|
SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status,
|
||||||
|
NULL AS start_air, NULL AS end_air, c0.poster, 'collection'::item_type AS type
|
||||||
|
FROM collections AS c0");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON shows;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger ON seasons;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER episode_slug_trigger ON episodes;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER track_slug_trigger ON tracks;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION track_slug_update;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER episode_track_slug_trigger ON episodes;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP FUNCTION episode_update_tracks_slug;");
|
||||||
|
// language=PostgreSQL
|
||||||
|
migrationBuilder.Sql(@"DROP VIEW library_items;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -26,16 +26,19 @@ namespace Kyoo.Postgresql
|
|||||||
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
|
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
|
||||||
/// </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
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
41
Kyoo.SqLite/Kyoo.SqLite.csproj
Normal file
41
Kyoo.SqLite/Kyoo.SqLite.csproj
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
|
||||||
|
<Company>SDG</Company>
|
||||||
|
<Authors>Zoe Roux</Authors>
|
||||||
|
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
|
||||||
|
<LangVersion>default</LangVersion>
|
||||||
|
<RootNamespace>Kyoo.SqLite</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- <PropertyGroup>-->
|
||||||
|
<!-- <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/sqlite</OutputPath>-->
|
||||||
|
<!-- <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>-->
|
||||||
|
<!-- <ProduceReferenceAssembly>false</ProduceReferenceAssembly>-->
|
||||||
|
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
|
||||||
|
<!-- <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>-->
|
||||||
|
<!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
|
||||||
|
<!-- </PropertyGroup>-->
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
|
||||||
|
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||||
|
<!-- <Private>false</Private>-->
|
||||||
|
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
|
||||||
|
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||||
|
<!-- <Private>false</Private>-->
|
||||||
|
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@ -1,50 +1,41 @@
|
|||||||
// <auto-generated />
|
// <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 =>
|
||||||
@ -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");
|
||||||
980
Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
generated
Normal file
980
Kyoo.SqLite/Migrations/20210626141347_Triggers.Designer.cs
generated
Normal file
@ -0,0 +1,980 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Kyoo.SqLite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace Kyoo.SqLite.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqLiteContext))]
|
||||||
|
[Migration("20210626141347_Triggers")]
|
||||||
|
partial class Triggers
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Collections");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("AbsoluteNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ReleaseDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Thumb")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("SeasonID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Episodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Genres");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Paths")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Libraries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Collection, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Collection>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Provider>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Show, Genre>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<User, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Episode>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<People>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Season>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ForPeople")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PeopleID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("PeopleID");
|
||||||
|
|
||||||
|
b.HasIndex("ShowID");
|
||||||
|
|
||||||
|
b.ToTable("PeopleRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Logo")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LogoExtension")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Providers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeasonNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ShowID", "SeasonNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Seasons");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Aliases")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Backdrop")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMovie")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Logo")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("StudioID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TrailerUrl")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("StudioID");
|
||||||
|
|
||||||
|
b.ToTable("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Studios");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Codec")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("EpisodeID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsForced")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("TrackIndex")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Tracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExtraData")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permissions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("WatchedPercentage")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("WatchedEpisodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Season", "Season")
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("SeasonID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Season");
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Collection", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany("CollectionLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("CollectionLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Collection", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("ProviderLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "First")
|
||||||
|
.WithMany("GenreLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Genre", "Second")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.User", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.People", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Season", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.People", "People")
|
||||||
|
.WithMany("Roles")
|
||||||
|
.HasForeignKey("PeopleID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("People")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("People");
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("Seasons")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("StudioID");
|
||||||
|
|
||||||
|
b.Navigation("Studio");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "Episode")
|
||||||
|
.WithMany("Tracks")
|
||||||
|
.HasForeignKey("EpisodeID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Episode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.User", "First")
|
||||||
|
.WithMany("CurrentlyWatching")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("Tracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CollectionLinks");
|
||||||
|
|
||||||
|
b.Navigation("ProviderLinks");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("Roles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CollectionLinks");
|
||||||
|
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("GenreLinks");
|
||||||
|
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
|
||||||
|
b.Navigation("People");
|
||||||
|
|
||||||
|
b.Navigation("Seasons");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CurrentlyWatching");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
Normal file
187
Kyoo.SqLite/Migrations/20210626141347_Triggers.cs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Kyoo.SqLite.Migrations
|
||||||
|
{
|
||||||
|
public partial class Triggers : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber
|
||||||
|
WHERE ID == new.ID;
|
||||||
|
END");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber
|
||||||
|
WHERE ID == new.ID;
|
||||||
|
END");
|
||||||
|
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Episodes
|
||||||
|
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||||
|
CASE
|
||||||
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
|
END
|
||||||
|
WHERE ID == new.ID;
|
||||||
|
END");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID
|
||||||
|
ON Episodes FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Episodes
|
||||||
|
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
|
||||||
|
CASE
|
||||||
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
|
END
|
||||||
|
WHERE ID == new.ID;
|
||||||
|
END");
|
||||||
|
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER TrackSlugInsert
|
||||||
|
AFTER INSERT ON Tracks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET TrackIndex = (
|
||||||
|
SELECT COUNT(*) FROM Tracks
|
||||||
|
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
|
||||||
|
AND Language = new.Language AND IsForced = new.IsForced
|
||||||
|
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||||
|
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || (TrackIndex)
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE ID = new.ID;
|
||||||
|
END;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER TrackSlugUpdate
|
||||||
|
AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET TrackIndex = (
|
||||||
|
SELECT COUNT(*) FROM Tracks
|
||||||
|
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
|
||||||
|
AND Language = new.Language AND IsForced = new.IsForced
|
||||||
|
) WHERE ID = new.ID AND TrackIndex = 0;
|
||||||
|
UPDATE Tracks SET Slug =
|
||||||
|
(SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || (TrackIndex)
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE ID = new.ID;
|
||||||
|
END;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER EpisodeUpdateTracksSlug
|
||||||
|
AFTER UPDATE OF Slug ON Episodes
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Tracks SET Slug =
|
||||||
|
NEW.Slug ||
|
||||||
|
'.' || Language ||
|
||||||
|
CASE (TrackIndex)
|
||||||
|
WHEN 0 THEN ''
|
||||||
|
ELSE '-' || TrackIndex
|
||||||
|
END ||
|
||||||
|
CASE (IsForced)
|
||||||
|
WHEN false THEN ''
|
||||||
|
ELSE '-forced'
|
||||||
|
END ||
|
||||||
|
CASE (Type)
|
||||||
|
WHEN 1 THEN '.video'
|
||||||
|
WHEN 2 THEN '.audio'
|
||||||
|
WHEN 3 THEN '.subtitle'
|
||||||
|
ELSE '.' || Type
|
||||||
|
END
|
||||||
|
WHERE EpisodeID = NEW.ID;
|
||||||
|
END;");
|
||||||
|
|
||||||
|
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
|
||||||
|
UPDATE Episodes
|
||||||
|
SET Slug = new.Slug ||
|
||||||
|
CASE
|
||||||
|
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
|
||||||
|
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
|
||||||
|
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
|
||||||
|
END
|
||||||
|
WHERE ShowID = new.ID;
|
||||||
|
END;");
|
||||||
|
|
||||||
|
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
CREATE VIEW LibraryItems AS
|
||||||
|
SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Poster, CASE
|
||||||
|
WHEN s.IsMovie THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS Type
|
||||||
|
FROM Shows AS s
|
||||||
|
WHERE NOT (EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM 'Link<Collection, Show>' AS l
|
||||||
|
INNER JOIN Collections AS c ON l.FirstID = c.ID
|
||||||
|
WHERE s.ID = l.SecondID))
|
||||||
|
UNION ALL
|
||||||
|
SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status,
|
||||||
|
NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type
|
||||||
|
FROM collections AS c0");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;");
|
||||||
|
// language=SQLite
|
||||||
|
migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
978
Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
Normal file
978
Kyoo.SqLite/Migrations/SqLiteContextModelSnapshot.cs
Normal file
@ -0,0 +1,978 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Kyoo.SqLite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace Kyoo.SqLite.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqLiteContext))]
|
||||||
|
partial class SqLiteContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Collections");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("AbsoluteNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ReleaseDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Thumb")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("SeasonID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Episodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Genres");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Paths")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Libraries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Collection, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Collection>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Provider>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Library, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<Show, Genre>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("Link<User, Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Episode>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<People>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Season>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DataID")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("MetadataID<Show>");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ForPeople")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PeopleID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("PeopleID");
|
||||||
|
|
||||||
|
b.HasIndex("ShowID");
|
||||||
|
|
||||||
|
b.ToTable("PeopleRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Logo")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LogoExtension")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Providers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeasonNumber")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ShowID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ShowID", "SeasonNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Seasons");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Aliases")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Backdrop")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMovie")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Logo")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Poster")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartAir")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("StudioID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TrailerUrl")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("StudioID");
|
||||||
|
|
||||||
|
b.ToTable("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Studios");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Codec")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("EpisodeID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExternal")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsForced")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("TrackIndex")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Tracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExtraData")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permissions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FirstID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SecondID")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("WatchedPercentage")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("FirstID", "SecondID");
|
||||||
|
|
||||||
|
b.HasIndex("SecondID");
|
||||||
|
|
||||||
|
b.ToTable("WatchedEpisodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Season", "Season")
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("SeasonID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Season");
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Collection", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany("CollectionLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("CollectionLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Collection", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("ProviderLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Library", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany("LibraryLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "First")
|
||||||
|
.WithMany("GenreLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Genre", "Second")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.User", "First")
|
||||||
|
.WithMany("ShowLinks")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.People", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Season", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "First")
|
||||||
|
.WithMany("ExternalIDs")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Provider", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.People", "People")
|
||||||
|
.WithMany("Roles")
|
||||||
|
.HasForeignKey("PeopleID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("People")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("People");
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Show", "Show")
|
||||||
|
.WithMany("Seasons")
|
||||||
|
.HasForeignKey("ShowID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Show");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Studio", "Studio")
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("StudioID");
|
||||||
|
|
||||||
|
b.Navigation("Studio");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Track", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "Episode")
|
||||||
|
.WithMany("Tracks")
|
||||||
|
.HasForeignKey("EpisodeID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Episode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Kyoo.Models.User", "First")
|
||||||
|
.WithMany("CurrentlyWatching")
|
||||||
|
.HasForeignKey("FirstID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Kyoo.Models.Episode", "Second")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SecondID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("First");
|
||||||
|
|
||||||
|
b.Navigation("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Collection", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Episode", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("Tracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CollectionLinks");
|
||||||
|
|
||||||
|
b.Navigation("ProviderLinks");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.People", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("Roles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Provider", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Season", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Show", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CollectionLinks");
|
||||||
|
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
|
||||||
|
b.Navigation("ExternalIDs");
|
||||||
|
|
||||||
|
b.Navigation("GenreLinks");
|
||||||
|
|
||||||
|
b.Navigation("LibraryLinks");
|
||||||
|
|
||||||
|
b.Navigation("People");
|
||||||
|
|
||||||
|
b.Navigation("Seasons");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.Studio", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Kyoo.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CurrentlyWatching");
|
||||||
|
|
||||||
|
b.Navigation("ShowLinks");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Kyoo.SqLite/SqLiteContext.cs
Normal file
134
Kyoo.SqLite/SqLiteContext.cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Kyoo.SqLite
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A sqlite implementation of <see cref="DatabaseContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SqLiteContext : DatabaseContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The connection string to use.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string _connection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this instance in debug mode?
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool _debugMode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool _skipConfigure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||||
|
/// </summary>
|
||||||
|
public SqLiteContext()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="SqLiteContext"/> using specific options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The options to use.</param>
|
||||||
|
public SqLiteContext(DbContextOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
_skipConfigure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection">The connection string to use</param>
|
||||||
|
/// <param name="debugMode">Is this instance in debug mode?</param>
|
||||||
|
public SqLiteContext(string connection, bool debugMode)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
_debugMode = debugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set connection information for this database context
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="optionsBuilder">An option builder to fill.</param>
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
if (!_skipConfigure)
|
||||||
|
{
|
||||||
|
if (_connection != null)
|
||||||
|
optionsBuilder.UseSqlite(_connection);
|
||||||
|
else
|
||||||
|
optionsBuilder.UseSqlite();
|
||||||
|
if (_debugMode)
|
||||||
|
optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set database parameters to support every types of Kyoo.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelBuilder">The database's model builder.</param>
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
ValueConverter<string[], string> arrayConvertor = new(
|
||||||
|
x => string.Join(";", x),
|
||||||
|
x => x.Split(';', StringSplitOptions.None));
|
||||||
|
modelBuilder.Entity<Library>()
|
||||||
|
.Property(x => x.Paths)
|
||||||
|
.HasConversion(arrayConvertor);
|
||||||
|
modelBuilder.Entity<Show>()
|
||||||
|
.Property(x => x.Aliases)
|
||||||
|
.HasConversion(arrayConvertor);
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.Property(x => x.Permissions)
|
||||||
|
.HasConversion(arrayConvertor);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Show>()
|
||||||
|
.Property(x => x.Status)
|
||||||
|
.HasConversion<int>();
|
||||||
|
modelBuilder.Entity<Track>()
|
||||||
|
.Property(x => x.Type)
|
||||||
|
.HasConversion<int>();
|
||||||
|
|
||||||
|
ValueConverter<Dictionary<string, string>, string> jsonConvertor = new(
|
||||||
|
x => JsonConvert.SerializeObject(x),
|
||||||
|
x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x));
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.Property(x => x.ExtraData)
|
||||||
|
.HasConversion(jsonConvertor);
|
||||||
|
|
||||||
|
modelBuilder.Entity<LibraryItem>()
|
||||||
|
.ToView("LibraryItems")
|
||||||
|
.HasKey(x => x.ID);
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool IsDuplicateException(Exception ex)
|
||||||
|
{
|
||||||
|
return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/}
|
||||||
|
or SqliteException { SqliteExtendedErrorCode: 1555 /*SQLITE_CONSTRAINT_PRIMARYKEY*/};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
|
||||||
|
{
|
||||||
|
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(EF.Functions.Like);
|
||||||
|
MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format));
|
||||||
|
|
||||||
|
return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Kyoo.SqLite/SqLiteModule.cs
Normal file
79
Kyoo.SqLite/SqLiteModule.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Kyoo.SqLite
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A module to add sqlite capacity to the app.
|
||||||
|
/// </summary>
|
||||||
|
public class SqLiteModule : IPlugin
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Slug => "sqlite";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "SqLite";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => "A database context for sqlite.";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<Type> Provides => new[]
|
||||||
|
{
|
||||||
|
typeof(DatabaseContext)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration to use. The database connection string is pulled from it.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The host environment to check if the app is in debug mode.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new postgres module instance and use the given configuration and environment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The configuration to use</param>
|
||||||
|
/// <param name="env">The environment that will be used (if the env is in development mode, more information will be displayed on errors.</param>
|
||||||
|
public SqLiteModule(IConfiguration configuration, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_environment = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
|
||||||
|
{
|
||||||
|
services.AddDbContext<DatabaseContext, SqLiteContext>(x =>
|
||||||
|
{
|
||||||
|
x.UseSqlite(_configuration.GetDatabaseConnection("sqlite"));
|
||||||
|
if (_environment.IsDevelopment())
|
||||||
|
x.EnableDetailedErrors().EnableSensitiveDataLogging();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Initialize(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
|
||||||
|
context.Database.Migrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Kyoo.Tests/KAssert.cs
Normal file
45
Kyoo.Tests/KAssert.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Sdk;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom assertions used by Kyoo's tests.
|
||||||
|
/// </summary>
|
||||||
|
public static class KAssert
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Check if every property of the item is equal to the other's object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expected">The value to check against</param>
|
||||||
|
/// <param name="value">The value to check</param>
|
||||||
|
/// <typeparam name="T">The type to check</typeparam>
|
||||||
|
[AssertionMethod]
|
||||||
|
public static void DeepEqual<T>(T expected, T value)
|
||||||
|
{
|
||||||
|
foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance))
|
||||||
|
Assert.Equal(property.GetValue(expected), property.GetValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explicitly fail a test.
|
||||||
|
/// </summary>
|
||||||
|
[AssertionMethod]
|
||||||
|
public static void Fail()
|
||||||
|
{
|
||||||
|
throw new XunitException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explicitly fail a test.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message that will be seen in the test report</param>
|
||||||
|
[AssertionMethod]
|
||||||
|
public static void Fail(string message)
|
||||||
|
{
|
||||||
|
throw new XunitException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,8 +14,9 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<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>
|
||||||
|
|||||||
67
Kyoo.Tests/Library/RepositoryActivator.cs
Normal file
67
Kyoo.Tests/Library/RepositoryActivator.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public class RepositoryActivator : IDisposable, IAsyncDisposable
|
||||||
|
{
|
||||||
|
public TestContext Context { get; }
|
||||||
|
public ILibraryManager LibraryManager { get; }
|
||||||
|
|
||||||
|
|
||||||
|
private readonly DatabaseContext _database;
|
||||||
|
|
||||||
|
public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null)
|
||||||
|
{
|
||||||
|
Context = postgres == null
|
||||||
|
? new SqLiteTestContext(output)
|
||||||
|
: new PostgresTestContext(postgres, output);
|
||||||
|
_database = Context.New();
|
||||||
|
|
||||||
|
ProviderRepository provider = new(_database);
|
||||||
|
LibraryRepository library = new(_database, provider);
|
||||||
|
CollectionRepository collection = new(_database);
|
||||||
|
GenreRepository genre = new(_database);
|
||||||
|
StudioRepository studio = new(_database);
|
||||||
|
PeopleRepository people = new(_database, provider,
|
||||||
|
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository));
|
||||||
|
ShowRepository show = new(_database, studio, people, genre, provider);
|
||||||
|
SeasonRepository season = new(_database, provider);
|
||||||
|
LibraryItemRepository libraryItem = new(_database,
|
||||||
|
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
|
||||||
|
TrackRepository track = new(_database);
|
||||||
|
EpisodeRepository episode = new(_database, provider, track);
|
||||||
|
UserRepository user = new(_database);
|
||||||
|
|
||||||
|
LibraryManager = new LibraryManager(new IBaseRepository[] {
|
||||||
|
provider,
|
||||||
|
library,
|
||||||
|
libraryItem,
|
||||||
|
collection,
|
||||||
|
show,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
track,
|
||||||
|
people,
|
||||||
|
studio,
|
||||||
|
genre,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_database.Dispose();
|
||||||
|
Context.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _database.DisposeAsync();
|
||||||
|
await Context.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
191
Kyoo.Tests/Library/RepositoryTests.cs
Normal file
191
Kyoo.Tests/Library/RepositoryTests.cs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Kyoo.Models.Exceptions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable
|
||||||
|
where T : class, IResource, new()
|
||||||
|
{
|
||||||
|
protected readonly RepositoryActivator Repositories;
|
||||||
|
private readonly IRepository<T> _repository;
|
||||||
|
|
||||||
|
protected RepositoryTests(RepositoryActivator repositories)
|
||||||
|
{
|
||||||
|
Repositories = repositories;
|
||||||
|
_repository = Repositories.LibraryManager.GetRepository<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Repositories.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return Repositories.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FillTest()
|
||||||
|
{
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
|
||||||
|
Assert.Equal(1, database.Shows.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByIdTest()
|
||||||
|
{
|
||||||
|
T value = await _repository.Get(TestSample.Get<T>().ID);
|
||||||
|
KAssert.DeepEqual(TestSample.Get<T>(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBySlugTest()
|
||||||
|
{
|
||||||
|
T value = await _repository.Get(TestSample.Get<T>().Slug);
|
||||||
|
KAssert.DeepEqual(TestSample.Get<T>(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByFakeIdTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByFakeSlugTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get("non-existent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteByIdTest()
|
||||||
|
{
|
||||||
|
await _repository.Delete(TestSample.Get<T>().ID);
|
||||||
|
Assert.Equal(0, await _repository.GetCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteBySlugTest()
|
||||||
|
{
|
||||||
|
await _repository.Delete(TestSample.Get<T>().Slug);
|
||||||
|
Assert.Equal(0, await _repository.GetCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteByValueTest()
|
||||||
|
{
|
||||||
|
await _repository.Delete(TestSample.Get<T>());
|
||||||
|
Assert.Equal(0, await _repository.GetCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<DuplicatedItemException>(() => _repository.Create(TestSample.Get<T>()));
|
||||||
|
await _repository.Delete(TestSample.Get<T>());
|
||||||
|
|
||||||
|
T expected = TestSample.Get<T>();
|
||||||
|
expected.ID = 0;
|
||||||
|
await _repository.Create(expected);
|
||||||
|
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateNullTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Create(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateIfNotExistNullTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.CreateIfNotExists(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateIfNotExistTest()
|
||||||
|
{
|
||||||
|
T expected = TestSample.Get<T>();
|
||||||
|
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
|
||||||
|
await _repository.Delete(TestSample.Get<T>());
|
||||||
|
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditNullTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditNonExistingTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T {ID = 56}, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetExpressionIDTest()
|
||||||
|
{
|
||||||
|
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.ID == TestSample.Get<T>().ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetExpressionSlugTest()
|
||||||
|
{
|
||||||
|
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.Slug == TestSample.Get<T>().Slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetExpressionNotFoundTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetExpressionNullTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Get((Expression<Func<T, bool>>)null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetOrDefaultTest()
|
||||||
|
{
|
||||||
|
Assert.Null(await _repository.GetOrDefault(56));
|
||||||
|
Assert.Null(await _repository.GetOrDefault("non-existing"));
|
||||||
|
Assert.Null(await _repository.GetOrDefault(x => x.Slug == "non-existing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCountWithFilterTest()
|
||||||
|
{
|
||||||
|
string slug = TestSample.Get<T>().Slug[2..4];
|
||||||
|
Assert.Equal(1, await _repository.GetCount(x => x.Slug.Contains(slug)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAllTest()
|
||||||
|
{
|
||||||
|
string slug = TestSample.Get<T>().Slug[2..4];
|
||||||
|
ICollection<T> ret = await _repository.GetAll(x => x.Slug.Contains(slug));
|
||||||
|
Assert.Equal(1, ret.Count);
|
||||||
|
KAssert.DeepEqual(TestSample.Get<T>(), ret.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAllTest()
|
||||||
|
{
|
||||||
|
string slug = TestSample.Get<T>().Slug[2..4];
|
||||||
|
await _repository.DeleteAll(x => x.Slug.Contains(slug));
|
||||||
|
Assert.Equal(0, await _repository.GetCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
namespace Kyoo.Tests
|
|
||||||
{
|
|
||||||
public class SetupTests
|
|
||||||
{
|
|
||||||
// TODO test libraries & repositories via a on-memory SQLite database.
|
|
||||||
// TODO Requires: Kyoo should be database agonistic and database implementations should be available via a plugin.
|
|
||||||
|
|
||||||
// [Fact]
|
|
||||||
// public void Get_Test()
|
|
||||||
// {
|
|
||||||
// TestContext context = new();
|
|
||||||
// using DatabaseContext database = context.New();
|
|
||||||
//
|
|
||||||
// Assert.Equal(1, database.Shows.Count());
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class CollectionTests : ACollectionTests
|
||||||
|
{
|
||||||
|
public CollectionTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class CollectionTests : ACollectionTests
|
||||||
|
{
|
||||||
|
public CollectionTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ACollectionTests : RepositoryTests<Collection>
|
||||||
|
{
|
||||||
|
private readonly ICollectionRepository _repository;
|
||||||
|
|
||||||
|
protected ACollectionTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.CollectionRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
192
Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
Normal file
192
Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class EpisodeTests : AEpisodeTests
|
||||||
|
{
|
||||||
|
public EpisodeTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class EpisodeTests : AEpisodeTests
|
||||||
|
{
|
||||||
|
public EpisodeTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AEpisodeTests : RepositoryTests<Episode>
|
||||||
|
{
|
||||||
|
private readonly IEpisodeRepository _repository;
|
||||||
|
|
||||||
|
protected AEpisodeTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = repositories.LibraryManager.EpisodeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SlugEditTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
|
||||||
|
Show show = new()
|
||||||
|
{
|
||||||
|
ID = episode.ShowID,
|
||||||
|
Slug = "new-slug"
|
||||||
|
};
|
||||||
|
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
|
||||||
|
episode = await _repository.Get(1);
|
||||||
|
Assert.Equal("new-slug-s1e1", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeasonNumberEditTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
|
||||||
|
episode = await _repository.Edit(new Episode
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
SeasonNumber = 2
|
||||||
|
}, false);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
|
||||||
|
episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EpisodeNumberEditTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
|
||||||
|
episode = await _repository.Edit(new Episode
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
EpisodeNumber = 2
|
||||||
|
}, false);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||||
|
episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EpisodeCreationSlugTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Create(new Episode
|
||||||
|
{
|
||||||
|
ShowID = TestSample.Get<Show>().ID,
|
||||||
|
SeasonNumber = 2,
|
||||||
|
EpisodeNumber = 4
|
||||||
|
});
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO absolute numbering tests
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbsoluteSlugTest()
|
||||||
|
{
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
|
||||||
|
TestSample.GetAbsoluteEpisode().Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EpisodeCreationAbsoluteSlugTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SlugEditAbsoluteTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
|
||||||
|
Show show = new()
|
||||||
|
{
|
||||||
|
ID = episode.ShowID,
|
||||||
|
Slug = "new-slug"
|
||||||
|
};
|
||||||
|
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
|
||||||
|
episode = await _repository.Get(2);
|
||||||
|
Assert.Equal($"new-slug-3", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AbsoluteNumberEditTest()
|
||||||
|
{
|
||||||
|
await _repository.Create(TestSample.GetAbsoluteEpisode());
|
||||||
|
Episode episode = await _repository.Edit(new Episode
|
||||||
|
{
|
||||||
|
ID = 2,
|
||||||
|
AbsoluteNumber = 56
|
||||||
|
}, false);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
|
||||||
|
episode = await _repository.Get(2);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AbsoluteToNormalEditTest()
|
||||||
|
{
|
||||||
|
await _repository.Create(TestSample.GetAbsoluteEpisode());
|
||||||
|
Episode episode = await _repository.Edit(new Episode
|
||||||
|
{
|
||||||
|
ID = 2,
|
||||||
|
SeasonNumber = 1,
|
||||||
|
EpisodeNumber = 2
|
||||||
|
}, false);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||||
|
episode = await _repository.Get(2);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NormalToAbsoluteEditTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Get(1);
|
||||||
|
episode.SeasonNumber = null;
|
||||||
|
episode.AbsoluteNumber = 12;
|
||||||
|
episode = await _repository.Edit(episode, true);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
|
||||||
|
episode = await _repository.Get(1);
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MovieEpisodeTest()
|
||||||
|
{
|
||||||
|
Episode episode = await _repository.Create(TestSample.GetMovieEpisode());
|
||||||
|
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
|
||||||
|
episode = await _repository.Get(3);
|
||||||
|
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MovieEpisodeEditTest()
|
||||||
|
{
|
||||||
|
await _repository.Create(TestSample.GetMovieEpisode());
|
||||||
|
await Repositories.LibraryManager.Edit(new Show
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "john-wick"
|
||||||
|
}, false);
|
||||||
|
Episode episode = await _repository.Get(3);
|
||||||
|
Assert.Equal("john-wick", episode.Slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/GenreTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/GenreTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class GenreTests : AGenreTests
|
||||||
|
{
|
||||||
|
public GenreTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class GenreTests : AGenreTests
|
||||||
|
{
|
||||||
|
public GenreTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AGenreTests : RepositoryTests<Genre>
|
||||||
|
{
|
||||||
|
private readonly IGenreRepository _repository;
|
||||||
|
|
||||||
|
protected AGenreTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.GenreRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
Normal file
89
Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class LibraryItemTest : ALibraryItemTest
|
||||||
|
{
|
||||||
|
public LibraryItemTest(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class LibraryItemTest : ALibraryItemTest
|
||||||
|
{
|
||||||
|
public LibraryItemTest(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ALibraryItemTest
|
||||||
|
{
|
||||||
|
private readonly ILibraryItemRepository _repository;
|
||||||
|
private readonly RepositoryActivator _repositories;
|
||||||
|
|
||||||
|
protected ALibraryItemTest(RepositoryActivator repositories)
|
||||||
|
{
|
||||||
|
_repositories = repositories;
|
||||||
|
_repository = repositories.LibraryManager.LibraryItemRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CountTest()
|
||||||
|
{
|
||||||
|
Assert.Equal(2, await _repository.GetCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetShowTests()
|
||||||
|
{
|
||||||
|
LibraryItem expected = new(TestSample.Get<Show>());
|
||||||
|
LibraryItem actual = await _repository.Get(1);
|
||||||
|
KAssert.DeepEqual(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCollectionTests()
|
||||||
|
{
|
||||||
|
LibraryItem expected = new(TestSample.Get<Show>());
|
||||||
|
LibraryItem actual = await _repository.Get(-1);
|
||||||
|
KAssert.DeepEqual(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetShowSlugTests()
|
||||||
|
{
|
||||||
|
LibraryItem expected = new(TestSample.Get<Show>());
|
||||||
|
LibraryItem actual = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
KAssert.DeepEqual(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCollectionSlugTests()
|
||||||
|
{
|
||||||
|
LibraryItem expected = new(TestSample.Get<Collection>());
|
||||||
|
LibraryItem actual = await _repository.Get(TestSample.Get<Collection>().Slug);
|
||||||
|
KAssert.DeepEqual(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDuplicatedSlugTests()
|
||||||
|
{
|
||||||
|
await _repositories.LibraryManager.Create(new Collection()
|
||||||
|
{
|
||||||
|
Slug = TestSample.Get<Show>().Slug
|
||||||
|
});
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
Normal file
36
Kyoo.Tests/Library/SpecificTests/LibraryTests.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class LibraryTests : ALibraryTests
|
||||||
|
{
|
||||||
|
public LibraryTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class LibraryTests : ALibraryTests
|
||||||
|
{
|
||||||
|
public LibraryTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ALibraryTests : RepositoryTests<Models.Library>
|
||||||
|
{
|
||||||
|
private readonly ILibraryRepository _repository;
|
||||||
|
|
||||||
|
protected ALibraryTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.LibraryRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/PeopleTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class PeopleTests : APeopleTests
|
||||||
|
{
|
||||||
|
public PeopleTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class PeopleTests : APeopleTests
|
||||||
|
{
|
||||||
|
public PeopleTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class APeopleTests : RepositoryTests<People>
|
||||||
|
{
|
||||||
|
private readonly IPeopleRepository _repository;
|
||||||
|
|
||||||
|
protected APeopleTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.PeopleRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/ProviderTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class ProviderTests : AProviderTests
|
||||||
|
{
|
||||||
|
public ProviderTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class ProviderTests : AProviderTests
|
||||||
|
{
|
||||||
|
public ProviderTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AProviderTests : RepositoryTests<Provider>
|
||||||
|
{
|
||||||
|
private readonly IProviderRepository _repository;
|
||||||
|
|
||||||
|
protected AProviderTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.ProviderRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/SanityTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/SanityTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
public class GlobalTests : IDisposable, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly RepositoryActivator _repositories;
|
||||||
|
|
||||||
|
public GlobalTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_repositories = new RepositoryActivator(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
|
||||||
|
public void SampleTest()
|
||||||
|
{
|
||||||
|
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_repositories.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return _repositories.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
Normal file
79
Kyoo.Tests/Library/SpecificTests/SeasonTests.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class SeasonTests : ASeasonTests
|
||||||
|
{
|
||||||
|
public SeasonTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class SeasonTests : ASeasonTests
|
||||||
|
{
|
||||||
|
public SeasonTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ASeasonTests : RepositoryTests<Season>
|
||||||
|
{
|
||||||
|
private readonly ISeasonRepository _repository;
|
||||||
|
|
||||||
|
protected ASeasonTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.SeasonRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SlugEditTest()
|
||||||
|
{
|
||||||
|
Season season = await _repository.Get(1);
|
||||||
|
Assert.Equal("anohana-s1", season.Slug);
|
||||||
|
Show show = new()
|
||||||
|
{
|
||||||
|
ID = season.ShowID,
|
||||||
|
Slug = "new-slug"
|
||||||
|
};
|
||||||
|
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
|
||||||
|
season = await _repository.Get(1);
|
||||||
|
Assert.Equal("new-slug-s1", season.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeasonNumberEditTest()
|
||||||
|
{
|
||||||
|
Season season = await _repository.Get(1);
|
||||||
|
Assert.Equal("anohana-s1", season.Slug);
|
||||||
|
await _repository.Edit(new Season
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
SeasonNumber = 2
|
||||||
|
}, false);
|
||||||
|
season = await _repository.Get(1);
|
||||||
|
Assert.Equal("anohana-s2", season.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeasonCreationSlugTest()
|
||||||
|
{
|
||||||
|
Season season = await _repository.Create(new Season
|
||||||
|
{
|
||||||
|
ShowID = TestSample.Get<Show>().ID,
|
||||||
|
SeasonNumber = 2
|
||||||
|
});
|
||||||
|
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
292
Kyoo.Tests/Library/SpecificTests/ShowTests.cs
Normal file
292
Kyoo.Tests/Library/SpecificTests/ShowTests.cs
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class ShowTests : AShowTests
|
||||||
|
{
|
||||||
|
public ShowTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class ShowTests : AShowTests
|
||||||
|
{
|
||||||
|
public ShowTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AShowTests : RepositoryTests<Show>
|
||||||
|
{
|
||||||
|
private readonly IShowRepository _repository;
|
||||||
|
|
||||||
|
protected AShowTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.ShowRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.Path = "/super";
|
||||||
|
value.Title = "New Title";
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
KAssert.DeepEqual(value, edited);
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows.FirstAsync();
|
||||||
|
|
||||||
|
KAssert.DeepEqual(show, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditGenreTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.Genres = new[] {new Genre("test")};
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, edited.Slug);
|
||||||
|
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name}));
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows
|
||||||
|
.Include(x => x.Genres)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, show.Slug);
|
||||||
|
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditStudioTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.Studio = new Studio("studio");
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, edited.Slug);
|
||||||
|
Assert.Equal("studio", edited.Studio.Slug);
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows
|
||||||
|
.Include(x => x.Genres)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, show.Slug);
|
||||||
|
Assert.Equal("studio", edited.Studio.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditAliasesTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.Aliases = new[] {"NiceNewAlias", "SecondAlias"};
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, edited.Slug);
|
||||||
|
Assert.Equal(value.Aliases, edited.Aliases);
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows.FirstAsync();
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, show.Slug);
|
||||||
|
Assert.Equal(value.Aliases, edited.Aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditPeopleTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.People = new[]
|
||||||
|
{
|
||||||
|
new PeopleRole
|
||||||
|
{
|
||||||
|
Show = value,
|
||||||
|
People = TestSample.Get<People>(),
|
||||||
|
ForPeople = false,
|
||||||
|
Type = "Actor",
|
||||||
|
Role = "NiceCharacter"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, edited.Slug);
|
||||||
|
Assert.Equal(edited.People.First().ShowID, value.ID);
|
||||||
|
Assert.Equal(
|
||||||
|
value.People.Select(x => new{x.Role, x.Slug, x.People.Name}),
|
||||||
|
edited.People.Select(x => new{x.Role, x.Slug, x.People.Name}));
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows
|
||||||
|
.Include(x => x.People)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, show.Slug);
|
||||||
|
Assert.Equal(
|
||||||
|
value.People.Select(x => new{x.Role, x.Slug, x.People.Name}),
|
||||||
|
edited.People.Select(x => new{x.Role, x.Slug, x.People.Name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditExternalIDsTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
value.ExternalIDs = new[]
|
||||||
|
{
|
||||||
|
new MetadataID<Show>()
|
||||||
|
{
|
||||||
|
First = value,
|
||||||
|
Second = new Provider("test", "test.png"),
|
||||||
|
DataID = "1234"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Show edited = await _repository.Edit(value, false);
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, edited.Slug);
|
||||||
|
Assert.Equal(
|
||||||
|
value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}),
|
||||||
|
edited.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}));
|
||||||
|
|
||||||
|
await using DatabaseContext database = Repositories.Context.New();
|
||||||
|
Show show = await database.Shows
|
||||||
|
.Include(x => x.ExternalIDs)
|
||||||
|
.ThenInclude(x => x.Second)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
Assert.Equal(value.Slug, show.Slug);
|
||||||
|
Assert.Equal(
|
||||||
|
value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}),
|
||||||
|
show.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditResetOldTest()
|
||||||
|
{
|
||||||
|
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
|
||||||
|
Show newValue = new()
|
||||||
|
{
|
||||||
|
ID = value.ID,
|
||||||
|
Title = "Reset"
|
||||||
|
};
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Edit(newValue, true));
|
||||||
|
|
||||||
|
newValue.Slug = "reset";
|
||||||
|
Show edited = await _repository.Edit(newValue, true);
|
||||||
|
|
||||||
|
Assert.Equal(value.ID, edited.ID);
|
||||||
|
Assert.Null(edited.Overview);
|
||||||
|
Assert.Equal("reset", edited.Slug);
|
||||||
|
Assert.Equal("Reset", edited.Title);
|
||||||
|
Assert.Null(edited.Aliases);
|
||||||
|
Assert.Null(edited.ExternalIDs);
|
||||||
|
Assert.Null(edited.People);
|
||||||
|
Assert.Null(edited.Genres);
|
||||||
|
Assert.Null(edited.Studio);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateWithRelationsTest()
|
||||||
|
{
|
||||||
|
Show expected = TestSample.Get<Show>();
|
||||||
|
expected.ID = 0;
|
||||||
|
expected.Slug = "created-relation-test";
|
||||||
|
expected.ExternalIDs = new[]
|
||||||
|
{
|
||||||
|
new MetadataID<Show>
|
||||||
|
{
|
||||||
|
First = expected,
|
||||||
|
Second = new Provider("provider", "provider.png"),
|
||||||
|
DataID = "ID"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expected.Genres = new[]
|
||||||
|
{
|
||||||
|
new Genre
|
||||||
|
{
|
||||||
|
Name = "Genre",
|
||||||
|
Slug = "genre"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expected.People = new[]
|
||||||
|
{
|
||||||
|
new PeopleRole
|
||||||
|
{
|
||||||
|
People = TestSample.Get<People>(),
|
||||||
|
Show = expected,
|
||||||
|
ForPeople = false,
|
||||||
|
Role = "actor"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expected.Studio = new Studio("studio");
|
||||||
|
Show created = await _repository.Create(expected);
|
||||||
|
KAssert.DeepEqual(expected, created);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SlugDuplicationTest()
|
||||||
|
{
|
||||||
|
Show test = TestSample.Get<Show>();
|
||||||
|
test.ID = 0;
|
||||||
|
test.Slug = "300";
|
||||||
|
Show created = await _repository.Create(test);
|
||||||
|
Assert.Equal("300!", created.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSlugTest()
|
||||||
|
{
|
||||||
|
Show reference = TestSample.Get<Show>();
|
||||||
|
Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("test")]
|
||||||
|
[InlineData("super")]
|
||||||
|
[InlineData("title")]
|
||||||
|
[InlineData("TiTlE")]
|
||||||
|
[InlineData("SuPeR")]
|
||||||
|
public async Task SearchTest(string query)
|
||||||
|
{
|
||||||
|
Show value = new()
|
||||||
|
{
|
||||||
|
Slug = "super-test",
|
||||||
|
Title = "This is a test title²"
|
||||||
|
};
|
||||||
|
await _repository.Create(value);
|
||||||
|
ICollection<Show> ret = await _repository.Search(query);
|
||||||
|
KAssert.DeepEqual(value, ret.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteShowWithEpisodeAndSeason()
|
||||||
|
{
|
||||||
|
Show show = TestSample.Get<Show>();
|
||||||
|
await Repositories.LibraryManager.Load(show, x => x.Seasons);
|
||||||
|
await Repositories.LibraryManager.Load(show, x => x.Episodes);
|
||||||
|
Assert.Equal(1, await _repository.GetCount());
|
||||||
|
Assert.Equal(1, show.Seasons.Count);
|
||||||
|
Assert.Equal(1, show.Episodes.Count);
|
||||||
|
await _repository.Delete(show);
|
||||||
|
Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount());
|
||||||
|
Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount());
|
||||||
|
Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/StudioTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/StudioTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class StudioTests : AStudioTests
|
||||||
|
{
|
||||||
|
public StudioTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class StudioTests : AStudioTests
|
||||||
|
{
|
||||||
|
public StudioTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AStudioTests : RepositoryTests<Studio>
|
||||||
|
{
|
||||||
|
private readonly IStudioRepository _repository;
|
||||||
|
|
||||||
|
protected AStudioTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.StudioRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Kyoo.Tests/Library/SpecificTests/TrackTests.cs
Normal file
51
Kyoo.Tests/Library/SpecificTests/TrackTests.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class TrackTests : ATrackTests
|
||||||
|
{
|
||||||
|
public TrackTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class TrackTests : ATrackTests
|
||||||
|
{
|
||||||
|
public TrackTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ATrackTests : RepositoryTests<Track>
|
||||||
|
{
|
||||||
|
private readonly ITrackRepository _repository;
|
||||||
|
|
||||||
|
protected ATrackTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = repositories.LibraryManager.TrackRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SlugEditTest()
|
||||||
|
{
|
||||||
|
await Repositories.LibraryManager.ShowRepository.Edit(new Show
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "new-slug"
|
||||||
|
}, false);
|
||||||
|
Track track = await _repository.Get(1);
|
||||||
|
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Kyoo.Tests/Library/SpecificTests/UserTests.cs
Normal file
37
Kyoo.Tests/Library/SpecificTests/UserTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Kyoo.Controllers;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests.Library
|
||||||
|
{
|
||||||
|
namespace SqLite
|
||||||
|
{
|
||||||
|
public class UserTests : AUserTests
|
||||||
|
{
|
||||||
|
public UserTests(ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PostgreSQL
|
||||||
|
{
|
||||||
|
[Collection(nameof(Postgresql))]
|
||||||
|
public class UserTests : AUserTests
|
||||||
|
{
|
||||||
|
public UserTests(PostgresFixture postgres, ITestOutputHelper output)
|
||||||
|
: base(new RepositoryActivator(output, postgres)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AUserTests : RepositoryTests<User>
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _repository;
|
||||||
|
|
||||||
|
protected AUserTests(RepositoryActivator repositories)
|
||||||
|
: base(repositories)
|
||||||
|
{
|
||||||
|
_repository = Repositories.LibraryManager.UserRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,79 +1,203 @@
|
|||||||
// using Kyoo.Models;
|
using 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
276
Kyoo.Tests/Library/TestSample.cs
Normal file
276
Kyoo.Tests/Library/TestSample.cs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Kyoo.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public static class TestSample
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
typeof(Show),
|
||||||
|
() => new Show()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly Dictionary<Type, Func<object>> Samples = new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
typeof(Models.Library),
|
||||||
|
() => new Models.Library
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "deck",
|
||||||
|
Name = "Deck",
|
||||||
|
Paths = new[] {"/path/to/deck"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Collection),
|
||||||
|
() => new Collection
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "collection",
|
||||||
|
Name = "Collection",
|
||||||
|
Overview = "A nice collection for tests",
|
||||||
|
Poster = "Poster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Show),
|
||||||
|
() => new Show
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "anohana",
|
||||||
|
Title = "Anohana: The Flower We Saw That Day",
|
||||||
|
Aliases = new[]
|
||||||
|
{
|
||||||
|
"Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
|
||||||
|
"AnoHana",
|
||||||
|
"We Still Don't Know the Name of the Flower We Saw That Day."
|
||||||
|
},
|
||||||
|
Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
|
||||||
|
"In time, however, these childhood friends drifted apart, and when they became high " +
|
||||||
|
"school students, they had long ceased to think of each other as friends.",
|
||||||
|
Status = Status.Finished,
|
||||||
|
TrailerUrl = null,
|
||||||
|
StartAir = new DateTime(2011, 1, 1),
|
||||||
|
EndAir = new DateTime(2011, 1, 1),
|
||||||
|
Poster = "poster",
|
||||||
|
Logo = "logo",
|
||||||
|
Backdrop = "backdrop",
|
||||||
|
IsMovie = false,
|
||||||
|
Studio = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Season),
|
||||||
|
() => new Season
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
ShowSlug = "anohana",
|
||||||
|
ShowID = 1,
|
||||||
|
SeasonNumber = 1,
|
||||||
|
Title = "Season 1",
|
||||||
|
Overview = "The first season",
|
||||||
|
StartDate = new DateTime(2020, 06, 05),
|
||||||
|
EndDate = new DateTime(2020, 07, 05),
|
||||||
|
Poster = "poster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Episode),
|
||||||
|
() => new Episode
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
ShowSlug = "anohana",
|
||||||
|
ShowID = 1,
|
||||||
|
SeasonID = 1,
|
||||||
|
SeasonNumber = 1,
|
||||||
|
EpisodeNumber = 1,
|
||||||
|
AbsoluteNumber = 1,
|
||||||
|
Path = "/home/kyoo/anohana-s1e1",
|
||||||
|
Thumb = "thumbnail",
|
||||||
|
Title = "Episode 1",
|
||||||
|
Overview = "Summary of the first episode",
|
||||||
|
ReleaseDate = new DateTime(2020, 06, 05)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Track),
|
||||||
|
() => new Track
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
EpisodeID = 1,
|
||||||
|
Codec = "subrip",
|
||||||
|
Language = "eng",
|
||||||
|
Path = "/path",
|
||||||
|
Title = "Subtitle track",
|
||||||
|
Type = StreamType.Subtitle,
|
||||||
|
EpisodeSlug = Get<Episode>().Slug,
|
||||||
|
IsDefault = true,
|
||||||
|
IsExternal = false,
|
||||||
|
IsForced = false,
|
||||||
|
TrackIndex = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(People),
|
||||||
|
() => new People
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "the-actor",
|
||||||
|
Name = "The Actor",
|
||||||
|
Poster = "NicePoster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Studio),
|
||||||
|
() => new Studio
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "hyper-studio",
|
||||||
|
Name = "Hyper studio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Genre),
|
||||||
|
() => new Genre
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "action",
|
||||||
|
Name = "Action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Provider),
|
||||||
|
() => new Provider
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "tvdb",
|
||||||
|
Name = "The TVDB",
|
||||||
|
Logo = "path/tvdb.svg",
|
||||||
|
LogoExtension = "svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(User),
|
||||||
|
() => new User
|
||||||
|
{
|
||||||
|
ID = 1,
|
||||||
|
Slug = "user",
|
||||||
|
Username = "User",
|
||||||
|
Email = "user@im-a-user.com",
|
||||||
|
Password = "MD5-encoded",
|
||||||
|
Permissions = new [] {"overall.read"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static T Get<T>()
|
||||||
|
{
|
||||||
|
return (T)Samples[typeof(T)]();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T GetNew<T>()
|
||||||
|
{
|
||||||
|
return (T)NewSamples[typeof(T)]();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FillDatabase(DatabaseContext context)
|
||||||
|
{
|
||||||
|
Collection collection = Get<Collection>();
|
||||||
|
collection.ID = 0;
|
||||||
|
context.Collections.Add(collection);
|
||||||
|
|
||||||
|
Show show = Get<Show>();
|
||||||
|
show.ID = 0;
|
||||||
|
context.Shows.Add(show);
|
||||||
|
|
||||||
|
Season season = Get<Season>();
|
||||||
|
season.ID = 0;
|
||||||
|
season.ShowID = 0;
|
||||||
|
season.Show = show;
|
||||||
|
context.Seasons.Add(season);
|
||||||
|
|
||||||
|
Episode episode = Get<Episode>();
|
||||||
|
episode.ID = 0;
|
||||||
|
episode.ShowID = 0;
|
||||||
|
episode.Show = show;
|
||||||
|
episode.SeasonID = 0;
|
||||||
|
episode.Season = season;
|
||||||
|
context.Episodes.Add(episode);
|
||||||
|
|
||||||
|
Track track = Get<Track>();
|
||||||
|
track.ID = 0;
|
||||||
|
track.EpisodeID = 0;
|
||||||
|
track.Episode = episode;
|
||||||
|
context.Tracks.Add(track);
|
||||||
|
|
||||||
|
Studio studio = Get<Studio>();
|
||||||
|
studio.ID = 0;
|
||||||
|
studio.Shows = new List<Show> {show};
|
||||||
|
context.Studios.Add(studio);
|
||||||
|
|
||||||
|
Genre genre = Get<Genre>();
|
||||||
|
genre.ID = 0;
|
||||||
|
genre.Shows = new List<Show> {show};
|
||||||
|
context.Genres.Add(genre);
|
||||||
|
|
||||||
|
People people = Get<People>();
|
||||||
|
people.ID = 0;
|
||||||
|
context.People.Add(people);
|
||||||
|
|
||||||
|
Provider provider = Get<Provider>();
|
||||||
|
provider.ID = 0;
|
||||||
|
context.Providers.Add(provider);
|
||||||
|
|
||||||
|
Models.Library library = Get<Models.Library>();
|
||||||
|
library.ID = 0;
|
||||||
|
library.Collections = new List<Collection> {collection};
|
||||||
|
library.Providers = new List<Provider> {provider};
|
||||||
|
context.Libraries.Add(library);
|
||||||
|
|
||||||
|
User user = Get<User>();
|
||||||
|
user.ID = 0;
|
||||||
|
context.Users.Add(user);
|
||||||
|
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Episode GetAbsoluteEpisode()
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
ID = 2,
|
||||||
|
ShowSlug = "anohana",
|
||||||
|
ShowID = 1,
|
||||||
|
SeasonNumber = null,
|
||||||
|
EpisodeNumber = null,
|
||||||
|
AbsoluteNumber = 3,
|
||||||
|
Path = "/home/kyoo/anohana-3",
|
||||||
|
Thumb = "thumbnail",
|
||||||
|
Title = "Episode 3",
|
||||||
|
Overview = "Summary of the third absolute episode",
|
||||||
|
ReleaseDate = new DateTime(2020, 06, 05)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Episode GetMovieEpisode()
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
ID = 3,
|
||||||
|
ShowSlug = "anohana",
|
||||||
|
ShowID = 1,
|
||||||
|
Path = "/home/kyoo/john-wick",
|
||||||
|
Thumb = "thumb",
|
||||||
|
Title = "John wick",
|
||||||
|
Overview = "A movie episode test",
|
||||||
|
ReleaseDate = new DateTime(1595, 05, 12)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Kyoo.Tests/Utility/EnumerableTests.cs
Normal file
71
Kyoo.Tests/Utility/EnumerableTests.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public class EnumerableTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MapTest()
|
||||||
|
{
|
||||||
|
int[] list = {1, 2, 3, 4};
|
||||||
|
Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list.Map(((Func<int, int, int>)null)!));
|
||||||
|
list = null;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list!.Map((x, _) => x + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MapAsyncTest()
|
||||||
|
{
|
||||||
|
int[] list = {1, 2, 3, 4};
|
||||||
|
await foreach((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i))))
|
||||||
|
{
|
||||||
|
Assert.Equal(x - 1, i);
|
||||||
|
}
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list.MapAsync(((Func<int, int, Task<int>>)null)!));
|
||||||
|
list = null;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list!.MapAsync((x, _) => Task.FromResult(x + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SelectAsyncTest()
|
||||||
|
{
|
||||||
|
int[] list = {1, 2, 3, 4};
|
||||||
|
int i = 2;
|
||||||
|
await foreach(int x in list.SelectAsync(x => Task.FromResult(x + 1)))
|
||||||
|
{
|
||||||
|
Assert.Equal(i++, x);
|
||||||
|
}
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list.SelectAsync(((Func<int, Task<int>>)null)!));
|
||||||
|
list = null;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list!.SelectAsync(x => Task.FromResult(x + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ToListAsyncTest()
|
||||||
|
{
|
||||||
|
int[] expected = {1, 2, 3, 4};
|
||||||
|
IAsyncEnumerable<int> list = expected.SelectAsync(Task.FromResult);
|
||||||
|
Assert.Equal(expected, await list.ToListAsync());
|
||||||
|
list = null;
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => list!.ToListAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IfEmptyTest()
|
||||||
|
{
|
||||||
|
int[] list = {1, 2, 3, 4};
|
||||||
|
list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray();
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list.IfEmpty(null!).ToList());
|
||||||
|
list = null;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => list!.IfEmpty(() => {}).ToList());
|
||||||
|
list = Array.Empty<int>();
|
||||||
|
Assert.Throws<ArgumentException>(() => list.IfEmpty(() => throw new ArgumentException()).ToList());
|
||||||
|
Assert.Empty(list.IfEmpty(() => {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Kyoo.Tests/Utility/MergerTests.cs
Normal file
21
Kyoo.Tests/Utility/MergerTests.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Kyoo.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public class MergerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NullifyTest()
|
||||||
|
{
|
||||||
|
Genre genre = new("test")
|
||||||
|
{
|
||||||
|
ID = 5
|
||||||
|
};
|
||||||
|
Merger.Nullify(genre);
|
||||||
|
Assert.Equal(0, genre.ID);
|
||||||
|
Assert.Null(genre.Name);
|
||||||
|
Assert.Null(genre.Slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Kyoo.Tests/Utility/TaskTests.cs
Normal file
76
Kyoo.Tests/Utility/TaskTests.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Kyoo.Tests
|
||||||
|
{
|
||||||
|
public class TaskTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task DefaultIfNullTest()
|
||||||
|
{
|
||||||
|
Assert.Equal(0, await TaskUtils.DefaultIfNull<int>(null));
|
||||||
|
Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ThenTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
|
||||||
|
.Then(_ => throw new ArgumentException()));
|
||||||
|
Assert.Equal(1, await Task.FromResult(1)
|
||||||
|
.Then(_ => {}));
|
||||||
|
|
||||||
|
static async Task<int> Faulted()
|
||||||
|
{
|
||||||
|
await Task.Delay(1);
|
||||||
|
throw new ArgumentException();
|
||||||
|
}
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => Faulted().Then(_ => KAssert.Fail()));
|
||||||
|
|
||||||
|
static async Task<int> Infinite()
|
||||||
|
{
|
||||||
|
await Task.Delay(100000);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationTokenSource token = new();
|
||||||
|
token.Cancel();
|
||||||
|
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
|
||||||
|
.Then(_ => {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MapTest()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
|
||||||
|
.Map<int, int>(_ => throw new ArgumentException()));
|
||||||
|
Assert.Equal(2, await Task.FromResult(1)
|
||||||
|
.Map(x => x + 1));
|
||||||
|
|
||||||
|
static async Task<int> Faulted()
|
||||||
|
{
|
||||||
|
await Task.Delay(1);
|
||||||
|
throw new ArgumentException();
|
||||||
|
}
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => Faulted()
|
||||||
|
.Map(x =>
|
||||||
|
{
|
||||||
|
KAssert.Fail();
|
||||||
|
return x;
|
||||||
|
}));
|
||||||
|
|
||||||
|
static async Task<int> Infinite()
|
||||||
|
{
|
||||||
|
await Task.Delay(100000);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationTokenSource token = new();
|
||||||
|
token.Cancel();
|
||||||
|
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
|
||||||
|
.Map(x => x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,12 +13,23 @@ namespace Kyoo.Tests
|
|||||||
Expression<Func<Show, int>> member = x => x.ID;
|
Expression<Func<Show, 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
Kyoo.sln
6
Kyoo.sln
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 override async Task<Episode> GetOrDefault(string slug)
|
public Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
|
||||||
{
|
{
|
||||||
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\d*)");
|
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
|
||||||
|
&& x.SeasonNumber == seasonNumber
|
||||||
if (match.Success)
|
&& x.EpisodeNumber == episodeNumber);
|
||||||
{
|
|
||||||
return await GetOrDefault(match.Groups["show"].Value,
|
|
||||||
int.Parse(match.Groups["season"].Value),
|
|
||||||
int.Parse(match.Groups["episode"].Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug);
|
|
||||||
if (episode != null)
|
|
||||||
episode.ShowSlug = slug;
|
|
||||||
return episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override async Task<Episode> GetOrDefault(Expression<Func<Episode, bool>> where)
|
|
||||||
{
|
|
||||||
Episode ret = await base.GetOrDefault(where);
|
|
||||||
if (ret != null)
|
|
||||||
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
|
|
||||||
{
|
|
||||||
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
|
|
||||||
&& x.SeasonNumber == seasonNumber
|
|
||||||
&& x.EpisodeNumber == episodeNumber);
|
|
||||||
if (ret != null)
|
|
||||||
ret.ShowSlug = showSlug;
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -122,61 +82,30 @@ 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 />
|
||||||
public override async Task<Episode> Create(Episode obj)
|
public override async Task<Episode> Create(Episode obj)
|
||||||
{
|
{
|
||||||
@ -185,6 +114,9 @@ namespace Kyoo.Controllers
|
|||||||
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
|
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 />
|
||||||
|
|||||||
@ -22,15 +22,7 @@ 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 />
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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 =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,73 +22,30 @@ 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/>
|
||||||
public async Task<Season> Get(int showID, int seasonNumber)
|
public async Task<Season> Get(int showID, int seasonNumber)
|
||||||
{
|
{
|
||||||
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,27 +75,13 @@ 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/>
|
||||||
public override async Task<Season> Create(Season obj)
|
public override async Task<Season> Create(Season obj)
|
||||||
{
|
{
|
||||||
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,13 +123,9 @@ 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;
|
|
||||||
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
|
|
||||||
await _database.SaveChangesAsync();
|
|
||||||
|
|
||||||
if (obj.Episodes != null)
|
_database.Remove(obj);
|
||||||
await _episodes.Value.DeleteRange(obj.Episodes);
|
await _database.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,62 +24,12 @@ 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,23 +40,19 @@ 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;
|
||||||
if (obj.EpisodeID <= 0)
|
if (obj.EpisodeID <= 0)
|
||||||
throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID}).");
|
throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
67
Kyoo/Models/Stream.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Kyoo.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Models.Watch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The unmanaged stream that the transcoder will return.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||||
|
public class Stream
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The title of the stream.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The language of this stream (as a ISO-639-2 language code)
|
||||||
|
/// </summary>
|
||||||
|
public string Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The codec of this stream.
|
||||||
|
/// </summary>
|
||||||
|
public string Codec { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this stream the default one of it's type?
|
||||||
|
/// </summary>
|
||||||
|
[MarshalAs(UnmanagedType.I1)] public bool IsDefault;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this stream tagged as forced?
|
||||||
|
/// </summary>
|
||||||
|
[MarshalAs(UnmanagedType.I1)] public bool IsForced;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of this track.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of this stream.
|
||||||
|
/// </summary>
|
||||||
|
[SerializeIgnore] public StreamType Type { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a track from this stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new track that represent this stream.</returns>
|
||||||
|
public Track ToTrack()
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
Language = Language,
|
||||||
|
Codec = Codec,
|
||||||
|
IsDefault = IsDefault,
|
||||||
|
IsForced = IsForced,
|
||||||
|
Path = Path,
|
||||||
|
Type = Type,
|
||||||
|
IsExternal = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,7 @@ namespace Kyoo
|
|||||||
_plugins.LoadPlugins(new IPlugin[] {
|
_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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,13 +294,19 @@ 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}";
|
|
||||||
await libraryManager.Create(show);
|
if (show.StartAir != null)
|
||||||
|
{
|
||||||
|
show.Slug += $"-{show.StartAir.Value.Year}";
|
||||||
|
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)
|
||||||
episode.Season = season;
|
{
|
||||||
episode.SeasonID = season?.ID;
|
season ??= await GetSeason(libraryManager, show, episode.SeasonNumber.Value, library);
|
||||||
|
episode.Season = season;
|
||||||
|
episode.SeasonID = season?.ID;
|
||||||
|
}
|
||||||
await ThumbnailsManager.Validate(episode);
|
await ThumbnailsManager.Validate(episode);
|
||||||
await GetTracks(episode);
|
await GetTracks(episode);
|
||||||
return episode;
|
return episode;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user