Merge pull request #30 from AnonymusRaccoon/sqlite

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

View File

@ -2,9 +2,10 @@ name: Analysis
on: [push, pull_request]
jobs:
build:
analysis:
name: Static Analysis
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v2
with:
@ -28,16 +29,27 @@ jobs:
run: |
mkdir -p ./.sonar/scanner
dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner
- name: Wait for tests to run
uses: lewagon/wait-on-check-action@master
with:
ref: ${{github.ref}}
check-name: tests
repo-token: ${{secrets.GITHUB_TOKEN}}
running-workflow-name: analysis
allowed-conclusions: success,skipped,cancelled,neutral,failure
- name: Download coverage report
uses: dawidd6/action-download-artifact@v2
with:
commit: ${{env.COMMIT_SHA}}
workflow: tests.yml
github_token: ${{secrets.GITHUB_TOKEN}}
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: bash
run: |
dotnet test \
'-p:CollectCoverage=true;CoverletOutputFormat=opencover' \
'-p:SkipTranscoder=true;SkipWebApp=true' || echo "Test failed. Skipping..."
find . -name 'coverage.opencover.xml'
dotnet build-server shutdown
./.sonar/scanner/dotnet-sonarscanner begin \

View File

@ -3,17 +3,40 @@ name: Testing
on: [push, pull_request]
jobs:
build:
tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
container: mcr.microsoft.com/dotnet/sdk:5.0
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true'
run: |
dotnet build --no-restore '-p:SkipWebApp=true;SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true
cp ./Kyoo.Common/bin/Debug/net5.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./Kyoo.Tests/bin/Debug/net5.0/
- name: Test
run: dotnet test --no-build
run: dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover'
env:
POSTGRES_HOST: postgres
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres
- name: Sanitize coverage output
if: ${{ always() }}
run: sed -i "s'$(pwd)'.'" Kyoo.Tests/coverage.opencover.xml
- name: Upload coverage report
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: coverage.xml
path: "**/coverage.opencover.xml"

View File

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

View File

@ -16,6 +16,6 @@ namespace Kyoo.Controllers
Task<Season> GetSeason(Show show, int seasonNumber);
Task<Episode> GetEpisode(Show show, int seasonNumber, int episodeNumber, int absoluteNumber);
Task<Episode> GetEpisode(Show show, int? seasonNumber, int? episodeNumber, int? absoluteNumber);
}
}

View File

@ -11,7 +11,7 @@ namespace Kyoo.Controllers
Task<Show> SearchShow(string showName, bool isMovie, Library library);
Task<IEnumerable<Show>> SearchShows(string showName, bool isMovie, Library library);
Task<Season> GetSeason(Show show, int seasonNumber, Library library);
Task<Episode> GetEpisode(Show show, string episodePath, int seasonNumber, int episodeNumber, int absoluteNumber, Library library);
Task<Episode> GetEpisode(Show show, string episodePath, int? seasonNumber, int? episodeNumber, int? absoluteNumber, Library library);
Task<ICollection<PeopleRole>> GetPeople(Show show, Library library);
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@ -244,47 +243,11 @@ namespace Kyoo.Controllers
Task Delete([NotNull] T obj);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">One or multiple resources to delete</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">An enumerable of resources to delete</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<T> objs);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">One or multiple resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">An enumerable of resource's id</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">One or multiple resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">An enumerable of resource's slug</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs);
/// <summary>
/// Delete a list of resources.
/// Delete all resources that match the predicate.
/// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
Task DeleteAll([NotNull] Expression<Func<T, bool>> where);
}
/// <summary>
@ -412,25 +375,7 @@ namespace Kyoo.Controllers
/// <summary>
/// A repository to handle tracks
/// </summary>
public interface ITrackRepository : IRepository<Track>
{
/// <summary>
/// Get a track from it's slug and it's type.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The track found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The track found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
}
public interface ITrackRepository : IRepository<Track> { }
/// <summary>
/// A repository to handle libraries.
@ -631,10 +576,12 @@ namespace Kyoo.Controllers
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <typeparam name="T">The type of metadata to retrieve</typeparam>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default);
Task<ICollection<MetadataID<T>>> GetMetadataID<T>(Expression<Func<MetadataID<T>, bool>> where = null,
Sort<MetadataID<T>> sort = default,
Pagination limit = default)
where T : class, IResource;
/// <summary>
/// Get a list of external ids that match all filters
@ -643,10 +590,11 @@ namespace Kyoo.Controllers
/// <param name="sort">A sort by expression</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID, object>> sort,
Task<ICollection<MetadataID<T>>> GetMetadataID<T>([Optional] Expression<Func<MetadataID<T>, bool>> where,
Expression<Func<MetadataID<T>, object>> sort,
Pagination limit = default
) => GetMetadataID(where, new Sort<MetadataID>(sort), limit);
) where T : class, IResource
=> GetMetadataID(where, new Sort<MetadataID<T>>(sort), limit);
}
/// <summary>

View File

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

View File

@ -0,0 +1,10 @@
using System;
namespace Kyoo.Models.Attributes
{
/// <summary>
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ComputedAttribute : NotMergeableAttribute { }
}

View File

@ -8,7 +8,8 @@ namespace Kyoo.Models.Attributes
/// An attribute to inform that the service will be injected automatically by a service provider.
/// </summary>
/// <remarks>
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>
/// It should only be used on <see cref="ITask"/> and will be injected before calling <see cref="ITask.Run"/>.
/// It can also be used on <see cref="IPlugin"/> and it will be injected before calling <see cref="IPlugin.ConfigureAspNet"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]

View File

@ -0,0 +1,13 @@
using System;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Common.Models.Attributes
{
/// <summary>
/// An attribute to mark Link properties on resource.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse]
public class LinkAttribute : SerializeIgnoreAttribute { }
}

View File

@ -1,17 +1,34 @@
using System;
using Kyoo.Controllers;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
/// <summary>
/// The targeted relation can be edited via calls to the repository's <see cref="IRepository{T}.Edit"/> method.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class EditableRelationAttribute : Attribute { }
/// <summary>
/// The targeted relation can be loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class LoadableRelationAttribute : Attribute
{
/// <summary>
/// The name of the field containing the related resource's ID.
/// </summary>
public string RelationID { get; }
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary>
public LoadableRelationAttribute() {}
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
/// </summary>
/// <param name="relationID">The name of the RelationID field.</param>
public LoadableRelationAttribute(string relationID)
{
RelationID = relationID;

View File

@ -2,17 +2,41 @@ using System;
namespace Kyoo.Models.Attributes
{
/// <summary>
/// Remove an property from the serialization pipeline. It will simply be skipped.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeIgnoreAttribute : Attribute {}
/// <summary>
/// Remove a property from the deserialization pipeline. The user can't input value for this property.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DeserializeIgnoreAttribute : Attribute {}
/// <summary>
/// Change the way the field is serialized. It allow one to use a string format like formatting instead of the default value.
/// This can be disabled for a request by setting the "internal" query string parameter to true.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SerializeAsAttribute : Attribute
{
/// <summary>
/// The format string to use.
/// </summary>
public string Format { get; }
/// <summary>
/// Create a new <see cref="SerializeAsAttribute"/> with the selected format.
/// </summary>
/// <remarks>
/// The format string can contains any property within {}. It will be replaced by the actual value of the property.
/// You can also use the special value {HOST} that will put the webhost address.
/// </remarks>
/// <example>
/// The show's poster serialized uses this format string: <code>{HOST}/api/shows/{Slug}/poster</code>
/// </example>
/// <param name="format">The format to use</param>
public SerializeAsAttribute(string format)
{
Format = format;

View File

@ -0,0 +1,38 @@
namespace Kyoo.Models
{
/// <summary>
/// A chapter to split an episode in multiple parts.
/// </summary>
public class Chapter
{
/// <summary>
/// The start time of the chapter (in second from the start of the episode).
/// </summary>
public float StartTime { get; set; }
/// <summary>
/// The end time of the chapter (in second from the start of the episode)&.
/// </summary>
public float EndTime { get; set; }
/// <summary>
/// The name of this chapter. This should be a human-readable name that could be presented to the user.
/// There should be well-known chapters name for commonly used chapters.
/// For example, use "Opening" for the introduction-song and "Credits" for the end chapter with credits.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Create a new <see cref="Chapter"/>.
/// </summary>
/// <param name="startTime">The start time of the chapter (in second)</param>
/// <param name="endTime">The end time of the chapter (in second)</param>
/// <param name="name">The name of this chapter</param>
public Chapter(float startTime, float endTime, string name)
{
StartTime = startTime;
EndTime = endTime;
Name = name;
}
}
}

View File

@ -1,10 +1,12 @@
using System;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// The type of item, ether a show, a movie or a collection.
/// </summary>
public enum ItemType
{
Show,
@ -12,22 +14,67 @@ namespace Kyoo.Models
Collection
}
/// <summary>
/// A type union between <see cref="Show"/> and <see cref="Collection"/>.
/// This is used to list content put inside a library.
/// </summary>
public class LibraryItem : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The title of the show or collection.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The summary of the show or collection.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// Is this show airing, not aired yet or finished? This is only applicable for shows.
/// </summary>
public Status? Status { get; set; }
public string TrailerUrl { get; set; }
public int? StartYear { get; set; }
public int? EndYear { get; set; }
[SerializeAs("{HOST}/api/{_type}/{Slug}/poster")] public string Poster { get; set; }
[UsedImplicitly] private string _type => Type == ItemType.Collection ? "collection" : "show";
/// <summary>
/// The date this show or collection started airing. It can be null if this is unknown.
/// </summary>
public DateTime? StartAir { get; set; }
/// <summary>
/// The date this show or collection finished airing.
/// It must be after the <see cref="StartAir"/> but can be the same (example: for movies).
/// It can also be null if this is unknown.
/// </summary>
public DateTime? EndAir { get; set; }
/// <summary>
/// The path of this item's poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The type of this item (ether a collection, a show or a movie).
/// </summary>
public ItemType Type { get; set; }
/// <summary>
/// Create a new, empty <see cref="LibraryItem"/>.
/// </summary>
public LibraryItem() {}
/// <summary>
/// Create a <see cref="LibraryItem"/> from a show.
/// </summary>
/// <param name="show">The show that this library item should represent.</param>
public LibraryItem(Show show)
{
ID = show.ID;
@ -35,13 +82,16 @@ namespace Kyoo.Models
Title = show.Title;
Overview = show.Overview;
Status = show.Status;
TrailerUrl = show.TrailerUrl;
StartYear = show.StartYear;
EndYear = show.EndYear;
StartAir = show.StartAir;
EndAir = show.EndAir;
Poster = show.Poster;
Type = show.IsMovie ? ItemType.Movie : ItemType.Show;
}
/// <summary>
/// Create a <see cref="LibraryItem"/> from a collection
/// </summary>
/// <param name="collection">The collection that this library item should represent.</param>
public LibraryItem(Collection collection)
{
ID = -collection.ID;
@ -49,13 +99,15 @@ namespace Kyoo.Models
Title = collection.Name;
Overview = collection.Overview;
Status = Models.Status.Unknown;
TrailerUrl = null;
StartYear = null;
EndYear = null;
StartAir = null;
EndAir = null;
Poster = collection.Poster;
Type = ItemType.Collection;
}
/// <summary>
/// An expression to create a <see cref="LibraryItem"/> representing a show.
/// </summary>
public static Expression<Func<Show, LibraryItem>> FromShow => x => new LibraryItem
{
ID = x.ID,
@ -63,13 +115,15 @@ namespace Kyoo.Models
Title = x.Title,
Overview = x.Overview,
Status = x.Status,
TrailerUrl = x.TrailerUrl,
StartYear = x.StartYear,
EndYear = x.EndYear,
StartAir = x.StartAir,
EndAir = x.EndAir,
Poster= x.Poster,
Type = x.IsMovie ? ItemType.Movie : ItemType.Show
};
/// <summary>
/// An expression to create a <see cref="LibraryItem"/> representing a collection.
/// </summary>
public static Expression<Func<Collection, LibraryItem>> FromCollection => x => new LibraryItem
{
ID = -x.ID,
@ -77,10 +131,9 @@ namespace Kyoo.Models
Title = x.Name,
Overview = x.Overview,
Status = Models.Status.Unknown,
TrailerUrl = null,
StartYear = null,
EndYear = null,
Poster= x.Poster,
StartAir = null,
EndAir = null,
Poster = x.Poster,
Type = ItemType.Collection
};
}

View File

@ -1,33 +1,64 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
namespace Kyoo.Models
{
/// <summary>
/// A class representing a link between two resources.
/// </summary>
/// <remarks>
/// Links should only be used on the data layer and not on other application code.
/// </remarks>
public class Link
{
/// <summary>
/// The ID of the first item of the link.
/// The first item of the link should be the one to own the link.
/// </summary>
public int FirstID { get; set; }
/// <summary>
/// The ID of the second item of this link
/// The second item of the link should be the owned resource.
/// </summary>
public int SecondID { get; set; }
/// <summary>
/// Create a new typeless <see cref="Link"/>.
/// </summary>
public Link() {}
/// <summary>
/// Create a new typeless <see cref="Link"/> with two IDs.
/// </summary>
/// <param name="firstID">The ID of the first resource</param>
/// <param name="secondID">The ID of the second resource</param>
public Link(int firstID, int secondID)
{
FirstID = firstID;
SecondID = secondID;
}
/// <summary>
/// Create a new typeless <see cref="Link"/> between two resources.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
public Link(IResource first, IResource second)
{
FirstID = first.ID;
SecondID = second.ID;
}
public static Link Create(IResource first, IResource second)
{
return new(first, second);
}
/// <summary>
/// Create a new typed link between two resources.
/// This method can be used instead of the constructor to make use of generic parameters deduction.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <typeparam name="T">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
/// <returns>A newly created typed link with both resources</returns>
public static Link<T, T2> Create<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
@ -35,6 +66,16 @@ namespace Kyoo.Models
return new(first, second);
}
/// <summary>
/// Create a new typed link between two resources without storing references to resources.
/// This is the same as <see cref="Create{T,T2}"/> but this method does not set <see cref="Link{T1,T2}.First"/>
/// and <see cref="Link{T1,T2}.Second"/> fields. Only IDs are stored and not references.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <typeparam name="T">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
/// <returns>A newly created typed link with both resources</returns>
public static Link<T, T2> UCreate<T, T2>(T first, T2 second)
where T : class, IResource
where T2 : class, IResource
@ -42,6 +83,9 @@ namespace Kyoo.Models
return new(first, second, true);
}
/// <summary>
/// The expression to retrieve the unique ID of a Link. This is an aggregate of the two resources IDs.
/// </summary>
public static Expression<Func<Link, object>> PrimaryKey
{
get
@ -51,17 +95,41 @@ namespace Kyoo.Models
}
}
/// <summary>
/// A strongly typed link between two resources.
/// </summary>
/// <typeparam name="T1">The type of the first resource</typeparam>
/// <typeparam name="T2">The type of the second resource</typeparam>
public class Link<T1, T2> : Link
where T1 : class, IResource
where T2 : class, IResource
{
public virtual T1 First { get; set; }
public virtual T2 Second { get; set; }
/// <summary>
/// A reference of the first resource.
/// </summary>
public T1 First { get; set; }
/// <summary>
/// A reference to the second resource.
/// </summary>
public T2 Second { get; set; }
/// <summary>
/// Create a new, empty, typed <see cref="Link{T1,T2}"/>.
/// </summary>
public Link() {}
[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
/// <summary>
/// Create a new typed link with two resources.
/// </summary>
/// <param name="first">The first resource</param>
/// <param name="second">The second resource</param>
/// <param name="privateItems">
/// True if no reference to resources should be kept, false otherwise.
/// The default is false (references are kept).
/// </param>
public Link(T1 first, T2 second, bool privateItems = false)
: base(first, second)
{
@ -71,10 +139,18 @@ namespace Kyoo.Models
Second = second;
}
/// <summary>
/// Create a new typed link with IDs only.
/// </summary>
/// <param name="firstID">The ID of the first resource</param>
/// <param name="secondID">The ID of the second resource</param>
public Link(int firstID, int secondID)
: base(firstID, secondID)
{ }
/// <summary>
/// The expression to retrieve the unique ID of a typed Link. This is an aggregate of the two resources IDs.
/// </summary>
public new static Expression<Func<Link<T1, T2>, object>> PrimaryKey
{
get

View File

@ -1,26 +1,34 @@
using Kyoo.Models.Attributes;
using System;
using System.Linq.Expressions;
namespace Kyoo.Models
{
public class MetadataID
/// <summary>
/// ID and link of an item on an external provider.
/// </summary>
/// <typeparam name="T"></typeparam>
public class MetadataID<T> : Link<T, Provider>
where T : class, IResource
{
[SerializeIgnore] public int ID { get; set; }
[SerializeIgnore] public int ProviderID { get; set; }
public virtual Provider Provider {get; set; }
[SerializeIgnore] public int? ShowID { get; set; }
[SerializeIgnore] public virtual Show Show { get; set; }
[SerializeIgnore] public int? EpisodeID { get; set; }
[SerializeIgnore] public virtual Episode Episode { get; set; }
[SerializeIgnore] public int? SeasonID { get; set; }
[SerializeIgnore] public virtual Season Season { get; set; }
[SerializeIgnore] public int? PeopleID { get; set; }
[SerializeIgnore] public virtual People People { get; set; }
/// <summary>
/// The ID of the resource on the external provider.
/// </summary>
public string DataID { get; set; }
/// <summary>
/// The URL of the resource on the external provider.
/// </summary>
public string Link { get; set; }
/// <summary>
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
/// </summary>
public new static Expression<Func<MetadataID<T>, object>> PrimaryKey
{
get
{
return x => new {First = x.FirstID, Second = x.SecondID};
}
}
}
}

View File

@ -3,22 +3,45 @@ using System.Linq;
namespace Kyoo.Models
{
/// <summary>
/// A page of resource that contains information about the pagination of resources.
/// </summary>
/// <typeparam name="T">The type of resource contained in this page.</typeparam>
public class Page<T> where T : IResource
{
public string This { get; set; }
public string First { get; set; }
public string Next { get; set; }
/// <summary>
/// The link of the current page.
/// </summary>
public string This { get; }
/// <summary>
/// The link of the first page.
/// </summary>
public string First { get; }
/// <summary>
/// The link of the next page.
/// </summary>
public string Next { get; }
/// <summary>
/// The number of items in the current page.
/// </summary>
public int Count => Items.Count;
public ICollection<T> Items { get; set; }
public Page() { }
/// <summary>
/// The list of items in the page.
/// </summary>
public ICollection<T> Items { get; }
public Page(ICollection<T> items)
{
Items = items;
}
/// <summary>
/// Create a new <see cref="Page{T}"/>.
/// </summary>
/// <param name="items">The list of items in the page.</param>
/// <param name="this">The link of the current page.</param>
/// <param name="next">The link of the next page.</param>
/// <param name="first">The link of the first page.</param>
public Page(ICollection<T> items, string @this, string next, string first)
{
Items = items;
@ -27,6 +50,13 @@ namespace Kyoo.Models
First = first;
}
/// <summary>
/// Create a new <see cref="Page{T}"/> and compute the urls.
/// </summary>
/// <param name="items">The list of items in the page.</param>
/// <param name="url">The base url of the resources available from this page.</param>
/// <param name="query">The list of query strings of the current page</param>
/// <param name="limit">The number of items requested for the current page.</param>
public Page(ICollection<T> items,
string url,
Dictionary<string, string> query,

View File

@ -1,17 +1,55 @@
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A role a person played for a show. It can be an actor, musician, voice actor, director, writer...
/// </summary>
/// <remarks>
/// This class is not serialized like other classes.
/// Based on the <see cref="ForPeople"/> field, it is serialized like
/// a show with two extra fields (<see cref="Role"/> and <see cref="Type"/>).
/// </remarks>
public class PeopleRole : IResource
{
[SerializeIgnore] public int ID { get; set; }
[SerializeIgnore] public string Slug => ForPeople ? Show.Slug : People.Slug;
[SerializeIgnore] public bool ForPeople;
[SerializeIgnore] public int PeopleID { get; set; }
[SerializeIgnore] public virtual People People { get; set; }
[SerializeIgnore] public int ShowID { get; set; }
[SerializeIgnore] public virtual Show Show { get; set; }
public string Role { get; set; }
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug => ForPeople ? Show.Slug : People.Slug;
/// <summary>
/// Should this role be used as a Show substitute (the value is <c>false</c>) or
/// as a People substitute (the value is <c>true</c>).
/// </summary>
public bool ForPeople { get; set; }
/// <summary>
/// The ID of the People playing the role.
/// </summary>
public int PeopleID { get; set; }
/// <summary>
/// The people that played this role.
/// </summary>
public People People { get; set; }
/// <summary>
/// The ID of the Show where the People playing in.
/// </summary>
public int ShowID { get; set; }
/// <summary>
/// The show where the People played in.
/// </summary>
public Show Show { get; set; }
/// <summary>
/// The type of work the person has done for the show.
/// That can be something like "Actor", "Writer", "Music", "Voice Actor"...
/// </summary>
public string Type { get; set; }
/// <summary>
/// The role the People played.
/// This is mostly used to inform witch character was played for actor and voice actors.
/// </summary>
public string Role { get; set; }
}
}

View File

@ -1,31 +1,59 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A class representing collections of <see cref="Show"/>.
/// A collection can also be stored in a <see cref="Library"/>.
/// </summary>
public class Collection : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this collection.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The description of this collection.
/// </summary>
public string Overview { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
/// <summary>
/// The list of shows contained in this collection.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of libraries that contains this collection.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
/// <summary>
/// The internal link between this collection and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Collection, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this collection and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
#endif
public Collection() { }
public Collection(string slug, string name, string overview, string poster)
{
Slug = slug;
Name = name;
Overview = overview;
Poster = poster;
}
}
}

View File

@ -1,57 +1,166 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
public class Episode : IResource, IOnMerge
/// <summary>
/// A class to represent a single show's episode.
/// </summary>
public class Episode : IResource
{
/// <inheritdoc />
public int ID { get; set; }
public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
[SerializeIgnore] public string ShowSlug { private get; set; }
[SerializeIgnore] public int ShowID { get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
[SerializeIgnore] public int? SeasonID { get; set; }
[LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; }
public int SeasonNumber { get; set; } = -1;
public int EpisodeNumber { get; set; } = -1;
public int AbsoluteNumber { get; set; } = -1;
/// <inheritdoc />
[Computed] public string Slug
{
get
{
if (ShowSlug == null && Show == null)
return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
}
[UsedImplicitly] [NotNull] private set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
if (match.Success)
{
ShowSlug = match.Groups["show"].Value;
SeasonNumber = int.Parse(match.Groups["season"].Value);
EpisodeNumber = int.Parse(match.Groups["episode"].Value);
}
else
{
match = Regex.Match(value, @"(?<show>.+)-(?<absolute>\d+)");
if (match.Success)
{
ShowSlug = match.Groups["show"].Value;
AbsoluteNumber = int.Parse(match.Groups["absolute"].Value);
}
else
ShowSlug = value;
SeasonNumber = null;
EpisodeNumber = null;
}
}
}
/// <summary>
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
/// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; }
/// <summary>
/// The ID of the Show containing this episode.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
/// <summary>
/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
/// <summary>
/// The ID of the Season containing this episode.
/// </summary>
[SerializeIgnore] public int? SeasonID { get; set; }
/// <summary>
/// The season that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// This can be null if the season is unknown and the episode is only identified by it's <see cref="AbsoluteNumber"/>.
/// </summary>
[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; }
/// <summary>
/// The season in witch this episode is in.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// The number of this episode is it's season.
/// </summary>
public int? EpisodeNumber { get; set; }
/// <summary>
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
/// </summary>
public int? AbsoluteNumber { get; set; }
/// <summary>
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The path of this episode's thumbnail.
/// By default, the http path for the thumbnail is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; }
/// <summary>
/// The title of this episode.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The overview of this episode.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// The release date of this episode. It can be null if unknown.
/// </summary>
public DateTime? ReleaseDate { get; set; }
public int Runtime { get; set; } //This runtime variable should be in minutes
/// <summary>
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Episode>> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
/// <summary>
/// The list of tracks this episode has. This lists video, audio and subtitles available.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<Track> Tracks { get; set; }
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber)
/// <summary>
/// Get the slug of an episode.
/// </summary>
/// <param name="showSlug">The slug of the show. It can't be null.</param>
/// <param name="seasonNumber">
/// The season in which the episode is.
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
/// </param>
/// <param name="episodeNumber">
/// The number of the episode in it's season.
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
/// </param>
/// <param name="absoluteNumber">
/// The absolute number of this show.
/// If you don't know it or this is a movie, use null
/// </param>
/// <returns>The slug corresponding to the given arguments</returns>
/// <exception cref="ArgumentNullException">The given show slug was null.</exception>
public static string GetSlug([NotNull] string showSlug,
int? seasonNumber,
int? episodeNumber,
int? absoluteNumber = null)
{
if (showSlug == null)
throw new ArgumentException("Show's slug is null. Can't find episode's slug.");
throw new ArgumentNullException(nameof(showSlug));
return seasonNumber switch
{
-1 when absoluteNumber == -1 => showSlug,
-1 => $"{showSlug}-{absoluteNumber}",
null when absoluteNumber == null => showSlug,
null => $"{showSlug}-{absoluteNumber}",
_ => $"{showSlug}-s{seasonNumber}e{episodeNumber}"
};
}
public void OnMerge(object merged)
{
Episode other = (Episode)merged;
if (SeasonNumber == -1 && other.SeasonNumber != -1)
SeasonNumber = other.SeasonNumber;
if (EpisodeNumber == -1 && other.EpisodeNumber != -1)
EpisodeNumber = other.EpisodeNumber;
if (AbsoluteNumber == -1 && other.AbsoluteNumber != -1)
AbsoluteNumber = other.AbsoluteNumber;
}
}
}

View File

@ -1,40 +1,51 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A genre that allow one to specify categories for shows.
/// </summary>
public class Genre : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this genre.
/// </summary>
public string Name { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of shows that have this genre.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this genre and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Show, Genre>> ShowLinks { get; set; }
#endif
/// <summary>
/// Create a new, empty <see cref="Genre"/>.
/// </summary>
public Genre() {}
/// <summary>
/// Create a new <see cref="Genre"/> and specify it's <see cref="Name"/>.
/// The <see cref="Slug"/> is automatically calculated from it's name.
/// </summary>
/// <param name="name">The name of this genre.</param>
public Genre(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
public Genre(string slug, string name)
{
Slug = slug;
Name = name;
}
public Genre(int id, string slug, string name)
{
ID = id;
Slug = slug;
Name = name;
}
}
}

View File

@ -1,3 +1,5 @@
using Kyoo.Controllers;
namespace Kyoo.Models
{
/// <summary>
@ -8,6 +10,10 @@ namespace Kyoo.Models
/// <summary>
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
/// </summary>
/// <remarks>
/// You don't need to specify an ID manually when creating a new resource,
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
/// </remarks>
public int ID { get; set; }
/// <summary>

View File

@ -1,24 +1,60 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A library containing <see cref="Show"/> and <see cref="Collection"/>.
/// </summary>
public class Library : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this library.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The list of paths that this library is responsible for. This is mainly used by the Scan task.
/// </summary>
public string[] Paths { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<Provider> Providers { get; set; }
/// <summary>
/// The list of <see cref="Provider"/> used for items in this library.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<Provider> Providers { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
/// <summary>
/// The list of shows in this library.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of collections in this library.
/// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
/// <summary>
/// The internal link between this library and provider in the <see cref="Providers"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
/// <summary>
/// The internal link between this library and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this library and collection in the <see cref="Collections"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif
}
}

View File

@ -3,14 +3,37 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
/// </summary>
public class People : IResource
{
/// <inheritdoc />
public int ID { get; set; }
public string Slug { get; set; }
public string Name { get; set; }
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<PeopleRole> Roles { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this person.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The link to metadata providers that this person has. See <see cref="MetadataID{T}"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<People>> ExternalIDs { get; set; }
/// <summary>
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<PeopleRole> Roles { get; set; }
}
}

View File

@ -1,37 +1,67 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// This class contains metadata about <see cref="IMetadataProvider"/>.
/// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>.
/// </summary>
public class Provider : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this provider.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this provider's logo.
/// By default, the http path for this logo is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; }
/// <summary>
/// The extension of the logo. This is used for http responses.
/// </summary>
[SerializeIgnore] public string LogoExtension { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
/// <summary>
/// The list of libraries that uses this provider.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; }
/// <summary>
/// The internal link between this provider and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
#endif
/// <summary>
/// Create a new, default, <see cref="Provider"/>
/// </summary>
public Provider() { }
/// <summary>
/// Create a new <see cref="Provider"/> and specify it's <see cref="Name"/>.
/// The <see cref="Slug"/> is automatically calculated from it's name.
/// </summary>
/// <param name="name">The name of this provider.</param>
/// <param name="logo">The logo of this provider.</param>
public Provider(string name, string logo)
{
Slug = Utility.ToSlug(name);
Name = name;
Logo = logo;
}
public Provider(int id, string name, string logo)
{
ID = id;
Slug = Utility.ToSlug(name);
Name = name;
Logo = logo;
}
}
}

View File

@ -1,25 +1,94 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A season of a <see cref="Show"/>.
/// </summary>
public class Season : IResource
{
/// <inheritdoc />
public int ID { get; set; }
public string Slug => $"{ShowSlug}-s{SeasonNumber}";
[SerializeIgnore] public int ShowID { get; set; }
/// <inheritdoc />
[Computed] public string Slug
{
get
{
if (ShowSlug == null && Show == null)
return $"{ShowID}-s{SeasonNumber}";
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
}
[UsedImplicitly] [NotNull] private set
{
Match match = Regex.Match(value ?? "", @"(?<show>.+)-s(?<season>\d+)");
if (!match.Success)
throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}");
ShowSlug = match.Groups["show"].Value;
SeasonNumber = int.Parse(match.Groups["season"].Value);
}
}
/// <summary>
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
/// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
public int SeasonNumber { get; set; } = -1;
/// <summary>
/// The ID of the Show containing this season.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
/// <summary>
/// The show that contains this season. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
/// <summary>
/// The number of this season. This can be set to 0 to indicate specials.
/// </summary>
public int SeasonNumber { get; set; }
/// <summary>
/// The title of this season.
/// </summary>
public string Title { get; set; }
/// <summary>
/// A quick overview of this season.
/// </summary>
public string Overview { get; set; }
public int? Year { get; set; }
/// <summary>
/// The starting air date of this season.
/// </summary>
public DateTime? StartDate { get; set; }
/// <summary>
/// The ending date of this season.
/// </summary>
public DateTime? EndDate { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
/// <summary>
/// The link to metadata providers that this episode has. See <see cref="MetadataID{T}"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Season>> ExternalIDs { get; set; }
/// <summary>
/// The list of episodes that this season contains.
/// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
}
}

View File

@ -1,57 +1,173 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A series or a movie.
/// </summary>
public class Show : IResource, IOnMerge
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The title of this show.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The list of alternative titles of this show.
/// </summary>
[EditableRelation] public string[] Aliases { get; set; }
/// <summary>
/// The path of the root directory of this show.
/// This can be any kind of path supported by <see cref="IFileManager"/>
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The summary of this show.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// Is this show airing, not aired yet or finished?
/// </summary>
public Status? Status { get; set; }
/// <summary>
/// An URL to a trailer. This could be any path supported by the <see cref="IFileManager"/>.
/// </summary>
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
public string TrailerUrl { get; set; }
public int? StartYear { get; set; }
public int? EndYear { 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; }
/// <summary>
/// The path of this show's poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The path of this show's logo.
/// By default, the http path for this logo is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/logo")] public string Logo { get; set; }
/// <summary>
/// The path of this show's backdrop.
/// By default, the http path for this backdrop is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/shows/{Slug}/backdrop")] public string Backdrop { get; set; }
/// <summary>
/// True if this show represent a movie, false otherwise.
/// </summary>
public bool IsMovie { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary>
/// The link to metadata providers that this show has. See <see cref="MetadataID{T}"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID<Show>> ExternalIDs { get; set; }
/// <summary>
/// The ID of the Studio that made this show.
/// </summary>
[SerializeIgnore] public int? StudioID { get; set; }
[LoadableRelation(nameof(StudioID))] [EditableRelation] public virtual Studio Studio { get; set; }
[LoadableRelation] [EditableRelation] public virtual ICollection<Genre> Genres { get; set; }
[LoadableRelation] [EditableRelation] public virtual ICollection<PeopleRole> People { get; set; }
[LoadableRelation] public virtual ICollection<Season> Seasons { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
/// <summary>
/// The Studio that made this show. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(StudioID))] [EditableRelation] public Studio Studio { get; set; }
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
[LoadableRelation] [EditableRelation] public ICollection<Genre> Genres { get; set; }
/// <summary>
/// The list of people that made this show.
/// </summary>
[LoadableRelation] [EditableRelation] public ICollection<PeopleRole> People { get; set; }
/// <summary>
/// The different seasons in this show. If this is a movie, this list is always null or empty.
/// </summary>
[LoadableRelation] public ICollection<Season> Seasons { get; set; }
/// <summary>
/// The list of episodes in this show.
/// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null).
/// Having an episode is necessary to store metadata and tracks.
/// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
/// <summary>
/// The list of libraries that contains this show.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
/// <summary>
/// The list of collections that contains this show.
/// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> GenreLinks { get; set; }
/// <summary>
/// The internal link between this show and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Show>> LibraryLinks { get; set; }
/// <summary>
/// The internal link between this show and collections in the <see cref="Collections"/> list.
/// </summary>
[Link] public ICollection<Link<Collection, Show>> CollectionLinks { get; set; }
/// <summary>
/// The internal link between this show and genres in the <see cref="Genres"/> list.
/// </summary>
[Link] public ICollection<Link<Show, Genre>> GenreLinks { get; set; }
#endif
/// <summary>
/// Retrieve the internal provider's ID of a show using it's provider slug.
/// </summary>
/// <remarks>This method will never return anything if the <see cref="ExternalIDs"/> are not loaded.</remarks>
/// <param name="provider">The slug of the provider</param>
/// <returns>The <see cref="MetadataID{T}.DataID"/> field of the asked provider.</returns>
public string GetID(string provider)
{
return ExternalIDs?.FirstOrDefault(x => x.Provider.Name == provider)?.DataID;
return ExternalIDs?.FirstOrDefault(x => x.Second.Slug == provider)?.DataID;
}
public virtual void OnMerge(object merged)
/// <inheritdoc />
public void OnMerge(object merged)
{
if (ExternalIDs != null)
foreach (MetadataID id in ExternalIDs)
id.Show = this;
foreach (MetadataID<Show> id in ExternalIDs)
id.First = this;
if (People != null)
foreach (PeopleRole link in People)
link.Show = this;
@ -64,5 +180,8 @@ namespace Kyoo.Models
}
}
/// <summary>
/// The enum containing show's status.
/// </summary>
public enum Status { Finished, Airing, Planned, Unknown }
}

View File

@ -3,31 +3,40 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A studio that make shows.
/// </summary>
public class Studio : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this studio.
/// </summary>
public string Name { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of shows that are made by this studio.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <summary>
/// Create a new, empty, <see cref="Studio"/>.
/// </summary>
public Studio() { }
/// <summary>
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
/// </summary>
/// <param name="name">The name of the studio.</param>
public Studio(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
public Studio(string slug, string name)
{
Slug = slug;
Name = name;
}
public static Studio Default()
{
return new Studio("unknown", "Unknown Studio");
}
}
}

View File

@ -1,11 +1,16 @@
using Kyoo.Models.Watch;
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// The list of available stream types.
/// Attachments are only used temporarily by the transcoder but are not stored in a database.
/// </summary>
public enum StreamType
{
Unknown = 0,
@ -15,61 +20,106 @@ namespace Kyoo.Models
Attachment = 4
}
namespace Watch
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class Stream
{
public string Title { get; set; }
public string Language { get; set; }
public string Codec { get; set; }
[MarshalAs(UnmanagedType.I1)] public bool isDefault;
[MarshalAs(UnmanagedType.I1)] public bool isForced;
[SerializeIgnore] public string Path { get; set; }
[SerializeIgnore] public StreamType Type { get; set; }
public Stream() {}
public Stream(string title, string language, string codec, bool isDefault, bool isForced, string path, StreamType type)
{
Title = title;
Language = language;
Codec = codec;
this.isDefault = isDefault;
this.isForced = isForced;
Path = path;
Type = type;
}
public Stream(Stream stream)
{
Title = stream.Title;
Language = stream.Language;
isDefault = stream.isDefault;
isForced = stream.isForced;
Codec = stream.Codec;
Path = stream.Path;
Type = stream.Type;
}
}
}
public class Track : Stream, IResource
/// <summary>
/// A video, audio or subtitle track for an episode.
/// </summary>
public class Track : IResource
{
/// <inheritdoc />
public int ID { get; set; }
[SerializeIgnore] public int EpisodeID { get; set; }
public int TrackIndex { get; set; }
public bool IsDefault
/// <inheritdoc />
[Computed] public string Slug
{
get => isDefault;
set => isDefault = value;
}
public bool IsForced
{
get => isForced;
set => isForced = value;
get
{
string type = Type.ToString().ToLower();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString();
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}";
}
[UsedImplicitly] private set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
Match match = Regex.Match(value,
@"(?<ep>[^\.]+)\.(?<lang>\w{0,3})(-(?<index>\d+))?(\.(?<forced>forced))?\.(?<type>\w+)(\.\w*)?");
if (!match.Success)
throw new ArgumentException("Invalid track slug. " +
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]");
EpisodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value;
TrackIndex = int.Parse(match.Groups["index"].Value);
IsForced = match.Groups["forced"].Success;
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
}
}
/// <summary>
/// The slug of the episode that contain this track. If this is not set, this track is ill-formed.
/// </summary>
[SerializeIgnore] public string EpisodeSlug { private get; set; }
/// <summary>
/// The title of the stream.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The language of this stream (as a ISO-639-2 language code)
/// </summary>
public string Language { get; set; }
/// <summary>
/// The codec of this stream.
/// </summary>
public string Codec { get; set; }
/// <summary>
/// Is this stream the default one of it's type?
/// </summary>
public bool IsDefault { get; set; }
/// <summary>
/// Is this stream tagged as forced?
/// </summary>
public bool IsForced { get; set; }
/// <summary>
/// Is this track extern to the episode's file?
/// </summary>
public bool IsExternal { get; set; }
/// <summary>
/// The path of this track.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The type of this stream.
/// </summary>
[SerializeIgnore] public StreamType Type { get; set; }
/// <summary>
/// The ID of the episode that uses this track.
/// </summary>
[SerializeIgnore] public int EpisodeID { get; set; }
/// <summary>
/// The episode that uses this track.
/// </summary>
[LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; }
/// <summary>
/// The index of this track on the episode.
/// </summary>
public int TrackIndex { get; set; }
/// <summary>
/// A user-friendly name for this track. It does not include the track type.
/// </summary>
public string DisplayName
{
get
@ -85,61 +135,16 @@ namespace Kyoo.Models
name += " Forced";
if (IsExternal)
name += " (External)";
if (Title != null && Title.Length > 1)
if (Title is {Length: > 1})
name += " - " + Title;
return name;
}
}
public string Slug
{
get
{
string type = Type switch
{
StreamType.Subtitle => "",
StreamType.Video => "video.",
StreamType.Audio => "audio.",
StreamType.Attachment => "font.",
_ => ""
};
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
string codec = Codec switch
{
"subrip" => ".srt",
{} x => $".{x}"
};
return $"{Episode.Slug}.{type}{Language}{index}{(IsForced ? "-forced" : "")}{codec}";
}
}
public bool IsExternal { get; set; }
[LoadableRelation(nameof(EpisodeID))] public virtual Episode Episode { get; set; }
public Track() { }
public Track(StreamType type,
string title,
string language,
bool isDefault,
bool isForced,
string codec,
bool isExternal,
string path)
: base(title, language, codec, isDefault, isForced, path, type)
{
IsExternal = isExternal;
}
public Track(Stream stream)
: base(stream)
{
IsExternal = false;
}
//Converting mkv track language to c# system language tag.
private static string GetLanguage(string mkvLanguage)
{
// TODO delete this and have a real way to get the language string from the ISO-639-2.
return mkvLanguage switch
{
"fre" => "fra",
@ -147,5 +152,32 @@ namespace Kyoo.Models
_ => mkvLanguage
};
}
/// <summary>
/// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored)
/// </summary>
/// <param name="baseSlug">The slug to edit</param>
/// <param name="type">The new type of this </param>
/// <param name="language"></param>
/// <param name="index"></param>
/// <param name="forced"></param>
/// <returns></returns>
public static string EditSlug(string baseSlug,
StreamType type = StreamType.Unknown,
string language = null,
int? index = null,
bool? forced = null)
{
Track track = new() {Slug = baseSlug};
if (type != StreamType.Unknown)
track.Type = type;
if (language != null)
track.Language = language;
if (index != null)
track.TrackIndex = index.Value;
if (forced != null)
track.IsForced = forced.Value;
return track.Slug;
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
namespace Kyoo.Models
{
@ -52,7 +53,7 @@ namespace Kyoo.Models
/// <summary>
/// Links between Users and Shows.
/// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; }
[Link] public ICollection<Link<User, Show>> ShowLinks { get; set; }
#endif
}
@ -62,7 +63,7 @@ namespace Kyoo.Models
public class WatchedEpisode : Link<User, Episode>
{
/// <summary>
/// Where the player has stopped watching the episode (-1 if not started, else between 0 and 100).
/// Where the player has stopped watching the episode (between 0 and 100).
/// </summary>
public int WatchedPercentage { get; set; }
}

View File

@ -2,14 +2,44 @@
namespace Kyoo.Models
{
/// <summary>
/// Results of a search request.
/// </summary>
public class SearchResult
{
public string Query;
public IEnumerable<Collection> Collections;
public IEnumerable<Show> Shows;
public IEnumerable<Episode> Episodes;
public IEnumerable<People> People;
public IEnumerable<Genre> Genres;
public IEnumerable<Studio> Studios;
/// <summary>
/// The query of the search request.
/// </summary>
public string Query { get; init; }
/// <summary>
/// The collections that matched the search.
/// </summary>
public ICollection<Collection> Collections { get; init; }
/// <summary>
/// The shows that matched the search.
/// </summary>
public ICollection<Show> Shows { get; init; }
/// <summary>
/// The episodes that matched the search.
/// </summary>
public ICollection<Episode> Episodes { get; init; }
/// <summary>
/// The people that matched the search.
/// </summary>
public ICollection<People> People { get; init; }
/// <summary>
/// The genres that matched the search.
/// </summary>
public ICollection<Genre> Genres { get; init; }
/// <summary>
/// The studios that matched the search.
/// </summary>
public ICollection<Studio> Studios { get; init; }
}
}

View File

@ -9,95 +9,136 @@ using PathIO = System.IO.Path;
namespace Kyoo.Models
{
public class Chapter
{
public float StartTime;
public float EndTime;
public string Name;
public Chapter(float startTime, float endTime, string name)
{
StartTime = startTime;
EndTime = endTime;
Name = name;
}
}
/// <summary>
/// A watch item give information useful for playback.
/// Information about tracks and display information that could be used by the player.
/// This contains mostly data from an <see cref="Episode"/> with another form.
/// </summary>
public class WatchItem
{
/// <summary>
/// The ID of the episode associated with this item.
/// </summary>
public int EpisodeID { get; set; }
public string ShowTitle { get; set; }
public string ShowSlug { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
public int AbsoluteNumber { get; set; }
public string Title { get; set; }
/// <summary>
/// The slug of this episode.
/// </summary>
public string Slug { get; set; }
/// <summary>
/// The title of the show containing this episode.
/// </summary>
public string ShowTitle { get; set; }
/// <summary>
/// The slug of the show containing this episode
/// </summary>
public string ShowSlug { get; set; }
/// <summary>
/// The season in witch this episode is in.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// The number of this episode is it's season.
/// </summary>
public int? EpisodeNumber { get; set; }
/// <summary>
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
/// </summary>
public int? AbsoluteNumber { get; set; }
/// <summary>
/// The title of this episode.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The release date of this episode. It can be null if unknown.
/// </summary>
public DateTime? ReleaseDate { get; set; }
/// <summary>
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The episode that come before this one if you follow usual watch orders.
/// If this is the first episode or this is a movie, it will be null.
/// </summary>
public Episode PreviousEpisode { get; set; }
/// <summary>
/// The episode that come after this one if you follow usual watch orders.
/// If this is the last aired episode or this is a movie, it will be null.
/// </summary>
public Episode NextEpisode { get; set; }
/// <summary>
/// <c>true</c> if this is a movie, <c>false</c> otherwise.
/// </summary>
public bool IsMovie { get; set; }
/// <summary>
/// The path of this item's poster.
/// By default, the http path for the poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; }
/// <summary>
/// The path of this item's logo.
/// By default, the http path for the logo is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; }
/// <summary>
/// The path of this item's backdrop.
/// By default, the http path for the backdrop is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; }
/// <summary>
/// The container of the video file of this episode.
/// Common containers are mp4, mkv, avi and so on.
/// </summary>
public string Container { get; set; }
/// <summary>
/// The video track. See <see cref="Track"/> for more information.
/// </summary>
public Track Video { get; set; }
/// <summary>
/// The list of audio tracks. See <see cref="Track"/> for more information.
/// </summary>
public ICollection<Track> Audios { get; set; }
/// <summary>
/// The list of subtitles tracks. See <see cref="Track"/> for more information.
/// </summary>
public ICollection<Track> Subtitles { get; set; }
/// <summary>
/// The list of chapters. See <see cref="Chapter"/> for more information.
/// </summary>
public ICollection<Chapter> Chapters { get; set; }
public WatchItem() { }
private WatchItem(int episodeID,
Show show,
int seasonNumber,
int episodeNumber,
int absoluteNumber,
string title,
DateTime? releaseDate,
string path)
{
EpisodeID = episodeID;
ShowTitle = show.Title;
ShowSlug = show.Slug;
SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber;
AbsoluteNumber = absoluteNumber;
Title = title;
ReleaseDate = releaseDate;
Path = path;
IsMovie = show.IsMovie;
Poster = show.Poster;
Logo = show.Logo;
Backdrop = show.Backdrop;
Container = Path.Substring(Path.LastIndexOf('.') + 1);
Slug = Episode.GetSlug(ShowSlug, seasonNumber, episodeNumber, absoluteNumber);
}
private WatchItem(int episodeID,
Show show,
int seasonNumber,
int episodeNumber,
int absoluteNumber,
string title,
DateTime? releaseDate,
string path,
Track video,
ICollection<Track> audios,
ICollection<Track> subtitles)
: this(episodeID, show, seasonNumber, episodeNumber, absoluteNumber, title, releaseDate, path)
{
Video = video;
Audios = audios;
Subtitles = subtitles;
}
/// <summary>
/// Create a <see cref="WatchItem"/> from an <see cref="Episode"/>.
/// </summary>
/// <param name="ep">The episode to transform.</param>
/// <param name="library">
/// A library manager to retrieve the next and previous episode and load the show & tracks of the episode.
/// </param>
/// <returns>A new WatchItem representing the given episode.</returns>
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library)
{
Episode previous = null;
@ -106,41 +147,53 @@ namespace Kyoo.Models
await library.Load(ep, x => x.Show);
await library.Load(ep, x => x.Tracks);
if (!ep.Show.IsMovie)
if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null)
{
if (ep.EpisodeNumber > 1)
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1);
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1);
else if (ep.SeasonNumber > 1)
{
int count = await library.GetCount<Episode>(x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber - 1);
previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count);
previous = (await library.GetAll(x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber.Value - 1,
limit: 1,
sort: new Sort<Episode>(x => x.EpisodeNumber, true))
).FirstOrDefault();
}
if (ep.EpisodeNumber >= await library.GetCount<Episode>(x => x.SeasonID == ep.SeasonID))
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1);
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1);
else
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1);
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1);
}
else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null)
{
previous = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID
&& x.AbsoluteNumber == ep.EpisodeNumber + 1);
next = await library.GetOrDefault<Episode>(x => x.ShowID == ep.ShowID
&& x.AbsoluteNumber == ep.AbsoluteNumber + 1);
}
return new WatchItem(ep.ID,
ep.Show,
ep.SeasonNumber,
ep.EpisodeNumber,
ep.AbsoluteNumber,
ep.Title,
ep.ReleaseDate,
ep.Path,
ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray())
return new WatchItem
{
EpisodeID = ep.ID,
ShowSlug = ep.Show.Slug,
SeasonNumber = ep.SeasonNumber,
EpisodeNumber = ep.EpisodeNumber,
AbsoluteNumber = ep.AbsoluteNumber,
Title = ep.Title,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
PreviousEpisode = previous,
NextEpisode = next,
Chapters = await GetChapters(ep.Path)
};
}
// TODO move this method in a controller to support abstraction.
// TODO use a IFileManager to retrieve and read files.
private static async Task<ICollection<Chapter>> GetChapters(string episodePath)
{
string path = PathIO.Combine(

View File

@ -1,703 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo
{
/// <summary>
/// A set of utility functions that can be used everywhere.
/// </summary>
public static class Utility
{
/// <summary>
/// Is the lambda expression a member (like x => x.Body).
/// </summary>
/// <param name="ex">The expression that should be checked</param>
/// <returns>True if the expression is a member, false otherwise</returns>
public static bool IsPropertyExpression(LambdaExpression ex)
{
return ex == null ||
ex.Body is MemberExpression ||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
}
/// <summary>
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
/// </summary>
/// <param name="ex">The expression</param>
/// <returns>The name of the expression</returns>
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
public static string GetPropertyName(LambdaExpression ex)
{
if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression;
return member!.Member.Name;
}
/// <summary>
/// Get the value of a member (property or field)
/// </summary>
/// <param name="member">The member value</param>
/// <param name="obj">The owner of this member</param>
/// <returns>The value boxed as an object</returns>
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
{
if (member == null)
throw new ArgumentNullException(nameof(member));
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return member switch
{
PropertyInfo property => property.GetValue(obj),
FieldInfo field => field.GetValue(obj),
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
};
}
/// <summary>
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
/// </summary>
/// <param name="str">The string to slugify</param>
/// <returns>The slug version of the given string</returns>
public static string ToSlug(string str)
{
if (str == null)
return null;
str = str.ToLowerInvariant();
string normalizedString = str.Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new();
foreach (char c in normalizedString)
{
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
stringBuilder.Append(c);
}
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled);
str = str.Trim('-', '_');
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
return str;
}
/// <summary>
/// Merge two lists, can keep duplicates or remove them.
/// </summary>
/// <param name="first">The first enumerable to merge</param>
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
/// <returns>The two list merged as an array</returns>
public static T[] MergeLists<T>(IEnumerable<T> first,
IEnumerable<T> second,
Func<T, T, bool> isEqual = null)
{
if (first == null)
return second.ToArray();
if (second == null)
return first.ToArray();
if (isEqual == null)
return first.Concat(second).ToArray();
List<T> list = first.ToList();
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
}
/// <summary>
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <param name="first">The object to assign</param>
/// <param name="second">The object containing new values</param>
/// <typeparam name="T">Fields of T will be used</typeparam>
/// <returns><see cref="first"/></returns>
public static T Assign<T>(T first, T second)
{
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <param name="first">The object to complete</param>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
/// <param name="where">Filter fields that will be merged</param>
/// <typeparam name="T">Fields of T will be completed</typeparam>
/// <returns><see cref="first"/></returns>
/// <exception cref="ArgumentNullException">If first is null</exception>
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// An advanced <see cref="Complete{T}"/> function.
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
/// Enumerable will be merged (concatenated).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary>
/// <param name="first">The object to complete</param>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
/// <typeparam name="T">Fields of T will be merged</typeparam>
/// <returns><see cref="first"/></returns>
public static T Merge<T>(T first, T second)
{
if (first == null)
return second;
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
property.SetValue(first, RunGenericMethod<object>(
typeof(Utility),
nameof(MergeLists),
GetEnumerableType(property.PropertyType),
oldValue, newValue, null));
}
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every fields of <see cref="obj"/> to the default value.
/// </summary>
/// <param name="obj">The object to nullify</param>
/// <typeparam name="T">Fields of T will be nullified</typeparam>
/// <returns><see cref="obj"/></returns>
public static T Nullify<T>(T obj)
{
Type type = typeof(T);
foreach (PropertyInfo property in type.GetProperties())
{
if (!property.CanWrite)
continue;
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
property.SetValue(obj, defaultValue);
}
return obj;
}
/// <summary>
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
/// </summary>
/// <param name="type">The starting type</param>
/// <returns>A list of types</returns>
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
for (; type != null; type = type.BaseType)
yield return type;
}
/// <summary>
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
/// </summary>
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>True if obj inherit from genericType. False otherwise</returns>
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return IsOfGenericType(obj.GetType(), genericType);
}
/// <summary>
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>True if obj inherit from genericType. False otherwise</returns>
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
IEnumerable<Type> types = genericType.IsInterface
? type.GetInterfaces()
: type.GetInheritanceTree();
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
/// <summary>
/// Get the generic definition of <see cref="genericType"/>.
/// For example, calling this function with List&lt;string&gt; and typeof(IEnumerable&lt;&gt;) will return IEnumerable&lt;string&gt;
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
IEnumerable<Type> types = genericType.IsInterface
? type.GetInterfaces()
: type.GetInheritanceTree();
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
/// <summary>
/// A Select where the index of the item can be used.
/// </summary>
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
/// <param name="mapper">The function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped.</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, T2> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return mapper(enumerator.Current, index);
index++;
}
}
/// <summary>
/// A map where the mapping function is asynchronous.
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
/// </summary>
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
/// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, int, Task<T2>> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return await mapper(enumerator.Current, index);
index++;
}
}
/// <summary>
/// An asynchronous version of Select.
/// </summary>
/// <param name="self">The IEnumerable to map</param>
/// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception>
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self,
[NotNull] Func<T, Task<T2>> mapper)
{
if (self == null)
yield break;
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
using IEnumerator<T> enumerator = self.GetEnumerator();
while (enumerator.MoveNext())
yield return await mapper(enumerator.Current);
}
/// <summary>
/// Convert an AsyncEnumerable to a List by waiting for every item.
/// </summary>
/// <param name="self">The async list</param>
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
/// <returns>A task that will return a simple list</returns>
/// <exception cref="ArgumentNullException">The list can't be null</exception>
public static async Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
List<T> ret = new();
await foreach(T i in self)
ret.Add(i);
return ret;
}
/// <summary>
/// If the enumerable is empty, execute an action.
/// </summary>
/// <param name="self">The enumerable to check</param>
/// <param name="action">The action to execute is the list is empty</param>
/// <typeparam name="T">The type of items inside the list</typeparam>
/// <returns></returns>
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
if (!enumerator.MoveNext())
{
action();
yield break;
}
do
{
yield return enumerator.Current;
}
while (enumerator.MoveNext());
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list</typeparam>
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
foreach (T i in self)
action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
{
if (self == null)
return;
foreach (object i in self)
action(i);
}
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
{
if (self == null)
return;
foreach (T i in self)
await action(i);
}
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
await foreach (T i in self)
action(i);
}
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
{
if (self == null)
return;
foreach (object i in self)
await action(i);
}
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
{
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
.Where(x => x.GetParameters().Length == args.Length)
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
$"{args.Length} arguments and {generics.Length} generic " +
$"types could not be found on {type.Name}."))
// TODO this won't work but I don't know why.
// .Where(x =>
// {
// int i = 0;
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
// })
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
// .Where(x =>
// {
// int i = 0;
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
// })
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
.Take(2)
.ToArray();
if (methods.Length == 1)
return methods[0];
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
}
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
[NotNull] Type type,
params object[] args)
{
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
}
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
[NotNull] Type[] types,
params object[] args)
{
if (owner == null)
throw new ArgumentNullException(nameof(owner));
if (methodName == null)
throw new ArgumentNullException(nameof(methodName));
if (types == null)
throw new ArgumentNullException(nameof(types));
if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
}
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type type,
params object[] args)
{
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
}
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type[] types,
params object[] args)
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));
if (methodName == null)
throw new ArgumentNullException(nameof(methodName));
if (types == null || types.Length == 0)
throw new ArgumentNullException(nameof(types));
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
}
[NotNull]
public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list)
{
if (list == null)
throw new ArgumentNullException(nameof(list));
Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t)
&& t.GetGenericArguments().Any()) ?? list.GetType();
return type.GetGenericArguments().First();
}
public static Type GetEnumerableType([NotNull] Type listType)
{
if (listType == null)
throw new ArgumentNullException(nameof(listType));
if (!typeof(IEnumerable).IsAssignableFrom(listType))
throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable.");
Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t)
&& t.GetGenericArguments().Any()) ?? listType;
return type.GetGenericArguments().First();
}
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
{
for (int i = 0; i < list.Count; i += countPerList)
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
}
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
{
T[] ret = new T[countPerList];
int i = 0;
using IEnumerator<T> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
ret[i] = enumerator.Current;
i++;
if (i < countPerList)
continue;
i = 0;
yield return ret;
}
Array.Resize(ref ret, i);
yield return ret;
}
public static string ToQueryString(this Dictionary<string, string> query)
{
if (!query.Any())
return string.Empty;
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
}
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void ReThrow([NotNull] this Exception ex)
{
if (ex == null)
throw new ArgumentNullException(nameof(ex));
ExceptionDispatchInfo.Capture(ex).Throw();
}
public static Task<T> Then<T>(this Task<T> task, Action<T> map)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
map(x.Result);
return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
return map(x.Result);
}, TaskContinuationOptions.ExecuteSynchronously);
}
public static Task<T> Cast<T>(this Task task)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
return (T)((dynamic)x).Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// Get a friendly type name (supporting generics)
/// For example a list of string will be displayed as List&lt;string&gt; and not as List`1.
/// </summary>
/// <param name="type">The type to use</param>
/// <returns>The friendly name of the type</returns>
public static string FriendlyName(this Type type)
{
if (!type.IsGenericType)
return type.Name;
string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName()));
return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>";
}
}
}

View File

@ -0,0 +1,279 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo
{
/// <summary>
/// A set of extensions class for enumerable.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// A Select where the index of the item can be used.
/// </summary>
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
/// <param name="mapper">The function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped.</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
[LinqTunnel]
public static IEnumerable<T2> Map<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, int, T2> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static IEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, T2> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return mapper(enumerator.Current, index);
index++;
}
}
return Generator(self, mapper);
}
/// <summary>
/// A map where the mapping function is asynchronous.
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
/// </summary>
/// <param name="self">The IEnumerable to map.</param>
/// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
[LinqTunnel]
public static IAsyncEnumerable<T2> MapAsync<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, int, Task<T2>> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return await mapper(enumerator.Current, index);
index++;
}
}
return Generator(self, mapper);
}
/// <summary>
/// An asynchronous version of Select.
/// </summary>
/// <param name="self">The IEnumerable to map</param>
/// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
[LinqTunnel]
public static IAsyncEnumerable<T2> SelectAsync<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, Task<T2>> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
while (enumerator.MoveNext())
yield return await mapper(enumerator.Current);
}
return Generator(self, mapper);
}
/// <summary>
/// Convert an AsyncEnumerable to a List by waiting for every item.
/// </summary>
/// <param name="self">The async list</param>
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
/// <returns>A task that will return a simple list</returns>
/// <exception cref="ArgumentNullException">The list can't be null</exception>
[LinqTunnel]
public static Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
{
List<T> ret = new();
await foreach (T i in self)
ret.Add(i);
return ret;
}
return ToList(self);
}
/// <summary>
/// If the enumerable is empty, execute an action.
/// </summary>
/// <param name="self">The enumerable to check</param>
/// <param name="action">The action to execute is the list is empty</param>
/// <typeparam name="T">The type of items inside the list</typeparam>
/// <exception cref="ArgumentNullException">The iterable and the action can't be null.</exception>
/// <returns>The iterator proxied, there is no dual iterations.</returns>
[LinqTunnel]
public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (action == null)
throw new ArgumentNullException(nameof(action));
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
if (!enumerator.MoveNext())
{
action();
yield break;
}
do
{
yield return enumerator.Current;
}
while (enumerator.MoveNext());
}
return Generator(self, action);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list</typeparam>
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
foreach (T i in self)
action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
public static void ForEach([CanBeNull] this IEnumerable self, Action<object> action)
{
if (self == null)
return;
foreach (object i in self)
action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func<object, Task> action)
{
if (self == null)
return;
foreach (object i in self)
await action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The asynchronous action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
public static async Task ForEachAsync<T>([CanBeNull] this IEnumerable<T> self, Func<T, Task> action)
{
if (self == null)
return;
foreach (T i in self)
await action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The async list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
public static async Task ForEachAsync<T>([CanBeNull] this IAsyncEnumerable<T> self, Action<T> action)
{
if (self == null)
return;
await foreach (T i in self)
action(i);
}
/// <summary>
/// Split a list in a small chunk of data.
/// </summary>
/// <param name="list">The list to split</param>
/// <param name="countPerList">The number of items in each chunk</param>
/// <typeparam name="T">The type of data in the initial list.</typeparam>
/// <returns>A list of chunks</returns>
[LinqTunnel]
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
{
for (int i = 0; i < list.Count; i += countPerList)
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
}
/// <summary>
/// Split a list in a small chunk of data.
/// </summary>
/// <param name="list">The list to split</param>
/// <param name="countPerList">The number of items in each chunk</param>
/// <typeparam name="T">The type of data in the initial list.</typeparam>
/// <returns>A list of chunks</returns>
[LinqTunnel]
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
{
T[] ret = new T[countPerList];
int i = 0;
using IEnumerator<T> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
ret[i] = enumerator.Current;
i++;
if (i < countPerList)
continue;
i = 0;
yield return ret;
}
Array.Resize(ref ret, i);
yield return ret;
}
}
}

View File

@ -0,0 +1,174 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo
{
/// <summary>
/// A class containing helper methods to merge objects.
/// </summary>
public static class Merger
{
/// <summary>
/// Merge two lists, can keep duplicates or remove them.
/// </summary>
/// <param name="first">The first enumerable to merge</param>
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
/// <returns>The two list merged as an array</returns>
public static T[] MergeLists<T>(IEnumerable<T> first,
IEnumerable<T> second,
Func<T, T, bool> isEqual = null)
{
if (first == null)
return second.ToArray();
if (second == null)
return first.ToArray();
if (isEqual == null)
return first.Concat(second).ToArray();
List<T> list = first.ToList();
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
}
/// <summary>
/// Set every fields of first to those of second. Ignore fields marked with the <see cref="NotMergeableAttribute"/> attribute
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <param name="first">The object to assign</param>
/// <param name="second">The object containing new values</param>
/// <typeparam name="T">Fields of T will be used</typeparam>
/// <returns><see cref="first"/></returns>
public static T Assign<T>(T first, T second)
{
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <param name="first">The object to complete</param>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
/// <param name="where">Filter fields that will be merged</param>
/// <typeparam name="T">Fields of T will be completed</typeparam>
/// <returns><see cref="first"/></returns>
/// <exception cref="ArgumentNullException">If first is null</exception>
public static T Complete<T>([NotNull] T first, [CanBeNull] T second, Func<PropertyInfo, bool> where = null)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? property.PropertyType.GetClrDefault();
if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// An advanced <see cref="Complete{T}"/> function.
/// This will set missing values of <see cref="first"/> to the corresponding values of <see cref="second"/>.
/// Enumerable will be merged (concatenated).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary>
/// <param name="first">The object to complete</param>
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
/// <typeparam name="T">Fields of T will be merged</typeparam>
/// <returns><see cref="first"/></returns>
public static T Merge<T>(T first, T second)
{
if (first == null)
return second;
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
.GenericTypeArguments
.First();
property.SetValue(first, Utility.RunGenericMethod<object>(
typeof(Utility),
nameof(MergeLists),
enumerableType,
oldValue, newValue, null));
}
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every fields of <see cref="obj"/> to the default value.
/// </summary>
/// <param name="obj">The object to nullify</param>
/// <typeparam name="T">Fields of T will be nullified</typeparam>
/// <returns><see cref="obj"/></returns>
public static T Nullify<T>(T obj)
{
Type type = typeof(T);
foreach (PropertyInfo property in type.GetProperties())
{
if (!property.CanWrite || property.GetCustomAttribute<ComputedAttribute>() != null)
continue;
property.SetValue(obj, property.PropertyType.GetClrDefault());
}
return obj;
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo
{
/// <summary>
/// A class containing helper method for tasks.
/// </summary>
public static class TaskUtils
{
/// <summary>
/// Run a method after the execution of the task.
/// </summary>
/// <param name="task">The task to wait.</param>
/// <param name="then">
/// The method to run after the task finish. This will only be run if the task finished successfully.
/// </param>
/// <typeparam name="T">The type of the item in the task.</typeparam>
/// <returns>A continuation task wrapping the initial task and adding a continuation method.</returns>
/// <exception cref="TaskCanceledException"></exception>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<T> Then<T>(this Task<T> task, Action<T> then)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
then(x.Result);
return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// Map the result of a task to another result.
/// </summary>
/// <param name="task">The task to map.</param>
/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
/// <typeparam name="T">The type of returns of the given task</typeparam>
/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
return map(x.Result);
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// A method to return the a default value from a task if the initial task is null.
/// </summary>
/// <param name="value">The initial task</param>
/// <typeparam name="T">The type that the task will return</typeparam>
/// <returns>A non-null task.</returns>
[NotNull]
public static Task<T> DefaultIfNull<T>([CanBeNull] Task<T> value)
{
return value ?? Task.FromResult<T>(default);
}
}
}

View File

@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
namespace Kyoo
{
/// <summary>
/// A set of utility functions that can be used everywhere.
/// </summary>
public static class Utility
{
/// <summary>
/// Is the lambda expression a member (like x => x.Body).
/// </summary>
/// <param name="ex">The expression that should be checked</param>
/// <returns>True if the expression is a member, false otherwise</returns>
public static bool IsPropertyExpression(LambdaExpression ex)
{
if (ex == null)
return false;
return ex.Body is MemberExpression ||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
}
/// <summary>
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
/// </summary>
/// <param name="ex">The expression</param>
/// <returns>The name of the expression</returns>
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
public static string GetPropertyName(LambdaExpression ex)
{
if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression;
return member!.Member.Name;
}
/// <summary>
/// Get the value of a member (property or field)
/// </summary>
/// <param name="member">The member value</param>
/// <param name="obj">The owner of this member</param>
/// <returns>The value boxed as an object</returns>
/// <exception cref="ArgumentNullException">if <see cref="member"/> or <see cref="obj"/> is null.</exception>
/// <exception cref="ArgumentException">The member is not a field or a property.</exception>
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
{
if (member == null)
throw new ArgumentNullException(nameof(member));
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return member switch
{
PropertyInfo property => property.GetValue(obj),
FieldInfo field => field.GetValue(obj),
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
};
}
/// <summary>
/// Slugify a string (Replace spaces by -, Uniformize accents é -> e)
/// </summary>
/// <param name="str">The string to slugify</param>
/// <returns>The slug version of the given string</returns>
public static string ToSlug(string str)
{
if (str == null)
return null;
str = str.ToLowerInvariant();
string normalizedString = str.Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new();
foreach (char c in normalizedString)
{
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
stringBuilder.Append(c);
}
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled);
str = str.Trim('-', '_');
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
return str;
}
/// <summary>
/// Get the default value of a type.
/// </summary>
/// <param name="type">The type to get the default value</param>
/// <returns>The default value of the given type.</returns>
public static object GetClrDefault(this Type type)
{
return type.IsValueType
? Activator.CreateInstance(type)
: null;
}
/// <summary>
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
/// </summary>
/// <param name="type">The starting type</param>
/// <returns>A list of types</returns>
/// <exception cref="ArgumentNullException"><see cref="type"/> can't be null</exception>
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
for (; type != null; type = type.BaseType)
yield return type;
}
/// <summary>
/// Check if <see cref="obj"/> inherit from a generic type <see cref="genericType"/>.
/// </summary>
/// <param name="obj">Does this object's type is a <see cref="genericType"/></param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>True if obj inherit from genericType. False otherwise</returns>
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return IsOfGenericType(obj.GetType(), genericType);
}
/// <summary>
/// Check if <see cref="type"/> inherit from a generic type <see cref="genericType"/>.
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>True if obj inherit from genericType. False otherwise</returns>
/// <exception cref="ArgumentNullException">obj and genericType can't be null</exception>
public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
IEnumerable<Type> types = genericType.IsInterface
? type.GetInterfaces()
: type.GetInheritanceTree();
return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
/// <summary>
/// Get the generic definition of <see cref="genericType"/>.
/// For example, calling this function with List&lt;string&gt; and typeof(IEnumerable&lt;&gt;) will return IEnumerable&lt;string&gt;
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable&lt;&gt;).</param>
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
/// <exception cref="ArgumentNullException"><see cref="type"/> and <see cref="genericType"/> can't be null</exception>
/// <exception cref="ArgumentException"><see cref="genericType"/> must be a generic type</exception>
public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
IEnumerable<Type> types = genericType.IsInterface
? type.GetInterfaces()
: type.GetInheritanceTree();
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
{
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
.Where(x => x.GetParameters().Length == args.Length)
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
$"{args.Length} arguments and {generics.Length} generic " +
$"types could not be found on {type.Name}."))
// TODO this won't work but I don't know why.
// .Where(x =>
// {
// int i = 0;
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
// })
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
// .Where(x =>
// {
// int i = 0;
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
// })
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
.Take(2)
.ToArray();
if (methods.Length == 1)
return methods[0];
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
}
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
[NotNull] Type type,
params object[] args)
{
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
}
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
[NotNull] Type[] types,
params object[] args)
{
if (owner == null)
throw new ArgumentNullException(nameof(owner));
if (methodName == null)
throw new ArgumentNullException(nameof(methodName));
if (types == null)
throw new ArgumentNullException(nameof(types));
if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args?.ToArray());
}
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type type,
params object[] args)
{
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
}
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
[NotNull] Type[] types,
params object[] args)
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));
if (methodName == null)
throw new ArgumentNullException(nameof(methodName));
if (types == null || types.Length == 0)
throw new ArgumentNullException(nameof(types));
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
}
public static string ToQueryString(this Dictionary<string, string> query)
{
if (!query.Any())
return string.Empty;
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
}
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void ReThrow([NotNull] this Exception ex)
{
if (ex == null)
throw new ArgumentNullException(nameof(ex));
ExceptionDispatchInfo.Capture(ex).Throw();
}
/// <summary>
/// Get a friendly type name (supporting generics)
/// For example a list of string will be displayed as List&lt;string&gt; and not as List`1.
/// </summary>
/// <param name="type">The type to use</param>
/// <returns>The friendly name of the type</returns>
public static string FriendlyName(this Type type)
{
if (!type.IsGenericType)
return type.Name;
string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName()));
return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>";
}
}
}

View File

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

View File

@ -61,10 +61,6 @@ namespace Kyoo
/// </summary>
public DbSet<Provider> Providers { get; set; }
/// <summary>
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
/// </summary>
public DbSet<MetadataID> MetadataIds { get; set; }
/// <summary>
/// The list of registered users.
/// </summary>
public DbSet<User> Users { get; set; }
@ -79,6 +75,25 @@ namespace Kyoo
/// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
/// <summary>
/// The list of library items (shows and collections that are part of a library - or the global one)
/// </summary>
/// <remarks>
/// This set is ready only, on most database this will be a view.
/// </remarks>
public DbSet<LibraryItem> LibraryItems { get; set; }
/// <summary>
/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID{T}"/>.
/// </summary>
/// <typeparam name="T">The metadata of this type will be returned.</typeparam>
/// <returns>A queryable of metadata ids for a type.</returns>
public DbSet<MetadataID<T>> MetadataIds<T>()
where T : class, IResource
{
return Set<MetadataID<T>>();
}
/// <summary>
/// Get a generic link between two resource types.
/// </summary>
@ -125,13 +140,27 @@ namespace Kyoo
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Track>()
.Property(t => t.IsDefault)
.ValueGeneratedNever();
modelBuilder.Entity<Show>()
.HasMany(x => x.Seasons)
.WithOne(x => x.Show)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Show>()
.HasMany(x => x.Episodes)
.WithOne(x => x.Show)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Season>()
.HasMany(x => x.Episodes)
.WithOne(x => x.Season)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Episode>()
.HasMany(x => x.Tracks)
.WithOne(x => x.Episode)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Track>()
.Property(t => t.IsForced)
.ValueGeneratedNever();
modelBuilder.Entity<Show>()
.HasOne(x => x.Studio)
.WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Provider>()
.HasMany(x => x.Libraries)
@ -205,25 +234,41 @@ namespace Kyoo
.WithMany(x => x.ShowLinks),
y => y.HasKey(Link<User, Show>.PrimaryKey));
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Show)
modelBuilder.Entity<MetadataID<Show>>()
.HasKey(MetadataID<Show>.PrimaryKey);
modelBuilder.Entity<MetadataID<Show>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Season)
modelBuilder.Entity<MetadataID<Season>>()
.HasKey(MetadataID<Season>.PrimaryKey);
modelBuilder.Entity<MetadataID<Season>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Episode)
modelBuilder.Entity<MetadataID<Episode>>()
.HasKey(MetadataID<Episode>.PrimaryKey);
modelBuilder.Entity<MetadataID<Episode>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.People)
modelBuilder.Entity<MetadataID<People>>()
.HasKey(MetadataID<People>.PrimaryKey);
modelBuilder.Entity<MetadataID<People>>()
.HasOne(x => x.First)
.WithMany(x => x.ExternalIDs)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID>()
.HasOne(x => x.Provider)
.WithMany(x => x.MetadataLinks)
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Season>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Episode>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<People>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataID<Show>>().HasOne(x => x.Second).WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WatchedEpisode>()
@ -262,15 +307,34 @@ namespace Kyoo
modelBuilder.Entity<Season>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber})
.IsUnique();
modelBuilder.Entity<Season>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Episode>()
.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
.IsUnique();
modelBuilder.Entity<Episode>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
.IsUnique();
modelBuilder.Entity<Track>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<User>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Season>()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity<Episode>()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity<Track>()
.Property(x => x.Slug)
.ValueGeneratedOnAddOrUpdate();
}
/// <summary>
@ -441,52 +505,6 @@ namespace Kyoo
}
}
/// <summary>
/// Save items or retry with a custom method if a duplicate is found.
/// </summary>
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
/// The second parameter is the current retry number.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <typeparam name="T">The type of the item to save</typeparam>
/// <returns>The number of state entries written to the database.</returns>
public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
{
return SaveOrRetry(obj, onFail, 0, cancellationToken);
}
/// <summary>
/// Save items or retry with a custom method if a duplicate is found.
/// </summary>
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
/// The second parameter is the current retry number.</param>
/// <param name="recurse">The current retry number.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <typeparam name="T">The type of the item to save</typeparam>
/// <returns>The number of state entries written to the database.</returns>
private async Task<T> SaveOrRetry<T>(T obj,
Func<T, int, T> onFail,
int recurse,
CancellationToken cancellationToken = new())
{
try
{
await base.SaveChangesAsync(true, cancellationToken);
return obj;
}
catch (DbUpdateException ex) when (IsDuplicateException(ex))
{
recurse++;
return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken);
}
catch (DbUpdateException)
{
DiscardChanges();
throw;
}
}
/// <summary>
/// Check if the exception is a duplicated exception.
/// </summary>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,782 @@
using System;
using System.Collections.Generic;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
.Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown")
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle,attachment");
migrationBuilder.CreateTable(
name: "collections",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true),
poster = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_collections", x => x.id);
});
migrationBuilder.CreateTable(
name: "genres",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_genres", x => x.id);
});
migrationBuilder.CreateTable(
name: "libraries",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true),
paths = table.Column<string[]>(type: "text[]", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_libraries", x => x.id);
});
migrationBuilder.CreateTable(
name: "people",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true),
poster = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_people", x => x.id);
});
migrationBuilder.CreateTable(
name: "providers",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true),
logo = table.Column<string>(type: "text", nullable: true),
logo_extension = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_providers", x => x.id);
});
migrationBuilder.CreateTable(
name: "studios",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_studios", x => x.id);
});
migrationBuilder.CreateTable(
name: "users",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
username = table.Column<string>(type: "text", nullable: true),
email = table.Column<string>(type: "text", nullable: true),
password = table.Column<string>(type: "text", nullable: true),
permissions = table.Column<string[]>(type: "text[]", nullable: true),
extra_data = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_users", x => x.id);
});
migrationBuilder.CreateTable(
name: "link_library_collection",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_collection", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_library_collection_collections_second_id",
column: x => x.second_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_collection_libraries_first_id",
column: x => x.first_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_library_provider",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_provider", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_library_provider_libraries_first_id",
column: x => x.first_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_provider_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "metadata_id_people",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_metadata_id_people", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_metadata_id_people_people_first_id",
column: x => x.first_id,
principalTable: "people",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_metadata_id_people_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "shows",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: false),
title = table.Column<string>(type: "text", nullable: true),
aliases = table.Column<string[]>(type: "text[]", nullable: true),
path = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true),
status = table.Column<Status>(type: "status", nullable: true),
trailer_url = table.Column<string>(type: "text", nullable: true),
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
poster = table.Column<string>(type: "text", nullable: true),
logo = table.Column<string>(type: "text", nullable: true),
backdrop = table.Column<string>(type: "text", nullable: true),
is_movie = table.Column<bool>(type: "boolean", nullable: false),
studio_id = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_shows", x => x.id);
table.ForeignKey(
name: "fk_shows_studios_studio_id",
column: x => x.studio_id,
principalTable: "studios",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "link_collection_show",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_collection_show", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_collection_show_collections_first_id",
column: x => x.first_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_collection_show_shows_second_id",
column: x => x.second_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_library_show",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_show", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_library_show_libraries_first_id",
column: x => x.first_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_show_shows_second_id",
column: x => x.second_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_show_genre",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_show_genre", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_show_genre_genres_second_id",
column: x => x.second_id,
principalTable: "genres",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_show_genre_shows_first_id",
column: x => x.first_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_user_show",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_user_show", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_link_user_show_shows_second_id",
column: x => x.second_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_user_show_users_first_id",
column: x => x.first_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "metadata_id_show",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_metadata_id_show", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_metadata_id_show_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_metadata_id_show_shows_first_id",
column: x => x.first_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "people_roles",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
for_people = table.Column<bool>(type: "boolean", nullable: false),
people_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false),
type = table.Column<string>(type: "text", nullable: true),
role = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_people_roles", x => x.id);
table.ForeignKey(
name: "fk_people_roles_people_people_id",
column: x => x.people_id,
principalTable: "people",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_people_roles_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "seasons",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: true),
show_id = table.Column<int>(type: "integer", nullable: false),
season_number = table.Column<int>(type: "integer", nullable: false),
title = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true),
start_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
end_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
poster = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_seasons", x => x.id);
table.ForeignKey(
name: "fk_seasons_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "episodes",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: true),
show_id = table.Column<int>(type: "integer", nullable: false),
season_id = table.Column<int>(type: "integer", nullable: true),
season_number = table.Column<int>(type: "integer", nullable: true),
episode_number = table.Column<int>(type: "integer", nullable: true),
absolute_number = table.Column<int>(type: "integer", nullable: true),
path = table.Column<string>(type: "text", nullable: true),
thumb = table.Column<string>(type: "text", nullable: true),
title = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true),
release_date = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_episodes", x => x.id);
table.ForeignKey(
name: "fk_episodes_seasons_season_id",
column: x => x.season_id,
principalTable: "seasons",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_episodes_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "metadata_id_season",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_metadata_id_season", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_metadata_id_season_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_metadata_id_season_seasons_first_id",
column: x => x.first_id,
principalTable: "seasons",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "metadata_id_episode",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false),
data_id = table.Column<string>(type: "text", nullable: true),
link = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_metadata_id_episode", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_metadata_id_episode_episodes_first_id",
column: x => x.first_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_metadata_id_episode_providers_second_id",
column: x => x.second_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "tracks",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "text", nullable: true),
title = table.Column<string>(type: "text", nullable: true),
language = table.Column<string>(type: "text", nullable: true),
codec = table.Column<string>(type: "text", nullable: true),
is_default = table.Column<bool>(type: "boolean", nullable: false),
is_forced = table.Column<bool>(type: "boolean", nullable: false),
is_external = table.Column<bool>(type: "boolean", nullable: false),
path = table.Column<string>(type: "text", nullable: true),
type = table.Column<StreamType>(type: "stream_type", nullable: false),
episode_id = table.Column<int>(type: "integer", nullable: false),
track_index = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_tracks", x => x.id);
table.ForeignKey(
name: "fk_tracks_episodes_episode_id",
column: x => x.episode_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "watched_episodes",
columns: table => new
{
first_id = table.Column<int>(type: "integer", nullable: false),
second_id = table.Column<int>(type: "integer", nullable: false),
watched_percentage = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_watched_episodes", x => new { x.first_id, x.second_id });
table.ForeignKey(
name: "fk_watched_episodes_episodes_second_id",
column: x => x.second_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_watched_episodes_users_first_id",
column: x => x.first_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_collections_slug",
table: "collections",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_episodes_season_id",
table: "episodes",
column: "season_id");
migrationBuilder.CreateIndex(
name: "ix_episodes_show_id_season_number_episode_number_absolute_numb",
table: "episodes",
columns: new[] { "show_id", "season_number", "episode_number", "absolute_number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_episodes_slug",
table: "episodes",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_genres_slug",
table: "genres",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_libraries_slug",
table: "libraries",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_link_collection_show_second_id",
table: "link_collection_show",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_collection_second_id",
table: "link_library_collection",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_provider_second_id",
table: "link_library_provider",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_show_second_id",
table: "link_library_show",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_link_show_genre_second_id",
table: "link_show_genre",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_link_user_show_second_id",
table: "link_user_show",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_episode_second_id",
table: "metadata_id_episode",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_people_second_id",
table: "metadata_id_people",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_season_second_id",
table: "metadata_id_season",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_metadata_id_show_second_id",
table: "metadata_id_show",
column: "second_id");
migrationBuilder.CreateIndex(
name: "ix_people_slug",
table: "people",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_people_roles_people_id",
table: "people_roles",
column: "people_id");
migrationBuilder.CreateIndex(
name: "ix_people_roles_show_id",
table: "people_roles",
column: "show_id");
migrationBuilder.CreateIndex(
name: "ix_providers_slug",
table: "providers",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_seasons_show_id_season_number",
table: "seasons",
columns: new[] { "show_id", "season_number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_seasons_slug",
table: "seasons",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_shows_slug",
table: "shows",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_shows_studio_id",
table: "shows",
column: "studio_id");
migrationBuilder.CreateIndex(
name: "ix_studios_slug",
table: "studios",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_tracks_episode_id_type_language_track_index_is_forced",
table: "tracks",
columns: new[] { "episode_id", "type", "language", "track_index", "is_forced" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_tracks_slug",
table: "tracks",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_users_slug",
table: "users",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_watched_episodes_second_id",
table: "watched_episodes",
column: "second_id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "link_collection_show");
migrationBuilder.DropTable(
name: "link_library_collection");
migrationBuilder.DropTable(
name: "link_library_provider");
migrationBuilder.DropTable(
name: "link_library_show");
migrationBuilder.DropTable(
name: "link_show_genre");
migrationBuilder.DropTable(
name: "link_user_show");
migrationBuilder.DropTable(
name: "metadata_id_episode");
migrationBuilder.DropTable(
name: "metadata_id_people");
migrationBuilder.DropTable(
name: "metadata_id_season");
migrationBuilder.DropTable(
name: "metadata_id_show");
migrationBuilder.DropTable(
name: "people_roles");
migrationBuilder.DropTable(
name: "tracks");
migrationBuilder.DropTable(
name: "watched_episodes");
migrationBuilder.DropTable(
name: "collections");
migrationBuilder.DropTable(
name: "libraries");
migrationBuilder.DropTable(
name: "genres");
migrationBuilder.DropTable(
name: "providers");
migrationBuilder.DropTable(
name: "people");
migrationBuilder.DropTable(
name: "episodes");
migrationBuilder.DropTable(
name: "users");
migrationBuilder.DropTable(
name: "seasons");
migrationBuilder.DropTable(
name: "shows");
migrationBuilder.DropTable(
name: "studios");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,186 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
{
public partial class Triggers : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE FUNCTION season_slug_update()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
NEW.slug := CONCAT(
(SELECT slug FROM shows WHERE id = NEW.show_id),
'-s',
NEW.season_number
);
RETURN NEW;
END
$$;");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE TRIGGER season_slug_trigger BEFORE INSERT OR UPDATE OF season_number, show_id ON seasons
FOR EACH ROW EXECUTE PROCEDURE season_slug_update();");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE FUNCTION episode_slug_update()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
NEW.slug := CONCAT(
(SELECT slug FROM shows WHERE id = NEW.show_id),
CASE
WHEN NEW.season_number IS NULL AND NEW.episode_number IS NULL THEN NULL
WHEN NEW.season_number IS NULL THEN CONCAT('-', NEW.absolute_number)
ELSE CONCAT('-s', NEW.season_number, 'e', NEW.episode_number)
END
);
RETURN NEW;
END
$$;");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE TRIGGER episode_slug_trigger
BEFORE INSERT OR UPDATE OF absolute_number, episode_number, season_number, show_id ON episodes
FOR EACH ROW EXECUTE PROCEDURE episode_slug_update();");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE FUNCTION show_slug_update()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
UPDATE seasons SET slug = CONCAT(NEW.slug, '-s', season_number) WHERE show_id = NEW.id;
UPDATE episodes SET slug = CASE
WHEN season_number IS NULL AND episode_number IS NULL THEN NEW.slug
WHEN season_number IS NULL THEN CONCAT(NEW.slug, '-', absolute_number)
ELSE CONCAT(NEW.slug, '-s', season_number, 'e', episode_number)
END WHERE show_id = NEW.id;
RETURN NEW;
END
$$;");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE TRIGGER show_slug_trigger AFTER UPDATE OF slug ON shows
FOR EACH ROW EXECUTE PROCEDURE show_slug_update();");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE FUNCTION episode_update_tracks_slug()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
UPDATE tracks SET slug = CONCAT(
NEW.slug,
'.', language,
CASE (track_index)
WHEN 0 THEN ''
ELSE CONCAT('-', track_index)
END,
CASE (is_forced)
WHEN false THEN ''
ELSE '-forced'
END,
'.', type
) WHERE episode_id = NEW.id;
RETURN NEW;
END;
$$;");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE TRIGGER episode_track_slug_trigger AFTER UPDATE OF slug ON episodes
FOR EACH ROW EXECUTE PROCEDURE episode_update_tracks_slug();");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE FUNCTION track_slug_update()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
IF NEW.track_index = 0 THEN
NEW.track_index := (SELECT COUNT(*) FROM tracks
WHERE episode_id = NEW.episode_id AND type = NEW.type
AND language = NEW.language AND is_forced = NEW.is_forced);
END IF;
NEW.slug := CONCAT(
(SELECT slug FROM episodes WHERE id = NEW.episode_id),
'.', NEW.language,
CASE (NEW.track_index)
WHEN 0 THEN ''
ELSE CONCAT('-', NEW.track_index)
END,
CASE (NEW.is_forced)
WHEN false THEN ''
ELSE '-forced'
END,
'.', NEW.type
);
RETURN NEW;
END
$$;");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE TRIGGER track_slug_trigger
BEFORE INSERT OR UPDATE OF episode_id, is_forced, language, track_index, type ON tracks
FOR EACH ROW EXECUTE PROCEDURE track_slug_update();");
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE VIEW library_items AS
SELECT s.id, s.slug, s.title, s.overview, s.status, s.start_air, s.end_air, s.poster, CASE
WHEN s.is_movie THEN 'movie'::item_type
ELSE 'show'::item_type
END AS type
FROM shows AS s
WHERE NOT (EXISTS (
SELECT 1
FROM link_collection_show AS l
INNER JOIN collections AS c ON l.first_id = c.id
WHERE s.id = l.second_id))
UNION ALL
SELECT -c0.id, c0.slug, c0.name AS title, c0.overview, 'unknown'::status AS status,
NULL AS start_air, NULL AS end_air, c0.poster, 'collection'::item_type AS type
FROM collections AS c0");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// language=PostgreSQL
migrationBuilder.Sql("DROP TRIGGER show_slug_trigger ON shows;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP FUNCTION show_slug_update;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP TRIGGER season_slug_trigger ON seasons;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP FUNCTION season_slug_update;");
// language=PostgreSQL
migrationBuilder.Sql("DROP TRIGGER episode_slug_trigger ON episodes;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP FUNCTION episode_slug_update;");
// language=PostgreSQL
migrationBuilder.Sql("DROP TRIGGER track_slug_trigger ON tracks;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP FUNCTION track_slug_update;");
// language=PostgreSQL
migrationBuilder.Sql("DROP TRIGGER episode_track_slug_trigger ON episodes;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP FUNCTION episode_update_tracks_slug;");
// language=PostgreSQL
migrationBuilder.Sql(@"DROP VIEW library_items;");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
<RootNamespace>Kyoo.SqLite</RootNamespace>
</PropertyGroup>
<!-- <PropertyGroup>-->
<!-- <OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/sqlite</OutputPath>-->
<!-- <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>-->
<!-- <ProduceReferenceAssembly>false</ProduceReferenceAssembly>-->
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
<!-- <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>-->
<!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
<!-- </PropertyGroup>-->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.CommonAPI/Kyoo.CommonAPI.csproj">
<!-- <PrivateAssets>all</PrivateAssets>-->
<!-- <Private>false</Private>-->
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
</ProjectReference>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<!-- <PrivateAssets>all</PrivateAssets>-->
<!-- <Private>false</Private>-->
<!-- <ExcludeAssets>runtime</ExcludeAssets>-->
</ProjectReference>
</ItemGroup>
</Project>

View File

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

View File

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

View File

@ -0,0 +1,980 @@
// <auto-generated />
using System;
using Kyoo.SqLite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kyoo.SqLite.Migrations
{
[DbContext(typeof(SqLiteContext))]
[Migration("20210626141347_Triggers")]
partial class Triggers
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.7");
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Collections");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("AbsoluteNumber")
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<int?>("SeasonID")
.HasColumnType("INTEGER");
b.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Thumb")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("SeasonID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber")
.IsUnique();
b.ToTable("Episodes");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Genres");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Paths")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Libraries");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Collection, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Collection>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Provider>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Show, Genre>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Episode>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<People>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Season>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Show>");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("People");
});
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("ForPeople")
.HasColumnType("INTEGER");
b.Property<int>("PeopleID")
.HasColumnType("INTEGER");
b.Property<string>("Role")
.HasColumnType("TEXT");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("PeopleID");
b.HasIndex("ShowID");
b.ToTable("PeopleRoles");
});
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("LogoExtension")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Providers");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<DateTime?>("StartDate")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber")
.IsUnique();
b.ToTable("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Aliases")
.HasColumnType("TEXT");
b.Property<string>("Backdrop")
.HasColumnType("TEXT");
b.Property<DateTime?>("EndAir")
.HasColumnType("TEXT");
b.Property<bool>("IsMovie")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("StartAir")
.HasColumnType("TEXT");
b.Property<int?>("Status")
.HasColumnType("INTEGER");
b.Property<int?>("StudioID")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TrailerUrl")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("StudioID");
b.ToTable("Shows");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Studios");
});
modelBuilder.Entity("Kyoo.Models.Track", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Codec")
.HasColumnType("TEXT");
b.Property<int>("EpisodeID")
.HasColumnType("INTEGER");
b.Property<bool>("IsDefault")
.HasColumnType("INTEGER");
b.Property<bool>("IsExternal")
.HasColumnType("INTEGER");
b.Property<bool>("IsForced")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackIndex")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<string>("ExtraData")
.HasColumnType("TEXT");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<int>("WatchedPercentage")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.HasOne("Kyoo.Models.Season", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Episodes")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Collection", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("CollectionLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("CollectionLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Collection", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("GenreLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Genre", "Second")
.WithMany("ShowLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
{
b.HasOne("Kyoo.Models.Episode", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
{
b.HasOne("Kyoo.Models.People", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
{
b.HasOne("Kyoo.Models.Season", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
{
b.HasOne("Kyoo.Models.People", "People")
.WithMany("Roles")
.HasForeignKey("PeopleID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("People")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("People");
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Seasons")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
.HasForeignKey("StudioID");
b.Navigation("Studio");
});
modelBuilder.Entity("Kyoo.Models.Track", b =>
{
b.HasOne("Kyoo.Models.Episode", "Episode")
.WithMany("Tracks")
.HasForeignKey("EpisodeID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Tracks");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("ProviderLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Roles");
});
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Navigation("LibraryLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
b.Navigation("GenreLinks");
b.Navigation("LibraryLinks");
b.Navigation("People");
b.Navigation("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Navigation("Shows");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,187 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.SqLite.Migrations
{
public partial class Triggers : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER SeasonSlugInsert AFTER INSERT ON Seasons FOR EACH ROW
BEGIN
UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber
WHERE ID == new.ID;
END");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER SeasonSlugUpdate AFTER UPDATE OF SeasonNumber, ShowID ON Seasons FOR EACH ROW
BEGIN
UPDATE Seasons SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) || '-s' || SeasonNumber
WHERE ID == new.ID;
END");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER EpisodeSlugInsert AFTER INSERT ON Episodes FOR EACH ROW
BEGIN
UPDATE Episodes
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
CASE
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
END
WHERE ID == new.ID;
END");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER EpisodeSlugUpdate AFTER UPDATE OF AbsoluteNumber, EpisodeNumber, SeasonNumber, ShowID
ON Episodes FOR EACH ROW
BEGIN
UPDATE Episodes
SET Slug = (SELECT Slug from Shows WHERE ID = ShowID) ||
CASE
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
END
WHERE ID == new.ID;
END");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER TrackSlugInsert
AFTER INSERT ON Tracks
FOR EACH ROW
BEGIN
UPDATE Tracks SET TrackIndex = (
SELECT COUNT(*) FROM Tracks
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
AND Language = new.Language AND IsForced = new.IsForced
) WHERE ID = new.ID AND TrackIndex = 0;
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
'.' || Language ||
CASE (TrackIndex)
WHEN 0 THEN ''
ELSE '-' || (TrackIndex)
END ||
CASE (IsForced)
WHEN false THEN ''
ELSE '-forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
WHEN 2 THEN '.audio'
WHEN 3 THEN '.subtitle'
ELSE '.' || Type
END
WHERE ID = new.ID;
END;");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER TrackSlugUpdate
AFTER UPDATE OF EpisodeID, IsForced, Language, TrackIndex, Type ON Tracks
FOR EACH ROW
BEGIN
UPDATE Tracks SET TrackIndex = (
SELECT COUNT(*) FROM Tracks
WHERE EpisodeID = new.EpisodeID AND Type = new.Type
AND Language = new.Language AND IsForced = new.IsForced
) WHERE ID = new.ID AND TrackIndex = 0;
UPDATE Tracks SET Slug =
(SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
'.' || Language ||
CASE (TrackIndex)
WHEN 0 THEN ''
ELSE '-' || (TrackIndex)
END ||
CASE (IsForced)
WHEN false THEN ''
ELSE '-forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
WHEN 2 THEN '.audio'
WHEN 3 THEN '.subtitle'
ELSE '.' || Type
END
WHERE ID = new.ID;
END;");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER EpisodeUpdateTracksSlug
AFTER UPDATE OF Slug ON Episodes
FOR EACH ROW
BEGIN
UPDATE Tracks SET Slug =
NEW.Slug ||
'.' || Language ||
CASE (TrackIndex)
WHEN 0 THEN ''
ELSE '-' || TrackIndex
END ||
CASE (IsForced)
WHEN false THEN ''
ELSE '-forced'
END ||
CASE (Type)
WHEN 1 THEN '.video'
WHEN 2 THEN '.audio'
WHEN 3 THEN '.subtitle'
ELSE '.' || Type
END
WHERE EpisodeID = NEW.ID;
END;");
// language=SQLite
migrationBuilder.Sql(@"
CREATE TRIGGER ShowSlugUpdate AFTER UPDATE OF Slug ON Shows FOR EACH ROW
BEGIN
UPDATE Seasons SET Slug = new.Slug || '-s' || SeasonNumber WHERE ShowID = new.ID;
UPDATE Episodes
SET Slug = new.Slug ||
CASE
WHEN SeasonNumber IS NULL AND AbsoluteNumber IS NULL THEN ''
WHEN SeasonNumber IS NULL THEN '-' || AbsoluteNumber
ELSE '-s' || SeasonNumber || 'e' || EpisodeNumber
END
WHERE ShowID = new.ID;
END;");
// language=SQLite
migrationBuilder.Sql(@"
CREATE VIEW LibraryItems AS
SELECT s.ID, s.Slug, s.Title, s.Overview, s.Status, s.StartAir, s.EndAir, s.Poster, CASE
WHEN s.IsMovie THEN 1
ELSE 0
END AS Type
FROM Shows AS s
WHERE NOT (EXISTS (
SELECT 1
FROM 'Link<Collection, Show>' AS l
INNER JOIN Collections AS c ON l.FirstID = c.ID
WHERE s.ID = l.SecondID))
UNION ALL
SELECT -c0.ID, c0.Slug, c0.Name AS Title, c0.Overview, 3 AS Status,
NULL AS StartAir, NULL AS EndAir, c0.Poster, 2 AS Type
FROM collections AS c0");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// language=SQLite
migrationBuilder.Sql("DROP TRIGGER SeasonSlugInsert;");
// language=SQLite
migrationBuilder.Sql("DROP TRIGGER SeasonSlugUpdate;");
// language=SQLite
migrationBuilder.Sql("DROP TRIGGER EpisodeSlugInsert;");
// language=SQLite
migrationBuilder.Sql("DROP TRIGGER EpisodeSlugUpdate;");
// language=SQLite
migrationBuilder.Sql("DROP TRIGGER ShowSlugUpdate;");
}
}
}

View File

@ -0,0 +1,978 @@
// <auto-generated />
using System;
using Kyoo.SqLite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Kyoo.SqLite.Migrations
{
[DbContext(typeof(SqLiteContext))]
partial class SqLiteContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.7");
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Collections");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("AbsoluteNumber")
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<int?>("SeasonID")
.HasColumnType("INTEGER");
b.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Thumb")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("SeasonID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber", "EpisodeNumber", "AbsoluteNumber")
.IsUnique();
b.ToTable("Episodes");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Genres");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Paths")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Libraries");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Collection, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Collection>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Provider>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Library, Show>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<Show, Genre>");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("Link<User, Show>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Episode>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<People>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Season>");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<string>("DataID")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("MetadataID<Show>");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("People");
});
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("ForPeople")
.HasColumnType("INTEGER");
b.Property<int>("PeopleID")
.HasColumnType("INTEGER");
b.Property<string>("Role")
.HasColumnType("TEXT");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("PeopleID");
b.HasIndex("ShowID");
b.ToTable("PeopleRoles");
});
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("LogoExtension")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Providers");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("ShowID")
.HasColumnType("INTEGER");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<DateTime?>("StartDate")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("ShowID", "SeasonNumber")
.IsUnique();
b.ToTable("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Aliases")
.HasColumnType("TEXT");
b.Property<string>("Backdrop")
.HasColumnType("TEXT");
b.Property<DateTime?>("EndAir")
.HasColumnType("TEXT");
b.Property<bool>("IsMovie")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("StartAir")
.HasColumnType("TEXT");
b.Property<int?>("Status")
.HasColumnType("INTEGER");
b.Property<int?>("StudioID")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TrailerUrl")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("StudioID");
b.ToTable("Shows");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Studios");
});
modelBuilder.Entity("Kyoo.Models.Track", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Codec")
.HasColumnType("TEXT");
b.Property<int>("EpisodeID")
.HasColumnType("INTEGER");
b.Property<bool>("IsDefault")
.HasColumnType("INTEGER");
b.Property<bool>("IsExternal")
.HasColumnType("INTEGER");
b.Property<bool>("IsForced")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackIndex")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.HasIndex("EpisodeID", "Type", "Language", "TrackIndex", "IsForced")
.IsUnique();
b.ToTable("Tracks");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<string>("ExtraData")
.HasColumnType("TEXT");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("ID");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.Property<int>("FirstID")
.HasColumnType("INTEGER");
b.Property<int>("SecondID")
.HasColumnType("INTEGER");
b.Property<int>("WatchedPercentage")
.HasColumnType("INTEGER");
b.HasKey("FirstID", "SecondID");
b.HasIndex("SecondID");
b.ToTable("WatchedEpisodes");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.HasOne("Kyoo.Models.Season", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Episodes")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Collection, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Collection", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("CollectionLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Collection>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("CollectionLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Collection", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Show, Kyoo.Models.Genre>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("GenreLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Genre", "Second")
.WithMany("ShowLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.User, Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("ShowLinks")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Episode>", b =>
{
b.HasOne("Kyoo.Models.Episode", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.People>", b =>
{
b.HasOne("Kyoo.Models.People", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Season>", b =>
{
b.HasOne("Kyoo.Models.Season", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.MetadataID<Kyoo.Models.Show>", b =>
{
b.HasOne("Kyoo.Models.Show", "First")
.WithMany("ExternalIDs")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.PeopleRole", b =>
{
b.HasOne("Kyoo.Models.People", "People")
.WithMany("Roles")
.HasForeignKey("PeopleID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("People")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("People");
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.HasOne("Kyoo.Models.Show", "Show")
.WithMany("Seasons")
.HasForeignKey("ShowID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Show");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.HasOne("Kyoo.Models.Studio", "Studio")
.WithMany("Shows")
.HasForeignKey("StudioID");
b.Navigation("Studio");
});
modelBuilder.Entity("Kyoo.Models.Track", b =>
{
b.HasOne("Kyoo.Models.Episode", "Episode")
.WithMany("Tracks")
.HasForeignKey("EpisodeID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Episode");
});
modelBuilder.Entity("Kyoo.Models.WatchedEpisode", b =>
{
b.HasOne("Kyoo.Models.User", "First")
.WithMany("CurrentlyWatching")
.HasForeignKey("FirstID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.Episode", "Second")
.WithMany()
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("First");
b.Navigation("Second");
});
modelBuilder.Entity("Kyoo.Models.Collection", b =>
{
b.Navigation("LibraryLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Episode", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Tracks");
});
modelBuilder.Entity("Kyoo.Models.Genre", b =>
{
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.Library", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("ProviderLinks");
b.Navigation("ShowLinks");
});
modelBuilder.Entity("Kyoo.Models.People", b =>
{
b.Navigation("ExternalIDs");
b.Navigation("Roles");
});
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Navigation("LibraryLinks");
});
modelBuilder.Entity("Kyoo.Models.Season", b =>
{
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
});
modelBuilder.Entity("Kyoo.Models.Show", b =>
{
b.Navigation("CollectionLinks");
b.Navigation("Episodes");
b.Navigation("ExternalIDs");
b.Navigation("GenreLinks");
b.Navigation("LibraryLinks");
b.Navigation("People");
b.Navigation("Seasons");
});
modelBuilder.Entity("Kyoo.Models.Studio", b =>
{
b.Navigation("Shows");
});
modelBuilder.Entity("Kyoo.Models.User", b =>
{
b.Navigation("CurrentlyWatching");
b.Navigation("ShowLinks");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Kyoo.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
namespace Kyoo.SqLite
{
/// <summary>
/// A sqlite implementation of <see cref="DatabaseContext"/>.
/// </summary>
public class SqLiteContext : DatabaseContext
{
/// <summary>
/// The connection string to use.
/// </summary>
private readonly string _connection;
/// <summary>
/// Is this instance in debug mode?
/// </summary>
private readonly bool _debugMode;
/// <summary>
/// Should the configure step be skipped? This is used when the database is created via DbContextOptions.
/// </summary>
private readonly bool _skipConfigure;
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
public SqLiteContext()
{ }
/// <summary>
/// Create a new <see cref="SqLiteContext"/> using specific options
/// </summary>
/// <param name="options">The options to use.</param>
public SqLiteContext(DbContextOptions options)
: base(options)
{
_skipConfigure = true;
}
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
/// <param name="connection">The connection string to use</param>
/// <param name="debugMode">Is this instance in debug mode?</param>
public SqLiteContext(string connection, bool debugMode)
{
_connection = connection;
_debugMode = debugMode;
}
/// <summary>
/// Set connection information for this database context
/// </summary>
/// <param name="optionsBuilder">An option builder to fill.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!_skipConfigure)
{
if (_connection != null)
optionsBuilder.UseSqlite(_connection);
else
optionsBuilder.UseSqlite();
if (_debugMode)
optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging();
}
base.OnConfiguring(optionsBuilder);
}
/// <summary>
/// Set database parameters to support every types of Kyoo.
/// </summary>
/// <param name="modelBuilder">The database's model builder.</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ValueConverter<string[], string> arrayConvertor = new(
x => string.Join(";", x),
x => x.Split(';', StringSplitOptions.None));
modelBuilder.Entity<Library>()
.Property(x => x.Paths)
.HasConversion(arrayConvertor);
modelBuilder.Entity<Show>()
.Property(x => x.Aliases)
.HasConversion(arrayConvertor);
modelBuilder.Entity<User>()
.Property(x => x.Permissions)
.HasConversion(arrayConvertor);
modelBuilder.Entity<Show>()
.Property(x => x.Status)
.HasConversion<int>();
modelBuilder.Entity<Track>()
.Property(x => x.Type)
.HasConversion<int>();
ValueConverter<Dictionary<string, string>, string> jsonConvertor = new(
x => JsonConvert.SerializeObject(x),
x => JsonConvert.DeserializeObject<Dictionary<string, string>>(x));
modelBuilder.Entity<User>()
.Property(x => x.ExtraData)
.HasConversion(jsonConvertor);
modelBuilder.Entity<LibraryItem>()
.ToView("LibraryItems")
.HasKey(x => x.ID);
base.OnModelCreating(modelBuilder);
}
/// <inheritdoc />
protected override bool IsDuplicateException(Exception ex)
{
return ex.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 /*SQLITE_CONSTRAINT_UNIQUE*/}
or SqliteException { SqliteExtendedErrorCode: 1555 /*SQLITE_CONSTRAINT_PRIMARYKEY*/};
}
/// <inheritdoc />
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
{
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(EF.Functions.Like);
MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format));
return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Kyoo.Controllers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Kyoo.SqLite
{
/// <summary>
/// A module to add sqlite capacity to the app.
/// </summary>
public class SqLiteModule : IPlugin
{
/// <inheritdoc />
public string Slug => "sqlite";
/// <inheritdoc />
public string Name => "SqLite";
/// <inheritdoc />
public string Description => "A database context for sqlite.";
/// <inheritdoc />
public ICollection<Type> Provides => new[]
{
typeof(DatabaseContext)
};
/// <inheritdoc />
public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc />
public ICollection<Type> Requires => ArraySegment<Type>.Empty;
/// <summary>
/// The configuration to use. The database connection string is pulled from it.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The host environment to check if the app is in debug mode.
/// </summary>
private readonly IWebHostEnvironment _environment;
/// <summary>
/// Create a new postgres module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="env">The environment that will be used (if the env is in development mode, more information will be displayed on errors.</param>
public SqLiteModule(IConfiguration configuration, IWebHostEnvironment env)
{
_configuration = configuration;
_environment = env;
}
/// <inheritdoc />
public void Configure(IServiceCollection services, ICollection<Type> availableTypes)
{
services.AddDbContext<DatabaseContext, SqLiteContext>(x =>
{
x.UseSqlite(_configuration.GetDatabaseConnection("sqlite"));
if (_environment.IsDevelopment())
x.EnableDetailedErrors().EnableSensitiveDataLogging();
});
}
/// <inheritdoc />
public void Initialize(IServiceProvider provider)
{
DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
context.Database.Migrate();
}
}
}

45
Kyoo.Tests/KAssert.cs Normal file
View File

@ -0,0 +1,45 @@
using System.Reflection;
using JetBrains.Annotations;
using Xunit;
using Xunit.Sdk;
namespace Kyoo.Tests
{
/// <summary>
/// Custom assertions used by Kyoo's tests.
/// </summary>
public static class KAssert
{
/// <summary>
/// Check if every property of the item is equal to the other's object.
/// </summary>
/// <param name="expected">The value to check against</param>
/// <param name="value">The value to check</param>
/// <typeparam name="T">The type to check</typeparam>
[AssertionMethod]
public static void DeepEqual<T>(T expected, T value)
{
foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Instance))
Assert.Equal(property.GetValue(expected), property.GetValue(value));
}
/// <summary>
/// Explicitly fail a test.
/// </summary>
[AssertionMethod]
public static void Fail()
{
throw new XunitException();
}
/// <summary>
/// Explicitly fail a test.
/// </summary>
/// <param name="message">The message that will be seen in the test report</param>
[AssertionMethod]
public static void Fail(string message)
{
throw new XunitException(message);
}
}
}

View File

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

View File

@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Xunit.Abstractions;
namespace Kyoo.Tests
{
public class RepositoryActivator : IDisposable, IAsyncDisposable
{
public TestContext Context { get; }
public ILibraryManager LibraryManager { get; }
private readonly DatabaseContext _database;
public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null)
{
Context = postgres == null
? new SqLiteTestContext(output)
: new PostgresTestContext(postgres, output);
_database = Context.New();
ProviderRepository provider = new(_database);
LibraryRepository library = new(_database, provider);
CollectionRepository collection = new(_database);
GenreRepository genre = new(_database);
StudioRepository studio = new(_database);
PeopleRepository people = new(_database, provider,
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository));
ShowRepository show = new(_database, studio, people, genre, provider);
SeasonRepository season = new(_database, provider);
LibraryItemRepository libraryItem = new(_database,
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository));
TrackRepository track = new(_database);
EpisodeRepository episode = new(_database, provider, track);
UserRepository user = new(_database);
LibraryManager = new LibraryManager(new IBaseRepository[] {
provider,
library,
libraryItem,
collection,
show,
season,
episode,
track,
people,
studio,
genre,
user
});
}
public void Dispose()
{
_database.Dispose();
Context.Dispose();
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await _database.DisposeAsync();
await Context.DisposeAsync();
}
}
}

View File

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Xunit;
namespace Kyoo.Tests
{
public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable
where T : class, IResource, new()
{
protected readonly RepositoryActivator Repositories;
private readonly IRepository<T> _repository;
protected RepositoryTests(RepositoryActivator repositories)
{
Repositories = repositories;
_repository = Repositories.LibraryManager.GetRepository<T>();
}
public void Dispose()
{
Repositories.Dispose();
}
public ValueTask DisposeAsync()
{
return Repositories.DisposeAsync();
}
[Fact]
public async Task FillTest()
{
await using DatabaseContext database = Repositories.Context.New();
Assert.Equal(1, database.Shows.Count());
}
[Fact]
public async Task GetByIdTest()
{
T value = await _repository.Get(TestSample.Get<T>().ID);
KAssert.DeepEqual(TestSample.Get<T>(), value);
}
[Fact]
public async Task GetBySlugTest()
{
T value = await _repository.Get(TestSample.Get<T>().Slug);
KAssert.DeepEqual(TestSample.Get<T>(), value);
}
[Fact]
public async Task GetByFakeIdTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(2));
}
[Fact]
public async Task GetByFakeSlugTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get("non-existent"));
}
[Fact]
public async Task DeleteByIdTest()
{
await _repository.Delete(TestSample.Get<T>().ID);
Assert.Equal(0, await _repository.GetCount());
}
[Fact]
public async Task DeleteBySlugTest()
{
await _repository.Delete(TestSample.Get<T>().Slug);
Assert.Equal(0, await _repository.GetCount());
}
[Fact]
public async Task DeleteByValueTest()
{
await _repository.Delete(TestSample.Get<T>());
Assert.Equal(0, await _repository.GetCount());
}
[Fact]
public async Task CreateTest()
{
await Assert.ThrowsAsync<DuplicatedItemException>(() => _repository.Create(TestSample.Get<T>()));
await _repository.Delete(TestSample.Get<T>());
T expected = TestSample.Get<T>();
expected.ID = 0;
await _repository.Create(expected);
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
}
[Fact]
public async Task CreateNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Create(null!));
}
[Fact]
public async Task CreateIfNotExistNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.CreateIfNotExists(null!));
}
[Fact]
public async Task CreateIfNotExistTest()
{
T expected = TestSample.Get<T>();
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
await _repository.Delete(TestSample.Get<T>());
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
}
[Fact]
public async Task EditNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false));
}
[Fact]
public async Task EditNonExistingTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T {ID = 56}, false));
}
[Fact]
public async Task GetExpressionIDTest()
{
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.ID == TestSample.Get<T>().ID));
}
[Fact]
public async Task GetExpressionSlugTest()
{
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.Slug == TestSample.Get<T>().Slug));
}
[Fact]
public async Task GetExpressionNotFoundTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing"));
}
[Fact]
public async Task GetExpressionNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Get((Expression<Func<T, bool>>)null!));
}
[Fact]
public async Task GetOrDefaultTest()
{
Assert.Null(await _repository.GetOrDefault(56));
Assert.Null(await _repository.GetOrDefault("non-existing"));
Assert.Null(await _repository.GetOrDefault(x => x.Slug == "non-existing"));
}
[Fact]
public async Task GetCountWithFilterTest()
{
string slug = TestSample.Get<T>().Slug[2..4];
Assert.Equal(1, await _repository.GetCount(x => x.Slug.Contains(slug)));
}
[Fact]
public async Task GetAllTest()
{
string slug = TestSample.Get<T>().Slug[2..4];
ICollection<T> ret = await _repository.GetAll(x => x.Slug.Contains(slug));
Assert.Equal(1, ret.Count);
KAssert.DeepEqual(TestSample.Get<T>(), ret.First());
}
[Fact]
public async Task DeleteAllTest()
{
string slug = TestSample.Get<T>().Slug[2..4];
await _repository.DeleteAll(x => x.Slug.Contains(slug));
Assert.Equal(0, await _repository.GetCount());
}
}
}

View File

@ -1,17 +0,0 @@
namespace Kyoo.Tests
{
public class SetupTests
{
// TODO test libraries & repositories via a on-memory SQLite database.
// TODO Requires: Kyoo should be database agonistic and database implementations should be available via a plugin.
// [Fact]
// public void Get_Test()
// {
// TestContext context = new();
// using DatabaseContext database = context.New();
//
// Assert.Equal(1, database.Shows.Count());
// }
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class CollectionTests : ACollectionTests
{
public CollectionTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class CollectionTests : ACollectionTests
{
public CollectionTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ACollectionTests : RepositoryTests<Collection>
{
private readonly ICollectionRepository _repository;
protected ACollectionTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.CollectionRepository;
}
}
}

View File

@ -0,0 +1,192 @@
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class EpisodeTests : AEpisodeTests
{
public EpisodeTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class EpisodeTests : AEpisodeTests
{
public EpisodeTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AEpisodeTests : RepositoryTests<Episode>
{
private readonly IEpisodeRepository _repository;
protected AEpisodeTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = repositories.LibraryManager.EpisodeRepository;
}
[Fact]
public async Task SlugEditTest()
{
Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
Show show = new()
{
ID = episode.ShowID,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
episode = await _repository.Get(1);
Assert.Equal("new-slug-s1e1", episode.Slug);
}
[Fact]
public async Task SeasonNumberEditTest()
{
Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await _repository.Edit(new Episode
{
ID = 1,
SeasonNumber = 2
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
}
[Fact]
public async Task EpisodeNumberEditTest()
{
Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await _repository.Edit(new Episode
{
ID = 1,
EpisodeNumber = 2
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
}
[Fact]
public async Task EpisodeCreationSlugTest()
{
Episode episode = await _repository.Create(new Episode
{
ShowID = TestSample.Get<Show>().ID,
SeasonNumber = 2,
EpisodeNumber = 4
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
}
// TODO absolute numbering tests
[Fact]
public void AbsoluteSlugTest()
{
Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}",
TestSample.GetAbsoluteEpisode().Slug);
}
[Fact]
public async Task EpisodeCreationAbsoluteSlugTest()
{
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
Assert.Equal($"{TestSample.Get<Show>().Slug}-{TestSample.GetAbsoluteEpisode().AbsoluteNumber}", episode.Slug);
}
[Fact]
public async Task SlugEditAbsoluteTest()
{
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
Show show = new()
{
ID = episode.ShowID,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
episode = await _repository.Get(2);
Assert.Equal($"new-slug-3", episode.Slug);
}
[Fact]
public async Task AbsoluteNumberEditTest()
{
await _repository.Create(TestSample.GetAbsoluteEpisode());
Episode episode = await _repository.Edit(new Episode
{
ID = 2,
AbsoluteNumber = 56
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
}
[Fact]
public async Task AbsoluteToNormalEditTest()
{
await _repository.Create(TestSample.GetAbsoluteEpisode());
Episode episode = await _repository.Edit(new Episode
{
ID = 2,
SeasonNumber = 1,
EpisodeNumber = 2
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
}
[Fact]
public async Task NormalToAbsoluteEditTest()
{
Episode episode = await _repository.Get(1);
episode.SeasonNumber = null;
episode.AbsoluteNumber = 12;
episode = await _repository.Edit(episode, true);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
}
[Fact]
public async Task MovieEpisodeTest()
{
Episode episode = await _repository.Create(TestSample.GetMovieEpisode());
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
episode = await _repository.Get(3);
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
}
[Fact]
public async Task MovieEpisodeEditTest()
{
await _repository.Create(TestSample.GetMovieEpisode());
await Repositories.LibraryManager.Edit(new Show
{
ID = 1,
Slug = "john-wick"
}, false);
Episode episode = await _repository.Get(3);
Assert.Equal("john-wick", episode.Slug);
}
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class GenreTests : AGenreTests
{
public GenreTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class GenreTests : AGenreTests
{
public GenreTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AGenreTests : RepositoryTests<Genre>
{
private readonly IGenreRepository _repository;
protected AGenreTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.GenreRepository;
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class LibraryItemTest : ALibraryItemTest
{
public LibraryItemTest(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryItemTest : ALibraryItemTest
{
public LibraryItemTest(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryItemTest
{
private readonly ILibraryItemRepository _repository;
private readonly RepositoryActivator _repositories;
protected ALibraryItemTest(RepositoryActivator repositories)
{
_repositories = repositories;
_repository = repositories.LibraryManager.LibraryItemRepository;
}
[Fact]
public async Task CountTest()
{
Assert.Equal(2, await _repository.GetCount());
}
[Fact]
public async Task GetShowTests()
{
LibraryItem expected = new(TestSample.Get<Show>());
LibraryItem actual = await _repository.Get(1);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetCollectionTests()
{
LibraryItem expected = new(TestSample.Get<Show>());
LibraryItem actual = await _repository.Get(-1);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetShowSlugTests()
{
LibraryItem expected = new(TestSample.Get<Show>());
LibraryItem actual = await _repository.Get(TestSample.Get<Show>().Slug);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetCollectionSlugTests()
{
LibraryItem expected = new(TestSample.Get<Collection>());
LibraryItem actual = await _repository.Get(TestSample.Get<Collection>().Slug);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetDuplicatedSlugTests()
{
await _repositories.LibraryManager.Create(new Collection()
{
Slug = TestSample.Get<Show>().Slug
});
await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug));
}
}
}

View File

@ -0,0 +1,36 @@
using Kyoo.Controllers;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class LibraryTests : ALibraryTests
{
public LibraryTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryTests : ALibraryTests
{
public LibraryTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryTests : RepositoryTests<Models.Library>
{
private readonly ILibraryRepository _repository;
protected ALibraryTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class PeopleTests : APeopleTests
{
public PeopleTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class PeopleTests : APeopleTests
{
public PeopleTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class APeopleTests : RepositoryTests<People>
{
private readonly IPeopleRepository _repository;
protected APeopleTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.PeopleRepository;
}
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class ProviderTests : AProviderTests
{
public ProviderTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class ProviderTests : AProviderTests
{
public ProviderTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AProviderTests : RepositoryTests<Provider>
{
private readonly IProviderRepository _repository;
protected AProviderTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.ProviderRepository;
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
public class GlobalTests : IDisposable, IAsyncDisposable
{
private readonly RepositoryActivator _repositories;
public GlobalTests(ITestOutputHelper output)
{
_repositories = new RepositoryActivator(output);
}
[Fact]
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
public void SampleTest()
{
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
}
public void Dispose()
{
_repositories.Dispose();
GC.SuppressFinalize(this);
}
public ValueTask DisposeAsync()
{
return _repositories.DisposeAsync();
}
}
}

View File

@ -0,0 +1,79 @@
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class SeasonTests : ASeasonTests
{
public SeasonTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class SeasonTests : ASeasonTests
{
public SeasonTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ASeasonTests : RepositoryTests<Season>
{
private readonly ISeasonRepository _repository;
protected ASeasonTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.SeasonRepository;
}
[Fact]
public async Task SlugEditTest()
{
Season season = await _repository.Get(1);
Assert.Equal("anohana-s1", season.Slug);
Show show = new()
{
ID = season.ShowID,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
season = await _repository.Get(1);
Assert.Equal("new-slug-s1", season.Slug);
}
[Fact]
public async Task SeasonNumberEditTest()
{
Season season = await _repository.Get(1);
Assert.Equal("anohana-s1", season.Slug);
await _repository.Edit(new Season
{
ID = 1,
SeasonNumber = 2
}, false);
season = await _repository.Get(1);
Assert.Equal("anohana-s2", season.Slug);
}
[Fact]
public async Task SeasonCreationSlugTest()
{
Season season = await _repository.Create(new Season
{
ShowID = TestSample.Get<Show>().ID,
SeasonNumber = 2
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
}
}
}

View File

@ -0,0 +1,292 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class ShowTests : AShowTests
{
public ShowTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class ShowTests : AShowTests
{
public ShowTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AShowTests : RepositoryTests<Show>
{
private readonly IShowRepository _repository;
protected AShowTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.ShowRepository;
}
[Fact]
public async Task EditTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Path = "/super";
value.Title = "New Title";
Show edited = await _repository.Edit(value, false);
KAssert.DeepEqual(value, edited);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows.FirstAsync();
KAssert.DeepEqual(show, value);
}
[Fact]
public async Task EditGenreTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Genres = new[] {new Genre("test")};
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name}));
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.Genres)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name}));
}
[Fact]
public async Task EditStudioTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Studio = new Studio("studio");
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal("studio", edited.Studio.Slug);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.Genres)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal("studio", edited.Studio.Slug);
}
[Fact]
public async Task EditAliasesTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Aliases = new[] {"NiceNewAlias", "SecondAlias"};
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Aliases, edited.Aliases);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(value.Aliases, edited.Aliases);
}
[Fact]
public async Task EditPeopleTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.People = new[]
{
new PeopleRole
{
Show = value,
People = TestSample.Get<People>(),
ForPeople = false,
Type = "Actor",
Role = "NiceCharacter"
}
};
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(edited.People.First().ShowID, value.ID);
Assert.Equal(
value.People.Select(x => new{x.Role, x.Slug, x.People.Name}),
edited.People.Select(x => new{x.Role, x.Slug, x.People.Name}));
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.People)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(
value.People.Select(x => new{x.Role, x.Slug, x.People.Name}),
edited.People.Select(x => new{x.Role, x.Slug, x.People.Name}));
}
[Fact]
public async Task EditExternalIDsTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.ExternalIDs = new[]
{
new MetadataID<Show>()
{
First = value,
Second = new Provider("test", "test.png"),
DataID = "1234"
}
};
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(
value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}),
edited.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}));
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Second)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(
value.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}),
show.ExternalIDs.Select(x => new {x.DataID, x.Second.Slug}));
}
[Fact]
public async Task EditResetOldTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
Show newValue = new()
{
ID = value.ID,
Title = "Reset"
};
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Edit(newValue, true));
newValue.Slug = "reset";
Show edited = await _repository.Edit(newValue, true);
Assert.Equal(value.ID, edited.ID);
Assert.Null(edited.Overview);
Assert.Equal("reset", edited.Slug);
Assert.Equal("Reset", edited.Title);
Assert.Null(edited.Aliases);
Assert.Null(edited.ExternalIDs);
Assert.Null(edited.People);
Assert.Null(edited.Genres);
Assert.Null(edited.Studio);
}
[Fact]
public async Task CreateWithRelationsTest()
{
Show expected = TestSample.Get<Show>();
expected.ID = 0;
expected.Slug = "created-relation-test";
expected.ExternalIDs = new[]
{
new MetadataID<Show>
{
First = expected,
Second = new Provider("provider", "provider.png"),
DataID = "ID"
}
};
expected.Genres = new[]
{
new Genre
{
Name = "Genre",
Slug = "genre"
}
};
expected.People = new[]
{
new PeopleRole
{
People = TestSample.Get<People>(),
Show = expected,
ForPeople = false,
Role = "actor"
}
};
expected.Studio = new Studio("studio");
Show created = await _repository.Create(expected);
KAssert.DeepEqual(expected, created);
}
[Fact]
public async Task SlugDuplicationTest()
{
Show test = TestSample.Get<Show>();
test.ID = 0;
test.Slug = "300";
Show created = await _repository.Create(test);
Assert.Equal("300!", created.Slug);
}
[Fact]
public async Task GetSlugTest()
{
Show reference = TestSample.Get<Show>();
Assert.Equal(reference.Slug, await _repository.GetSlug(reference.ID));
}
[Theory]
[InlineData("test")]
[InlineData("super")]
[InlineData("title")]
[InlineData("TiTlE")]
[InlineData("SuPeR")]
public async Task SearchTest(string query)
{
Show value = new()
{
Slug = "super-test",
Title = "This is a test title²"
};
await _repository.Create(value);
ICollection<Show> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
[Fact]
public async Task DeleteShowWithEpisodeAndSeason()
{
Show show = TestSample.Get<Show>();
await Repositories.LibraryManager.Load(show, x => x.Seasons);
await Repositories.LibraryManager.Load(show, x => x.Episodes);
Assert.Equal(1, await _repository.GetCount());
Assert.Equal(1, show.Seasons.Count);
Assert.Equal(1, show.Episodes.Count);
await _repository.Delete(show);
Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount());
}
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class StudioTests : AStudioTests
{
public StudioTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class StudioTests : AStudioTests
{
public StudioTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AStudioTests : RepositoryTests<Studio>
{
private readonly IStudioRepository _repository;
protected AStudioTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.StudioRepository;
}
}
}

View File

@ -0,0 +1,51 @@
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class TrackTests : ATrackTests
{
public TrackTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class TrackTests : ATrackTests
{
public TrackTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ATrackTests : RepositoryTests<Track>
{
private readonly ITrackRepository _repository;
protected ATrackTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = repositories.LibraryManager.TrackRepository;
}
[Fact]
public async Task SlugEditTest()
{
await Repositories.LibraryManager.ShowRepository.Edit(new Show
{
ID = 1,
Slug = "new-slug"
}, false);
Track track = await _repository.Get(1);
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
}
}
}

View File

@ -0,0 +1,37 @@
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
{
namespace SqLite
{
public class UserTests : AUserTests
{
public UserTests(ITestOutputHelper output)
: base(new RepositoryActivator(output)) { }
}
}
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class UserTests : AUserTests
{
public UserTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AUserTests : RepositoryTests<User>
{
private readonly IUserRepository _repository;
protected AUserTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.UserRepository;
}
}
}

View File

@ -1,79 +1,203 @@
// using Kyoo.Models;
// using Microsoft.Data.Sqlite;
// using Microsoft.EntityFrameworkCore;
//
// namespace Kyoo.Tests
// {
// /// <summary>
// /// Class responsible to fill and create in memory databases for unit tests.
// /// </summary>
// public class TestContext
// {
// /// <summary>
// /// The context's options that specify to use an in memory Sqlite database.
// /// </summary>
// private readonly DbContextOptions<DatabaseContext> _context;
//
// /// <summary>
// /// Create a new database and fill it with information.
// /// </summary>
// public TestContext()
// {
// SqliteConnection connection = new("DataSource=:memory:");
// connection.Open();
//
// try
// {
// _context = new DbContextOptionsBuilder<DatabaseContext>()
// .UseSqlite(connection)
// .Options;
// FillDatabase();
// }
// finally
// {
// connection.Close();
// }
// }
//
// /// <summary>
// /// Fill the database with pre defined values using a clean context.
// /// </summary>
// private void FillDatabase()
// {
// using DatabaseContext context = new(_context);
// context.Shows.Add(new Show
// {
// ID = 67,
// Slug = "anohana",
// Title = "Anohana: The Flower We Saw That Day",
// Aliases = new[]
// {
// "Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
// "AnoHana",
// "We Still Don't Know the Name of the Flower We Saw That Day."
// },
// Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
// "In time, however, these childhood friends drifted apart, and when they became high " +
// "school students, they had long ceased to think of each other as friends.",
// Status = Status.Finished,
// TrailerUrl = null,
// StartYear = 2011,
// EndYear = 2011,
// Poster = "poster",
// Logo = "logo",
// Backdrop = "backdrop",
// IsMovie = false,
// Studio = null
// });
// }
//
// /// <summary>
// /// Get a new database context connected to a in memory Sqlite database.
// /// </summary>
// /// <returns>A valid DatabaseContext</returns>
// public DatabaseContext New()
// {
// return new(_context);
// }
// }
// }
using System;
using System.Threading.Tasks;
using Kyoo.Postgresql;
using Kyoo.SqLite;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests
{
public sealed class SqLiteTestContext : TestContext
{
/// <summary>
/// The internal sqlite connection used by all context returned by this class.
/// </summary>
private readonly SqliteConnection _connection;
/// <summary>
/// The context's options that specify to use an in memory Sqlite database.
/// </summary>
private readonly DbContextOptions<DatabaseContext> _context;
public SqLiteTestContext(ITestOutputHelper output)
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
_context = new DbContextOptionsBuilder<DatabaseContext>()
.UseSqlite(_connection)
.UseLoggerFactory(LoggerFactory.Create(x =>
{
x.ClearProviders();
x.AddXunit(output);
}))
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
.Options;
using DatabaseContext context = New();
context.Database.Migrate();
TestSample.FillDatabase(context);
}
public override void Dispose()
{
_connection.Close();
}
public override async ValueTask DisposeAsync()
{
await _connection.CloseAsync();
}
public override DatabaseContext New()
{
return new SqLiteContext(_context);
}
}
[CollectionDefinition(nameof(Postgresql))]
public class PostgresCollection : ICollectionFixture<PostgresFixture>
{}
public sealed class PostgresFixture : IDisposable
{
private readonly DbContextOptions<DatabaseContext> _options;
public string Template { get; }
public string Connection => PostgresTestContext.GetConnectionString(Template);
public PostgresFixture()
{
// TODO Assert.Skip when postgres is not available. (this needs xunit v3)
string id = Guid.NewGuid().ToString().Replace('-', '_');
Template = $"kyoo_template_{id}";
_options = new DbContextOptionsBuilder<DatabaseContext>()
.UseNpgsql(Connection)
.Options;
using PostgresContext context = new(_options);
context.Database.Migrate();
using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
conn.Open();
conn.ReloadTypes();
TestSample.FillDatabase(context);
conn.Close();
}
public void Dispose()
{
using PostgresContext context = new(_options);
context.Database.EnsureDeleted();
}
}
public sealed class PostgresTestContext : TestContext
{
private readonly NpgsqlConnection _connection;
private readonly DbContextOptions<DatabaseContext> _context;
public PostgresTestContext(PostgresFixture template, ITestOutputHelper output)
{
string id = Guid.NewGuid().ToString().Replace('-', '_');
string database = $"kyoo_test_{id}";
using (NpgsqlConnection connection = new(template.Connection))
{
connection.Open();
using NpgsqlCommand cmd = new($"CREATE DATABASE {database} WITH TEMPLATE {template.Template}", connection);
cmd.ExecuteNonQuery();
}
_connection = new NpgsqlConnection(GetConnectionString(database));
_connection.Open();
_context = new DbContextOptionsBuilder<DatabaseContext>()
.UseNpgsql(_connection)
.UseLoggerFactory(LoggerFactory.Create(x =>
{
x.ClearProviders();
x.AddXunit(output);
}))
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
.Options;
}
public static string GetConnectionString(string database)
{
string server = Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "127.0.0.1";
string port = Environment.GetEnvironmentVariable("POSTGRES_PORT") ?? "5432";
string username = Environment.GetEnvironmentVariable("POSTGRES_USERNAME") ?? "kyoo";
string password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "kyooPassword";
return $"Server={server};Port={port};Database={database};User ID={username};Password={password};Include Error Detail=true";
}
public override void Dispose()
{
using DatabaseContext db = New();
db.Database.EnsureDeleted();
_connection.Close();
}
public override async ValueTask DisposeAsync()
{
await using DatabaseContext db = New();
await db.Database.EnsureDeletedAsync();
await _connection.CloseAsync();
}
public override DatabaseContext New()
{
return new PostgresContext(_context);
}
}
/// <summary>
/// Class responsible to fill and create in memory databases for unit tests.
/// </summary>
public abstract class TestContext : IDisposable, IAsyncDisposable
{
/// <summary>
/// Add an arbitrary data to the test context.
/// </summary>
public void Add<T>(T obj)
where T : class
{
using DatabaseContext context = New();
context.Set<T>().Add(obj);
context.SaveChanges();
}
/// <summary>
/// Add an arbitrary data to the test context.
/// </summary>
public async Task AddAsync<T>(T obj)
where T : class
{
await using DatabaseContext context = New();
await context.Set<T>().AddAsync(obj);
await context.SaveChangesAsync();
}
/// <summary>
/// Get a new database context connected to a in memory Sqlite database.
/// </summary>
/// <returns>A valid DatabaseContext</returns>
public abstract DatabaseContext New();
public abstract void Dispose();
public abstract ValueTask DisposeAsync();
}
}

View File

@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using Kyoo.Models;
namespace Kyoo.Tests
{
public static class TestSample
{
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
{
{
typeof(Show),
() => new Show()
}
};
private static readonly Dictionary<Type, Func<object>> Samples = new()
{
{
typeof(Models.Library),
() => new Models.Library
{
ID = 1,
Slug = "deck",
Name = "Deck",
Paths = new[] {"/path/to/deck"}
}
},
{
typeof(Collection),
() => new Collection
{
ID = 1,
Slug = "collection",
Name = "Collection",
Overview = "A nice collection for tests",
Poster = "Poster"
}
},
{
typeof(Show),
() => new Show
{
ID = 1,
Slug = "anohana",
Title = "Anohana: The Flower We Saw That Day",
Aliases = new[]
{
"Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
"AnoHana",
"We Still Don't Know the Name of the Flower We Saw That Day."
},
Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
"In time, however, these childhood friends drifted apart, and when they became high " +
"school students, they had long ceased to think of each other as friends.",
Status = Status.Finished,
TrailerUrl = null,
StartAir = new DateTime(2011, 1, 1),
EndAir = new DateTime(2011, 1, 1),
Poster = "poster",
Logo = "logo",
Backdrop = "backdrop",
IsMovie = false,
Studio = null
}
},
{
typeof(Season),
() => new Season
{
ID = 1,
ShowSlug = "anohana",
ShowID = 1,
SeasonNumber = 1,
Title = "Season 1",
Overview = "The first season",
StartDate = new DateTime(2020, 06, 05),
EndDate = new DateTime(2020, 07, 05),
Poster = "poster"
}
},
{
typeof(Episode),
() => new Episode
{
ID = 1,
ShowSlug = "anohana",
ShowID = 1,
SeasonID = 1,
SeasonNumber = 1,
EpisodeNumber = 1,
AbsoluteNumber = 1,
Path = "/home/kyoo/anohana-s1e1",
Thumb = "thumbnail",
Title = "Episode 1",
Overview = "Summary of the first episode",
ReleaseDate = new DateTime(2020, 06, 05)
}
},
{
typeof(Track),
() => new Track
{
ID = 1,
EpisodeID = 1,
Codec = "subrip",
Language = "eng",
Path = "/path",
Title = "Subtitle track",
Type = StreamType.Subtitle,
EpisodeSlug = Get<Episode>().Slug,
IsDefault = true,
IsExternal = false,
IsForced = false,
TrackIndex = 1
}
},
{
typeof(People),
() => new People
{
ID = 1,
Slug = "the-actor",
Name = "The Actor",
Poster = "NicePoster"
}
},
{
typeof(Studio),
() => new Studio
{
ID = 1,
Slug = "hyper-studio",
Name = "Hyper studio"
}
},
{
typeof(Genre),
() => new Genre
{
ID = 1,
Slug = "action",
Name = "Action"
}
},
{
typeof(Provider),
() => new Provider
{
ID = 1,
Slug = "tvdb",
Name = "The TVDB",
Logo = "path/tvdb.svg",
LogoExtension = "svg"
}
},
{
typeof(User),
() => new User
{
ID = 1,
Slug = "user",
Username = "User",
Email = "user@im-a-user.com",
Password = "MD5-encoded",
Permissions = new [] {"overall.read"}
}
}
};
public static T Get<T>()
{
return (T)Samples[typeof(T)]();
}
public static T GetNew<T>()
{
return (T)NewSamples[typeof(T)]();
}
public static void FillDatabase(DatabaseContext context)
{
Collection collection = Get<Collection>();
collection.ID = 0;
context.Collections.Add(collection);
Show show = Get<Show>();
show.ID = 0;
context.Shows.Add(show);
Season season = Get<Season>();
season.ID = 0;
season.ShowID = 0;
season.Show = show;
context.Seasons.Add(season);
Episode episode = Get<Episode>();
episode.ID = 0;
episode.ShowID = 0;
episode.Show = show;
episode.SeasonID = 0;
episode.Season = season;
context.Episodes.Add(episode);
Track track = Get<Track>();
track.ID = 0;
track.EpisodeID = 0;
track.Episode = episode;
context.Tracks.Add(track);
Studio studio = Get<Studio>();
studio.ID = 0;
studio.Shows = new List<Show> {show};
context.Studios.Add(studio);
Genre genre = Get<Genre>();
genre.ID = 0;
genre.Shows = new List<Show> {show};
context.Genres.Add(genre);
People people = Get<People>();
people.ID = 0;
context.People.Add(people);
Provider provider = Get<Provider>();
provider.ID = 0;
context.Providers.Add(provider);
Models.Library library = Get<Models.Library>();
library.ID = 0;
library.Collections = new List<Collection> {collection};
library.Providers = new List<Provider> {provider};
context.Libraries.Add(library);
User user = Get<User>();
user.ID = 0;
context.Users.Add(user);
context.SaveChanges();
}
public static Episode GetAbsoluteEpisode()
{
return new()
{
ID = 2,
ShowSlug = "anohana",
ShowID = 1,
SeasonNumber = null,
EpisodeNumber = null,
AbsoluteNumber = 3,
Path = "/home/kyoo/anohana-3",
Thumb = "thumbnail",
Title = "Episode 3",
Overview = "Summary of the third absolute episode",
ReleaseDate = new DateTime(2020, 06, 05)
};
}
public static Episode GetMovieEpisode()
{
return new()
{
ID = 3,
ShowSlug = "anohana",
ShowID = 1,
Path = "/home/kyoo/john-wick",
Thumb = "thumb",
Title = "John wick",
Overview = "A movie episode test",
ReleaseDate = new DateTime(1595, 05, 12)
};
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Kyoo.Tests
{
public class EnumerableTests
{
[Fact]
public void MapTest()
{
int[] list = {1, 2, 3, 4};
Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i));
Assert.Throws<ArgumentNullException>(() => list.Map(((Func<int, int, int>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.Map((x, _) => x + 1));
}
[Fact]
public async Task MapAsyncTest()
{
int[] list = {1, 2, 3, 4};
await foreach((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i))))
{
Assert.Equal(x - 1, i);
}
Assert.Throws<ArgumentNullException>(() => list.MapAsync(((Func<int, int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.MapAsync((x, _) => Task.FromResult(x + 1)));
}
[Fact]
public async Task SelectAsyncTest()
{
int[] list = {1, 2, 3, 4};
int i = 2;
await foreach(int x in list.SelectAsync(x => Task.FromResult(x + 1)))
{
Assert.Equal(i++, x);
}
Assert.Throws<ArgumentNullException>(() => list.SelectAsync(((Func<int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.SelectAsync(x => Task.FromResult(x + 1)));
}
[Fact]
public async Task ToListAsyncTest()
{
int[] expected = {1, 2, 3, 4};
IAsyncEnumerable<int> list = expected.SelectAsync(Task.FromResult);
Assert.Equal(expected, await list.ToListAsync());
list = null;
await Assert.ThrowsAsync<ArgumentNullException>(() => list!.ToListAsync());
}
[Fact]
public void IfEmptyTest()
{
int[] list = {1, 2, 3, 4};
list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray();
Assert.Throws<ArgumentNullException>(() => list.IfEmpty(null!).ToList());
list = null;
Assert.Throws<ArgumentNullException>(() => list!.IfEmpty(() => {}).ToList());
list = Array.Empty<int>();
Assert.Throws<ArgumentException>(() => list.IfEmpty(() => throw new ArgumentException()).ToList());
Assert.Empty(list.IfEmpty(() => {}));
}
}
}

View File

@ -0,0 +1,21 @@
using Kyoo.Models;
using Xunit;
namespace Kyoo.Tests
{
public class MergerTests
{
[Fact]
public void NullifyTest()
{
Genre genre = new("test")
{
ID = 5
};
Merger.Nullify(genre);
Assert.Equal(0, genre.ID);
Assert.Null(genre.Name);
Assert.Null(genre.Slug);
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Kyoo.Tests
{
public class TaskTests
{
[Fact]
public async Task DefaultIfNullTest()
{
Assert.Equal(0, await TaskUtils.DefaultIfNull<int>(null));
Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1)));
}
[Fact]
public async Task ThenTest()
{
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
.Then(_ => throw new ArgumentException()));
Assert.Equal(1, await Task.FromResult(1)
.Then(_ => {}));
static async Task<int> Faulted()
{
await Task.Delay(1);
throw new ArgumentException();
}
await Assert.ThrowsAsync<ArgumentException>(() => Faulted().Then(_ => KAssert.Fail()));
static async Task<int> Infinite()
{
await Task.Delay(100000);
return 1;
}
CancellationTokenSource token = new();
token.Cancel();
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Then(_ => {}));
}
[Fact]
public async Task MapTest()
{
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
.Map<int, int>(_ => throw new ArgumentException()));
Assert.Equal(2, await Task.FromResult(1)
.Map(x => x + 1));
static async Task<int> Faulted()
{
await Task.Delay(1);
throw new ArgumentException();
}
await Assert.ThrowsAsync<ArgumentException>(() => Faulted()
.Map(x =>
{
KAssert.Fail();
return x;
}));
static async Task<int> Infinite()
{
await Task.Delay(100000);
return 1;
}
CancellationTokenSource token = new();
token.Cancel();
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Map(x => x));
}
}
}

View File

@ -13,12 +13,23 @@ namespace Kyoo.Tests
Expression<Func<Show, int>> member = x => x.ID;
Expression<Func<Show, object>> memberCast = x => x.ID;
Assert.True(Utility.IsPropertyExpression(null));
Assert.False(Utility.IsPropertyExpression(null));
Assert.True(Utility.IsPropertyExpression(member));
Assert.True(Utility.IsPropertyExpression(memberCast));
Expression<Func<Show, object>> call = x => x.GetID("test");
Assert.False(Utility.IsPropertyExpression(call));
}
[Fact]
public void GetPropertyName_Test()
{
Expression<Func<Show, int>> member = x => x.ID;
Expression<Func<Show, object>> memberCast = x => x.ID;
Assert.Equal("ID", Utility.GetPropertyName(member));
Assert.Equal("ID", Utility.GetPropertyName(memberCast));
Assert.Throws<ArgumentException>(() => Utility.GetPropertyName(null));
}
}
}

View File

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

View File

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

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
@ -16,7 +15,7 @@ namespace Kyoo.Controllers
public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository
{
/// <summary>
/// The databse handle
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
@ -24,10 +23,6 @@ namespace Kyoo.Controllers
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A show repository to get show's slug from their ID and keep the slug in each episode.
/// </summary>
private readonly IShowRepository _shows;
/// <summary>
/// A track repository to handle creation and deletion of tracks related to the current episode.
/// </summary>
private readonly ITrackRepository _tracks;
@ -41,66 +36,31 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="database">The database handle to use.</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="tracks">A track repository</param>
public EpisodeRepository(DatabaseContext database,
IProviderRepository providers,
IShowRepository shows,
ITrackRepository tracks)
: base(database)
{
_database = database;
_providers = providers;
_shows = shows;
_tracks = tracks;
}
/// <inheritdoc />
public override async Task<Episode> GetOrDefault(int id)
public Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{
Episode ret = await base.GetOrDefault(id);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
}
/// <inheritdoc />
public override async Task<Episode> GetOrDefault(string slug)
public Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\d*)");
if (match.Success)
{
return await GetOrDefault(match.Groups["show"].Value,
int.Parse(match.Groups["season"].Value),
int.Parse(match.Groups["episode"].Value));
}
Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug);
if (episode != null)
episode.ShowSlug = slug;
return episode;
}
/// <inheritdoc />
public override async Task<Episode> GetOrDefault(Expression<Func<Episode, bool>> where)
{
Episode ret = await base.GetOrDefault(where);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
}
/// <inheritdoc />
@ -122,59 +82,28 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public async Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
public Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.AbsoluteNumber == absoluteNumber);
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
public Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.AbsoluteNumber == absoluteNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber);
if (ret != null)
ret.ShowSlug = showSlug;
return ret;
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber);
}
/// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query)
{
List<Episode> episodes = await _database.Episodes
.Where(x => x.EpisodeNumber != -1)
return await _database.Episodes
.Where(x => x.EpisodeNumber != null)
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
foreach (Episode episode in episodes)
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
return episodes;
}
/// <inheritdoc />
public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default,
Pagination limit = default)
{
ICollection<Episode> episodes = await base.GetAll(where, sort, limit);
foreach (Episode episode in episodes)
episode.ShowSlug = await _shows.GetSlug(episode.ShowID);
return episodes;
}
/// <inheritdoc />
@ -185,6 +114,9 @@ namespace Kyoo.Controllers
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists).");
return await ValidateTracks(obj);
// TODO check if this is needed
// obj.Slug = await _database.Entry(obj).Property(x => x.Slug).
// return obj;
}
/// <inheritdoc />
@ -195,7 +127,7 @@ namespace Kyoo.Controllers
if (changed.Tracks != null || resetOld)
{
await _tracks.DeleteRange(x => x.EpisodeID == resource.ID);
await _tracks.DeleteAll(x => x.EpisodeID == resource.ID);
resource.Tracks = changed.Tracks;
await ValidateTracks(resource);
}
@ -213,18 +145,15 @@ namespace Kyoo.Controllers
/// Set track's index and ensure that every tracks is well-formed.
/// </summary>
/// <param name="resource">The resource to fix.</param>
/// <returns>The <see cref="resource"/> parameter is returnned.</returns>
/// <returns>The <see cref="resource"/> parameter is returned.</returns>
private async Task<Episode> ValidateTracks(Episode resource)
{
resource.Tracks = await resource.Tracks.MapAsync((x, i) =>
resource.Tracks = await TaskUtils.DefaultIfNull(resource.Tracks?.SelectAsync(x =>
{
x.Episode = resource;
x.TrackIndex = resource.Tracks.Take(i).Count(y => x.Language == y.Language
&& x.IsForced == y.IsForced
&& x.Codec == y.Codec
&& x.Type == y.Type);
x.EpisodeSlug = resource.Slug;
return _tracks.Create(x);
}).ToListAsync();
}).ToListAsync());
return resource;
}
@ -232,13 +161,12 @@ namespace Kyoo.Controllers
protected override async Task Validate(Episode resource)
{
await base.Validate(resource);
resource.ExternalIDs = await resource.ExternalIDs.SelectAsync(async x =>
await resource.ExternalIDs.ForEachAsync(async x =>
{
x.Provider = await _providers.CreateIfNotExists(x.Provider);
x.ProviderID = x.Provider.ID;
_database.Entry(x.Provider).State = EntityState.Detached;
return x;
}).ToListAsync();
x.Second = await _providers.CreateIfNotExists(x.Second);
x.SecondID = x.Second.ID;
_database.Entry(x.Second).State = EntityState.Detached;
});
}
/// <inheritdoc />

View File

@ -22,14 +22,6 @@ namespace Kyoo.Controllers
/// A lazy loaded library repository to validate queries (check if a library does exist)
/// </summary>
private readonly Lazy<ILibraryRepository> _libraries;
/// <summary>
/// A lazy loaded show repository to get a show from it's id.
/// </summary>
private readonly Lazy<IShowRepository> _shows;
/// <summary>
/// A lazy loaded collection repository to get a collection from it's id.
/// </summary>
private readonly Lazy<ICollectionRepository> _collections;
/// <inheritdoc />
protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title;
@ -38,60 +30,41 @@ namespace Kyoo.Controllers
/// <summary>
/// Create a new <see cref="LibraryItemRepository"/>.
/// </summary>
/// <param name="database">The databse instance</param>
/// <param name="database">The database instance</param>
/// <param name="libraries">A lazy loaded library repository</param>
/// <param name="shows">A lazy loaded show repository</param>
/// <param name="collections">A lazy loaded collection repository</param>
public LibraryItemRepository(DatabaseContext database,
Lazy<ILibraryRepository> libraries,
Lazy<IShowRepository> shows,
Lazy<ICollectionRepository> collections)
Lazy<ILibraryRepository> libraries)
: base(database)
{
_database = database;
_libraries = libraries;
_shows = shows;
_collections = collections;
}
/// <inheritdoc />
public override async Task<LibraryItem> GetOrDefault(int id)
public override Task<LibraryItem> GetOrDefault(int id)
{
return id > 0
? new LibraryItem(await _shows.Value.GetOrDefault(id))
: new LibraryItem(await _collections.Value.GetOrDefault(-id));
return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id);
}
/// <inheritdoc />
public override Task<LibraryItem> GetOrDefault(string slug)
{
throw new InvalidOperationException("You can't get a library item by a slug.");
return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug);
}
/// <summary>
/// Get a basic queryable with the right mapping from shows & collections.
/// Shows contained in a collection are excluded.
/// </summary>
private IQueryable<LibraryItem> ItemsQuery
=> _database.Shows
.Where(x => !x.Collections.Any())
.Select(LibraryItem.FromShow)
.Concat(_database.Collections
.Select(LibraryItem.FromCollection));
/// <inheritdoc />
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
{
return ApplyFilters(ItemsQuery, where, sort, limit);
return ApplyFilters(_database.LibraryItems, where, sort, limit);
}
/// <inheritdoc />
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
{
IQueryable<LibraryItem> query = ItemsQuery;
IQueryable<LibraryItem> query = _database.LibraryItems;
if (where != null)
query = query.Where(where);
return query.CountAsync();
@ -100,7 +73,7 @@ namespace Kyoo.Controllers
/// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query)
{
return await ItemsQuery
return await _database.LibraryItems
.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
@ -109,7 +82,6 @@ namespace Kyoo.Controllers
/// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc />

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
@ -23,64 +22,22 @@ namespace Kyoo.Controllers
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A show repository to get show's slug from their ID and keep the slug in each episode.
/// </summary>
private readonly IShowRepository _shows;
/// <summary>
/// A lazilly loaded episode repository to handle deletion of episodes with the season.
/// </summary>
private readonly Lazy<IEpisodeRepository> _episodes;
/// <inheritdoc/>
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
/// <summary>
/// Create a new <see cref="SeasonRepository"/> using the provided handle, a provider & a show repository and
/// a service provider to lazilly request an episode repository.
/// Create a new <see cref="SeasonRepository"/>.
/// </summary>
/// <param name="database">The database handle that will be used</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="episodes">A lazy loaded episode repository.</param>
public SeasonRepository(DatabaseContext database,
IProviderRepository providers,
IShowRepository shows,
Lazy<IEpisodeRepository> episodes)
IProviderRepository providers)
: base(database)
{
_database = database;
_providers = providers;
_shows = shows;
_episodes = episodes;
}
/// <inheritdoc/>
public override async Task<Season> Get(int id)
{
Season ret = await base.Get(id);
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
/// <inheritdoc/>
public override async Task<Season> Get(Expression<Func<Season, bool>> where)
{
Season ret = await base.Get(where);
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
/// <inheritdoc/>
public override Task<Season> Get(string slug)
{
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)");
if (!match.Success)
throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}");
return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value));
}
/// <inheritdoc/>
@ -89,7 +46,6 @@ namespace Kyoo.Controllers
Season ret = await GetOrDefault(showID, seasonNumber);
if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID);
return ret;
}
@ -99,7 +55,6 @@ namespace Kyoo.Controllers
Season ret = await GetOrDefault(showSlug, seasonNumber);
if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug;
return ret;
}
@ -120,25 +75,11 @@ namespace Kyoo.Controllers
/// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query)
{
List<Season> seasons = await _database.Seasons
return await _database.Seasons
.Where(_database.Like<Season>(x => x.Title, $"%{query}%"))
.OrderBy(DefaultSort)
.Take(20)
.ToListAsync();
foreach (Season season in seasons)
season.ShowSlug = await _shows.GetSlug(season.ShowID);
return seasons;
}
/// <inheritdoc/>
public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null,
Sort<Season> sort = default,
Pagination limit = default)
{
ICollection<Season> seasons = await base.GetAll(where, sort, limit);
foreach (Season season in seasons)
season.ShowSlug = await _shows.GetSlug(season.ShowID);
return seasons;
}
/// <inheritdoc/>
@ -160,9 +101,9 @@ namespace Kyoo.Controllers
await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id =>
{
id.Provider = await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached;
id.Second = await _providers.CreateIfNotExists(id.Second);
id.SecondID = id.Second.ID;
_database.Entry(id.Second).State = EntityState.Detached;
});
}
@ -183,12 +124,8 @@ namespace Kyoo.Controllers
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
_database.Remove(obj);
await _database.SaveChangesAsync();
if (obj.Episodes != null)
await _episodes.Value.DeleteRange(obj.Episodes);
}
}
}

View File

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

View File

@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
@ -16,7 +13,7 @@ namespace Kyoo.Controllers
public class TrackRepository : LocalRepository<Track>, ITrackRepository
{
/// <summary>
/// The databse handle
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
@ -27,63 +24,13 @@ namespace Kyoo.Controllers
/// <summary>
/// Create a new <see cref="TrackRepository"/>.
/// </summary>
/// <param name="database">The datatabse handle</param>
/// <param name="database">The database handle</param>
public TrackRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
Task<Track> IRepository<Track>.Get(string slug)
{
return Get(slug);
}
/// <inheritdoc />
public async Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
{
Track ret = await GetOrDefault(slug, type);
if (ret == null)
throw new ItemNotFoundException($"No track found with the slug {slug} and the type {type}.");
return ret;
}
/// <inheritdoc />
public Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
{
Match match = Regex.Match(slug,
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success)
{
if (int.TryParse(slug, out int id))
return GetOrDefault(id);
match = Regex.Match(slug, @"(?<show>.*)\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
if (!match.Success)
throw new ArgumentException("Invalid track slug. " +
"Format: {episodeSlug}.{language}[-forced][.{extension}]");
}
string showSlug = match.Groups["show"].Value;
int seasonNumber = match.Groups["season"].Success ? int.Parse(match.Groups["season"].Value) : -1;
int episodeNumber = match.Groups["episode"].Success ? int.Parse(match.Groups["episode"].Value) : -1;
string language = match.Groups["language"].Value;
bool forced = match.Groups["forced"].Success;
if (match.Groups["type"].Success)
type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
IQueryable<Track> query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug
&& x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber
&& x.Language == language
&& x.IsForced == forced);
if (type != StreamType.Unknown)
return query.FirstOrDefaultAsync(x => x.Type == type);
return query.FirstOrDefaultAsync();
}
/// <inheritdoc />
public override Task<ICollection<Track>> Search(string query)
{
@ -93,6 +40,9 @@ namespace Kyoo.Controllers
/// <inheritdoc />
public override async Task<Track> Create(Track obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (obj.EpisodeID <= 0)
{
obj.EpisodeID = obj.Episode?.ID ?? 0;
@ -102,14 +52,7 @@ namespace Kyoo.Controllers
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
await _database.SaveOrRetry(obj, (x, i) =>
{
if (i > 10)
throw new DuplicatedItemException($"More than 10 same tracks exists {x.Slug}. Aborting...");
x.TrackIndex++;
return x;
});
await _database.SaveChangesAsync();
return obj;
}

View File

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

View File

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

67
Kyoo/Models/Stream.cs Normal file
View File

@ -0,0 +1,67 @@
using System.Runtime.InteropServices;
using Kyoo.Models.Attributes;
namespace Kyoo.Models.Watch
{
/// <summary>
/// The unmanaged stream that the transcoder will return.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class Stream
{
/// <summary>
/// The title of the stream.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The language of this stream (as a ISO-639-2 language code)
/// </summary>
public string Language { get; set; }
/// <summary>
/// The codec of this stream.
/// </summary>
public string Codec { get; set; }
/// <summary>
/// Is this stream the default one of it's type?
/// </summary>
[MarshalAs(UnmanagedType.I1)] public bool IsDefault;
/// <summary>
/// Is this stream tagged as forced?
/// </summary>
[MarshalAs(UnmanagedType.I1)] public bool IsForced;
/// <summary>
/// The path of this track.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The type of this stream.
/// </summary>
[SerializeIgnore] public StreamType Type { get; set; }
/// <summary>
/// Create a track from this stream.
/// </summary>
/// <returns>A new track that represent this stream.</returns>
public Track ToTrack()
{
return new()
{
Title = Title,
Language = Language,
Codec = Codec,
IsDefault = IsDefault,
IsForced = IsForced,
Path = Path,
Type = Type,
IsExternal = false
};
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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