Feat rework images, delete providers (#191)

This commit is contained in:
Zoe Roux 2023-09-04 18:24:40 +02:00 committed by GitHub
commit 105aa7874f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
195 changed files with 12826 additions and 15130 deletions

View File

@ -33,10 +33,11 @@
<Rule Id="SA1513" Action="None"/> <!-- ClosingBraceMustBeFollowedByBlankLine --> <Rule Id="SA1513" Action="None"/> <!-- ClosingBraceMustBeFollowedByBlankLine -->
</Rules> </Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
<Rule Id="SA1600" Action="None" /> <!-- Elements Shuld be Documented -->
<Rule Id="SA1602" Action="None" /> <!-- Enums should be documented -->
<Rule Id="SA1642" Action="None" /> <!-- ConstructorSummaryDocumentationMustBeginWithStandardText --> <Rule Id="SA1642" Action="None" /> <!-- ConstructorSummaryDocumentationMustBeginWithStandardText -->
<Rule Id="SA1643" Action="None" /> <!-- DestructorSummaryDocumentationMustBeginWithStandardText --> <Rule Id="SA1643" Action="None" /> <!-- DestructorSummaryDocumentationMustBeginWithStandardText -->
<Rule Id="SA1623" Action="None" /> <!-- PropertySummaryDocumentationMustMatchAccessors --> <Rule Id="SA1623" Action="None" /> <!-- PropertySummaryDocumentationMustMatchAccessors -->
<Rule Id="SA1629" Action="None" /> <!-- DocumentationTextMustEndWithAPeriod --> <Rule Id="SA1629" Action="None" /> <!-- DocumentationTextMustEndWithAPeriod -->
<Rule Id="SA1600" Action="None" /> <!-- Elements Shuld be Documented -->
</Rules> </Rules>
</RuleSet> </RuleSet>

View File

@ -46,7 +46,7 @@
<PropertyGroup Condition="$(CheckCodingStyle) == true"> <PropertyGroup Condition="$(CheckCodingStyle) == true">
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
<NoWarn>1591</NoWarn> <NoWarn>1591;1305;8618</NoWarn>
<!-- <AnalysisMode>All</AnalysisMode> --> <!-- <AnalysisMode>All</AnalysisMode> -->
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -20,7 +20,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
@ -40,11 +39,6 @@ namespace Kyoo.Abstractions.Controllers
IRepository<T> GetRepository<T>() IRepository<T> GetRepository<T>()
where T : class, IResource; where T : class, IResource;
/// <summary>
/// The repository that handle libraries.
/// </summary>
ILibraryRepository LibraryRepository { get; }
/// <summary> /// <summary>
/// The repository that handle libraries items (a wrapper around shows and collections). /// The repository that handle libraries items (a wrapper around shows and collections).
/// </summary> /// </summary>
@ -55,6 +49,11 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
ICollectionRepository CollectionRepository { get; } ICollectionRepository CollectionRepository { get; }
/// <summary>
/// The repository that handle shows.
/// </summary>
IMovieRepository MovieRepository { get; }
/// <summary> /// <summary>
/// The repository that handle shows. /// The repository that handle shows.
/// </summary> /// </summary>
@ -80,16 +79,6 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
IStudioRepository StudioRepository { get; } IStudioRepository StudioRepository { get; }
/// <summary>
/// The repository that handle genres.
/// </summary>
IGenreRepository GenreRepository { get; }
/// <summary>
/// The repository that handle providers.
/// </summary>
IProviderRepository ProviderRepository { get; }
/// <summary> /// <summary>
/// The repository that handle users. /// The repository that handle users.
/// </summary> /// </summary>
@ -102,7 +91,6 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get<T>(int id) Task<T> Get<T>(int id)
where T : class, IResource; where T : class, IResource;
@ -113,7 +101,6 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get<T>(string slug) Task<T> Get<T>(string slug)
where T : class, IResource; where T : class, IResource;
@ -124,7 +111,6 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The first resource found that match the where function</returns> /// <returns>The first resource found that match the where function</returns>
[ItemNotNull]
Task<T> Get<T>(Expression<Func<T, bool>> where) Task<T> Get<T>(Expression<Func<T, bool>> where)
where T : class, IResource; where T : class, IResource;
@ -135,7 +121,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
[ItemNotNull]
Task<Season> Get(int showID, int seasonNumber); Task<Season> Get(int showID, int seasonNumber);
/// <summary> /// <summary>
@ -145,7 +130,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns> /// <returns>The season found</returns>
[ItemNotNull]
Task<Season> Get(string showSlug, int seasonNumber); Task<Season> Get(string showSlug, int seasonNumber);
/// <summary> /// <summary>
@ -156,7 +140,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
[ItemNotNull]
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber); Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
@ -167,7 +150,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
[ItemNotNull]
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
@ -176,8 +158,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="id">The id of the resource</param> /// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] Task<T?> GetOrDefault<T>(int id)
Task<T> GetOrDefault<T>(int id)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -186,8 +167,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] Task<T?> GetOrDefault<T>(string slug)
Task<T> GetOrDefault<T>(string slug)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -197,8 +177,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param> /// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <typeparam name="T">The type of the resource</typeparam> /// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The first resource found that match the where function</returns> /// <returns>The first resource found that match the where function</returns>
[ItemCanBeNull] Task<T?> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T>? sortBy = default)
Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -207,8 +186,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="showID">The id of the show</param> /// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns> /// <returns>The season found</returns>
[ItemCanBeNull] Task<Season?> GetOrDefault(int showID, int seasonNumber);
Task<Season> GetOrDefault(int showID, int seasonNumber);
/// <summary> /// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found. /// Get a season from it's show slug and it's seasonNumber or null if it is not found.
@ -216,8 +194,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="showSlug">The slug of the show</param> /// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns> /// <returns>The season found</returns>
[ItemCanBeNull] Task<Season?> GetOrDefault(string showSlug, int seasonNumber);
Task<Season> GetOrDefault(string showSlug, int seasonNumber);
/// <summary> /// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found. /// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
@ -226,8 +203,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
[ItemCanBeNull] Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found. /// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
@ -236,8 +212,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="seasonNumber">The season's number</param> /// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param> /// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns> /// <returns>The episode found</returns>
[ItemCanBeNull] Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
/// Load a related resource /// Load a related resource
@ -253,7 +228,7 @@ namespace Kyoo.Abstractions.Controllers
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/> /// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/> /// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false) Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource where T : class, IResource
where T2 : class, IResource; where T2 : class, IResource;
@ -271,7 +246,7 @@ namespace Kyoo.Abstractions.Controllers
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/> /// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/> /// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false) Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource where T : class, IResource
where T2 : class; where T2 : class;
@ -288,7 +263,7 @@ namespace Kyoo.Abstractions.Controllers
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/> /// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/> /// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/> /// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T>([NotNull] T obj, string memberName, bool force = false) Task<T> Load<T>(T obj, string memberName, bool force = false)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -303,35 +278,7 @@ namespace Kyoo.Abstractions.Controllers
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/> /// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/> /// <seealso cref="Load{T}(T, string, bool)"/>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Load([NotNull] IResource obj, string memberName, bool force = false); Task Load(IResource obj, string memberName, bool force = false);
/// <summary>
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default);
/// <summary>
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default);
/// <summary> /// <summary>
/// Get people's roles from a show. /// Get people's roles from a show.
@ -343,9 +290,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception> /// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a show. /// Get people's roles from a show.
@ -357,9 +304,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception> /// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a person. /// Get people's roles from a person.
@ -371,9 +318,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception> /// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id, Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a person. /// Get people's roles from a person.
@ -385,27 +332,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception> /// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug, Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary>
/// Setup relations between a show, a library and a collection
/// </summary>
/// <param name="showID">The show's ID to setup relations with</param>
/// <param name="libraryID">The library's ID to setup relations with (optional)</param>
/// <param name="collectionID">The collection's ID to setup relations with (optional)</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary>
/// Setup relations between a show, a library and a collection
/// </summary>
/// <param name="show">The show to setup relations with</param>
/// <param name="library">The library to setup relations with (optional)</param>
/// <param name="collection">The collection to setup relations with (optional)</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task AddShowLink([NotNull] Show show, Library library, Collection collection);
/// <summary> /// <summary>
/// Get all resources with filters /// Get all resources with filters
@ -415,9 +344,9 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="limit">How many items to return and where to start</param> /// <param name="limit">How many items to return and where to start</param>
/// <typeparam name="T">The type of resources to load</typeparam> /// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns> /// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null, Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>>? where = null,
Sort<T> sort = default, Sort<T>? sort = default,
Pagination limit = default) Pagination? limit = default)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -426,7 +355,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param> /// <param name="where">A filter function</param>
/// <typeparam name="T">The type of resources to load</typeparam> /// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns> /// <returns>A list of resources that match every filters</returns>
Task<int> GetCount<T>(Expression<Func<T, bool>> where = null) Task<int> GetCount<T>(Expression<Func<T, bool>>? where = null)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -444,7 +373,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item">The item to register</param> /// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam> /// <typeparam name="T">The type of resource</typeparam>
/// <returns>The resource registers and completed by database's information (related items and so on)</returns> /// <returns>The resource registers and completed by database's information (related items and so on)</returns>
Task<T> Create<T>([NotNull] T item) Task<T> Create<T>(T item)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
@ -453,18 +382,31 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item">The item to register</param> /// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam> /// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns> /// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists<T>([NotNull] T item) Task<T> CreateIfNotExists<T>(T item)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>
/// Edit a resource /// Edit a resource
/// </summary> /// </summary>
/// <param name="item">The resource to edit, it's ID can't change.</param> /// <param name="item">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam> /// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns> /// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Edit<T>(T item, bool resetOld) Task<T> Edit<T>(T item)
where T : class, IResource;
/// <summary>
/// Edit only specific properties of a resource
/// </summary>
/// <param name="id">The id of the resource to edit</param>
/// <param name="patch">
/// A method that will be called when you need to update every properties that you want to
/// persist. It can return false to abort the process via an ArgumentException
/// </param>
/// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
where T : class, IResource; where T : class, IResource;
/// <summary> /// <summary>

View File

@ -20,7 +20,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
@ -45,7 +44,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="id">The id of the resource</param> /// <param name="id">The id of the resource</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get(int id); Task<T> Get(int id);
/// <summary> /// <summary>
@ -54,7 +52,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get(string slug); Task<T> Get(string slug);
/// <summary> /// <summary>
@ -63,7 +60,6 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A predicate to filter the resource.</param> /// <param name="where">A predicate to filter the resource.</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get(Expression<Func<T, bool>> where); Task<T> Get(Expression<Func<T, bool>> where);
/// <summary> /// <summary>
@ -71,16 +67,14 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the resource</param> /// <param name="id">The id of the resource</param>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] Task<T?> GetOrDefault(int id);
Task<T> GetOrDefault(int id);
/// <summary> /// <summary>
/// Get a resource from it's slug or null if it is not found. /// Get a resource from it's slug or null if it is not found.
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] Task<T?> GetOrDefault(string slug);
Task<T> GetOrDefault(string slug);
/// <summary> /// <summary>
/// Get the first resource that match the predicate or null if it is not found. /// Get the first resource that match the predicate or null if it is not found.
@ -88,15 +82,13 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A predicate to filter the resource.</param> /// <param name="where">A predicate to filter the resource.</param>
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param> /// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
[ItemCanBeNull] Task<T?> GetOrDefault(Expression<Func<T, bool>> where, Sort<T>? sortBy = default);
Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default);
/// <summary> /// <summary>
/// Search for resources. /// Search for resources.
/// </summary> /// </summary>
/// <param name="query">The query string.</param> /// <param name="query">The query string.</param>
/// <returns>A list of resources found</returns> /// <returns>A list of resources found</returns>
[ItemNotNull]
Task<ICollection<T>> Search(string query); Task<ICollection<T>> Search(string query);
/// <summary> /// <summary>
@ -106,33 +98,30 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="sort">Sort information about the query (sort by, sort order)</param> /// <param name="sort">Sort information about the query (sort by, sort order)</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param> /// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <returns>A list of resources that match every filters</returns> /// <returns>A list of resources that match every filters</returns>
[ItemNotNull] Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null,
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, Sort<T>? sort = default,
Sort<T> sort = default, Pagination? limit = default);
Pagination limit = default);
/// <summary> /// <summary>
/// Get the number of resources that match the filter's predicate. /// Get the number of resources that match the filter's predicate.
/// </summary> /// </summary>
/// <param name="where">A filter predicate</param> /// <param name="where">A filter predicate</param>
/// <returns>How many resources matched that filter</returns> /// <returns>How many resources matched that filter</returns>
Task<int> GetCount(Expression<Func<T, bool>> where = null); Task<int> GetCount(Expression<Func<T, bool>>? where = null);
/// <summary> /// <summary>
/// Create a new resource. /// Create a new resource.
/// </summary> /// </summary>
/// <param name="obj">The item to register</param> /// <param name="obj">The item to register</param>
/// <returns>The resource registers and completed by database's information (related items and so on)</returns> /// <returns>The resource registers and completed by database's information (related items and so on)</returns>
[ItemNotNull] Task<T> Create(T obj);
Task<T> Create([NotNull] T obj);
/// <summary> /// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead. /// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary> /// </summary>
/// <param name="obj">The object to create</param> /// <param name="obj">The object to create</param>
/// <returns>The newly created item or the existing value if it existed.</returns> /// <returns>The newly created item or the existing value if it existed.</returns>
[ItemNotNull] Task<T> CreateIfNotExists(T obj);
Task<T> CreateIfNotExists([NotNull] T obj);
/// <summary> /// <summary>
/// Called when a resource has been created. /// Called when a resource has been created.
@ -140,14 +129,24 @@ namespace Kyoo.Abstractions.Controllers
event ResourceEventHandler OnCreated; event ResourceEventHandler OnCreated;
/// <summary> /// <summary>
/// Edit a resource /// Edit a resource and replace every property
/// </summary> /// </summary>
/// <param name="edited">The resource to edit, it's ID can't change.</param> /// <param name="edited">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns> /// <returns>The resource edited and completed by database's information (related items and so on)</returns>
[ItemNotNull] Task<T> Edit(T edited);
Task<T> Edit([NotNull] T edited, bool resetOld);
/// <summary>
/// Edit only specific properties of a resource
/// </summary>
/// <param name="id">The id of the resource to edit</param>
/// <param name="patch">
/// A method that will be called when you need to update every properties that you want to
/// persist. It can return false to abort the process via an ArgumentException
/// </param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Patch(int id, Func<T, Task<bool>> patch);
/// <summary> /// <summary>
/// Called when a resource has been edited. /// Called when a resource has been edited.
@ -176,14 +175,14 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="obj">The resource to delete</param> /// <param name="obj">The resource to delete</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete([NotNull] T obj); Task Delete(T obj);
/// <summary> /// <summary>
/// Delete all resources that match the predicate. /// Delete all resources that match the predicate.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteAll([NotNull] Expression<Func<T, bool>> where); Task DeleteAll(Expression<Func<T, bool>> where);
/// <summary> /// <summary>
/// Called when a resource has been edited. /// Called when a resource has been edited.
@ -202,21 +201,16 @@ namespace Kyoo.Abstractions.Controllers
Type RepositoryType { get; } Type RepositoryType { get; }
} }
/// <summary>
/// A repository to handle shows.
/// </summary>
public interface IMovieRepository : IRepository<Movie> { }
/// <summary> /// <summary>
/// A repository to handle shows. /// A repository to handle shows.
/// </summary> /// </summary>
public interface IShowRepository : IRepository<Show> public interface IShowRepository : IRepository<Show>
{ {
/// <summary>
/// Link a show to a collection and/or a library. The given show is now part of those containers.
/// If both a library and a collection are given, the collection is added to the library too.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="libraryID">The ID of the library (optional)</param>
/// <param name="collectionID">The ID of the collection (optional)</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary> /// <summary>
/// Get a show's slug from it's ID. /// Get a show's slug from it's ID.
/// </summary> /// </summary>
@ -330,55 +324,16 @@ namespace Kyoo.Abstractions.Controllers
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber); Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
} }
/// <summary>
/// A repository to handle libraries.
/// </summary>
public interface ILibraryRepository : IRepository<Library> { }
/// <summary> /// <summary>
/// A repository to handle library items (A wrapper around shows and collections). /// A repository to handle library items (A wrapper around shows and collections).
/// </summary> /// </summary>
public interface ILibraryItemRepository : IRepository<LibraryItem> public interface ILibraryItemRepository : IRepository<ILibraryItem> { }
{
/// <summary>
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default);
/// <summary>
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort information (sort order and sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <exception cref="ItemNotFoundException">No library exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default);
}
/// <summary> /// <summary>
/// A repository for collections /// A repository for collections
/// </summary> /// </summary>
public interface ICollectionRepository : IRepository<Collection> { } public interface ICollectionRepository : IRepository<Collection> { }
/// <summary>
/// A repository for genres.
/// </summary>
public interface IGenreRepository : IRepository<Genre> { }
/// <summary> /// <summary>
/// A repository for studios. /// A repository for studios.
/// </summary> /// </summary>
@ -399,9 +354,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception> /// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a show. /// Get people's roles from a show.
@ -413,9 +368,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception> /// <exception cref="ItemNotFoundException">No <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a person. /// Get people's roles from a person.
@ -427,9 +382,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception> /// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id, Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
/// <summary> /// <summary>
/// Get people's roles from a person. /// Get people's roles from a person.
@ -441,28 +396,9 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception> /// <exception cref="ItemNotFoundException">No <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns> /// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug, Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole>? sort = default,
Pagination limit = default); Pagination? limit = default);
}
/// <summary>
/// A repository to handle providers.
/// </summary>
public interface IProviderRepository : IRepository<Provider>
{
/// <summary>
/// Get a list of external ids that match all filters
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order and 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<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)
where T : class, IMetadata;
} }
/// <summary> /// <summary>

View File

@ -35,22 +35,20 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item"> /// <param name="item">
/// The item to cache images. /// The item to cache images.
/// </param> /// </param>
/// <param name="alwaysDownload">
/// <c>true</c> if images should be downloaded even if they already exists locally, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the item</typeparam> /// <typeparam name="T">The type of the item</typeparam>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false) Task DownloadImages<T>(T item)
where T : IThumbnails; where T : IThumbnails;
/// <summary> /// <summary>
/// Retrieve the local path of an image of the given item. /// Retrieve the local path of an image of the given item.
/// </summary> /// </summary>
/// <param name="item">The item to retrieve the poster from.</param> /// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param> /// <param name="image">The ID of the image.</param>
/// <param name="quality">The quality of the image</param>
/// <typeparam name="T">The type of the item</typeparam> /// <typeparam name="T">The type of the item</typeparam>
/// <returns>The path of the image for the given resource or null if it does not exists.</returns> /// <returns>The path of the image for the given resource or null if it does not exists.</returns>
string? GetImagePath<T>(T item, int imageId) string GetImagePath<T>(T item, string image, ImageQuality quality)
where T : IThumbnails; where T : IThumbnails;
} }
} }

View File

@ -83,6 +83,7 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">A dependency that this action will use.</typeparam> /// <typeparam name="T">A dependency that this action will use.</typeparam>
/// <returns>A new <see cref="StartupAction"/></returns> /// <returns>A new <see cref="StartupAction"/></returns>
public static StartupAction<T> New<T>(Action<T> action, int priority) public static StartupAction<T> New<T>(Action<T> action, int priority)
where T : notnull
=> new(action, priority); => new(action, priority);
/// <summary> /// <summary>
@ -94,6 +95,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T2">A second dependency that this action will use.</typeparam> /// <typeparam name="T2">A second dependency that this action will use.</typeparam>
/// <returns>A new <see cref="StartupAction"/></returns> /// <returns>A new <see cref="StartupAction"/></returns>
public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority) public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
where T : notnull
where T2 : notnull
=> new(action, priority); => new(action, priority);
/// <summary> /// <summary>
@ -106,6 +109,9 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T3">A third dependency that this action will use.</typeparam> /// <typeparam name="T3">A third dependency that this action will use.</typeparam>
/// <returns>A new <see cref="StartupAction"/></returns> /// <returns>A new <see cref="StartupAction"/></returns>
public static StartupAction<T, T2, T3> New<T, T2, T3>(Action<T, T2, T3> action, int priority) public static StartupAction<T, T2, T3> New<T, T2, T3>(Action<T, T2, T3> action, int priority)
where T : notnull
where T2 : notnull
where T3 : notnull
=> new(action, priority); => new(action, priority);
/// <summary> /// <summary>
@ -144,6 +150,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary> /// </summary>
/// <typeparam name="T">The dependency to use.</typeparam> /// <typeparam name="T">The dependency to use.</typeparam>
public class StartupAction<T> : IStartupAction public class StartupAction<T> : IStartupAction
where T : notnull
{ {
/// <summary> /// <summary>
/// The action to execute at startup. /// The action to execute at startup.
@ -177,6 +184,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The dependency to use.</typeparam> /// <typeparam name="T">The dependency to use.</typeparam>
/// <typeparam name="T2">The second dependency to use.</typeparam> /// <typeparam name="T2">The second dependency to use.</typeparam>
public class StartupAction<T, T2> : IStartupAction public class StartupAction<T, T2> : IStartupAction
where T : notnull
where T2 : notnull
{ {
/// <summary> /// <summary>
/// The action to execute at startup. /// The action to execute at startup.
@ -214,6 +223,9 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T2">The second dependency to use.</typeparam> /// <typeparam name="T2">The second dependency to use.</typeparam>
/// <typeparam name="T3">The third dependency to use.</typeparam> /// <typeparam name="T3">The third dependency to use.</typeparam>
public class StartupAction<T, T2, T3> : IStartupAction public class StartupAction<T, T2, T3> : IStartupAction
where T : notnull
where T2 : notnull
where T3 : notnull
{ {
/// <summary> /// <summary>
/// The action to execute at startup. /// The action to execute at startup.

View File

@ -3,6 +3,7 @@
<Title>Kyoo.Abstractions</Title> <Title>Kyoo.Abstractions</Title>
<Description>Base package to create plugins for Kyoo.</Description> <Description>Base package to create plugins for Kyoo.</Description>
<RootNamespace>Kyoo.Abstractions</RootNamespace> <RootNamespace>Kyoo.Abstractions</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -17,7 +17,6 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Attributes namespace Kyoo.Abstractions.Models.Attributes
{ {
@ -32,23 +31,21 @@ namespace Kyoo.Abstractions.Models.Attributes
/// <summary> /// <summary>
/// The public name of this api. /// The public name of this api.
/// </summary> /// </summary>
[NotNull] public string Name { get; } public string Name { get; }
/// <summary> /// <summary>
/// The name of the group in witch this API is. You can also specify a custom sort order using the following /// The name of the group in witch this API is. You can also specify a custom sort order using the following
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for /// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
/// th alphabetical ordering. /// th alphabetical ordering.
/// </summary> /// </summary>
public string Group { get; set; } public string? Group { get; set; }
/// <summary> /// <summary>
/// Create a new <see cref="ApiDefinitionAttribute"/>. /// Create a new <see cref="ApiDefinitionAttribute"/>.
/// </summary> /// </summary>
/// <param name="name">The name of the api that will be used on the documentation page.</param> /// <param name="name">The name of the api that will be used on the documentation page.</param>
public ApiDefinitionAttribute([NotNull] string name) public ApiDefinitionAttribute(string name)
{ {
if (name == null)
throw new ArgumentNullException(nameof(name));
Name = name; Name = name;
} }
} }

View File

@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Models.Attributes
/// <summary> /// <summary>
/// The name of the field containing the related resource's ID. /// The name of the field containing the related resource's ID.
/// </summary> /// </summary>
public string RelationID { get; } public string? RelationID { get; }
/// <summary> /// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>. /// Create a new <see cref="LoadableRelationAttribute"/>.

View File

@ -32,17 +32,17 @@ namespace Kyoo.Abstractions.Models.Permissions
/// <summary> /// <summary>
/// The needed permission type. /// The needed permission type.
/// </summary> /// </summary>
public string Type { get; } public string? Type { get; }
/// <summary> /// <summary>
/// The needed permission kind. /// The needed permission kind.
/// </summary> /// </summary>
public Kind Kind { get; } public Kind? Kind { get; }
/// <summary> /// <summary>
/// The group of this permission. /// The group of this permission.
/// </summary> /// </summary>
public Group Group { get; set; } public Group? Group { get; set; }
/// <summary> /// <summary>
/// Ask a permission to run an action. /// Ask a permission to run an action.

View File

@ -1,121 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Utils;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A class given information about a strongly typed configuration.
/// </summary>
public class ConfigurationReference
{
/// <summary>
/// The path of the resource (separated by ':')
/// </summary>
public string Path { get; }
/// <summary>
/// The type of the resource.
/// </summary>
public Type Type { get; }
/// <summary>
/// Create a new <see cref="ConfigurationReference"/> using a given path and type.
/// This method does not create sub configuration resources. Please see <see cref="CreateReference"/>
/// </summary>
/// <param name="path">The path of the resource (separated by ':' or "__")</param>
/// <param name="type">The type of the resource</param>
/// <seealso cref="CreateReference"/>
public ConfigurationReference(string path, Type type)
{
Path = path;
Type = type;
}
/// <summary>
/// Return the list of configuration reference a type has.
/// </summary>
/// <param name="path">
/// The base path of the type (separated by ':' or "__". If empty, it will start at root)
/// </param>
/// <param name="type">The type of the object</param>
/// <returns>The list of configuration reference a type has.</returns>
public static IEnumerable<ConfigurationReference> CreateReference(string path, [NotNull] Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
List<ConfigurationReference> ret = new()
{
new ConfigurationReference(path, type)
};
if (!type.IsClass || type.AssemblyQualifiedName?.StartsWith("System") == true)
return ret;
Type enumerable = Utility.GetGenericDefinition(type, typeof(IEnumerable<>));
Type dictionary = Utility.GetGenericDefinition(type, typeof(IDictionary<,>));
Type dictionaryKey = dictionary?.GetGenericArguments()[0];
if (dictionary != null && dictionaryKey == typeof(string))
ret.AddRange(CreateReference($"{path}:{type.Name}:*", dictionary.GetGenericArguments()[1]));
else if (dictionary != null && dictionaryKey == typeof(int))
ret.AddRange(CreateReference($"{path}:{type.Name}:", dictionary.GetGenericArguments()[1]));
else if (enumerable != null)
ret.AddRange(CreateReference($"{path}:{type.Name}:", enumerable.GetGenericArguments()[0]));
else
{
foreach (PropertyInfo child in type.GetProperties())
ret.AddRange(CreateReference($"{path}:{child.Name}", child.PropertyType));
}
return ret;
}
/// <summary>
/// Return the list of configuration reference a type has.
/// </summary>
/// <param name="path">
/// The base path of the type (separated by ':' or "__". If empty, it will start at root)
/// </param>
/// <typeparam name="T">The type of the object</typeparam>
/// <returns>The list of configuration reference a type has.</returns>
public static IEnumerable<ConfigurationReference> CreateReference<T>(string path)
{
return CreateReference(path, typeof(T));
}
/// <summary>
/// Return a <see cref="ConfigurationReference"/> meaning that the given path is of any type.
/// It means that the type can't be edited.
/// </summary>
/// <param name="path">
/// The path that will be untyped (separated by ':' or "__". If empty, it will start at root).
/// </param>
/// <returns>A configuration reference representing a path of any type.</returns>
public static ConfigurationReference CreateUntyped(string path)
{
return new ConfigurationReference(path, null);
}
}
}

View File

@ -30,13 +30,13 @@ namespace Kyoo.Abstractions.Models.Exceptions
/// <summary> /// <summary>
/// The existing object. /// The existing object.
/// </summary> /// </summary>
public object Existing { get; } public object? Existing { get; }
/// <summary> /// <summary>
/// Create a new <see cref="DuplicatedItemException"/> with the default message. /// Create a new <see cref="DuplicatedItemException"/> with the default message.
/// </summary> /// </summary>
/// <param name="existing">The existing object.</param> /// <param name="existing">The existing object.</param>
public DuplicatedItemException(object existing = null) public DuplicatedItemException(object? existing = null)
: base("Already exists in the database.") : base("Already exists in the database.")
{ {
Existing = existing; Existing = existing;

View File

@ -19,13 +19,27 @@
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
/// <summary> /// <summary>
/// An interface to represent resources that should have a link field in their return values (like videos). /// A genre that allow one to specify categories for shows.
/// </summary> /// </summary>
public interface ILink public enum Genre
{ {
/// <summary> Action,
/// The link to return, in most cases this should be a string. Adventure,
/// </summary> Animation,
public object Link { get; } Comedy,
Crime,
Documentary,
Drama,
Family,
Fantasy,
History,
Horror,
Music,
Mystery,
Romance,
ScienceFiction,
Thriller,
War,
Western,
} }
} }

View File

@ -18,15 +18,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions; using System.Text.Json.Serialization;
using Kyoo.Utils;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
/// <summary> /// <summary>
/// The type of item, ether a show, a movie or a collection. /// The type of item, ether a show, a movie or a collection.
/// </summary> /// </summary>
public enum ItemType public enum ItemKind
{ {
/// <summary> /// <summary>
/// The <see cref="LibraryItem"/> is a <see cref="Show"/>. /// The <see cref="LibraryItem"/> is a <see cref="Show"/>.
@ -34,8 +35,7 @@ namespace Kyoo.Abstractions.Models
Show, Show,
/// <summary> /// <summary>
/// The <see cref="LibraryItem"/> is a Movie (a <see cref="Show"/> with /// The <see cref="LibraryItem"/> is a Movie.
/// <see cref="Models.Show.IsMovie"/> equals to true).
/// </summary> /// </summary>
Movie, Movie,
@ -45,128 +45,135 @@ namespace Kyoo.Abstractions.Models
Collection Collection
} }
/// <summary> public class LibraryItem : IResource, ILibraryItem, IThumbnails, IMetadata
/// 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 : CustomTypeDescriptor, IResource, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
/// The title of the show or collection. /// The title of this show.
/// </summary> /// </summary>
public string Title { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The summary of the show or collection. /// A catchphrase for this movie.
/// </summary> /// </summary>
public string Overview { get; set; } public string? Tagline { get; set; }
/// <summary> /// <summary>
/// Is this show airing, not aired yet or finished? This is only applicable for shows. /// The list of alternative titles of this show.
/// </summary> /// </summary>
public Status? Status { get; set; } public string[] Aliases { get; set; } = Array.Empty<string>();
/// <summary> /// <summary>
/// The date this show or collection started airing. It can be null if this is unknown. /// The path of the movie video file.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// The summary of this show.
/// </summary>
public string? Overview { get; set; }
/// <summary>
/// A list of tags that match this movie.
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
public Genre[] Genres { get; set; } = Array.Empty<Genre>();
/// <summary>
/// Is this show airing, not aired yet or finished?
/// </summary>
public Status Status { get; set; }
/// <summary>
/// The date this show started airing. It can be null if this is unknown.
/// </summary> /// </summary>
public DateTime? StartAir { get; set; } public DateTime? StartAir { get; set; }
/// <summary> /// <summary>
/// The date this show or collection finished airing. /// 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. /// It can also be null if this is unknown.
/// </summary> /// </summary>
public DateTime? EndAir { get; set; } public DateTime? EndAir { get; set; }
/// <summary>
/// The date this movie aired.
/// </summary>
public DateTime? AirDate { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Image? Poster { get; set; }
/// <inheritdoc />
public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <summary> /// <summary>
/// The type of this item (ether a collection, a show or a movie). /// A video of a few minutes that tease the content.
/// </summary> /// </summary>
public ItemType Type { get; set; } public string? Trailer { get; set; }
/// <inheritdoc />
public ItemKind Kind { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary> /// <summary>
/// Create a new, empty <see cref="LibraryItem"/>. /// Links to watch this movie.
/// </summary> /// </summary>
public VideoLinks? Links => Kind == ItemKind.Movie ? new()
{
Direct = $"/video/movie/{Slug}/direct",
Hls = $"/video/movie/{Slug}/master.m3u8",
}
: null;
public LibraryItem() { } public LibraryItem() { }
/// <summary> [JsonConstructor]
/// Create a <see cref="LibraryItem"/> from a show. public LibraryItem(string name)
/// </summary>
/// <param name="show">The show that this library item should represent.</param>
public LibraryItem(Show show)
{ {
ID = show.ID; Slug = Utility.ToSlug(name);
Slug = show.Slug; Name = name;
Title = show.Title; }
Overview = show.Overview;
Status = show.Status;
StartAir = show.StartAir;
EndAir = show.EndAir;
Images = show.Images;
Type = show.IsMovie ? ItemType.Movie : ItemType.Show;
} }
/// <summary> /// <summary>
/// Create a <see cref="LibraryItem"/> from a collection /// A type union between <see cref="Show"/> and <see cref="Collection"/>.
/// This is used to list content put inside a library.
/// </summary> /// </summary>
/// <param name="collection">The collection that this library item should represent.</param> public interface ILibraryItem : IResource
public LibraryItem(Collection collection)
{ {
ID = -collection.ID; /// <summary>
Slug = collection.Slug; /// Is the item a collection, a movie or a show?
Title = collection.Name; /// </summary>
Overview = collection.Overview; public ItemKind Kind { get; }
Status = Models.Status.Unknown;
StartAir = null;
EndAir = null;
Images = collection.Images;
Type = ItemType.Collection;
}
/// <summary> /// <summary>
/// An expression to create a <see cref="LibraryItem"/> representing a show. /// The title of this show.
/// </summary> /// </summary>
public static Expression<Func<Show, LibraryItem>> FromShow => x => new LibraryItem public string Name { get; }
{
ID = x.ID,
Slug = x.Slug,
Title = x.Title,
Overview = x.Overview,
Status = x.Status,
StartAir = x.StartAir,
EndAir = x.EndAir,
Images = x.Images,
Type = x.IsMovie ? ItemType.Movie : ItemType.Show
};
/// <summary> /// <summary>
/// An expression to create a <see cref="LibraryItem"/> representing a collection. /// The summary of this show.
/// </summary> /// </summary>
public static Expression<Func<Collection, LibraryItem>> FromCollection => x => new LibraryItem public string? Overview { get; }
{
ID = -x.ID,
Slug = x.Slug,
Title = x.Name,
Overview = x.Overview,
Status = Models.Status.Unknown,
StartAir = null,
EndAir = null,
Images = x.Images,
Type = ItemType.Collection
};
/// <inheritdoc /> /// <summary>
public override string GetClassName() /// The date this movie aired.
{ /// </summary>
return Type.ToString(); public DateTime? AirDate { get; }
}
} }
} }

View File

@ -16,48 +16,21 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Linq.Expressions;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
/// <summary> /// <summary>
/// ID and link of an item on an external provider. /// ID and link of an item on an external provider.
/// </summary> /// </summary>
public class MetadataID public class MetadataId
{ {
/// <summary>
/// The expression to retrieve the unique ID of a MetadataID. This is an aggregate of the two resources IDs.
/// </summary>
public static Expression<Func<MetadataID, object>> PrimaryKey
{
get { return x => new { First = x.ResourceID, Second = x.ProviderID }; }
}
/// <summary>
/// The ID of the resource which possess the metadata.
/// </summary>
[SerializeIgnore] public int ResourceID { get; set; }
/// <summary>
/// The ID of the provider.
/// </summary>
[SerializeIgnore] public int ProviderID { get; set; }
/// <summary>
/// The provider that can do something with this ID.
/// </summary>
public Provider Provider { get; set; }
/// <summary> /// <summary>
/// The ID of the resource on the external provider. /// The ID of the resource on the external provider.
/// </summary> /// </summary>
public string DataID { get; set; } public string DataId { get; set; }
/// <summary> /// <summary>
/// The URL of the resource on the external provider. /// The URL of the resource on the external provider.
/// </summary> /// </summary>
public string Link { get; set; } public string? Link { get; set; }
} }
} }

View File

@ -93,14 +93,14 @@ namespace Kyoo.Abstractions.Models
if (items.Count > 0 && query.ContainsKey("afterID")) if (items.Count > 0 && query.ContainsKey("afterID"))
{ {
query["afterID"] = items.First().ID.ToString(); query["afterID"] = items.First().Id.ToString();
query["reverse"] = "true"; query["reverse"] = "true";
Previous = url + query.ToQueryString(); Previous = url + query.ToQueryString();
} }
query.Remove("reverse"); query.Remove("reverse");
if (items.Count == limit && limit > 0) if (items.Count == limit && limit > 0)
{ {
query["afterID"] = items.Last().ID.ToString(); query["afterID"] = items.Last().Id.ToString();
Next = url + query.ToQueryString(); Next = url + query.ToQueryString();
} }

View File

@ -16,33 +16,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis; namespace Kyoo.Models;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database public class PartialResource
{ {
namespace PostgreSQL public int? Id { get; set; }
{
[Collection(nameof(Postgresql))]
public class ProviderTests : AProviderTests
{
public ProviderTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AProviderTests : RepositoryTests<Provider> public string? Slug { get; set; }
{
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private readonly IProviderRepository _repository;
protected AProviderTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.ProviderRepository;
}
}
} }

View File

@ -29,10 +29,10 @@ namespace Kyoo.Abstractions.Models
public class PeopleRole : IResource public class PeopleRole : IResource
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string Slug => ForPeople ? Show.Slug : People.Slug; public string Slug => ForPeople ? Show!.Slug : People.Slug;
/// <summary> /// <summary>
/// Should this role be used as a Show substitute (the value is <c>true</c>) or /// Should this role be used as a Show substitute (the value is <c>true</c>) or
@ -53,12 +53,16 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The ID of the Show where the People playing in. /// The ID of the Show where the People playing in.
/// </summary> /// </summary>
public int ShowID { get; set; } public int? ShowID { get; set; }
/// <summary> /// <summary>
/// The show where the People played in. /// The show where the People played in.
/// </summary> /// </summary>
public Show Show { get; set; } public Show? Show { get; set; }
public int? MovieID { get; set; }
public Movie? Movie { get; set; }
/// <summary> /// <summary>
/// The type of work the person has done for the show. /// The type of work the person has done for the show.

View File

@ -17,46 +17,63 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
/// <summary> /// <summary>
/// A class representing collections of <see cref="Show"/>. /// A class representing collections of <see cref="Show"/>.
/// A collection can also be stored in a <see cref="Library"/>.
/// </summary> /// </summary>
public class Collection : IResource, IMetadata, IThumbnails public class Collection : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string Slug { get; set; } [MaxLength(256)] public string Slug { get; set; }
/// <summary> /// <summary>
/// The name of this collection. /// The name of this collection.
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The description of this collection. /// The description of this collection.
/// </summary> /// </summary>
public string Overview { get; set; } public string? Overview { get; set; }
/// <inheritdoc />
public Image? Poster { get; set; }
/// <inheritdoc />
public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <summary>
/// The list of movies contained in this collection.
/// </summary>
[LoadableRelation] public ICollection<Movie>? Movies { get; set; }
/// <summary> /// <summary>
/// The list of shows contained in this collection. /// The list of shows contained in this collection.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; } [LoadableRelation] public ICollection<Show>? Shows { get; set; }
/// <summary>
/// The list of libraries that contains this collection.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
public Collection() { }
[JsonConstructor]
public Collection(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
} }
} }

View File

@ -18,6 +18,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -31,28 +32,23 @@ namespace Kyoo.Abstractions.Models
public class Episode : IResource, IMetadata, IThumbnails public class Episode : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[Computed] [Computed]
[MaxLength(256)]
public string Slug public string Slug
{ {
get get
{ {
if (ShowSlug != null || Show?.Slug != null) if (ShowSlug != null || Show?.Slug != null)
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
return ShowID != 0 return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
: null;
} }
[UsedImplicitly] [UsedImplicitly]
[NotNull]
private set private set
{ {
if (value == null)
throw new ArgumentNullException(nameof(value));
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)"); Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
if (match.Success) if (match.Success)
@ -80,22 +76,22 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
/// </summary> /// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; } [SerializeIgnore] public string? ShowSlug { private get; set; }
/// <summary> /// <summary>
/// The ID of the Show containing this episode. /// The ID of the Show containing this episode.
/// </summary> /// </summary>
[SerializeIgnore] public int ShowID { get; set; } [SerializeIgnore] public int ShowId { get; set; }
/// <summary> /// <summary>
/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; } [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
/// <summary> /// <summary>
/// The ID of the Season containing this episode. /// The ID of the Season containing this episode.
/// </summary> /// </summary>
[SerializeIgnore] public int? SeasonID { get; set; } [SerializeIgnore] public int? SeasonId { get; set; }
/// <summary> /// <summary>
/// The season that contains this episode. /// The season that contains this episode.
@ -105,7 +101,7 @@ namespace Kyoo.Abstractions.Models
/// This can be null if the season is unknown and the episode is only identified /// This can be null if the season is unknown and the episode is only identified
/// by it's <see cref="AbsoluteNumber"/>. /// by it's <see cref="AbsoluteNumber"/>.
/// </remarks> /// </remarks>
[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } [LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; }
/// <summary> /// <summary>
/// The season in witch this episode is in. /// The season in witch this episode is in.
@ -127,18 +123,15 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public string Path { get; set; } public string Path { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The title of this episode. /// The title of this episode.
/// </summary> /// </summary>
public string Title { get; set; } public string? Name { get; set; }
/// <summary> /// <summary>
/// The overview of this episode. /// The overview of this episode.
/// </summary> /// </summary>
public string Overview { get; set; } public string? Overview { get; set; }
/// <summary> /// <summary>
/// The release date of this episode. It can be null if unknown. /// The release date of this episode. It can be null if unknown.
@ -146,7 +139,35 @@ namespace Kyoo.Abstractions.Models
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Image? Poster { get; set; }
/// <inheritdoc />
public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary>
/// The previous episode that should be seen before viewing this one.
/// </summary>
[LoadableRelation] public Episode? PreviousEpisode { get; set; }
/// <summary>
/// The next episode to watch after this one.
/// </summary>
[LoadableRelation] public Episode? NextEpisode { get; set; }
/// <summary>
/// Links to watch this episode.
/// </summary>
public VideoLinks Links => new()
{
Direct = $"/video/episode/{Slug}/direct",
Hls = $"/video/episode/{Slug}/master.m3u8",
};
/// <summary> /// <summary>
/// Get the slug of an episode. /// Get the slug of an episode.
@ -165,14 +186,11 @@ namespace Kyoo.Abstractions.Models
/// If you don't know it or this is a movie, use null /// If you don't know it or this is a movie, use null
/// </param> /// </param>
/// <returns>The slug corresponding to the given arguments</returns> /// <returns>The slug corresponding to the given arguments</returns>
/// <exception cref="ArgumentNullException">The given show slug was null.</exception> public static string GetSlug(string showSlug,
public static string GetSlug([NotNull] string showSlug,
int? seasonNumber, int? seasonNumber,
int? episodeNumber, int? episodeNumber,
int? absoluteNumber = null) int? absoluteNumber = null)
{ {
if (showSlug == null)
throw new ArgumentNullException(nameof(showSlug));
return seasonNumber switch return seasonNumber switch
{ {
null when absoluteNumber == null => showSlug, null when absoluteNumber == null => showSlug,

View File

@ -1,62 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
namespace Kyoo.Abstractions.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; }
/// <summary>
/// The list of shows that have this genre.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <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;
}
}
}

View File

@ -16,11 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -30,69 +26,8 @@ namespace Kyoo.Abstractions.Models
public interface IMetadata public interface IMetadata
{ {
/// <summary> /// <summary>
/// The link to metadata providers that this show has. See <see cref="MetadataID"/> for more information. /// The link to metadata providers that this show has. See <see cref="MetadataId"/> for more information.
/// </summary> /// </summary>
[EditableRelation] public Dictionary<string, MetadataId> ExternalId { get; set; }
[LoadableRelation]
public ICollection<MetadataID> ExternalIDs { get; set; }
}
/// <summary>
/// A static class containing extensions method for every <see cref="IMetadata"/> class.
/// This allow one to use metadata more easily.
/// </summary>
public static class MetadataExtension
{
/// <summary>
/// Retrieve the internal provider's ID of an item using it's provider slug.
/// </summary>
/// <remarks>
/// This method will never return anything if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
/// </remarks>
/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
/// <param name="provider">The slug of the provider</param>
/// <returns>The <see cref="MetadataID.DataID"/> field of the asked provider.</returns>
[CanBeNull]
public static string GetID(this IMetadata self, string provider)
{
return self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID;
}
/// <summary>
/// Retrieve the internal provider's ID of an item using it's provider slug.
/// If the ID could be found, it is converted to the <typeparamref name="T"/> type and <c>true</c> is returned.
/// </summary>
/// <remarks>
/// This method will never succeed if the <see cref="IMetadata.ExternalIDs"/> are not loaded.
/// </remarks>
/// <param name="self">An instance of <see cref="IMetadata"/> to retrieve the ID from.</param>
/// <param name="provider">The slug of the provider</param>
/// <param name="id">
/// The <see cref="MetadataID.DataID"/> field of the asked provider parsed
/// and converted to the <typeparamref name="T"/> type.
/// It is only relevant if this method returns <c>true</c>.
/// </param>
/// <typeparam name="T">The type to convert the <see cref="MetadataID.DataID"/> to.</typeparam>
/// <returns><c>true</c> if this method succeeded, <c>false</c> otherwise.</returns>
public static bool TryGetID<T>(this IMetadata self, string provider, out T id)
{
string dataID = self.ExternalIDs?.FirstOrDefault(x => x.Provider.Slug == provider)?.DataID;
if (dataID == null)
{
id = default;
return false;
}
try
{
id = (T)Convert.ChangeType(dataID, typeof(T));
}
catch
{
id = default;
return false;
}
return true;
}
} }
} }

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
@ -32,7 +33,7 @@ namespace Kyoo.Abstractions.Models
/// You don't need to specify an ID manually when creating a new resource, /// 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}"/>. /// this field is automatically assigned by the <see cref="IRepository{T}"/>.
/// </remarks> /// </remarks>
public int ID { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// A human-readable identifier that can be used instead of an ID. /// A human-readable identifier that can be used instead of an ID.
@ -42,6 +43,7 @@ namespace Kyoo.Abstractions.Models
/// There is no setter for a slug since it can be computed from other fields. /// There is no setter for a slug since it can be computed from other fields.
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}. /// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
/// </remarks> /// </remarks>
[MaxLength(256)]
public string Slug { get; } public string Slug { get; }
} }
} }

View File

@ -16,8 +16,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic; using System;
using Kyoo.Abstractions.Controllers; using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -27,51 +30,104 @@ namespace Kyoo.Abstractions.Models
public interface IThumbnails public interface IThumbnails
{ {
/// <summary> /// <summary>
/// The list of images mapped to a certain index. /// A poster is a 2/3 format image with the cover of the resource.
/// </summary> /// </summary>
/// <remarks> public Image? Poster { get; set; }
/// An arbitrary index should not be used, instead use indexes from <see cref="Models.Images"/>
/// </remarks>
/// <example>{"0": "example.com/dune/poster"}</example>
public Dictionary<int, string> Images { get; set; }
}
/// <summary>
/// A class containing constant values for images. To be used as index of a <see cref="IThumbnails.Images"/>.
/// </summary>
public static class Images
{
/// <summary>
/// A poster is a 9/16 format image with the cover of the resource.
/// </summary>
public const int Poster = 0;
/// <summary> /// <summary>
/// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually /// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually
/// is not an official image. /// is not an official image.
/// </summary> /// </summary>
public const int Thumbnail = 1; public Image? Thumbnail { get; set; }
/// <summary> /// <summary>
/// A logo is a small image representing the resource. /// A logo is a small image representing the resource.
/// </summary> /// </summary>
public const int Logo = 2; public Image? Logo { get; set; }
}
/// <summary> [TypeConverter(typeof(ImageConvertor))]
/// A video of a few minutes that tease the content. public class Image
/// </summary>
public const int Trailer = 3;
/// <summary>
/// Retrieve the name of an image using it's ID. It is also used by the serializer to retrieve all named images.
/// If a plugin adds a new image type, it should add it's value and name here to allow the serializer to add it.
/// </summary>
public static Dictionary<int, string> ImageName { get; } = new()
{ {
[Poster] = nameof(Poster), /// <summary>
[Thumbnail] = nameof(Thumbnail), /// The original image from another server.
[Logo] = nameof(Logo), /// </summary>
[Trailer] = nameof(Trailer) public string Source { get; set; }
};
/// <summary>
/// A hash to display as placeholder while the image is loading.
/// </summary>
[MaxLength(32)]
public string Blurhash { get; set; }
[SerializeIgnore]
public string Path { private get; set; }
/// <summary>
/// The url to retrieve the low quality image.
/// </summary>
public string Low => $"{Path}?quality=low";
/// <summary>
/// The url to retrieve the medium quality image.
/// </summary>
public string Medium => $"{Path}?quality=medium";
/// <summary>
/// The url to retrieve the high quality image.
/// </summary>
public string High => $"{Path}?quality=high";
public Image(string source, string? blurhash = null)
{
Source = source;
Blurhash = blurhash ?? "00000000000000";
}
public class ImageConvertor : TypeConverter
{
/// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
/// <inheritdoc />
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is not string source)
return base.ConvertFrom(context, culture, value)!;
return new Image(source);
}
/// <inheritdoc />
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return false;
}
}
}
/// <summary>
/// The quality of an image
/// </summary>
public enum ImageQuality
{
/// <summary>
/// Small
/// </summary>
Low,
/// <summary>
/// Medium
/// </summary>
Medium,
/// <summary>
/// Large
/// </summary>
High,
} }
} }

View File

@ -1,60 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.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; }
/// <summary>
/// The list of <see cref="Provider"/> used for items in this library.
/// </summary>
[EditableRelation][LoadableRelation] public ICollection<Provider> Providers { 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; }
}
}

View File

@ -0,0 +1,152 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A series or a movie.
/// </summary>
public class Movie : IResource, IMetadata, IOnMerge, IThumbnails
{
/// <inheritdoc />
public int Id { get; set; }
/// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; }
/// <summary>
/// The title of this show.
/// </summary>
public string Name { get; set; }
/// <summary>
/// A catchphrase for this movie.
/// </summary>
public string? Tagline { get; set; }
/// <summary>
/// The list of alternative titles of this show.
/// </summary>
public string[] Aliases { get; set; } = Array.Empty<string>();
/// <summary>
/// The path of the movie video file.
/// </summary>
public string Path { get; set; }
/// <summary>
/// The summary of this show.
/// </summary>
public string? Overview { get; set; }
/// <summary>
/// A list of tags that match this movie.
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
public Genre[] Genres { get; set; } = Array.Empty<Genre>();
/// <summary>
/// Is this show airing, not aired yet or finished?
/// </summary>
public Status Status { get; set; }
/// <summary>
/// The date this movie aired.
/// </summary>
public DateTime? AirDate { get; set; }
/// <inheritdoc />
public Image? Poster { get; set; }
/// <inheritdoc />
public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <summary>
/// A video of a few minutes that tease the content.
/// </summary>
public string? Trailer { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary>
/// The ID of the Studio that made this show.
/// </summary>
[SerializeIgnore] public int? StudioID { 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 people that made this show.
/// </summary>
[LoadableRelation][EditableRelation] public ICollection<PeopleRole>? People { get; set; }
/// <summary>
/// The list of collections that contains this show.
/// </summary>
[LoadableRelation] public ICollection<Collection>? Collections { get; set; }
/// <summary>
/// Links to watch this movie.
/// </summary>
public VideoLinks Links => new()
{
Direct = $"/video/movie/{Slug}/direct",
Hls = $"/video/movie/{Slug}/master.m3u8",
};
/// <inheritdoc />
public void OnMerge(object merged)
{
if (People != null)
{
foreach (PeopleRole link in People)
link.Movie = this;
}
}
public Movie() { }
[JsonConstructor]
public Movie(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
}
}

View File

@ -17,7 +17,10 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -27,9 +30,10 @@ namespace Kyoo.Abstractions.Models
public class People : IResource, IMetadata, IThumbnails public class People : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
@ -38,14 +42,29 @@ namespace Kyoo.Abstractions.Models
public string Name { get; set; } public string Name { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Image? Poster { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary> /// <summary>
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information. /// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
/// </summary> /// </summary>
[EditableRelation][LoadableRelation] public ICollection<PeopleRole> Roles { get; set; } [EditableRelation][LoadableRelation] public ICollection<PeopleRole>? Roles { get; set; }
public People() { }
[JsonConstructor]
public People(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
} }
} }

View File

@ -1,71 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
namespace Kyoo.Abstractions.Models
{
/// <summary>
/// A dead class that will be removed later.
/// </summary>
// TODO: Delete this class
public class Provider : IResource, IThumbnails
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this provider.
/// </summary>
public string Name { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// The list of libraries that uses this provider.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
/// <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;
Images = new Dictionary<int, string>
{
[Models.Images.Logo] = logo
};
}
}
}

View File

@ -18,6 +18,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -31,16 +32,17 @@ namespace Kyoo.Abstractions.Models
public class Season : IResource, IMetadata, IThumbnails public class Season : IResource, IMetadata, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[Computed] [Computed]
[MaxLength(256)]
public string Slug public string Slug
{ {
get get
{ {
if (ShowSlug == null && Show == null) if (ShowSlug == null && Show == null)
return $"{ShowID}-s{SeasonNumber}"; return $"{ShowId}-s{SeasonNumber}";
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}"; return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
} }
@ -48,7 +50,7 @@ namespace Kyoo.Abstractions.Models
[NotNull] [NotNull]
private set private set
{ {
Match match = Regex.Match(value ?? string.Empty, @"(?<show>.+)-s(?<season>\d+)"); Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
if (!match.Success) if (!match.Success)
throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}");
@ -60,18 +62,18 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
/// </summary> /// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; } [SerializeIgnore] public string? ShowSlug { private get; set; }
/// <summary> /// <summary>
/// The ID of the Show containing this season. /// The ID of the Show containing this season.
/// </summary> /// </summary>
[SerializeIgnore] public int ShowID { get; set; } [SerializeIgnore] public int ShowId { get; set; }
/// <summary> /// <summary>
/// The show that contains this season. /// The show that contains this season.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; } [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
/// <summary> /// <summary>
/// The number of this season. This can be set to 0 to indicate specials. /// The number of this season. This can be set to 0 to indicate specials.
@ -81,12 +83,12 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The title of this season. /// The title of this season.
/// </summary> /// </summary>
public string Title { get; set; } public string? Name { get; set; }
/// <summary> /// <summary>
/// A quick overview of this season. /// A quick overview of this season.
/// </summary> /// </summary>
public string Overview { get; set; } public string? Overview { get; set; }
/// <summary> /// <summary>
/// The starting air date of this season. /// The starting air date of this season.
@ -99,14 +101,20 @@ namespace Kyoo.Abstractions.Models
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Image? Poster { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary> /// <summary>
/// The list of episodes that this season contains. /// The list of episodes that this season contains.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; } [LoadableRelation] public ICollection<Episode>? Episodes { get; set; }
} }
} }

View File

@ -18,8 +18,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -29,43 +32,47 @@ namespace Kyoo.Abstractions.Models
public class Show : IResource, IMetadata, IOnMerge, IThumbnails public class Show : IResource, IMetadata, IOnMerge, IThumbnails
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
/// The title of this show. /// The title of this show.
/// </summary> /// </summary>
public string Title { get; set; } public string Name { get; set; }
/// <summary>
/// A catchphrase for this show.
/// </summary>
public string? Tagline { get; set; }
/// <summary> /// <summary>
/// The list of alternative titles of this show. /// The list of alternative titles of this show.
/// </summary> /// </summary>
[EditableRelation] public string[] Aliases { get; set; } public List<string> Aliases { get; set; } = new();
/// <summary>
/// The path of the root directory of this show.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary> /// <summary>
/// The summary of this show. /// The summary of this show.
/// </summary> /// </summary>
public string Overview { get; set; } public string? Overview { get; set; }
/// <summary>
/// A list of tags that match this movie.
/// </summary>
public List<string> Tags { get; set; } = new();
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
public List<Genre> Genres { get; set; } = new();
/// <summary> /// <summary>
/// Is this show airing, not aired yet or finished? /// Is this show airing, not aired yet or finished?
/// </summary> /// </summary>
public Status Status { get; set; } public Status Status { get; set; }
/// <summary>
/// An URL to a trailer.
/// </summary>
/// TODO for now, this is set to a youtube url. It should be cached and converted to a local file.
[Obsolete("Use Images instead of this, this is only kept for the API response.")]
public string TrailerUrl => Images?.GetValueOrDefault(Models.Images.Trailer);
/// <summary> /// <summary>
/// The date this show started airing. It can be null if this is unknown. /// The date this show started airing. It can be null if this is unknown.
/// </summary> /// </summary>
@ -73,64 +80,61 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The date this show finished airing. /// 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. /// It can also be null if this is unknown.
/// </summary> /// </summary>
public DateTime? EndAir { get; set; } public DateTime? EndAir { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<int, string> Images { get; set; } public Image? Poster { get; set; }
/// <summary>
/// True if this show represent a movie, false otherwise.
/// </summary>
public bool IsMovie { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Image? Thumbnail { get; set; }
/// <inheritdoc />
public Image? Logo { get; set; }
/// <summary>
/// A video of a few minutes that tease the content.
/// </summary>
public string? Trailer { get; set; }
[SerializeIgnore] public DateTime? AirDate => StartAir;
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary> /// <summary>
/// The ID of the Studio that made this show. /// The ID of the Studio that made this show.
/// </summary> /// </summary>
[SerializeIgnore] public int? StudioID { get; set; } [SerializeIgnore] public int? StudioId { get; set; }
/// <summary> /// <summary>
/// The Studio that made this show. /// The Studio that made this show.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>. /// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary> /// </summary>
[LoadableRelation(nameof(StudioID))][EditableRelation] public Studio Studio { get; set; } [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> /// <summary>
/// The list of people that made this show. /// The list of people that made this show.
/// </summary> /// </summary>
[LoadableRelation][EditableRelation] public ICollection<PeopleRole> People { get; set; } [LoadableRelation][EditableRelation] public ICollection<PeopleRole>? People { get; set; }
/// <summary> /// <summary>
/// The different seasons in this show. If this is a movie, this list is always null or empty. /// The different seasons in this show. If this is a movie, this list is always null or empty.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Season> Seasons { get; set; } [LoadableRelation] public ICollection<Season>? Seasons { get; set; }
/// <summary> /// <summary>
/// The list of episodes in this show. /// 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). /// 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. /// Having an episode is necessary to store metadata and tracks.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; } [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> /// <summary>
/// The list of collections that contains this show. /// The list of collections that contains this show.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; } [LoadableRelation] public ICollection<Collection>? Collections { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public void OnMerge(object merged) public void OnMerge(object merged)
@ -153,6 +157,15 @@ namespace Kyoo.Abstractions.Models
episode.Show = this; episode.Show = this;
} }
} }
public Show() { }
[JsonConstructor]
public Show(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
} }
/// <summary> /// <summary>

View File

@ -17,8 +17,10 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils; using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
@ -28,9 +30,10 @@ namespace Kyoo.Abstractions.Models
public class Studio : IResource, IMetadata public class Studio : IResource, IMetadata
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
@ -41,10 +44,15 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The list of shows that are made by this studio. /// The list of shows that are made by this studio.
/// </summary> /// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; } [LoadableRelation] public ICollection<Show>? Shows { get; set; }
/// <summary>
/// The list of movies that are made by this studio.
/// </summary>
[LoadableRelation] public ICollection<Movie>? Movies { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[EditableRelation][LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; } public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <summary> /// <summary>
/// Create a new, empty, <see cref="Studio"/>. /// Create a new, empty, <see cref="Studio"/>.
@ -55,6 +63,7 @@ namespace Kyoo.Abstractions.Models
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically. /// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
/// </summary> /// </summary>
/// <param name="name">The name of the studio.</param> /// <param name="name">The name of the studio.</param>
[JsonConstructor]
public Studio(string name) public Studio(string name)
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);

View File

@ -16,20 +16,25 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models namespace Kyoo.Abstractions.Models
{ {
/// <summary> /// <summary>
/// A single user of the app. /// A single user of the app.
/// </summary> /// </summary>
public class User : IResource, IThumbnails public class User : IResource
{ {
/// <inheritdoc /> /// <inheritdoc />
public int ID { get; set; } public int Id { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[MaxLength(256)]
public string Slug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
@ -51,27 +56,32 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The list of permissions of the user. The format of this is implementation dependent. /// The list of permissions of the user. The format of this is implementation dependent.
/// </summary> /// </summary>
public string[] Permissions { get; set; } public string[] Permissions { get; set; } = Array.Empty<string>();
/// <summary> /// <summary>
/// Arbitrary extra data that can be used by specific authentication implementations. /// A logo is a small image representing the resource.
/// </summary> /// </summary>
[SerializeIgnore] public Image? Logo { get; set; }
public Dictionary<string, string> ExtraData { get; set; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary> /// <summary>
/// The list of shows the user has finished. /// The list of shows the user has finished.
/// </summary> /// </summary>
[SerializeIgnore] [SerializeIgnore]
public ICollection<Show> Watched { get; set; } public ICollection<Show>? Watched { get; set; }
/// <summary> /// <summary>
/// The list of episodes the user is watching (stopped in progress or the next episode of the show) /// The list of episodes the user is watching (stopped in progress or the next episode of the show)
/// </summary> /// </summary>
[SerializeIgnore] [SerializeIgnore]
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; } public ICollection<WatchedEpisode>? CurrentlyWatching { get; set; }
public User() { }
[JsonConstructor]
public User(string username)
{
Slug = Utility.ToSlug(username);
Username = username;
}
} }
} }

View File

@ -36,7 +36,7 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The <see cref="Episode"/> started. /// The <see cref="Episode"/> started.
/// </summary> /// </summary>
public Episode Episode { get; set; } public Episode? Episode { get; set; }
/// <summary> /// <summary>
/// Where the player has stopped watching the episode (between 0 and 100). /// Where the player has stopped watching the episode (between 0 and 100).

View File

@ -35,6 +35,16 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public ICollection<Collection> Collections { get; init; } public ICollection<Collection> Collections { get; init; }
/// <summary>
/// The items that matched the search.
/// </summary>
public ICollection<ILibraryItem> Items { get; init; }
/// <summary>
/// The movies that matched the search.
/// </summary>
public ICollection<Movie> Movies { get; init; }
/// <summary> /// <summary>
/// The shows that matched the search. /// The shows that matched the search.
/// </summary> /// </summary>
@ -50,11 +60,6 @@ namespace Kyoo.Abstractions.Models
/// </summary> /// </summary>
public ICollection<People> People { get; init; } public ICollection<People> People { get; init; }
/// <summary>
/// The genres that matched the search.
/// </summary>
public ICollection<Genre> Genres { get; init; }
/// <summary> /// <summary>
/// The studios that matched the search. /// The studios that matched the search.
/// </summary> /// </summary>

View File

@ -23,7 +23,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Utils namespace Kyoo.Abstractions.Models.Utils
{ {
@ -43,7 +42,7 @@ namespace Kyoo.Abstractions.Models.Utils
/// <summary> /// <summary>
/// The slug of the resource or null if the id is specified. /// The slug of the resource or null if the id is specified.
/// </summary> /// </summary>
private readonly string _slug; private readonly string? _slug;
/// <summary> /// <summary>
/// Create a new <see cref="Identifier"/> for the given id. /// Create a new <see cref="Identifier"/> for the given id.
@ -58,10 +57,8 @@ namespace Kyoo.Abstractions.Models.Utils
/// Create a new <see cref="Identifier"/> for the given slug. /// Create a new <see cref="Identifier"/> for the given slug.
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource.</param> /// <param name="slug">The slug of the resource.</param>
public Identifier([NotNull] string slug) public Identifier(string slug)
{ {
if (slug == null)
throw new ArgumentNullException(nameof(slug));
_slug = slug; _slug = slug;
} }
@ -87,7 +84,7 @@ namespace Kyoo.Abstractions.Models.Utils
{ {
return _id.HasValue return _id.HasValue
? idFunc(_id.Value) ? idFunc(_id.Value)
: slugFunc(_slug); : slugFunc(_slug!);
} }
/// <summary> /// <summary>
@ -139,7 +136,7 @@ namespace Kyoo.Abstractions.Models.Utils
public bool IsSame(IResource resource) public bool IsSame(IResource resource)
{ {
return Match( return Match(
id => resource.ID == id, id => resource.Id == id,
slug => resource.Slug == slug slug => resource.Slug == slug
); );
} }
@ -155,7 +152,7 @@ namespace Kyoo.Abstractions.Models.Utils
where T : IResource where T : IResource
{ {
return _id.HasValue return _id.HasValue
? x => x.ID == _id.Value ? x => x.Id == _id.Value
: x => x.Slug == _slug; : x => x.Slug == _slug;
} }
@ -174,7 +171,7 @@ namespace Kyoo.Abstractions.Models.Utils
.Where(x => x.Name == nameof(Enumerable.Any)) .Where(x => x.Name == nameof(Enumerable.Any))
.FirstOrDefault(x => x.GetParameters().Length == 2)! .FirstOrDefault(x => x.GetParameters().Length == 2)!
.MakeGenericMethod(typeof(T2)); .MakeGenericMethod(typeof(T2));
MethodCallExpression call = Expression.Call(null, method!, listGetter.Body, IsSame<T2>()); MethodCallExpression call = Expression.Call(null, method, listGetter.Body, IsSame<T2>());
return Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters); return Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters);
} }
@ -183,7 +180,7 @@ namespace Kyoo.Abstractions.Models.Utils
{ {
return _id.HasValue return _id.HasValue
? _id.Value.ToString() ? _id.Value.ToString()
: _slug; : _slug!;
} }
/// <summary> /// <summary>
@ -192,7 +189,7 @@ namespace Kyoo.Abstractions.Models.Utils
public class IdentifierConvertor : TypeConverter public class IdentifierConvertor : TypeConverter
{ {
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{ {
if (sourceType == typeof(int) || sourceType == typeof(string)) if (sourceType == typeof(int) || sourceType == typeof(string))
return true; return true;
@ -200,12 +197,12 @@ namespace Kyoo.Abstractions.Models.Utils
} }
/// <inheritdoc /> /// <inheritdoc />
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{ {
if (value is int id) if (value is int id)
return new Identifier(id); return new Identifier(id);
if (value is not string slug) if (value is not string slug)
return base.ConvertFrom(context, culture, value); return base.ConvertFrom(context, culture, value)!;
return int.TryParse(slug, out id) return int.TryParse(slug, out id)
? new Identifier(id) ? new Identifier(id)
: new Identifier(slug); : new Identifier(slug);

View File

@ -31,13 +31,13 @@ namespace Kyoo.Abstractions.Models.Utils
/// The list of errors that where made in the request. /// The list of errors that where made in the request.
/// </summary> /// </summary>
/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example> /// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
[NotNull] public string[] Errors { get; set; } public string[] Errors { get; set; }
/// <summary> /// <summary>
/// Create a new <see cref="RequestError"/> with one error. /// Create a new <see cref="RequestError"/> with one error.
/// </summary> /// </summary>
/// <param name="error">The error to specify in the response.</param> /// <param name="error">The error to specify in the response.</param>
public RequestError([NotNull] string error) public RequestError(string error)
{ {
if (error == null) if (error == null)
throw new ArgumentNullException(nameof(error)); throw new ArgumentNullException(nameof(error));
@ -48,7 +48,7 @@ namespace Kyoo.Abstractions.Models.Utils
/// Create a new <see cref="RequestError"/> with multiple errors. /// Create a new <see cref="RequestError"/> with multiple errors.
/// </summary> /// </summary>
/// <param name="errors">The errors to specify in the response.</param> /// <param name="errors">The errors to specify in the response.</param>
public RequestError([NotNull] string[] errors) public RequestError(string[] errors)
{ {
if (errors == null || !errors.Any()) if (errors == null || !errors.Any())
throw new ArgumentException("Errors must be non null and not empty", nameof(errors)); throw new ArgumentException("Errors must be non null and not empty", nameof(errors));

View File

@ -33,11 +33,11 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Sort by a specific key /// Sort by a specific key
/// </summary> /// </summary>
/// <param name="key">The sort keys. This members will be used to sort the results.</param> /// <param name="Key">The sort keys. This members will be used to sort the results.</param>
/// <param name="desendant"> /// <param name="Desendant">
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order. /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
/// </param> /// </param>
public record By(string key, bool desendant = false) : Sort<T> public record By(string Key, bool Desendant = false) : Sort<T>
{ {
/// <summary> /// <summary>
/// Sort by a specific key /// Sort by a specific key
@ -53,8 +53,8 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Sort by multiple keys. /// Sort by multiple keys.
/// </summary> /// </summary>
/// <param name="list">The list of keys to sort by.</param> /// <param name="List">The list of keys to sort by.</param>
public record Conglomerate(params Sort<T>[] list) : Sort<T>; public record Conglomerate(params Sort<T>[] List) : Sort<T>;
/// <summary>The default sort method for the given type.</summary> /// <summary>The default sort method for the given type.</summary>
public record Default : Sort<T>; public record Default : Sort<T>;
@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Controllers
return new Conglomerate(sortBy.Split(',').Select(From).ToArray()); return new Conglomerate(sortBy.Split(',').Select(From).ToArray());
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy; string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null; string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
bool desendant = order switch bool desendant = order switch
{ {
"desc" => true, "desc" => true,
@ -81,7 +81,7 @@ namespace Kyoo.Abstractions.Controllers
null => false, null => false,
_ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.")
}; };
PropertyInfo property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); PropertyInfo? property = typeof(T).GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (property == null) if (property == null)
throw new ArgumentException("The given sort key is not valid."); throw new ArgumentException("The given sort key is not valid.");
return new By(property.Name, desendant); return new By(property.Name, desendant);

View File

@ -16,33 +16,21 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis; namespace Kyoo.Abstractions.Models
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{ {
namespace PostgreSQL /// <summary>
/// The links to see a movie or an episode.
/// </summary>
public class VideoLinks
{ {
[Collection(nameof(Postgresql))] /// <summary>
public class GenreTests : AGenreTests /// The direct link to the unprocessed video (pristine quality).
{ /// </summary>
public GenreTests(PostgresFixture postgres, ITestOutputHelper output) public string Direct { get; set; }
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AGenreTests : RepositoryTests<Genre> /// <summary>
{ /// The link to an HLS master playlist containing all qualities available for this video.
[SuppressMessage("ReSharper", "NotAccessedField.Local")] /// </summary>
private readonly IGenreRepository _repository; public string Hls { get; set; }
protected AGenreTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.GenreRepository;
}
} }
} }

View File

@ -1,186 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models
{
/// <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 : CustomTypeDescriptor, IThumbnails, ILink
{
/// <summary>
/// The ID of the episode associated with this item.
/// </summary>
public int EpisodeID { 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 summary 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; }
/// <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; }
/// <inheritdoc />
public Dictionary<int, string> Images { get; set; }
/// <summary>
/// The transcoder's info for this item. This include subtitles, fonts, chapters...
/// </summary>
public object Info { get; set; }
[SerializeIgnore]
private string _Type => IsMovie ? "movie" : "episode";
/// <inheritdoc/>
public object Link => new
{
Direct = $"/video/{_Type}/{Slug}/direct",
Hls = $"/video/{_Type}/{Slug}/master.m3u8",
};
/// <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 and tracks of the episode.
/// </param>
/// <param name="client">A http client to reach the transcoder.</param>
/// <returns>A new WatchItem representing the given episode.</returns>
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library, HttpClient client)
{
await library.Load(ep, x => x.Show);
return new WatchItem
{
EpisodeID = ep.ID,
Slug = ep.Slug,
ShowSlug = ep.Show.Slug,
ShowTitle = ep.Show.Title,
SeasonNumber = ep.SeasonNumber,
EpisodeNumber = ep.EpisodeNumber,
AbsoluteNumber = ep.AbsoluteNumber,
Title = ep.Title,
Overview = ep.Overview,
ReleaseDate = ep.ReleaseDate,
Images = ep.Show.Images,
PreviousEpisode = ep.Show.IsMovie
? null
: (await library.GetAll<Episode>(
where: x => x.ShowID == ep.ShowID,
limit: new Pagination(1, ep.ID, true)
)).FirstOrDefault(),
NextEpisode = ep.Show.IsMovie
? null
: (await library.GetAll<Episode>(
where: x => x.ShowID == ep.ShowID,
limit: new Pagination(1, ep.ID)
)).FirstOrDefault(),
IsMovie = ep.Show.IsMovie,
Info = await _GetInfo(ep, client),
};
}
private static async Task<object> _GetInfo(Episode ep, HttpClient client)
{
HttpResponseMessage ret = await client.GetAsync($"http://transcoder:7666/{(ep.Show.IsMovie ? "movie" : "episode")}/{ep.Slug}/info");
ret.EnsureSuccessStatusCode();
string content = await ret.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<object>(content);
}
/// <inheritdoc />
public override string GetClassName()
{
return nameof(Show);
}
/// <inheritdoc />
public override string GetComponentName()
{
return ShowSlug;
}
}
}

View File

@ -43,7 +43,7 @@ namespace Kyoo.Abstractions
{ {
return builder.RegisterType<T>() return builder.RegisterType<T>()
.As<IBaseRepository>() .As<IBaseRepository>()
.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))) .As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
.InstancePerLifetimeScope(); .InstancePerLifetimeScope();
} }
@ -59,6 +59,7 @@ namespace Kyoo.Abstractions
/// <returns>The initial container.</returns> /// <returns>The initial container.</returns>
public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle> public static IRegistrationBuilder<T2, ConcreteReflectionActivatorData, SingleRegistrationStyle>
RegisterRepository<T, T2>(this ContainerBuilder builder) RegisterRepository<T, T2>(this ContainerBuilder builder)
where T : notnull
where T2 : IBaseRepository, T where T2 : IBaseRepository, T
{ {
return builder.RegisterRepository<T2>().As<T>(); return builder.RegisterRepository<T2>().As<T>();

View File

@ -17,9 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Kyoo.Utils namespace Kyoo.Utils
@ -29,141 +27,16 @@ namespace Kyoo.Utils
/// </summary> /// </summary>
public static class EnumerableExtensions 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 <paramref name="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 <paramref name="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 <paramref name="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> /// <summary>
/// If the enumerable is empty, execute an action. /// If the enumerable is empty, execute an action.
/// </summary> /// </summary>
/// <param name="self">The enumerable to check</param> /// <param name="self">The enumerable to check</param>
/// <param name="action">The action to execute is the list is empty</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> /// <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> /// <returns>The iterator proxied, there is no dual iterations.</returns>
[LinqTunnel] [LinqTunnel]
public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action) public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, 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) static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
{ {
using IEnumerator<T> enumerator = self.GetEnumerator(); using IEnumerator<T> enumerator = self.GetEnumerator();
@ -190,111 +63,12 @@ namespace Kyoo.Utils
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param> /// <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> /// <param name="action">The action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list</typeparam> /// <typeparam name="T">The type of items in the list</typeparam>
public static void ForEach<T>([CanBeNull] this IEnumerable<T> self, Action<T> action) public static void ForEach<T>(this IEnumerable<T>? self, Action<T> action)
{ {
if (self == null) if (self == null)
return; return;
foreach (T i in self) foreach (T i in self)
action(i); 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>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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

@ -17,13 +17,10 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Utils namespace Kyoo.Utils
@ -33,99 +30,9 @@ namespace Kyoo.Utils
/// </summary> /// </summary>
public static class Merger 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>
/// <typeparam name="T">The type of items in the lists to merge.</typeparam>
/// <returns>The two list merged as an array</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T[] MergeLists<T>([CanBeNull] IEnumerable<T> first,
[CanBeNull] IEnumerable<T> second,
[CanBeNull] 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>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
/// <seealso cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2},out bool)"/>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second)
{
return MergeDictionaries(first, second, out bool _);
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
foreach ((T key, T2 value) in second)
{
bool success = first.TryAdd(key, value);
hasChanged |= success;
if (success || first[key]?.Equals(default) == false || value?.Equals(default) != false)
continue;
first[key] = value;
hasChanged = true;
}
return first;
}
/// <summary> /// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept. /// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
/// </summary> /// </summary>
/// <remarks>
/// The only difference in this function compared to
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2}, out bool)"/>
/// is the way <paramref name="hasChanged"/> is calculated and the order of the arguments.
/// <code>
/// MergeDictionaries(first, second);
/// </code>
/// will do the same thing as
/// <code>
/// CompleteDictionaries(second, first, out bool _);
/// </code>
/// </remarks>
/// <param name="first">The first dictionary to merge</param> /// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param> /// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged"> /// <param name="hasChanged">
@ -138,8 +45,8 @@ namespace Kyoo.Utils
/// set to those of <paramref name="first"/>. /// set to those of <paramref name="first"/>.
/// </returns> /// </returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> CompleteDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first, public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(IDictionary<T, T2>? first,
[CanBeNull] IDictionary<T, T2> second, IDictionary<T, T2>? second,
out bool hasChanged) out bool hasChanged)
{ {
if (first == null) if (first == null)
@ -151,49 +58,17 @@ namespace Kyoo.Utils
hasChanged = false; hasChanged = false;
if (second == null) if (second == null)
return first; return first;
hasChanged = second.Any(x => x.Value?.Equals(first[x.Key]) == false); hasChanged = second.Any(x => !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false);
foreach ((T key, T2 value) in first) foreach ((T key, T2 value) in first)
second.TryAdd(key, value); second.TryAdd(key, value);
return second; return second;
} }
/// <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><paramref name="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> /// <summary>
/// Set every non-default values of seconds to the corresponding property of second. /// Set every non-default values of seconds to the corresponding property of second.
/// Dictionaries are handled like anonymous objects with a property per key/pair value /// Dictionaries are handled like anonymous objects with a property per key/pair value
/// (see
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2})"/>
/// for more details).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/> /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary> /// </summary>
/// <remarks>
/// This does the opposite of <see cref="Merge{T}"/>.
/// </remarks>
/// <example> /// <example>
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"} /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
/// </example> /// </example>
@ -208,19 +83,16 @@ namespace Kyoo.Utils
/// </param> /// </param>
/// <typeparam name="T">Fields of T will be completed</typeparam> /// <typeparam name="T">Fields of T will be completed</typeparam>
/// <returns><paramref name="first"/></returns> /// <returns><paramref name="first"/></returns>
/// <exception cref="ArgumentNullException">If first is null</exception> public static T Complete<T>(T first,
public static T Complete<T>([NotNull] T first, T? second,
[CanBeNull] T second, [InstantHandle] Func<PropertyInfo, bool>? where = null)
[InstantHandle] Func<PropertyInfo, bool> where = null)
{ {
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null) if (second == null)
return first; return first;
Type type = typeof(T); Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties() IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite .Where(x => x is { CanRead: true, CanWrite: true }
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null) if (where != null)
@ -228,17 +100,16 @@ namespace Kyoo.Utils
foreach (PropertyInfo property in properties) foreach (PropertyInfo property in properties)
{ {
object value = property.GetValue(second); object? value = property.GetValue(second);
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? property.PropertyType.GetClrDefault();
if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first))) if (value?.Equals(property.GetValue(first)) == true)
continue; continue;
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{ {
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
.GenericTypeArguments; .GenericTypeArguments;
object[] parameters = object?[] parameters =
{ {
property.GetValue(first), property.GetValue(first),
value, value,
@ -248,8 +119,8 @@ namespace Kyoo.Utils
typeof(Merger), typeof(Merger),
nameof(CompleteDictionaries), nameof(CompleteDictionaries),
dictionaryTypes, dictionaryTypes,
parameters); parameters)!;
if ((bool)parameters[2]) if ((bool)parameters[2]!)
property.SetValue(first, newDictionary); property.SetValue(first, newDictionary);
} }
else else
@ -260,109 +131,5 @@ namespace Kyoo.Utils
merge.OnMerge(second); merge.OnMerge(second);
return first; return first;
} }
/// <summary>
/// This will set missing values of <paramref name="first"/> to the corresponding values of <paramref name="second"/>.
/// Enumerable will be merged (concatenated) and Dictionaries too.
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary>
/// <example>
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}
/// </example>
/// <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 merged</typeparam>
/// <returns><paramref name="first"/></returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T Merge<T>([CanBeNull] T first,
[CanBeNull] T second,
[InstantHandle] Func<PropertyInfo, bool> where = null)
{
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);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties)
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.GetClrDefault();
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
else if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters =
{
oldValue,
newValue,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(MergeDictionaries),
dictionaryTypes,
parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
.GenericTypeArguments
.First();
Func<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
? (x, y) => x.Slug == y.Slug
: null;
property.SetValue(first, Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(MergeLists),
enumerableType,
oldValue, newValue, equalityComparer));
}
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every fields of <paramref name="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><paramref name="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

@ -18,7 +18,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Utils namespace Kyoo.Utils
{ {
@ -49,38 +48,5 @@ namespace Kyoo.Utils
return x.Result; return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously); }, 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

@ -41,8 +41,6 @@ namespace Kyoo.Utils
/// <returns>True if the expression is a member, false otherwise</returns> /// <returns>True if the expression is a member, false otherwise</returns>
public static bool IsPropertyExpression(LambdaExpression ex) public static bool IsPropertyExpression(LambdaExpression ex)
{ {
if (ex == null)
return false;
return ex.Body is MemberExpression return ex.Body is MemberExpression
|| (ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression); || (ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression);
} }
@ -57,44 +55,19 @@ namespace Kyoo.Utils
{ {
if (!IsPropertyExpression(ex)) if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression."); throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert MemberExpression? member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression ? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression; : ex.Body as MemberExpression;
return member!.Member.Name; 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 <paramref name="member"/> or <paramref name="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> /// <summary>
/// Slugify a string (Replace spaces by -, Uniformize accents) /// Slugify a string (Replace spaces by -, Uniformize accents)
/// </summary> /// </summary>
/// <param name="str">The string to slugify</param> /// <param name="str">The string to slugify</param>
/// <returns>The slug version of the given string</returns> /// <returns>The slug version of the given string</returns>
public static string ToSlug([CanBeNull] string str) public static string ToSlug(string str)
{ {
if (str == null)
return null;
str = str.ToLowerInvariant(); str = str.ToLowerInvariant();
string normalizedString = str.Normalize(NormalizationForm.FormD); string normalizedString = str.Normalize(NormalizationForm.FormD);
@ -114,59 +87,25 @@ namespace Kyoo.Utils
return str; 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> /// <summary>
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned) /// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
/// </summary> /// </summary>
/// <param name="type">The starting type</param> /// <param name="self">The starting type</param>
/// <returns>A list of types</returns> /// <returns>A list of types</returns>
/// <exception cref="ArgumentNullException"><paramref name="type"/> can't be null</exception> public static IEnumerable<Type> GetInheritanceTree(this Type self)
public static IEnumerable<Type> GetInheritanceTree([NotNull] this Type type)
{ {
if (type == null) for (Type? type = self; type != null; type = type.BaseType)
throw new ArgumentNullException(nameof(type));
for (; type != null; type = type.BaseType)
yield return type; yield return type;
} }
/// <summary>
/// Check if <paramref name="obj"/> inherit from a generic type <paramref name="genericType"/>.
/// </summary>
/// <param name="obj">Does this object's type is a <paramref name="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> /// <summary>
/// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>. /// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>.
/// </summary> /// </summary>
/// <param name="type">The type to check</param> /// <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> /// <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> /// <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(Type type, Type genericType)
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) if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type."); throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
@ -184,14 +123,9 @@ namespace Kyoo.Utils
/// <param name="type">The type to check</param> /// <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> /// <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> /// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
/// <exception cref="ArgumentNullException"><paramref name="type"/> and <paramref name="genericType"/> can't be null</exception>
/// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception> /// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception>
public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) public static Type? GetGenericDefinition(Type type, Type genericType)
{ {
if (type == null)
throw new ArgumentNullException(nameof(type));
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericType) if (!genericType.IsGenericType)
throw new ArgumentException($"{nameof(genericType)} is not a generic type."); throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
@ -224,20 +158,12 @@ namespace Kyoo.Utils
/// <exception cref="ArgumentException">No method match the given constraints.</exception> /// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The method handle of the matching method.</returns> /// <returns>The method handle of the matching method.</returns>
[PublicAPI] [PublicAPI]
[NotNull] public static MethodInfo GetMethod(Type type,
public static MethodInfo GetMethod([NotNull] Type type,
BindingFlags flag, BindingFlags flag,
string name, string name,
[NotNull] Type[] generics, Type[] generics,
[NotNull] object[] args) object?[] args)
{ {
if (type == null)
throw new ArgumentNullException(nameof(type));
if (generics == null)
throw new ArgumentNullException(nameof(generics));
if (args == null)
throw new ArgumentNullException(nameof(args));
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public) MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name) .Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length) .Where(x => x.GetGenericArguments().Length == generics.Length)
@ -275,7 +201,7 @@ namespace Kyoo.Utils
/// Run a generic static method for a runtime <see cref="Type"/>. /// Run a generic static method for a runtime <see cref="Type"/>.
/// </summary> /// </summary>
/// <example> /// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type, /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
/// you could do: /// you could do:
/// <code lang="C#"> /// <code lang="C#">
/// Utility.RunGenericMethod&lt;object&gt;( /// Utility.RunGenericMethod&lt;object&gt;(
@ -294,12 +220,11 @@ namespace Kyoo.Utils
/// </typeparam> /// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception> /// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns> /// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/> /// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
public static T RunGenericMethod<T>( public static T? RunGenericMethod<T>(
[NotNull] Type owner, Type owner,
[NotNull] string methodName, string methodName,
[NotNull] Type type, Type type,
params object[] args) params object[] args)
{ {
return RunGenericMethod<T>(owner, methodName, new[] { type }, args); return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
@ -311,7 +236,7 @@ namespace Kyoo.Utils
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/> /// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
/// </summary> /// </summary>
/// <example> /// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type, /// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
/// you could do: /// you could do:
/// <code> /// <code>
/// Utility.RunGenericMethod&lt;object&gt;( /// Utility.RunGenericMethod&lt;object&gt;(
@ -330,102 +255,17 @@ namespace Kyoo.Utils
/// </typeparam> /// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception> /// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns> /// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/> /// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
[PublicAPI] public static T? RunGenericMethod<T>(
public static T RunGenericMethod<T>( Type owner,
[NotNull] Type owner, string methodName,
[NotNull] string methodName, Type[] types,
[NotNull] Type[] types, params object?[] args)
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) if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args); return (T?)method.MakeGenericMethod(types).Invoke(null, args);
}
/// <summary>
/// Run a generic method for a runtime <see cref="Type"/>.
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="instance">The <c>this</c> of the method to run.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="type">The generic type to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
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);
}
/// <summary>
/// Run a generic method for a multiple runtime <see cref="Type"/>.
/// If your generic method only needs one type, see
/// <see cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="instance">The <c>this</c> of the method to run.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="types">The list of generic types to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
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());
} }
/// <summary> /// <summary>
@ -446,25 +286,11 @@ namespace Kyoo.Utils
/// </summary> /// </summary>
/// <param name="ex">The exception to rethrow.</param> /// <param name="ex">The exception to rethrow.</param>
[System.Diagnostics.CodeAnalysis.DoesNotReturn] [System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void ReThrow([NotNull] this Exception ex) public static void ReThrow(this Exception ex)
{ {
if (ex == null) if (ex == null)
throw new ArgumentNullException(nameof(ex)); throw new ArgumentNullException(nameof(ex));
ExceptionDispatchInfo.Capture(ex).Throw(); 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

@ -63,7 +63,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute) public IFilterMetadata Create(PartialPermissionAttribute attribute)
{ {
return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, attribute.Group, _options); return new PermissionValidatorFilter(((object?)attribute.Type ?? attribute.Kind)!, attribute.Group, _options);
} }
/// <summary> /// <summary>

View File

@ -61,7 +61,7 @@ namespace Kyoo.Authentication
: string.Empty; : string.Empty;
List<Claim> claims = new() List<Claim> claims = new()
{ {
new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)), new Claim(Claims.Id, user.Id.ToString(CultureInfo.InvariantCulture)),
new Claim(Claims.Name, user.Username), new Claim(Claims.Name, user.Username),
new Claim(Claims.Permissions, permissions), new Claim(Claims.Permissions, permissions),
new Claim(Claims.Type, "access") new Claim(Claims.Type, "access")
@ -85,7 +85,7 @@ namespace Kyoo.Authentication
signingCredentials: credential, signingCredentials: credential,
claims: new[] claims: new[]
{ {
new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)), new Claim(Claims.Id, user.Id.ToString(CultureInfo.InvariantCulture)),
new Claim(Claims.Guid, Guid.NewGuid().ToString()), new Claim(Claims.Guid, Guid.NewGuid().ToString()),
new Claim(Claims.Type, "refresh") new Claim(Claims.Type, "refresh")
}, },

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Utils; using Kyoo.Utils;
@ -72,7 +71,6 @@ namespace Kyoo.Authentication.Models.DTO
Username = Username, Username = Username,
Password = BCryptNet.HashPassword(Password), Password = BCryptNet.HashPassword(Password),
Email = Email, Email = Email,
ExtraData = new Dictionary<string, string>()
}; };
} }
} }

View File

@ -27,6 +27,7 @@ using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Authentication.Models; using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO; using Kyoo.Authentication.Models.DTO;
using Kyoo.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -228,8 +229,8 @@ namespace Kyoo.Authentication.Views
return Unauthorized(new RequestError("User not authenticated or token invalid.")); return Unauthorized(new RequestError("User not authenticated or token invalid."));
try try
{ {
user.ID = userID; user.Id = userID;
return await _users.Edit(user, true); return await _users.Edit(user);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
@ -252,14 +253,15 @@ namespace Kyoo.Authentication.Views
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<User>> PatchMe(User user) public async Task<ActionResult<User>> PatchMe(PartialResource user)
{ {
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID)) if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
return Unauthorized(new RequestError("User not authenticated or token invalid.")); return Unauthorized(new RequestError("User not authenticated or token invalid."));
try try
{ {
user.ID = userID; if (user.Id.HasValue && user.Id != userID)
return await _users.Edit(user, false); throw new ArgumentException("Can't edit your user id.");
return await _users.Patch(userID, TryUpdateModelAsync);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {

View File

@ -39,15 +39,15 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly IBaseRepository[] _repositories; private readonly IBaseRepository[] _repositories;
/// <inheritdoc />
public ILibraryRepository LibraryRepository { get; }
/// <inheritdoc /> /// <inheritdoc />
public ILibraryItemRepository LibraryItemRepository { get; } public ILibraryItemRepository LibraryItemRepository { get; }
/// <inheritdoc /> /// <inheritdoc />
public ICollectionRepository CollectionRepository { get; } public ICollectionRepository CollectionRepository { get; }
/// <inheritdoc />
public IMovieRepository MovieRepository { get; }
/// <inheritdoc /> /// <inheritdoc />
public IShowRepository ShowRepository { get; } public IShowRepository ShowRepository { get; }
@ -63,12 +63,6 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public IStudioRepository StudioRepository { get; } public IStudioRepository StudioRepository { get; }
/// <inheritdoc />
public IGenreRepository GenreRepository { get; }
/// <inheritdoc />
public IProviderRepository ProviderRepository { get; }
/// <inheritdoc /> /// <inheritdoc />
public IUserRepository UserRepository { get; } public IUserRepository UserRepository { get; }
@ -80,16 +74,14 @@ namespace Kyoo.Core.Controllers
public LibraryManager(IEnumerable<IBaseRepository> repositories) public LibraryManager(IEnumerable<IBaseRepository> repositories)
{ {
_repositories = repositories.ToArray(); _repositories = repositories.ToArray();
LibraryRepository = GetRepository<Library>() as ILibraryRepository; LibraryItemRepository = GetRepository<ILibraryItem>() as ILibraryItemRepository;
LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
CollectionRepository = GetRepository<Collection>() as ICollectionRepository; CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
MovieRepository = GetRepository<Movie>() as IMovieRepository;
ShowRepository = GetRepository<Show>() as IShowRepository; ShowRepository = GetRepository<Show>() as IShowRepository;
SeasonRepository = GetRepository<Season>() as ISeasonRepository; SeasonRepository = GetRepository<Season>() as ISeasonRepository;
EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository; EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
PeopleRepository = GetRepository<People>() as IPeopleRepository; PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository; StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
UserRepository = GetRepository<User>() as IUserRepository; UserRepository = GetRepository<User>() as IUserRepository;
} }
@ -217,8 +209,6 @@ namespace Kyoo.Core.Controllers
where T : class, IResource where T : class, IResource
where T2 : class, IResource where T2 : class, IResource
{ {
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force); return Load(obj, Utility.GetPropertyName(member), force);
} }
@ -227,8 +217,6 @@ namespace Kyoo.Core.Controllers
where T : class, IResource where T : class, IResource
where T2 : class where T2 : class
{ {
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force); return Load(obj, Utility.GetPropertyName(member), force);
} }
@ -259,166 +247,119 @@ namespace Kyoo.Core.Controllers
return (obj, member: memberName) switch return (obj, member: memberName) switch
{ {
(Library l, nameof(Library.Providers)) => ProviderRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Providers = x),
(Library l, nameof(Library.Shows)) => ShowRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Shows = x),
(Library l, nameof(Library.Collections)) => CollectionRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
.Then(x => l.Collections = x),
(Collection c, nameof(Collection.ExternalIDs)) => _SetRelation(c,
ProviderRepository.GetMetadataID<Collection>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Collection c, nameof(Collection.Shows)) => ShowRepository (Collection c, nameof(Collection.Shows)) => ShowRepository
.GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) .GetAll(x => x.Collections.Any(y => y.Id == obj.Id))
.Then(x => c.Shows = x), .Then(x => c.Shows = x),
(Collection c, nameof(Collection.Libraries)) => LibraryRepository (Collection c, nameof(Collection.Movies)) => MovieRepository
.GetAll(x => x.Collections.Any(y => y.ID == obj.ID)) .GetAll(x => x.Collections.Any(y => y.Id == obj.Id))
.Then(x => c.Libraries = x), .Then(x => c.Movies = x),
(Show s, nameof(Show.ExternalIDs)) => _SetRelation(s, (Movie m, nameof(Movie.People)) => PeopleRepository
ProviderRepository.GetMetadataID<Show>(x => x.ResourceID == obj.ID), .GetFromShow(obj.Id)
(x, y) => x.ExternalIDs = y, .Then(x => m.People = x),
(x, y) => { x.ResourceID = y.ID; }),
(Movie m, nameof(Movie.Collections)) => CollectionRepository
.GetAll(x => x.Movies.Any(y => y.Id == obj.Id))
.Then(x => m.Collections = x),
(Movie m, nameof(Movie.Studio)) => StudioRepository
.GetOrDefault(x => x.Movies.Any(y => y.Id == obj.Id))
.Then(x =>
{
m.Studio = x;
m.StudioID = x?.Id ?? 0;
}),
(Show s, nameof(Show.Genres)) => GenreRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Genres = x),
(Show s, nameof(Show.People)) => PeopleRepository (Show s, nameof(Show.People)) => PeopleRepository
.GetFromShow(obj.ID) .GetFromShow(obj.Id)
.Then(x => s.People = x), .Then(x => s.People = x),
(Show s, nameof(Show.Seasons)) => _SetRelation(s, (Show s, nameof(Show.Seasons)) => _SetRelation(s,
SeasonRepository.GetAll(x => x.Show.ID == obj.ID), SeasonRepository.GetAll(x => x.Show.Id == obj.Id),
(x, y) => x.Seasons = y, (x, y) => x.Seasons = y,
(x, y) => { x.Show = y; x.ShowID = y.ID; }), (x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Episodes)) => _SetRelation(s, (Show s, nameof(Show.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Show.ID == obj.ID), EpisodeRepository.GetAll(x => x.Show.Id == obj.Id),
(x, y) => x.Episodes = y, (x, y) => x.Episodes = y,
(x, y) => { x.Show = y; x.ShowID = y.ID; }), (x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Libraries)) => LibraryRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => s.Libraries = x),
(Show s, nameof(Show.Collections)) => CollectionRepository (Show s, nameof(Show.Collections)) => CollectionRepository
.GetAll(x => x.Shows.Any(y => y.ID == obj.ID)) .GetAll(x => x.Shows.Any(y => y.Id == obj.Id))
.Then(x => s.Collections = x), .Then(x => s.Collections = x),
(Show s, nameof(Show.Studio)) => StudioRepository (Show s, nameof(Show.Studio)) => StudioRepository
.GetOrDefault(x => x.Shows.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Shows.Any(y => y.Id == obj.Id))
.Then(x => .Then(x =>
{ {
s.Studio = x; s.Studio = x;
s.StudioID = x?.ID ?? 0; s.StudioId = x?.Id ?? 0;
}), }),
(Season s, nameof(Season.ExternalIDs)) => _SetRelation(s,
ProviderRepository.GetMetadataID<Season>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Season s, nameof(Season.Episodes)) => _SetRelation(s, (Season s, nameof(Season.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.ID == obj.ID), EpisodeRepository.GetAll(x => x.Season.Id == obj.Id),
(x, y) => x.Episodes = y, (x, y) => x.Episodes = y,
(x, y) => { x.Season = y; x.SeasonID = y.ID; }), (x, y) => { x.Season = y; x.SeasonId = y.Id; }),
(Season s, nameof(Season.Show)) => ShowRepository (Season s, nameof(Season.Show)) => ShowRepository
.GetOrDefault(x => x.Seasons.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Seasons.Any(y => y.Id == obj.Id))
.Then(x => .Then(x =>
{ {
s.Show = x; s.Show = x;
s.ShowID = x?.ID ?? 0; s.ShowId = x?.Id ?? 0;
}), }),
(Episode e, nameof(Episode.ExternalIDs)) => _SetRelation(e,
ProviderRepository.GetMetadataID<Episode>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository (Episode e, nameof(Episode.Show)) => ShowRepository
.GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.Id == obj.Id))
.Then(x => .Then(x =>
{ {
e.Show = x; e.Show = x;
e.ShowID = x?.ID ?? 0; e.ShowId = x?.Id ?? 0;
}), }),
(Episode e, nameof(Episode.Season)) => SeasonRepository (Episode e, nameof(Episode.Season)) => SeasonRepository
.GetOrDefault(x => x.Episodes.Any(y => y.ID == e.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.Id == e.Id))
.Then(x => .Then(x =>
{ {
e.Season = x; e.Season = x;
e.SeasonID = x?.ID ?? 0; e.SeasonId = x?.Id ?? 0;
}), }),
(Episode e, nameof(Episode.PreviousEpisode)) => EpisodeRepository
.GetAll(
where: x => x.ShowId == e.ShowId,
limit: new Pagination(1, e.Id, true)
).Then(x => e.PreviousEpisode = x.FirstOrDefault()),
(Genre g, nameof(Genre.Shows)) => ShowRepository (Episode e, nameof(Episode.NextEpisode)) => EpisodeRepository
.GetAll(x => x.Genres.Any(y => y.ID == obj.ID)) .GetAll(
.Then(x => g.Shows = x), where: x => x.ShowId == e.ShowId,
limit: new Pagination(1, e.Id)
).Then(x => e.NextEpisode = x.FirstOrDefault()),
(Studio s, nameof(Studio.Shows)) => ShowRepository (Studio s, nameof(Studio.Shows)) => ShowRepository
.GetAll(x => x.Studio.ID == obj.ID) .GetAll(x => x.Studio.Id == obj.Id)
.Then(x => s.Shows = x), .Then(x => s.Shows = x),
(Studio s, nameof(Studio.ExternalIDs)) => _SetRelation(s, (Studio s, nameof(Studio.Movies)) => MovieRepository
ProviderRepository.GetMetadataID<Studio>(x => x.ResourceID == obj.ID), .GetAll(x => x.Studio.Id == obj.Id)
(x, y) => x.ExternalIDs = y, .Then(x => s.Movies = x),
(x, y) => { x.ResourceID = y.ID; }),
(People p, nameof(People.ExternalIDs)) => _SetRelation(p,
ProviderRepository.GetMetadataID<People>(x => x.ResourceID == obj.ID),
(x, y) => x.ExternalIDs = y,
(x, y) => { x.ResourceID = y.ID; }),
(People p, nameof(People.Roles)) => PeopleRepository (People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.ID) .GetFromPeople(obj.Id)
.Then(x => p.Roles = x), .Then(x => p.Roles = x),
(Provider p, nameof(Provider.Libraries)) => LibraryRepository
.GetAll(x => x.Providers.Any(y => y.ID == obj.ID))
.Then(x => p.Libraries = x),
_ => throw new ArgumentException($"Couldn't find a way to load {memberName} of {obj.Slug}.") _ => throw new ArgumentException($"Couldn't find a way to load {memberName} of {obj.Slug}.")
}; };
} }
/// <inheritdoc />
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
{
return LibraryItemRepository.GetFromLibrary(id, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
{
return LibraryItemRepository.GetFromLibrary(slug, where, sort, limit);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
@ -455,20 +396,6 @@ namespace Kyoo.Core.Controllers
return PeopleRepository.GetFromPeople(slug, where, sort, limit); return PeopleRepository.GetFromPeople(slug, where, sort, limit);
} }
/// <inheritdoc />
public Task AddShowLink(int showID, int? libraryID, int? collectionID)
{
return ShowRepository.AddShowLink(showID, libraryID, collectionID);
}
/// <inheritdoc />
public Task AddShowLink(Show show, Library library, Collection collection)
{
if (show == null)
throw new ArgumentNullException(nameof(show));
return ShowRepository.AddShowLink(show.ID, library?.ID, collection?.ID);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null, public Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
@ -507,10 +434,17 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<T> Edit<T>(T item, bool resetOld) public Task<T> Edit<T>(T item)
where T : class, IResource where T : class, IResource
{ {
return GetRepository<T>().Edit(item, resetOld); return GetRepository<T>().Edit(item);
}
/// <inheritdoc />
public Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
where T : class, IResource
{
return GetRepository<T>().Patch(id, patch);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -37,11 +37,6 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc /> /// <inheritdoc />
protected override Sort<Collection> DefaultSort => new Sort<Collection>.By(nameof(Collection.Name)); protected override Sort<Collection> DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
@ -49,22 +44,22 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="CollectionRepository"/>. /// Create a new <see cref="CollectionRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The database handle to use</param> /// <param name="database">The database handle to use</param>
/// /// <param name="providers">A provider repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
public CollectionRepository(DatabaseContext database, IProviderRepository providers) public CollectionRepository(DatabaseContext database, IThumbnailsManager thumbs)
: base(database) : base(database, thumbs)
{ {
_database = database; _database = database;
_providers = providers;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Collection>> Search(string query) public override async Task<ICollection<Collection>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.Collections _database.Collections
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%")) .Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%"))
.Take(20) .Take(20)
).ToListAsync(); ).ToListAsync())
.Select(SetBackingImageSelf).ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -82,41 +77,13 @@ namespace Kyoo.Core.Controllers
{ {
await base.Validate(resource); await base.Validate(resource);
if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("The collection's slug must be set and not empty");
if (string.IsNullOrEmpty(resource.Name)) if (string.IsNullOrEmpty(resource.Name))
throw new ArgumentException("The collection's name must be set and not empty"); throw new ArgumentException("The collection's name must be set and not empty");
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(Collection obj) public override async Task Delete(Collection obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);

View File

@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
private readonly IShowRepository _shows; private readonly IShowRepository _shows;
/// <inheritdoc /> /// <inheritdoc />
@ -59,20 +54,19 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
/// <param name="database">The database handle to use.</param> /// <param name="database">The database handle to use.</param>
/// <param name="shows">A show repository</param> /// <param name="shows">A show repository</param>
/// <param name="providers">A provider repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
public EpisodeRepository(DatabaseContext database, public EpisodeRepository(DatabaseContext database,
IShowRepository shows, IShowRepository shows,
IProviderRepository providers) IThumbnailsManager thumbs)
: base(database) : base(database, thumbs)
{ {
_database = database; _database = database;
_providers = providers;
_shows = shows; _shows = shows;
// Edit episode slugs when the show's slug changes. // Edit episode slugs when the show's slug changes.
shows.OnEdited += (show) => shows.OnEdited += (show) =>
{ {
List<Episode> episodes = _database.Episodes.AsTracking().Where(x => x.ShowID == show.ID).ToList(); List<Episode> episodes = _database.Episodes.AsTracking().Where(x => x.ShowId == show.Id).ToList();
foreach (Episode ep in episodes) foreach (Episode ep in episodes)
{ {
ep.ShowSlug = show.Slug; ep.ShowSlug = show.Slug;
@ -85,9 +79,9 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber) public Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -95,7 +89,7 @@ namespace Kyoo.Core.Controllers
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber); && x.EpisodeNumber == episodeNumber).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -119,15 +113,15 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public Task<Episode> GetAbsolute(int showID, int absoluteNumber) public Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.AbsoluteNumber == absoluteNumber); && x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<Episode> GetAbsolute(string showSlug, int absoluteNumber) public Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{ {
return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber); && x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -137,80 +131,56 @@ namespace Kyoo.Core.Controllers
_database.Episodes _database.Episodes
.Include(x => x.Show) .Include(x => x.Show)
.Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null) .Where(x => x.EpisodeNumber != null || x.AbsoluteNumber != null)
.Where(_database.Like<Episode>(x => x.Title, $"%{query}%")) .Where(_database.Like<Episode>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
foreach (Episode ep in ret) foreach (Episode ep in ret)
{
ep.Show.Episodes = null; ep.Show.Episodes = null;
SetBackingImage(ep);
}
return ret; return ret;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<Episode> Create(Episode obj) public override async Task<Episode> Create(Episode obj)
{ {
obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.Id == obj.ShowId).Slug;
await base.Create(obj); await base.Create(obj);
obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.ID == obj.ShowID).Slug;
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => await _database.SaveChangesAsync(() =>
obj.SeasonNumber != null && obj.EpisodeNumber != null obj.SeasonNumber != null && obj.EpisodeNumber != null
? Get(obj.ShowID, obj.SeasonNumber.Value, obj.EpisodeNumber.Value) ? Get(obj.ShowId, obj.SeasonNumber.Value, obj.EpisodeNumber.Value)
: GetAbsolute(obj.ShowID, obj.AbsoluteNumber.Value)); : GetAbsolute(obj.ShowId, obj.AbsoluteNumber.Value));
OnResourceCreated(obj); OnResourceCreated(obj);
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
}
/// <inheritdoc /> /// <inheritdoc />
protected override async Task Validate(Episode resource) protected override async Task Validate(Episode resource)
{ {
await base.Validate(resource); await base.Validate(resource);
if (resource.ShowID <= 0) if (resource.ShowId <= 0)
{ {
if (resource.Show == null) if (resource.Show == null)
{ {
throw new ArgumentException($"Can't store an episode not related " + throw new ArgumentException($"Can't store an episode not related " +
$"to any show (showID: {resource.ShowID})."); $"to any show (showID: {resource.ShowId}).");
} }
resource.ShowID = resource.Show.ID; resource.ShowId = resource.Show.Id;
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(Episode obj) public override async Task Delete(Episode obj)
{ {
if (obj == null) int epCount = await _database.Episodes.Where(x => x.ShowId == obj.ShowId).Take(2).CountAsync();
throw new ArgumentNullException(nameof(obj));
int epCount = await _database.Episodes.Where(x => x.ShowID == obj.ShowID).Take(2).CountAsync();
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);
if (epCount == 1) if (epCount == 1)
await _shows.Delete(obj.ShowID); await _shows.Delete(obj.ShowId);
} }
} }
} }

View File

@ -1,93 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository for genres.
/// </summary>
public class GenreRepository : LocalRepository<Genre>, IGenreRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Sort<Genre> DefaultSort => new Sort<Genre>.By(x => x.Slug);
/// <summary>
/// Create a new <see cref="GenreRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
public GenreRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
public override async Task<ICollection<Genre>> Search(string query)
{
return await Sort(
_database.Genres
.Where(_database.Like<Genre>(x => x.Name, $"%{query}%"))
)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
protected override async Task Validate(Genre resource)
{
resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource);
}
/// <inheritdoc />
public override async Task<Genre> Create(Genre obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
public override async Task Delete(Genre obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
}
}
}

View File

@ -23,8 +23,8 @@ using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
@ -32,84 +32,90 @@ namespace Kyoo.Core.Controllers
/// <summary> /// <summary>
/// A local repository to handle library items. /// A local repository to handle library items.
/// </summary> /// </summary>
public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository public class LibraryItemRepository : LocalRepository<ILibraryItem>, ILibraryItemRepository
{ {
/// <summary> /// <summary>
/// The database handle /// The database handle
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A lazy loaded library repository to validate queries (check if a library does exist)
/// </summary>
private readonly Lazy<ILibraryRepository> _libraries;
/// <inheritdoc /> /// <inheritdoc />
protected override Sort<LibraryItem> DefaultSort => new Sort<LibraryItem>.By(x => x.Title); protected override Sort<ILibraryItem> DefaultSort => new Sort<ILibraryItem>.By(x => x.Name);
/// <summary> /// <summary>
/// Create a new <see cref="LibraryItemRepository"/>. /// Create a new <see cref="ILibraryItemRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The database instance</param> /// <param name="database">The database instance</param>
/// <param name="libraries">A lazy loaded library repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
public LibraryItemRepository(DatabaseContext database, public LibraryItemRepository(DatabaseContext database, IThumbnailsManager thumbs)
Lazy<ILibraryRepository> libraries) : base(database, thumbs)
: base(database)
{ {
_database = database; _database = database;
_libraries = libraries;
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> GetOrDefault(int id) public override async Task<ILibraryItem> GetOrDefault(int id)
{ {
return _database.LibraryItems.FirstOrDefaultAsync(x => x.ID == id); return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Id == id).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> GetOrDefault(string slug) public override async Task<ILibraryItem> GetOrDefault(string slug)
{ {
return _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug); return await _database.LibraryItems.SingleOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null, public override async Task<ILibraryItem> GetOrDefault(Expression<Func<ILibraryItem, bool>> where, Sort<ILibraryItem> sortBy = default)
Sort<LibraryItem> sort = default, {
return await Sort(_database.LibraryItems, sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage);
}
/// <inheritdoc />
public override async Task<ICollection<ILibraryItem>> GetAll(Expression<Func<ILibraryItem, bool>> where = null,
Sort<ILibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return ApplyFilters(_database.LibraryItems, where, sort, limit); return (await ApplyFilters(_database.LibraryItems, where, sort, limit))
.Select(SetBackingImageSelf).ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null) public override Task<int> GetCount(Expression<Func<ILibraryItem, bool>> where = null)
{ {
IQueryable<LibraryItem> query = _database.LibraryItems; IQueryable<ILibraryItem> query = _database.LibraryItems;
if (where != null) if (where != null)
query = query.Where(where); query = query.Where(where);
return query.CountAsync(); return query.CountAsync();
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query) public override async Task<ICollection<ILibraryItem>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.LibraryItems _database.LibraryItems
.Where(_database.Like<LibraryItem>(x => x.Title, $"%{query}%")) .Where(_database.Like<LibraryItem>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) public override Task<ILibraryItem> Create(ILibraryItem obj)
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj) public override Task<ILibraryItem> CreateIfNotExists(ILibraryItem obj)
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />
public override Task<LibraryItem> Edit(LibraryItem obj, bool resetOld) public override Task<ILibraryItem> Edit(ILibraryItem edited)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task<ILibraryItem> Patch(int id, Func<ILibraryItem, Task<bool>> patch)
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />
@ -121,58 +127,7 @@ namespace Kyoo.Core.Controllers
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
/// <inheritdoc /> /// <inheritdoc />
public override Task Delete(LibraryItem obj) public override Task Delete(ILibraryItem obj)
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
/// <summary>
/// Get a basic queryable for a library with the right mapping from shows and collections.
/// Shows contained in a collection are excluded.
/// </summary>
/// <param name="selector">Only items that are part of a library that match this predicate will be returned.</param>
/// <returns>A queryable containing items that are part of a library matching the selector.</returns>
private IQueryable<LibraryItem> _LibraryRelatedQuery(Expression<Func<Library, bool>> selector)
=> _database.Libraries
.Where(selector)
.SelectMany(x => x.Shows)
.Where(x => !x.Collections.Any())
.Select(LibraryItem.FromShow)
.Concat(_database.Libraries
.Where(selector)
.SelectMany(x => x.Collections)
.Select(LibraryItem.FromCollection));
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
{
ICollection<LibraryItem> items = await ApplyFilters(
_LibraryRelatedQuery(x => x.ID == id),
where,
sort,
limit
);
if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null)
throw new ItemNotFoundException();
return items;
}
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
{
ICollection<LibraryItem> items = await ApplyFilters(
_LibraryRelatedQuery(x => x.Slug == slug),
where,
sort,
limit
);
if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null)
throw new ItemNotFoundException();
return items;
}
} }
} }

View File

@ -1,129 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle libraries.
/// </summary>
public class LibraryRepository : LocalRepository<Library>, ILibraryRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Sort<Library> DefaultSort => new Sort<Library>.By(x => x.ID);
/// <summary>
/// Create a new <see cref="LibraryRepository"/> instance.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">The provider repository</param>
public LibraryRepository(DatabaseContext database, IProviderRepository providers)
: base(database)
{
_database = database;
_providers = providers;
}
/// <inheritdoc />
public override async Task<ICollection<Library>> Search(string query)
{
return await Sort(
_database.Libraries
.Where(_database.Like<Library>(x => x.Name + " " + x.Slug, $"%{query}%"))
)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Library> Create(Library obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
protected override async Task Validate(Library resource)
{
await base.Validate(resource);
if (string.IsNullOrEmpty(resource.Slug))
throw new ArgumentException("The library's slug must be set and not empty");
if (string.IsNullOrEmpty(resource.Name))
throw new ArgumentException("The library's name must be set and not empty");
if (resource.Paths == null || !resource.Paths.Any())
throw new ArgumentException("The library should have a least one path.");
if (resource.Providers != null)
{
resource.Providers = await resource.Providers
.SelectAsync(async x =>
_database.LocalEntity<Provider>(x.Slug)
?? await _providers.CreateIfNotExists(x)
)
.ToListAsync();
_database.AttachRange(resource.Providers);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{
await Validate(changed);
if (changed.Providers != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.Providers).LoadAsync();
resource.Providers = changed.Providers;
}
}
/// <inheritdoc />
public override async Task Delete(Library obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
}
}
}

View File

@ -45,6 +45,11 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
protected DbContext Database { get; } protected DbContext Database { get; }
/// <summary>
/// The thumbnail manager used to store images.
/// </summary>
private readonly IThumbnailsManager _thumbs;
/// <summary> /// <summary>
/// The default sort order that will be used for this resource's type. /// The default sort order that will be used for this resource's type.
/// </summary> /// </summary>
@ -54,9 +59,11 @@ namespace Kyoo.Core.Controllers
/// Create a new base <see cref="LocalRepository{T}"/> with the given database handle. /// Create a new base <see cref="LocalRepository{T}"/> with the given database handle.
/// </summary> /// </summary>
/// <param name="database">A database connection to load resources of type <typeparamref name="T"/></param> /// <param name="database">A database connection to load resources of type <typeparamref name="T"/></param>
protected LocalRepository(DbContext database) /// <param name="thumbs">The thumbnail manager used to store images.</param>
protected LocalRepository(DbContext database, IThumbnailsManager thumbs)
{ {
Database = database; Database = database;
_thumbs = thumbs;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -112,7 +119,7 @@ namespace Kyoo.Core.Controllers
throw new SwitchExpressionException(); throw new SwitchExpressionException();
} }
} }
return _Sort(query, sortBy, false).ThenBy(x => x.ID); return _Sort(query, sortBy, false).ThenBy(x => x.Id);
} }
private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression( private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression(
@ -144,7 +151,7 @@ namespace Kyoo.Core.Controllers
/// <param name="reference">The reference item (the AfterID query)</param> /// <param name="reference">The reference item (the AfterID query)</param>
/// <param name="next">True if the following page should be returned, false for the previous.</param> /// <param name="next">True if the following page should be returned, false for the previous.</param>
/// <returns>An expression ready to be added to a Where close of a sorted query to handle the AfterID</returns> /// <returns>An expression ready to be added to a Where close of a sorted query to handle the AfterID</returns>
protected Expression<Func<T, bool>> KeysetPaginatate( protected Expression<Func<T, bool>> KeysetPaginate(
Sort<T> sort, Sort<T> sort,
T reference, T reference,
bool next = true) bool next = true)
@ -155,20 +162,20 @@ namespace Kyoo.Core.Controllers
ParameterExpression x = Expression.Parameter(typeof(T), "x"); ParameterExpression x = Expression.Parameter(typeof(T), "x");
ConstantExpression referenceC = Expression.Constant(reference, typeof(T)); ConstantExpression referenceC = Expression.Constant(reference, typeof(T));
IEnumerable<Sort<T>.By> _GetSortsBy(Sort<T> sort) IEnumerable<Sort<T>.By> GetSortsBy(Sort<T> sort)
{ {
return sort switch return sort switch
{ {
Sort<T>.Default => _GetSortsBy(DefaultSort), Sort<T>.Default => GetSortsBy(DefaultSort),
Sort<T>.By @sortBy => new[] { sortBy }, Sort<T>.By @sortBy => new[] { sortBy },
Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy), Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
_ => Array.Empty<Sort<T>.By>(), _ => Array.Empty<Sort<T>.By>(),
}; };
} }
// Don't forget that every sorts must end with a ID sort (to differenciate equalities). // Don't forget that every sorts must end with a ID sort (to differentiate equalities).
Sort<T>.By id = new(x => x.ID); Sort<T>.By id = new(x => x.Id);
IEnumerable<Sort<T>.By> sorts = _GetSortsBy(sort).Append(id); IEnumerable<Sort<T>.By> sorts = GetSortsBy(sort).Append(id);
BinaryExpression filter = null; BinaryExpression filter = null;
List<Sort<T>.By> previousSteps = new(); List<Sort<T>.By> previousSteps = new();
@ -180,9 +187,9 @@ namespace Kyoo.Core.Controllers
PropertyInfo property = typeof(T).GetProperty(key); PropertyInfo property = typeof(T).GetProperty(key);
// Comparing a value with null always return false so we short opt < > comparisons with null. // Comparing a value with null always return false so we short opt < > comparisons with null.
if (property.GetValue(reference) == null) if (property!.GetValue(reference) == null)
{ {
previousSteps.Add(new(key, desc)); previousSteps.Add(new Sort<T>.By(key, desc));
continue; continue;
} }
@ -206,7 +213,7 @@ namespace Kyoo.Core.Controllers
// Comparing a value with null always return false for nulls so we must add nulls to the results manually. // Comparing a value with null always return false for nulls so we must add nulls to the results manually.
// Postgres sorts them after values so we will do the same // Postgres sorts them after values so we will do the same
// We only add this condition if the collumn type is nullable // We only add this condition if the column type is nullable
if (Nullable.GetUnderlyingType(property.PropertyType) != null) if (Nullable.GetUnderlyingType(property.PropertyType) != null)
{ {
BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null)); BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null));
@ -223,7 +230,29 @@ namespace Kyoo.Core.Controllers
previousSteps.Add(new(key, desc)); previousSteps.Add(new(key, desc));
} }
return Expression.Lambda<Func<T, bool>>(filter, x); return Expression.Lambda<Func<T, bool>>(filter!, x);
}
protected void SetBackingImage(T obj)
{
if (obj is not IThumbnails thumbs)
return;
string type = obj is ILibraryItem item
? item.Kind.ToString().ToLowerInvariant()
: typeof(T).Name.ToLowerInvariant();
if (thumbs.Poster != null)
thumbs.Poster.Path = $"/{type}/{obj.Slug}/poster";
if (thumbs.Thumbnail != null)
thumbs.Thumbnail.Path = $"/{type}/{obj.Slug}/thumbnail";
if (thumbs.Logo != null)
thumbs.Logo.Path = $"/{type}/{obj.Slug}/logo";
}
protected T SetBackingImageSelf(T obj)
{
SetBackingImage(obj);
return obj;
} }
/// <summary> /// <summary>
@ -234,7 +263,8 @@ namespace Kyoo.Core.Controllers
/// <returns>The tracked resource with the given ID</returns> /// <returns>The tracked resource with the given ID</returns>
protected virtual async Task<T> GetWithTracking(int id) protected virtual async Task<T> GetWithTracking(int id)
{ {
T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id); T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.Id == id);
SetBackingImage(ret);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}"); throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret; return ret;
@ -270,30 +300,31 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<T> GetOrDefault(int id) public virtual Task<T> GetOrDefault(int id)
{ {
return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id); return Database.Set<T>().FirstOrDefaultAsync(x => x.Id == id).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<T> GetOrDefault(string slug) public virtual Task<T> GetOrDefault(string slug)
{ {
return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug); return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage);
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default) public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where, Sort<T> sortBy = default)
{ {
return Sort(Database.Set<T>(), sortBy).FirstOrDefaultAsync(where); return Sort(Database.Set<T>(), sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage);
} }
/// <inheritdoc/> /// <inheritdoc/>
public abstract Task<ICollection<T>> Search(string query); public abstract Task<ICollection<T>> Search(string query);
/// <inheritdoc/> /// <inheritdoc/>
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, public virtual async Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return ApplyFilters(Database.Set<T>(), where, sort, limit); return (await ApplyFilters(Database.Set<T>(), where, sort, limit))
.Select(SetBackingImageSelf).ToList();
} }
/// <summary> /// <summary>
@ -316,7 +347,7 @@ namespace Kyoo.Core.Controllers
if (limit?.AfterID != null) if (limit?.AfterID != null)
{ {
T reference = await Get(limit.AfterID.Value); T reference = await Get(limit.AfterID.Value);
query = query.Where(KeysetPaginatate(sort, reference, !limit.Reverse)); query = query.Where(KeysetPaginate(sort, reference, !limit.Reverse));
} }
if (limit?.Reverse == true) if (limit?.Reverse == true)
query = query.Reverse(); query = query.Reverse();
@ -338,9 +369,18 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<T> Create(T obj) public virtual async Task<T> Create(T obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
await Validate(obj); await Validate(obj);
if (obj is IThumbnails thumbs)
{
await _thumbs.DownloadImages(thumbs);
if (thumbs.Poster != null)
Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry.State = EntityState.Added;
if (thumbs.Thumbnail != null)
Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry.State = EntityState.Added;
if (thumbs.Logo != null)
Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry.State = EntityState.Added;
}
SetBackingImage(obj);
return obj; return obj;
} }
@ -367,9 +407,6 @@ namespace Kyoo.Core.Controllers
{ {
try try
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
T old = await GetOrDefault(obj.Slug); T old = await GetOrDefault(obj.Slug);
if (old != null) if (old != null)
return old; return old;
@ -383,23 +420,19 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<T> Edit(T edited, bool resetOld) public virtual async Task<T> Edit(T edited)
{ {
if (edited == null)
throw new ArgumentNullException(nameof(edited));
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false; Database.ChangeTracker.LazyLoadingEnabled = false;
try try
{ {
T old = await GetWithTracking(edited.ID); T old = await GetWithTracking(edited.Id);
if (resetOld)
old = Merger.Nullify(old);
Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null); Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
await EditRelations(old, edited, resetOld); await EditRelations(old, edited);
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
OnEdited?.Invoke(old); OnEdited?.Invoke(old);
SetBackingImage(old);
return old; return old;
} }
finally finally
@ -409,6 +442,30 @@ namespace Kyoo.Core.Controllers
} }
} }
/// <inheritdoc/>
public virtual async Task<T> Patch(int id, Func<T, Task<bool>> patch)
{
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false;
try
{
T resource = await GetWithTracking(id);
if (!await patch(resource))
throw new ArgumentException("Could not patch resource");
await Database.SaveChangesAsync();
OnEdited?.Invoke(resource);
SetBackingImage(resource);
return resource;
}
finally
{
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
Database.ChangeTracker.Clear();
}
}
/// <summary> /// <summary>
/// An overridable method to edit relation of a resource. /// An overridable method to edit relation of a resource.
/// </summary> /// </summary>
@ -419,12 +476,15 @@ namespace Kyoo.Core.Controllers
/// The new version of <paramref name="resource"/>. /// The new version of <paramref name="resource"/>.
/// This item will be saved on the database and replace <paramref name="resource"/> /// This item will be saved on the database and replace <paramref name="resource"/>
/// </param> /// </param>
/// <param name="resetOld">
/// A boolean to indicate if all values of resource should be discarded or not.
/// </param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected virtual Task EditRelations(T resource, T changed, bool resetOld) protected virtual Task EditRelations(T resource, T changed)
{ {
if (resource is IThumbnails thumbs && changed is IThumbnails chng)
{
Database.Entry(thumbs).Reference(x => x.Poster).IsModified = thumbs.Poster != chng.Poster;
Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != chng.Thumbnail;
Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo;
}
return Validate(resource); return Validate(resource);
} }

View File

@ -0,0 +1,142 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle shows
/// </summary>
public class MovieRepository : LocalRepository<Movie>, IMovieRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A studio repository to handle creation/validation of related studios.
/// </summary>
private readonly IStudioRepository _studios;
/// <summary>
/// A people repository to handle creation/validation of related people.
/// </summary>
private readonly IPeopleRepository _people;
/// <inheritdoc />
protected override Sort<Movie> DefaultSort => new Sort<Movie>.By(x => x.Name);
/// <summary>
/// Create a new <see cref="MovieRepository"/>.
/// </summary>
/// <param name="database">The database handle to use</param>
/// <param name="studios">A studio repository</param>
/// <param name="people">A people repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public MovieRepository(DatabaseContext database,
IStudioRepository studios,
IPeopleRepository people,
IThumbnailsManager thumbs)
: base(database, thumbs)
{
_database = database;
_studios = studios;
_people = people;
}
/// <inheritdoc />
public override async Task<ICollection<Movie>> Search(string query)
{
query = $"%{query}%";
return (await Sort(
_database.Movies
.Where(_database.Like<Movie>(x => x.Name + " " + x.Slug, query))
)
.Take(20)
.ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
}
/// <inheritdoc />
public override async Task<Movie> Create(Movie obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
protected override async Task Validate(Movie resource)
{
await base.Validate(resource);
if (resource.Studio != null)
{
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.StudioID = resource.Studio.Id;
}
if (resource.People != null)
{
foreach (PeopleRole role in resource.People)
{
role.People = _database.LocalEntity<People>(role.People.Slug)
?? await _people.CreateIfNotExists(role.People);
role.PeopleID = role.People.Id;
_database.Entry(role).State = EntityState.Added;
}
}
}
/// <inheritdoc />
protected override async Task EditRelations(Movie resource, Movie changed)
{
await Validate(changed);
if (changed.Studio != null || changed.StudioID == null)
{
await Database.Entry(resource).Reference(x => x.Studio).LoadAsync();
resource.Studio = changed.Studio;
}
if (changed.People != null)
{
await Database.Entry(resource).Collection(x => x.People).LoadAsync();
resource.People = changed.People;
}
}
/// <inheritdoc />
public override async Task Delete(Movie obj)
{
_database.Remove(obj);
await _database.SaveChangesAsync();
await base.Delete(obj);
}
}
}

View File

@ -39,11 +39,6 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <summary> /// <summary>
/// A lazy loaded show repository to validate requests from shows. /// A lazy loaded show repository to validate requests from shows.
/// </summary> /// </summary>
@ -56,27 +51,28 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="PeopleRepository"/> /// Create a new <see cref="PeopleRepository"/>
/// </summary> /// </summary>
/// <param name="database">The database handle</param> /// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A lazy loaded show repository</param> /// <param name="shows">A lazy loaded show repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public PeopleRepository(DatabaseContext database, public PeopleRepository(DatabaseContext database,
IProviderRepository providers, Lazy<IShowRepository> shows,
Lazy<IShowRepository> shows) IThumbnailsManager thumbs)
: base(database) : base(database, thumbs)
{ {
_database = database; _database = database;
_providers = providers;
_shows = shows; _shows = shows;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<People>> Search(string query) public override async Task<ICollection<People>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.People _database.People
.Where(_database.Like<People>(x => x.Name, $"%{query}%")) .Where(_database.Like<People>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -94,55 +90,34 @@ namespace Kyoo.Core.Controllers
{ {
await base.Validate(resource); await base.Validate(resource);
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<People>().AttachRange(resource.ExternalIDs);
}
if (resource.Roles != null) if (resource.Roles != null)
{ {
foreach (PeopleRole role in resource.Roles) foreach (PeopleRole role in resource.Roles)
{ {
role.Show = _database.LocalEntity<Show>(role.Show.Slug) role.Show = _database.LocalEntity<Show>(role.Show.Slug)
?? await _shows.Value.CreateIfNotExists(role.Show); ?? await _shows.Value.CreateIfNotExists(role.Show);
role.ShowID = role.Show.ID; role.ShowID = role.Show.Id;
_database.Entry(role).State = EntityState.Added; _database.Entry(role).State = EntityState.Added;
} }
} }
} }
/// <inheritdoc /> /// <inheritdoc />
protected override async Task EditRelations(People resource, People changed, bool resetOld) protected override async Task EditRelations(People resource, People changed)
{ {
await Validate(changed); await Validate(changed);
if (changed.Roles != null || resetOld) if (changed.Roles != null)
{ {
await Database.Entry(resource).Collection(x => x.Roles).LoadAsync(); await Database.Entry(resource).Collection(x => x.Roles).LoadAsync();
resource.Roles = changed.Roles; resource.Roles = changed.Roles;
} }
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(People obj) public override async Task Delete(People obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
obj.ExternalIDs.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted); obj.Roles.ForEach(x => _database.Entry(x).State = EntityState.Deleted);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);

View File

@ -1,98 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle providers.
/// </summary>
public class ProviderRepository : LocalRepository<Provider>, IProviderRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// Create a new <see cref="ProviderRepository" />.
/// </summary>
/// <param name="database">The database handle</param>
public ProviderRepository(DatabaseContext database)
: base(database)
{
_database = database;
}
/// <inheritdoc />
protected override Sort<Provider> DefaultSort => new Sort<Provider>.By(x => x.Slug);
/// <inheritdoc />
public override async Task<ICollection<Provider>> Search(string query)
{
return await Sort(
_database.Providers
.Where(_database.Like<Provider>(x => x.Name, $"%{query}%"))
)
.Take(20)
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Provider> Create(Provider obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj);
return obj;
}
/// <inheritdoc />
public override async Task Delete(Provider obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
}
/// <inheritdoc />
public async Task<ICollection<MetadataID>> GetMetadataID<T>(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)
where T : class, IMetadata
{
return await _database.MetadataIds<T>()
.Include(y => y.Provider)
.Where(where)
.ToListAsync();
}
}
}

View File

@ -24,6 +24,7 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
@ -38,11 +39,6 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc/> /// <inheritdoc/>
protected override Sort<Season> DefaultSort => new Sort<Season>.By(x => x.SeasonNumber); protected override Sort<Season> DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
@ -51,19 +47,18 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
/// <param name="database">The database handle that will be used</param> /// <param name="database">The database handle that will be used</param>
/// <param name="shows">A shows repository</param> /// <param name="shows">A shows repository</param>
/// <param name="providers">A provider repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
public SeasonRepository(DatabaseContext database, public SeasonRepository(DatabaseContext database,
IShowRepository shows, IShowRepository shows,
IProviderRepository providers) IThumbnailsManager thumbs)
: base(database) : base(database, thumbs)
{ {
_database = database; _database = database;
_providers = providers;
// Edit seasons slugs when the show's slug changes. // Edit seasons slugs when the show's slug changes.
shows.OnEdited += (show) => shows.OnEdited += (show) =>
{ {
List<Season> seasons = _database.Seasons.AsTracking().Where(x => x.ShowID == show.ID).ToList(); List<Season> seasons = _database.Seasons.AsTracking().Where(x => x.ShowId == show.Id).ToList();
foreach (Season season in seasons) foreach (Season season in seasons)
{ {
season.ShowSlug = show.Slug; season.ShowSlug = show.Slug;
@ -94,35 +89,37 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc/> /// <inheritdoc/>
public Task<Season> GetOrDefault(int showID, int seasonNumber) public Task<Season> GetOrDefault(int showID, int seasonNumber)
{ {
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber); && x.SeasonNumber == seasonNumber).Then(SetBackingImage);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<Season> GetOrDefault(string showSlug, int seasonNumber) public Task<Season> GetOrDefault(string showSlug, int seasonNumber)
{ {
return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber); && x.SeasonNumber == seasonNumber).Then(SetBackingImage);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query) public override async Task<ICollection<Season>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.Seasons _database.Seasons
.Where(_database.Like<Season>(x => x.Title, $"%{query}%")) .Where(_database.Like<Season>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<Season> Create(Season obj) public override async Task<Season> Create(Season obj)
{ {
await base.Create(obj); await base.Create(obj);
obj.ShowSlug = _database.Shows.First(x => x.ID == obj.ShowID).Slug; obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug;
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.ShowID, obj.SeasonNumber)); await _database.SaveChangesAsync(() => Get(obj.ShowId, obj.SeasonNumber));
OnResourceCreated(obj); OnResourceCreated(obj);
return obj; return obj;
} }
@ -131,46 +128,20 @@ namespace Kyoo.Core.Controllers
protected override async Task Validate(Season resource) protected override async Task Validate(Season resource)
{ {
await base.Validate(resource); await base.Validate(resource);
if (resource.ShowID <= 0) if (resource.ShowId <= 0)
{ {
if (resource.Show == null) if (resource.Show == null)
{ {
throw new ArgumentException($"Can't store a season not related to any show " + throw new ArgumentException($"Can't store a season not related to any show " +
$"(showID: {resource.ShowID})."); $"(showID: {resource.ShowId}).");
} }
resource.ShowID = resource.Show.ID; resource.ShowId = resource.Show.Id;
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc/>
protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{
await Validate(changed);
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task Delete(Season obj) public override async Task Delete(Season obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Remove(obj); _database.Remove(obj);
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);

View File

@ -47,18 +47,8 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly IPeopleRepository _people; private readonly IPeopleRepository _people;
/// <summary>
/// A genres repository to handle creation/validation of related genres.
/// </summary>
private readonly IGenreRepository _genres;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc /> /// <inheritdoc />
protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Title); protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Name);
/// <summary> /// <summary>
/// Create a new <see cref="ShowRepository"/>. /// Create a new <see cref="ShowRepository"/>.
@ -66,32 +56,30 @@ namespace Kyoo.Core.Controllers
/// <param name="database">The database handle to use</param> /// <param name="database">The database handle to use</param>
/// <param name="studios">A studio repository</param> /// <param name="studios">A studio repository</param>
/// <param name="people">A people repository</param> /// <param name="people">A people repository</param>
/// <param name="genres">A genres repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
/// <param name="providers">A provider repository</param>
public ShowRepository(DatabaseContext database, public ShowRepository(DatabaseContext database,
IStudioRepository studios, IStudioRepository studios,
IPeopleRepository people, IPeopleRepository people,
IGenreRepository genres, IThumbnailsManager thumbs)
IProviderRepository providers) : base(database, thumbs)
: base(database)
{ {
_database = database; _database = database;
_studios = studios; _studios = studios;
_people = people; _people = people;
_genres = genres;
_providers = providers;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Show>> Search(string query) public override async Task<ICollection<Show>> Search(string query)
{ {
query = $"%{query}%"; query = $"%{query}%";
return await Sort( return (await Sort(
_database.Shows _database.Shows
.Where(_database.Like<Show>(x => x.Title + " " + x.Slug, query)) .Where(_database.Like<Show>(x => x.Name + " " + x.Slug, query))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -107,32 +95,13 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
protected override async Task Validate(Show resource) protected override async Task Validate(Show resource)
{ {
resource.Slug ??= Utility.ToSlug(resource.Title); resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource); await base.Validate(resource);
if (resource.Studio != null) if (resource.Studio != null)
{ {
resource.Studio = await _studios.CreateIfNotExists(resource.Studio); resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.StudioID = resource.Studio.ID; resource.StudioId = resource.Studio.Id;
}
if (resource.Genres != null)
{
resource.Genres = await resource.Genres
.SelectAsync(x => _genres.CreateIfNotExists(x))
.ToListAsync();
_database.AttachRange(resource.Genres);
}
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
} }
if (resource.People != null) if (resource.People != null)
@ -141,70 +110,34 @@ namespace Kyoo.Core.Controllers
{ {
role.People = _database.LocalEntity<People>(role.People.Slug) role.People = _database.LocalEntity<People>(role.People.Slug)
?? await _people.CreateIfNotExists(role.People); ?? await _people.CreateIfNotExists(role.People);
role.PeopleID = role.People.ID; role.PeopleID = role.People.Id;
_database.Entry(role).State = EntityState.Added; _database.Entry(role).State = EntityState.Added;
} }
} }
} }
/// <inheritdoc /> /// <inheritdoc />
protected override async Task EditRelations(Show resource, Show changed, bool resetOld) protected override async Task EditRelations(Show resource, Show changed)
{ {
await Validate(changed); await Validate(changed);
if (changed.Aliases != null || resetOld) if (changed.Studio != null || changed.StudioId == null)
resource.Aliases = changed.Aliases;
if (changed.Studio != null || resetOld)
{ {
await Database.Entry(resource).Reference(x => x.Studio).LoadAsync(); await Database.Entry(resource).Reference(x => x.Studio).LoadAsync();
resource.Studio = changed.Studio; resource.Studio = changed.Studio;
} }
if (changed.Genres != null || resetOld) if (changed.People != null)
{
await Database.Entry(resource).Collection(x => x.Genres).LoadAsync();
resource.Genres = changed.Genres;
}
if (changed.People != null || resetOld)
{ {
await Database.Entry(resource).Collection(x => x.People).LoadAsync(); await Database.Entry(resource).Collection(x => x.People).LoadAsync();
resource.People = changed.People; resource.People = changed.People;
} }
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
}
/// <inheritdoc />
public async Task AddShowLink(int showID, int? libraryID, int? collectionID)
{
if (collectionID != null)
{
await _database.AddLinks<Collection, Show>(collectionID.Value, showID);
await _database.SaveIfNoDuplicates();
if (libraryID != null)
{
await _database.AddLinks<Library, Collection>(libraryID.Value, collectionID.Value);
await _database.SaveIfNoDuplicates();
}
}
if (libraryID != null)
{
await _database.AddLinks<Library, Show>(libraryID.Value, showID);
await _database.SaveIfNoDuplicates();
}
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<string> GetSlug(int showID) public Task<string> GetSlug(int showID)
{ {
return _database.Shows.Where(x => x.ID == showID) return _database.Shows.Where(x => x.Id == showID)
.Select(x => x.Slug) .Select(x => x.Slug)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -38,11 +37,6 @@ namespace Kyoo.Core.Controllers
/// </summary> /// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc /> /// <inheritdoc />
protected override Sort<Studio> DefaultSort => new Sort<Studio>.By(x => x.Name); protected override Sort<Studio> DefaultSort => new Sort<Studio>.By(x => x.Name);
@ -50,23 +44,24 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="StudioRepository"/>. /// Create a new <see cref="StudioRepository"/>.
/// </summary> /// </summary>
/// <param name="database">The database handle</param> /// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param> /// <param name="thumbs">The thumbnail manager used to store images.</param>
public StudioRepository(DatabaseContext database, IProviderRepository providers) public StudioRepository(DatabaseContext database, IThumbnailsManager thumbs)
: base(database) : base(database, thumbs)
{ {
_database = database; _database = database;
_providers = providers;
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<Studio>> Search(string query) public override async Task<ICollection<Studio>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.Studios _database.Studios
.Where(_database.Like<Studio>(x => x.Name, $"%{query}%")) .Where(_database.Like<Studio>(x => x.Name, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -83,38 +78,12 @@ namespace Kyoo.Core.Controllers
protected override async Task Validate(Studio resource) protected override async Task Validate(Studio resource)
{ {
resource.Slug ??= Utility.ToSlug(resource.Name); resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource); await base.Validate(resource);
if (resource.ExternalIDs != null)
{
foreach (MetadataID id in resource.ExternalIDs)
{
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
?? await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID;
}
_database.MetadataIds<Studio>().AttachRange(resource.ExternalIDs);
}
}
/// <inheritdoc />
protected override async Task EditRelations(Studio resource, Studio changed, bool resetOld)
{
if (changed.ExternalIDs != null || resetOld)
{
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
resource.ExternalIDs = changed.ExternalIDs;
}
await base.EditRelations(resource, changed, resetOld);
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(Studio obj) public override async Task Delete(Studio obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -44,8 +43,9 @@ namespace Kyoo.Core.Controllers
/// Create a new <see cref="UserRepository"/> /// Create a new <see cref="UserRepository"/>
/// </summary> /// </summary>
/// <param name="database">The database handle to use</param> /// <param name="database">The database handle to use</param>
public UserRepository(DatabaseContext database) /// <param name="thumbs">The thumbnail manager used to store images.</param>
: base(database) public UserRepository(DatabaseContext database, IThumbnailsManager thumbs)
: base(database, thumbs)
{ {
_database = database; _database = database;
} }
@ -53,12 +53,14 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task<ICollection<User>> Search(string query) public override async Task<ICollection<User>> Search(string query)
{ {
return await Sort( return (await Sort(
_database.Users _database.Users
.Where(_database.Like<User>(x => x.Username, $"%{query}%")) .Where(_database.Like<User>(x => x.Username, $"%{query}%"))
) )
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync())
.Select(SetBackingImageSelf)
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -66,6 +68,8 @@ namespace Kyoo.Core.Controllers
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
if (obj.Logo != null)
_database.Entry(obj).Reference(x => x.Logo).TargetEntry.State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.Slug)); await _database.SaveChangesAsync(() => Get(obj.Slug));
OnResourceCreated(obj); OnResourceCreated(obj);
return obj; return obj;
@ -74,9 +78,6 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(User obj) public override async Task Delete(User obj)
{ {
if (obj == null)
throw new ArgumentNullException(nameof(obj));
_database.Entry(obj).State = EntityState.Deleted; _database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
await base.Delete(obj); await base.Delete(obj);

View File

@ -18,13 +18,13 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Blurhash.SkiaSharp;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SkiaSharp;
#nullable enable #nullable enable
@ -54,105 +54,77 @@ namespace Kyoo.Core.Controllers
_logger = logger; _logger = logger;
} }
/// <summary> private static async Task _WriteTo(SKBitmap bitmap, string path, int quality)
/// An helper function to download an image.
/// </summary>
/// <param name="url">The distant url of the image</param>
/// <param name="localPath">The local path of the image</param>
/// <param name="what">What is currently downloaded (used for errors)</param>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
private async Task<bool> _DownloadImage(string url, string localPath, string what)
{ {
if (url == localPath) SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, quality);
return false; await using Stream reader = data.AsStream();
await using Stream file = File.Create(path);
await reader.CopyToAsync(file);
}
private async Task _DownloadImage(Image? image, string localPath, string what)
{
if (image == null)
return;
try try
{ {
_logger.LogInformation("Downloading image {What}", what); _logger.LogInformation("Downloading image {What}", what);
HttpClient client = _clientFactory.CreateClient(); HttpClient client = _clientFactory.CreateClient();
HttpResponseMessage response = await client.GetAsync(url); HttpResponseMessage response = await client.GetAsync(image.Source);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
string mime = response.Content.Headers.ContentType?.MediaType!;
await using Stream reader = await response.Content.ReadAsStreamAsync(); await using Stream reader = await response.Content.ReadAsStreamAsync();
using SKCodec codec = SKCodec.Create(reader);
SKImageInfo info = codec.Info;
info.ColorType = SKColorType.Rgba8888;
using SKBitmap original = SKBitmap.Decode(codec, info);
string extension = new FileExtensionContentTypeProvider() using SKBitmap high = original.Resize(new SKSizeI(original.Width, original.Height), SKFilterQuality.High);
.Mappings.FirstOrDefault(x => x.Value == mime) await _WriteTo(original, $"{localPath}.{ImageQuality.High.ToString().ToLowerInvariant()}.webp", 80);
.Key;
await using Stream local = File.Create(localPath + extension); using SKBitmap medium = high.Resize(new SKSizeI((int)(high.Width / 1.5), (int)(high.Height / 1.5)), SKFilterQuality.Medium);
await reader.CopyToAsync(local); await _WriteTo(medium, $"{localPath}.{ImageQuality.Medium.ToString().ToLowerInvariant()}.webp", 55);
return true;
using SKBitmap low = medium.Resize(new SKSizeI(original.Width / 2, original.Height / 2), SKFilterQuality.Low);
await _WriteTo(low, $"{localPath}.{ImageQuality.Low.ToString().ToLowerInvariant()}.webp", 25);
image.Blurhash = Blurhasher.Encode(low, 4, 3);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "{What} could not be downloaded", what); _logger.LogError(ex, "{What} could not be downloaded", what);
return false;
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> DownloadImages<T>(T item, bool alwaysDownload = false) public async Task DownloadImages<T>(T item)
where T : IThumbnails where T : IThumbnails
{ {
if (item == null) if (item == null)
throw new ArgumentNullException(nameof(item)); throw new ArgumentNullException(nameof(item));
if (item.Images == null)
return false;
string name = item is IResource res ? res.Slug : "???"; string name = item is IResource res ? res.Slug : "???";
bool ret = false; await _DownloadImage(item.Poster, _GetBaseImagePath(item, "poster"), $"The poster of {name}");
await _DownloadImage(item.Thumbnail, _GetBaseImagePath(item, "thumbnail"), $"The poster of {name}");
foreach ((int id, string image) in item.Images.Where(x => x.Value != null)) await _DownloadImage(item.Logo, _GetBaseImagePath(item, "logo"), $"The poster of {name}");
{
string localPath = _GetPrivateImagePath(item, id);
if (alwaysDownload || !Path.Exists(localPath))
ret |= await _DownloadImage(image, localPath, $"The image n {id} of {name}");
} }
return ret; private static string _GetBaseImagePath<T>(T item, string image)
}
/// <summary>
/// Retrieve the local path of an image of the given item <b>without an extension</b>.
/// </summary>
/// <param name="item">The item to retrieve the poster from.</param>
/// <param name="imageId">The ID of the image. See <see cref="Images"/> for values.</param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns>The path of the image for the given resource, <b>even if it does not exists</b></returns>
private static string _GetPrivateImagePath<T>(T item, int imageId)
{ {
if (item == null)
throw new ArgumentNullException(nameof(item));
string directory = item switch string directory = item switch
{ {
IResource res => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant(), res.Slug), IResource res => Path.Combine("./metadata", item.GetType().Name.ToLowerInvariant(), res.Slug),
_ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant()) _ => Path.Combine("./metadata", typeof(T).Name.ToLowerInvariant())
}; };
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
string imageName = imageId switch return Path.Combine(directory, image);
{
Images.Poster => "poster",
Images.Logo => "logo",
Images.Thumbnail => "thumbnail",
Images.Trailer => "trailer",
_ => $"{imageId}"
};
return Path.Combine(directory, imageName);
} }
/// <inheritdoc /> /// <inheritdoc />
public string? GetImagePath<T>(T item, int imageId) public string GetImagePath<T>(T item, string image, ImageQuality quality)
where T : IThumbnails where T : IThumbnails
{ {
string basePath = _GetPrivateImagePath(item, imageId); return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp";
string directory = Path.GetDirectoryName(basePath)!;
string baseFile = Path.GetFileName(basePath);
if (!Directory.Exists(directory))
return null;
return Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly)
.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x) == baseFile);
} }
} }
} }

View File

@ -30,6 +30,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using JsonOptions = Kyoo.Core.Api.JsonOptions; using JsonOptions = Kyoo.Core.Api.JsonOptions;
namespace Kyoo.Core namespace Kyoo.Core
@ -48,16 +49,14 @@ namespace Kyoo.Core
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope(); builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope(); builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
builder.RegisterRepository<ILibraryRepository, LibraryRepository>();
builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>(); builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>();
builder.RegisterRepository<ICollectionRepository, CollectionRepository>(); builder.RegisterRepository<ICollectionRepository, CollectionRepository>();
builder.RegisterRepository<IMovieRepository, MovieRepository>();
builder.RegisterRepository<IShowRepository, ShowRepository>(); builder.RegisterRepository<IShowRepository, ShowRepository>();
builder.RegisterRepository<ISeasonRepository, SeasonRepository>(); builder.RegisterRepository<ISeasonRepository, SeasonRepository>();
builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>(); builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>();
builder.RegisterRepository<IPeopleRepository, PeopleRepository>(); builder.RegisterRepository<IPeopleRepository, PeopleRepository>();
builder.RegisterRepository<IStudioRepository, StudioRepository>(); builder.RegisterRepository<IStudioRepository, StudioRepository>();
builder.RegisterRepository<IGenreRepository, GenreRepository>();
builder.RegisterRepository<IProviderRepository, ProviderRepository>();
builder.RegisterRepository<IUserRepository, UserRepository>(); builder.RegisterRepository<IUserRepository, UserRepository>();
} }
@ -74,6 +73,7 @@ namespace Kyoo.Core
.AddNewtonsoftJson(x => .AddNewtonsoftJson(x =>
{ {
x.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; x.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
x.SerializerSettings.Converters.Add(new StringEnumConverter());
}) })
.AddDataAnnotations() .AddDataAnnotations()
.AddControllersAsServices() .AddControllersAsServices()

View File

@ -6,9 +6,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.Proxy" Version="4.4.0" /> <PackageReference Include="AspNetCore.Proxy" Version="4.4.0" />
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -16,12 +16,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -161,12 +163,12 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Edit([FromBody] T resource) public async Task<ActionResult<T>> Edit([FromBody] T resource)
{ {
if (resource.ID > 0) if (resource.Id > 0)
return await Repository.Edit(resource, true); return await Repository.Edit(resource);
T old = await Repository.Get(resource.Slug); T old = await Repository.Get(resource.Slug);
resource.ID = old.ID; resource.Id = old.Id;
return await Repository.Edit(resource, true); return await Repository.Edit(resource);
} }
/// <summary> /// <summary>
@ -185,14 +187,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Patch([FromBody] T resource) public async Task<ActionResult<T>> Patch([FromBody] PartialResource resource)
{ {
if (resource.ID > 0) if (resource.Id.HasValue)
return await Repository.Edit(resource, false); return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync);
if (resource.Slug == null)
throw new ArgumentException("Either the Id or the slug of the resource has to be defined to edit it.");
T old = await Repository.Get(resource.Slug); T old = await Repository.Get(resource.Slug);
resource.ID = old.ID; return await Repository.Patch(old.Id, TryUpdateModelAsync);
return await Repository.Edit(resource, false);
} }
/// <summary> /// <summary>

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -25,7 +24,6 @@ using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using static Kyoo.Abstractions.Models.Utils.Constants; using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
@ -59,37 +57,7 @@ namespace Kyoo.Core.Api
_thumbs = thumbs; _thumbs = thumbs;
} }
/// <summary> private async Task<IActionResult> _GetImage(Identifier identifier, string image, ImageQuality? quality)
/// Get the content type of a file using it's extension.
/// </summary>
/// <param name="path">The path of the file</param>
/// <exception cref="NotImplementedException">The extension of the file is not known.</exception>
/// <returns>The content type of the file</returns>
private static string _GetContentType(string path)
{
FileExtensionContentTypeProvider provider = new();
if (provider.TryGetContentType(path, out string contentType))
return contentType;
throw new NotImplementedException($"Can't get the content type of the file at: {path}");
}
/// <summary>
/// Get image
/// </summary>
/// <remarks>
/// Get an image for the specified item.
/// List of commonly available images:<br/>
/// - Poster: Image 0, also available at /poster<br/>
/// - Thumbnail: Image 1, also available at /thumbnail<br/>
/// - Logo: Image 3, also available at /logo<br/>
/// <br/>
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
private async Task<IActionResult> _GetImage(Identifier identifier, int image)
{ {
T resource = await identifier.Match( T resource = await identifier.Match(
id => Repository.GetOrDefault(id), id => Repository.GetOrDefault(id),
@ -97,10 +65,10 @@ namespace Kyoo.Core.Api
); );
if (resource == null) if (resource == null)
return NotFound(); return NotFound();
string path = _thumbs.GetImagePath(resource, image); string path = _thumbs.GetImagePath(resource, image, quality ?? ImageQuality.High);
if (path == null || !System.IO.File.Exists(path)) if (path == null || !System.IO.File.Exists(path))
return NotFound(); return NotFound();
return PhysicalFile(Path.GetFullPath(path), _GetContentType(path), true); return PhysicalFile(Path.GetFullPath(path), "image/webp", true);
} }
/// <summary> /// <summary>
@ -110,17 +78,18 @@ namespace Kyoo.Core.Api
/// Get the poster for the specified item. /// Get the poster for the specified item.
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param> /// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns> /// <returns>The image asked.</returns>
/// <response code="404"> /// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo. /// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response> /// </response>
[HttpGet("{identifier:id}/poster", Order = AlternativeRoute)] [HttpGet("{identifier:id}/poster")]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetPoster(Identifier identifier) public Task<IActionResult> GetPoster(Identifier identifier, [FromQuery] ImageQuality? quality)
{ {
return _GetImage(identifier, Images.Poster); return _GetImage(identifier, "poster", quality);
} }
/// <summary> /// <summary>
@ -130,17 +99,18 @@ namespace Kyoo.Core.Api
/// Get the logo for the specified item. /// Get the logo for the specified item.
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param> /// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns> /// <returns>The image asked.</returns>
/// <response code="404"> /// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo. /// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response> /// </response>
[HttpGet("{identifier:id}/logo", Order = AlternativeRoute)] [HttpGet("{identifier:id}/logo")]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetLogo(Identifier identifier) public Task<IActionResult> GetLogo(Identifier identifier, [FromQuery] ImageQuality? quality)
{ {
return _GetImage(identifier, Images.Logo); return _GetImage(identifier, "logo", quality);
} }
/// <summary> /// <summary>
@ -150,24 +120,16 @@ namespace Kyoo.Core.Api
/// Get the thumbnail for the specified item. /// Get the thumbnail for the specified item.
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param> /// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="quality">The quality of the image to retrieve.</param>
/// <returns>The image asked.</returns> /// <returns>The image asked.</returns>
/// <response code="404"> /// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo. /// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response> /// </response>
[HttpGet("{identifier:id}/thumbnail")]
[HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)] [HttpGet("{identifier:id}/backdrop", Order = AlternativeRoute)]
[HttpGet("{identifier:id}/thumbnail", Order = AlternativeRoute)] public Task<IActionResult> GetBackdrop(Identifier identifier, [FromQuery] ImageQuality? quality)
public Task<IActionResult> GetBackdrop(Identifier identifier)
{ {
return _GetImage(identifier, Images.Thumbnail); return _GetImage(identifier, "thumbnail", quality);
}
/// <inheritdoc/>
public override async Task<ActionResult<T>> Create([FromBody] T resource)
{
// TODO: Remove this method and use a websocket API to do that.
ActionResult<T> ret = await base.Create(resource);
await _thumbs.DownloadImages(ret.Value);
return ret;
} }
} }
} }

View File

@ -16,9 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection; using System.Reflection;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
@ -74,78 +72,5 @@ namespace Kyoo.Core.Api
property.ShouldDeserialize = _ => false; property.ShouldDeserialize = _ => false;
return property; return property;
} }
/// <inheritdoc />
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
if (!type.IsAssignableTo(typeof(IThumbnails)))
return properties;
foreach ((int id, string image) in Images.ImageName)
{
properties.Add(new JsonProperty
{
DeclaringType = type,
PropertyName = image.ToLower(),
UnderlyingName = image,
PropertyType = typeof(string),
Readable = true,
Writable = false,
ItemIsReference = false,
TypeNameHandling = TypeNameHandling.None,
ShouldSerialize = x =>
{
IThumbnails thumb = (IThumbnails)x;
return thumb?.Images?.ContainsKey(id) == true;
},
ValueProvider = new ThumbnailProvider(id)
});
}
return properties;
}
/// <summary>
/// A custom <see cref="IValueProvider"/> that uses the
/// <see cref="IThumbnails"/>.<see cref="IThumbnails.Images"/> as a value.
/// </summary>
private class ThumbnailProvider : IValueProvider
{
/// <summary>
/// The index/ID of the image to retrieve/set.
/// </summary>
private readonly int _imageIndex;
/// <summary>
/// Create a new <see cref="ThumbnailProvider"/>.
/// </summary>
/// <param name="imageIndex">The index/ID of the image to retrieve/set.</param>
public ThumbnailProvider(int imageIndex)
{
_imageIndex = imageIndex;
}
/// <inheritdoc />
public void SetValue(object target, object value)
{
if (target is not IThumbnails thumb)
throw new ArgumentException($"The given object is not an Thumbnail.");
thumb.Images[_imageIndex] = value as string;
}
/// <inheritdoc />
public object GetValue(object target)
{
string slug = (target as IResource)?.Slug ?? (target as ICustomTypeDescriptor)?.GetComponentName();
if (target is not IThumbnails thumb
|| slug == null
|| string.IsNullOrEmpty(thumb.Images?.GetValueOrDefault(_imageIndex)))
return null;
string type = target is ICustomTypeDescriptor descriptor
? descriptor.GetClassName()
: target.GetType().Name;
return $"/{type}/{slug}/{Images.ImageName[_imageIndex]}".ToLowerInvariant();
}
}
} }
} }

View File

@ -1,95 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Genre"/>.
/// </summary>
[Route("genres")]
[Route("genre", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Genre))]
[ApiDefinition("Genres", Group = MetadataGroup)]
public class GenreApi : CrudApi<Genre>
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="GenreApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
public GenreApi(ILibraryManager libraryManager)
: base(libraryManager.GenreRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows with genre
/// </summary>
/// <remarks>
/// Lists the shows that have the selected genre.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Genre"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of shows to return and where to start.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No genre with the given ID could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Genre>(x => x.Genres)),
Sort<Show>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
}
}

View File

@ -83,7 +83,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination) [FromQuery] Pagination pagination)
{ {
ICollection<Show> resources = await _libraryManager.GetAll( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioID, x => x.Studio.Slug)), ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio.Slug)),
Sort<Show>.From(sortBy), Sort<Show>.From(sortBy),
pagination pagination
); );

View File

@ -93,40 +93,5 @@ namespace Kyoo.Core.Api
return NotFound(); return NotFound();
return Page(resources, pagination.Limit); return Page(resources, pagination.Limit);
} }
/// <summary>
/// Get libraries containing this collection
/// </summary>
/// <remarks>
/// Lists the libraries that contain the collection with the given id or slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort libraries by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of libraries to return.</param>
/// <returns>A page of libraries.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/libraries")]
[HttpGet("{identifier:id}/library", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Collection>(x => x.Collections)),
Sort<Library>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
} }
} }

View File

@ -1,171 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Library"/>.
/// </summary>
[Route("libraries")]
[Route("library", Order = AlternativeRoute)]
[ApiController]
[ResourceView]
[PartialPermission(nameof(Library), Group = Group.Admin)]
[ApiDefinition("Library", Group = ResourcesGroup)]
public class LibraryApi : CrudApi<Library>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public LibraryApi(ILibraryManager libraryManager)
: base(libraryManager.LibraryRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows
/// </summary>
/// <remarks>
/// List the shows that are part of this library.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of shows to return.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Library>(x => x.Libraries)),
Sort<Show>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
/// <summary>
/// Get collections
/// </summary>
/// <remarks>
/// List the collections that are part of this library.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of collections to return.</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/collections")]
[HttpGet("{identifier:id}/collection", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Library>(x => x.Libraries)),
Sort<Collection>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
/// <summary>
/// Get items
/// </summary>
/// <remarks>
/// List all items of this library.
/// An item can ether represent a collection or a show.
/// This endpoint allow one to retrieve all collections and shows that are not contained in a collection.
/// This is what is displayed on the /browse/library page of the webapp.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort items by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of items to return.</param>
/// <returns>A page of items.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/items")]
[HttpGet("{identifier:id}/item", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<LibraryItem>>> GetItems(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
Expression<Func<LibraryItem, bool>> whereQuery = ApiHelper.ParseWhere<LibraryItem>(where);
Sort<LibraryItem> sort = Sort<LibraryItem>.From(sortBy);
ICollection<LibraryItem> resources = await identifier.Match(
id => _libraryManager.GetItemsFromLibrary(id, whereQuery, sort, pagination),
slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination)
);
return Page(resources, pagination.Limit);
}
}
}

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -38,7 +37,7 @@ namespace Kyoo.Core.Api
[Route("item", Order = AlternativeRoute)] [Route("item", Order = AlternativeRoute)]
[ApiController] [ApiController]
[ResourceView] [ResourceView]
[PartialPermission(nameof(LibraryItem))] [PartialPermission("LibraryItem")]
[ApiDefinition("Items", Group = ResourcesGroup)] [ApiDefinition("Items", Group = ResourcesGroup)]
public class LibraryItemApi : BaseApi public class LibraryItemApi : BaseApi
{ {
@ -78,14 +77,14 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<LibraryItem>>> GetAll( public async Task<ActionResult<Page<ILibraryItem>>> GetAll(
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination) [FromQuery] Pagination pagination)
{ {
ICollection<LibraryItem> resources = await _libraryItems.GetAll( ICollection<ILibraryItem> resources = await _libraryItems.GetAll(
ApiHelper.ParseWhere<LibraryItem>(where), ApiHelper.ParseWhere<ILibraryItem>(where),
Sort<LibraryItem>.From(sortBy), Sort<ILibraryItem>.From(sortBy),
pagination pagination
); );

View File

@ -0,0 +1,149 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Information about one or multiple <see cref="Movie"/>.
/// </summary>
[Route("movies")]
[Route("movie", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(Show))]
[ApiDefinition("Shows", Group = ResourcesGroup)]
public class MovieApi : CrudThumbsApi<Movie>
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="ShowApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public MovieApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.MovieRepository, thumbs)
{
_libraryManager = libraryManager;
}
// /// <summary>
// /// Get staff
// /// </summary>
// /// <remarks>
// /// List staff members that made this show.
// /// </remarks>
// /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
// /// <param name="sortBy">A key to sort staff members by.</param>
// /// <param name="where">An optional list of filters.</param>
// /// <param name="pagination">The number of people to return.</param>
// /// <returns>A page of people.</returns>
// /// <response code="400">The filters or the sort parameters are invalid.</response>
// /// <response code="404">No show with the given ID or slug could be found.</response>
// [HttpGet("{identifier:id}/staff")]
// [HttpGet("{identifier:id}/people", Order = AlternativeRoute)]
// [PartialPermission(Kind.Read)]
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<Page<PeopleRole>>> GetPeople(Identifier identifier,
// [FromQuery] string sortBy,
// [FromQuery] Dictionary<string, string> where,
// [FromQuery] Pagination pagination)
// {
// Expression<Func<PeopleRole, bool>> whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
// Sort<PeopleRole> sort = Sort<PeopleRole>.From(sortBy);
//
// ICollection<PeopleRole> resources = await identifier.Match(
// id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination),
// slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination)
// );
// return Page(resources, pagination.Limit);
// }
/// <summary>
/// Get studio that made the show
/// </summary>
/// <remarks>
/// Get the studio that made the show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <returns>The studio that made the show.</returns>
/// <response code="404">No show with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/studio")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Studio>> GetStudio(Identifier identifier)
{
return await _libraryManager.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies));
}
/// <summary>
/// Get collections containing this show
/// </summary>
/// <remarks>
/// List the collections that contain this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of collections to return.</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/collections")]
[HttpGet("{identifier:id}/collection", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
Sort<Collection>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Movie>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
}
}

View File

@ -76,10 +76,11 @@ namespace Kyoo.Core.Api
{ {
Query = query, Query = query,
Collections = await _libraryManager.Search<Collection>(query), Collections = await _libraryManager.Search<Collection>(query),
Items = await _libraryManager.Search<ILibraryItem>(query),
Movies = await _libraryManager.Search<Movie>(query),
Shows = await _libraryManager.Search<Show>(query), Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query), Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query), People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query) Studios = await _libraryManager.Search<Studio>(query)
}; };
} }
@ -133,9 +134,9 @@ namespace Kyoo.Core.Api
[Permission(nameof(Show), Kind.Read)] [Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Items")] [ApiDefinition("Items")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<LibraryItem>> SearchItems(string query) public Task<ICollection<ILibraryItem>> SearchItems(string query)
{ {
return _libraryManager.Search<LibraryItem>(query); return _libraryManager.Search<ILibraryItem>(query);
} }
/// <summary> /// <summary>
@ -175,24 +176,6 @@ namespace Kyoo.Core.Api
return _libraryManager.Search<People>(query); return _libraryManager.Search<People>(query);
} }
/// <summary>
/// Search genres
/// </summary>
/// <remarks>
/// Search for genres
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of genres found for the specified query.</returns>
[HttpGet("genres")]
[HttpGet("genre", Order = AlternativeRoute)]
[Permission(nameof(Genre), Kind.Read)]
[ApiDefinition("Genres")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.Search<Genre>(query);
}
/// <summary> /// <summary>
/// Search studios /// Search studios
/// </summary> /// </summary>

View File

@ -84,7 +84,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination) [FromQuery] Pagination pagination)
{ {
ICollection<Episode> resources = await _libraryManager.GetAll( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonID, x => x.Season.Slug)), ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season.Slug)),
Sort<Episode>.From(sortBy), Sort<Episode>.From(sortBy),
pagination pagination
); );

View File

@ -37,8 +37,6 @@ namespace Kyoo.Core.Api
/// </summary> /// </summary>
[Route("shows")] [Route("shows")]
[Route("show", Order = AlternativeRoute)] [Route("show", Order = AlternativeRoute)]
[Route("movie", Order = AlternativeRoute)]
[Route("movies", Order = AlternativeRoute)]
[ApiController] [ApiController]
[PartialPermission(nameof(Show))] [PartialPermission(nameof(Show))]
[ApiDefinition("Shows", Group = ResourcesGroup)] [ApiDefinition("Shows", Group = ResourcesGroup)]
@ -63,24 +61,6 @@ namespace Kyoo.Core.Api
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
/// <inheritdoc/>
public override async Task<ActionResult<Show>> Create([FromBody] Show resource)
{
ActionResult<Show> ret = await base.Create(resource);
if (ret.Value.IsMovie)
{
Episode episode = new()
{
Show = ret.Value,
Title = ret.Value.Title,
Path = ret.Value.Path
};
await _libraryManager.Create(episode);
}
return ret;
}
/// <summary> /// <summary>
/// Get seasons of this show /// Get seasons of this show
/// </summary> /// </summary>
@ -106,7 +86,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination) [FromQuery] Pagination pagination)
{ {
ICollection<Season> resources = await _libraryManager.GetAll( ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)), ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show.Slug)),
Sort<Season>.From(sortBy), Sort<Season>.From(sortBy),
pagination pagination
); );
@ -141,7 +121,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination) [FromQuery] Pagination pagination)
{ {
ICollection<Episode> resources = await _libraryManager.GetAll( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowID, x => x.Show.Slug)), ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show.Slug)),
Sort<Episode>.From(sortBy), Sort<Episode>.From(sortBy),
pagination pagination
); );
@ -185,41 +165,6 @@ namespace Kyoo.Core.Api
return Page(resources, pagination.Limit); return Page(resources, pagination.Limit);
} }
/// <summary>
/// Get genres of this show
/// </summary>
/// <remarks>
/// List the genres that represent this show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort genres by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of genres to return.</param>
/// <returns>A page of genres.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/genres")]
[HttpGet("{identifier:id}/genre", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Genre>>> GetGenres(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Genre, Show>(x => x.Shows)),
Sort<Genre>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
/// <summary> /// <summary>
/// Get studio that made the show /// Get studio that made the show
/// </summary> /// </summary>
@ -238,42 +183,6 @@ namespace Kyoo.Core.Api
return await _libraryManager.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows)); return await _libraryManager.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows));
} }
/// <summary>
/// Get libraries containing this show
/// </summary>
/// <remarks>
/// List the libraries that contain this show. If this show is contained in a collection that is contained in
/// a library, this library will be returned too.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort libraries by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of libraries to return.</param>
/// <returns>A page of libraries.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No show with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/libraries")]
[HttpGet("{identifier:id}/library", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Library, Show>(x => x.Shows)),
Sort<Library>.From(sortBy),
pagination
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
/// <summary> /// <summary>
/// Get collections containing this show /// Get collections containing this show
/// </summary> /// </summary>

View File

@ -1,91 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Net.Http;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// Retrieve information of an <see cref="Episode"/> as a <see cref="WatchItem"/>.
/// A watch item is another representation of an episode in a form easier to read and display for playback.
/// It contains streams (video, audio, subtitles) information, chapters, next and previous episodes and a bit of
/// information of the show.
/// </summary>
[Route("watch")]
[Route("watchitem", Order = AlternativeRoute)]
[ApiController]
[ApiDefinition("Watch Items", Group = WatchGroup)]
public class WatchApi : ControllerBase
{
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The http client to reach transcoder.
/// </summary>
private readonly HttpClient _client;
/// <summary>
/// Create a new <see cref="WatchApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="client">The http client to reach transcoder.</param>
public WatchApi(ILibraryManager libraryManager, HttpClient client)
{
_libraryManager = libraryManager;
_client = client;
}
/// <summary>
/// Get a watch item
/// </summary>
/// <remarks>
/// Retrieve a watch item of an episode.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>A page of items.</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}")]
[Permission("watch", Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<WatchItem>> GetWatchItem(Identifier identifier)
{
Episode item = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
);
if (item == null)
return NotFound();
return await WatchItem.FromEpisode(item, _libraryManager, _client);
}
}
}

View File

@ -20,6 +20,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -40,16 +41,16 @@ namespace Kyoo.Postgresql
/// </remarks> /// </remarks>
public abstract class DatabaseContext : DbContext public abstract class DatabaseContext : DbContext
{ {
/// <summary>
/// All libraries of Kyoo. See <see cref="Library"/>.
/// </summary>
public DbSet<Library> Libraries { get; set; }
/// <summary> /// <summary>
/// All collections of Kyoo. See <see cref="Collection"/>. /// All collections of Kyoo. See <see cref="Collection"/>.
/// </summary> /// </summary>
public DbSet<Collection> Collections { get; set; } public DbSet<Collection> Collections { get; set; }
/// <summary>
/// All movies of Kyoo. See <see cref="Movie"/>.
/// </summary>
public DbSet<Movie> Movies { get; set; }
/// <summary> /// <summary>
/// All shows of Kyoo. See <see cref="Show"/>. /// All shows of Kyoo. See <see cref="Show"/>.
/// </summary> /// </summary>
@ -65,11 +66,6 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
public DbSet<Episode> Episodes { get; set; } public DbSet<Episode> Episodes { get; set; }
/// <summary>
/// All genres of Kyoo. See <see cref="Genres"/>.
/// </summary>
public DbSet<Genre> Genres { get; set; }
/// <summary> /// <summary>
/// All people of Kyoo. See <see cref="People"/>. /// All people of Kyoo. See <see cref="People"/>.
/// </summary> /// </summary>
@ -80,11 +76,6 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
public DbSet<Studio> Studios { get; set; } public DbSet<Studio> Studios { get; set; }
/// <summary>
/// All providers of Kyoo. See <see cref="Provider"/>.
/// </summary>
public DbSet<Provider> Providers { get; set; }
/// <summary> /// <summary>
/// The list of registered users. /// The list of registered users.
/// </summary> /// </summary>
@ -95,11 +86,6 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
public DbSet<PeopleRole> PeopleRoles { get; set; } public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>.
/// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
/// <summary> /// <summary>
/// The list of library items (shows and collections that are part of a library - or the global one). /// The list of library items (shows and collections that are part of a library - or the global one).
/// </summary> /// </summary>
@ -108,17 +94,6 @@ namespace Kyoo.Postgresql
/// </remarks> /// </remarks>
public DbSet<LibraryItem> LibraryItems { get; set; } public DbSet<LibraryItem> LibraryItems { get; set; }
/// <summary>
/// Get all metadataIDs (ExternalIDs) of a given resource. See <see cref="MetadataID"/>.
/// </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> MetadataIds<T>()
where T : class, IMetadata
{
return Set<MetadataID>(MetadataName<T>());
}
/// <summary> /// <summary>
/// Add a many to many link between two resources. /// Add a many to many link between two resources.
/// </summary> /// </summary>
@ -153,14 +128,6 @@ namespace Kyoo.Postgresql
: base(options) : base(options)
{ } { }
/// <summary>
/// Get the name of the metadata table of the given type.
/// </summary>
/// <typeparam name="T">The type related to the metadata</typeparam>
/// <returns>The name of the table containing the metadata.</returns>
protected abstract string MetadataName<T>()
where T : IMetadata;
/// <summary> /// <summary>
/// Get the name of the link table of the two given types. /// Get the name of the link table of the two given types.
/// </summary> /// </summary>
@ -194,17 +161,33 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
/// <param name="modelBuilder">The database model builder</param> /// <param name="modelBuilder">The database model builder</param>
/// <typeparam name="T">The type to add metadata to.</typeparam> /// <typeparam name="T">The type to add metadata to.</typeparam>
private void _HasMetadata<T>(ModelBuilder modelBuilder) private static void _HasMetadata<T>(ModelBuilder modelBuilder)
where T : class, IMetadata where T : class, IMetadata
{ {
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>()) // TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
.HasKey(MetadataID.PrimaryKey); // modelBuilder.Entity<T>()
// .OwnsOne(x => x.ExternalIDs, x =>
// {
// x.ToJson();
// });
modelBuilder.Entity<T>()
.Property(x => x.ExternalId)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Dictionary<string, MetadataId>>(v, (JsonSerializerOptions)null)
)
.HasColumnType("json");
}
modelBuilder.SharedTypeEntity<MetadataID>(MetadataName<T>()) private static void _HasImages<T>(ModelBuilder modelBuilder)
.HasOne<T>() where T : class, IThumbnails
.WithMany(x => x.ExternalIDs) {
.HasForeignKey(x => x.ResourceID) modelBuilder.Entity<T>()
.OnDelete(DeleteBehavior.Cascade); .OwnsOne(x => x.Poster);
modelBuilder.Entity<T>()
.OwnsOne(x => x.Thumbnail);
modelBuilder.Entity<T>()
.OwnsOne(x => x.Logo);
} }
/// <summary> /// <summary>
@ -248,6 +231,10 @@ namespace Kyoo.Postgresql
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Episode>()
.Ignore(x => x.PreviousEpisode)
.Ignore(x => x.NextEpisode);
modelBuilder.Entity<PeopleRole>() modelBuilder.Entity<PeopleRole>()
.Ignore(x => x.ForPeople); .Ignore(x => x.ForPeople);
@ -264,72 +251,68 @@ namespace Kyoo.Postgresql
.WithOne(x => x.Season) .WithOne(x => x.Season)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Movie>()
.HasOne(x => x.Studio)
.WithMany(x => x.Movies)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Show>() modelBuilder.Entity<Show>()
.HasOne(x => x.Studio) .HasOne(x => x.Studio)
.WithMany(x => x.Shows) .WithMany(x => x.Shows)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
_HasManyToMany<Library, Provider>(modelBuilder, x => x.Providers, x => x.Libraries); _HasManyToMany<Collection, Movie>(modelBuilder, x => x.Movies, x => x.Collections);
_HasManyToMany<Library, Collection>(modelBuilder, x => x.Collections, x => x.Libraries);
_HasManyToMany<Library, Show>(modelBuilder, x => x.Shows, x => x.Libraries);
_HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections); _HasManyToMany<Collection, Show>(modelBuilder, x => x.Shows, x => x.Collections);
_HasManyToMany<Show, Genre>(modelBuilder, x => x.Genres, x => x.Shows);
modelBuilder.Entity<User>() modelBuilder.Entity<User>()
.HasMany(x => x.Watched) .HasMany(x => x.Watched)
.WithMany("Users") .WithMany("Users")
.UsingEntity(x => x.ToTable(LinkName<User, Show>())); .UsingEntity(x => x.ToTable(LinkName<User, Show>()));
_HasMetadata<LibraryItem>(modelBuilder);
_HasMetadata<Collection>(modelBuilder); _HasMetadata<Collection>(modelBuilder);
_HasMetadata<Movie>(modelBuilder);
_HasMetadata<Show>(modelBuilder); _HasMetadata<Show>(modelBuilder);
_HasMetadata<Season>(modelBuilder); _HasMetadata<Season>(modelBuilder);
_HasMetadata<Episode>(modelBuilder); _HasMetadata<Episode>(modelBuilder);
_HasMetadata<People>(modelBuilder); _HasMetadata<People>(modelBuilder);
_HasMetadata<Studio>(modelBuilder); _HasMetadata<Studio>(modelBuilder);
_HasImages<LibraryItem>(modelBuilder);
_HasImages<Collection>(modelBuilder);
_HasImages<Movie>(modelBuilder);
_HasImages<Show>(modelBuilder);
_HasImages<Season>(modelBuilder);
_HasImages<Episode>(modelBuilder);
_HasImages<People>(modelBuilder);
modelBuilder.Entity<User>().OwnsOne(x => x.Logo);
modelBuilder.Entity<WatchedEpisode>() modelBuilder.Entity<WatchedEpisode>()
.HasKey(x => new { User = x.UserID, Episode = x.EpisodeID }); .HasKey(x => new { User = x.UserID, Episode = x.EpisodeID });
modelBuilder.Entity<Collection>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Season>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Episode>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<User>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Collection>() modelBuilder.Entity<Collection>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Genre>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Library>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<People>() modelBuilder.Entity<People>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Movie>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Show>() modelBuilder.Entity<Show>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Studio>() modelBuilder.Entity<Studio>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Provider>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Season>() modelBuilder.Entity<Season>()
.HasIndex(x => new { x.ShowID, x.SeasonNumber }) .HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber })
.IsUnique(); .IsUnique();
modelBuilder.Entity<Season>() modelBuilder.Entity<Season>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Episode>() modelBuilder.Entity<Episode>()
.HasIndex(x => new { x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber }) .HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber })
.IsUnique(); .IsUnique();
modelBuilder.Entity<Episode>() modelBuilder.Entity<Episode>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
@ -338,8 +321,10 @@ namespace Kyoo.Postgresql
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Movie>()
.Ignore(x => x.Links);
modelBuilder.Entity<LibraryItem>() modelBuilder.Entity<LibraryItem>()
.ToView("library_items"); .Ignore(x => x.Links);
} }
/// <summary> /// <summary>
@ -352,7 +337,7 @@ namespace Kyoo.Postgresql
public T GetTemporaryObject<T>(T model) public T GetTemporaryObject<T>(T model)
where T : class, IResource where T : class, IResource
{ {
T tmp = Set<T>().Local.FirstOrDefault(x => x.ID == model.ID); T tmp = Set<T>().Local.FirstOrDefault(x => x.Id == model.Id);
if (tmp != null) if (tmp != null)
return tmp; return tmp;
Entry(model).State = EntityState.Unchanged; Entry(model).State = EntityState.Unchanged;

View File

@ -18,4 +18,8 @@
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" /> <ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,872 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using Kyoo.Abstractions.Models;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Postgresql.Migrations
{
/// <summary>
/// The initial migration that build most of the database.
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20210801171613_Initial")]
public partial class Initial : Migration
{
/// <inheritdoc/>
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_users", x => x.id);
});
migrationBuilder.CreateTable(
name: "link_library_collection",
columns: table => new
{
collection_id = table.Column<int>(type: "integer", nullable: false),
library_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_collection", x => new { x.collection_id, x.library_id });
table.ForeignKey(
name: "fk_link_library_collection_collections_collection_id",
column: x => x.collection_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_collection_libraries_library_id",
column: x => x.library_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "collection_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_collection_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_collection_metadata_id_collections_collection_id",
column: x => x.resource_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_collection_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_library_provider",
columns: table => new
{
library_id = table.Column<int>(type: "integer", nullable: false),
provider_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_provider", x => new { x.library_id, x.provider_id });
table.ForeignKey(
name: "fk_link_library_provider_libraries_library_id",
column: x => x.library_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_provider_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "people_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_people_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_people_metadata_id_people_people_id",
column: x => x.resource_id,
principalTable: "people",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_people_metadata_id_providers_provider_id",
column: x => x.provider_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: false),
start_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
end_air = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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: "studio_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_studio_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_studio_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_studio_metadata_id_studios_studio_id",
column: x => x.resource_id,
principalTable: "studios",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_collection_show",
columns: table => new
{
collection_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id });
table.ForeignKey(
name: "fk_link_collection_show_collections_collection_id",
column: x => x.collection_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_collection_show_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_library_show",
columns: table => new
{
library_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_library_show", x => new { x.library_id, x.show_id });
table.ForeignKey(
name: "fk_link_library_show_libraries_library_id",
column: x => x.library_id,
principalTable: "libraries",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_library_show_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_show_genre",
columns: table => new
{
genre_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_show_genre", x => new { x.genre_id, x.show_id });
table.ForeignKey(
name: "fk_link_show_genre_genres_genre_id",
column: x => x.genre_id,
principalTable: "genres",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_show_genre_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_user_show",
columns: table => new
{
users_id = table.Column<int>(type: "integer", nullable: false),
watched_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_user_show", x => new { x.users_id, x.watched_id });
table.ForeignKey(
name: "fk_link_user_show_shows_watched_id",
column: x => x.watched_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_user_show_users_users_id",
column: x => x.users_id,
principalTable: "users",
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),
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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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: "show_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_show_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_show_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_show_metadata_id_shows_show_id",
column: x => x.resource_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),
images = table.Column<Dictionary<int, string>>(type: "jsonb", 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: "season_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_season_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_season_metadata_id_providers_provider_id",
column: x => x.provider_id,
principalTable: "providers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_season_metadata_id_seasons_season_id",
column: x => x.resource_id,
principalTable: "seasons",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "episode_metadata_id",
columns: table => new
{
resource_id = table.Column<int>(type: "integer", nullable: false),
provider_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_episode_metadata_id", x => new { x.resource_id, x.provider_id });
table.ForeignKey(
name: "fk_episode_metadata_id_episodes_episode_id",
column: x => x.resource_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_episode_metadata_id_providers_provider_id",
column: x => x.provider_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<object>(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
{
user_id = table.Column<int>(type: "integer", nullable: false),
episode_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.user_id, x.episode_id });
table.ForeignKey(
name: "fk_watched_episodes_episodes_episode_id",
column: x => x.episode_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_watched_episodes_users_user_id",
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_collection_metadata_id_provider_id",
table: "collection_metadata_id",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_collections_slug",
table: "collections",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_episode_metadata_id_provider_id",
table: "episode_metadata_id",
column: "provider_id");
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_show_id",
table: "link_collection_show",
column: "show_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_collection_library_id",
table: "link_library_collection",
column: "library_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_provider_provider_id",
table: "link_library_provider",
column: "provider_id");
migrationBuilder.CreateIndex(
name: "ix_link_library_show_show_id",
table: "link_library_show",
column: "show_id");
migrationBuilder.CreateIndex(
name: "ix_link_show_genre_show_id",
table: "link_show_genre",
column: "show_id");
migrationBuilder.CreateIndex(
name: "ix_link_user_show_watched_id",
table: "link_user_show",
column: "watched_id");
migrationBuilder.CreateIndex(
name: "ix_people_slug",
table: "people",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_people_metadata_id_provider_id",
table: "people_metadata_id",
column: "provider_id");
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_season_metadata_id_provider_id",
table: "season_metadata_id",
column: "provider_id");
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_show_metadata_id_provider_id",
table: "show_metadata_id",
column: "provider_id");
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_studio_metadata_id_provider_id",
table: "studio_metadata_id",
column: "provider_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_episode_id",
table: "watched_episodes",
column: "episode_id");
}
/// <inheritdoc/>
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "collection_metadata_id");
migrationBuilder.DropTable(
name: "episode_metadata_id");
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: "people_metadata_id");
migrationBuilder.DropTable(
name: "people_roles");
migrationBuilder.DropTable(
name: "season_metadata_id");
migrationBuilder.DropTable(
name: "show_metadata_id");
migrationBuilder.DropTable(
name: "studio_metadata_id");
migrationBuilder.DropTable(
name: "tracks");
migrationBuilder.DropTable(
name: "watched_episodes");
migrationBuilder.DropTable(
name: "collections");
migrationBuilder.DropTable(
name: "libraries");
migrationBuilder.DropTable(
name: "genres");
migrationBuilder.DropTable(
name: "people");
migrationBuilder.DropTable(
name: "providers");
migrationBuilder.DropTable(
name: "episodes");
migrationBuilder.DropTable(
name: "users");
migrationBuilder.DropTable(
name: "seasons");
migrationBuilder.DropTable(
name: "shows");
migrationBuilder.DropTable(
name: "studios");
}
}
}

View File

@ -1,192 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
{
/// <summary>
/// A migration that adds postgres triggers to update slugs.
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20210801171641_Triggers")]
public partial class Triggers : Migration
{
/// <inheritdoc/>
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),
'.', COALESCE(NEW.language, 'und'),
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();");
MigrationHelper.CreateLibraryItemsView(migrationBuilder);
}
/// <inheritdoc/>
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;");
MigrationHelper.DropLibraryItemsView(migrationBuilder);
}
}
}

View File

@ -1,114 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Kyoo.Postgresql.Migrations
{
/// <summary>
/// Remove triggers
/// </summary>
[DbContext(typeof(PostgresContext))]
[Migration("20230724144449_RemoveTriggers")]
public partial class RemoveTriggers : Migration
{
/// <inheritdoc/>
protected override void Up(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;");
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "tracks",
type: "text",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "seasons",
type: "text",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "episodes",
type: "text",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
/// <inheritdoc/>
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "tracks",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "seasons",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "slug",
table: "episodes",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,138 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
[DbContext(typeof(PostgresContext))]
[Migration("20230726100747_Timestamp")]
public partial class Timestamp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationHelper.DropLibraryItemsView(migrationBuilder);
migrationBuilder.AlterColumn<DateTime>(
name: "start_air",
table: "shows",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp without time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "end_air",
table: "shows",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp without time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "seasons",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp without time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "end_date",
table: "seasons",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp without time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "release_date",
table: "episodes",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp without time zone",
oldNullable: true);
MigrationHelper.CreateLibraryItemsView(migrationBuilder);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
MigrationHelper.DropLibraryItemsView(migrationBuilder);
migrationBuilder.AlterColumn<DateTime>(
name: "start_air",
table: "shows",
type: "timestamp without time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "end_air",
table: "shows",
type: "timestamp without time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "seasons",
type: "timestamp without time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "end_date",
table: "seasons",
type: "timestamp without time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "release_date",
table: "episodes",
type: "timestamp without time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
MigrationHelper.CreateLibraryItemsView(migrationBuilder);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,95 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
public partial class RemoveTracks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "tracks");
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.OldAnnotation("Npgsql:Enum:item_type", "show,movie,collection")
.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.OldAnnotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:item_type", "show,movie,collection")
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.Annotation("Npgsql:Enum:stream_type", "unknown,video,audio,subtitle")
.OldAnnotation("Npgsql:Enum:item_type", "show,movie,collection")
.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned");
migrationBuilder.CreateTable(
name: "tracks",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
episode_id = table.Column<int>(type: "integer", nullable: false),
codec = table.Column<string>(type: "text", nullable: true),
is_default = table.Column<bool>(type: "boolean", nullable: false),
is_external = table.Column<bool>(type: "boolean", nullable: false),
is_forced = table.Column<bool>(type: "boolean", nullable: false),
language = table.Column<string>(type: "text", nullable: true),
path = table.Column<string>(type: "text", nullable: true),
slug = table.Column<string>(type: "text", nullable: false),
title = table.Column<string>(type: "text", nullable: true),
track_index = table.Column<int>(type: "integer", nullable: false),
type = table.Column<string>(type: "stream_type", nullable: true)
},
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.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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,547 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using Kyoo.Abstractions.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:Enum:genre", "action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western")
.Annotation("Npgsql:Enum:item_kind", "show,movie,collection")
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned");
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: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "text", nullable: false),
overview = table.Column<string>(type: "text", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
external_id = table.Column<string>(type: "json", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_collections", 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: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "text", nullable: false),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
external_id = table.Column<string>(type: "json", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_people", 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: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "text", nullable: false),
external_id = table.Column<string>(type: "json", nullable: false)
},
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: "character varying(256)", maxLength: 256, nullable: false),
username = table.Column<string>(type: "text", nullable: false),
email = table.Column<string>(type: "text", nullable: false),
password = table.Column<string>(type: "text", nullable: false),
permissions = table.Column<string[]>(type: "text[]", nullable: false),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_users", x => x.id);
});
migrationBuilder.CreateTable(
name: "movies",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
slug = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "text", nullable: false),
tagline = table.Column<string>(type: "text", nullable: true),
aliases = table.Column<string[]>(type: "text[]", nullable: false),
path = table.Column<string>(type: "text", nullable: false),
overview = table.Column<string>(type: "text", nullable: true),
tags = table.Column<string[]>(type: "text[]", nullable: false),
genres = table.Column<Genre[]>(type: "genre[]", nullable: false),
status = table.Column<Status>(type: "status", nullable: false),
air_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
trailer = table.Column<string>(type: "text", nullable: true),
external_id = table.Column<string>(type: "json", nullable: false),
studio_id = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_movies", x => x.id);
table.ForeignKey(
name: "fk_movies_studios_studio_id",
column: x => x.studio_id,
principalTable: "studios",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
});
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: "character varying(256)", maxLength: 256, nullable: false),
name = table.Column<string>(type: "text", nullable: false),
tagline = table.Column<string>(type: "text", nullable: true),
aliases = table.Column<string[]>(type: "text[]", nullable: false),
overview = table.Column<string>(type: "text", nullable: true),
tags = table.Column<string[]>(type: "text[]", nullable: false),
genres = table.Column<Genre[]>(type: "genre[]", nullable: false),
status = table.Column<Status>(type: "status", nullable: false),
start_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
end_air = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
trailer = table.Column<string>(type: "text", nullable: true),
external_id = table.Column<string>(type: "json", 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_movie",
columns: table => new
{
collection_id = table.Column<int>(type: "integer", nullable: false),
movie_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_collection_movie", x => new { x.collection_id, x.movie_id });
table.ForeignKey(
name: "fk_link_collection_movie_collections_collection_id",
column: x => x.collection_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_collection_movie_movies_movie_id",
column: x => x.movie_id,
principalTable: "movies",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_collection_show",
columns: table => new
{
collection_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_collection_show", x => new { x.collection_id, x.show_id });
table.ForeignKey(
name: "fk_link_collection_show_collections_collection_id",
column: x => x.collection_id,
principalTable: "collections",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_collection_show_shows_show_id",
column: x => x.show_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "link_user_show",
columns: table => new
{
users_id = table.Column<int>(type: "integer", nullable: false),
watched_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_link_user_show", x => new { x.users_id, x.watched_id });
table.ForeignKey(
name: "fk_link_user_show_shows_watched_id",
column: x => x.watched_id,
principalTable: "shows",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_link_user_show_users_users_id",
column: x => x.users_id,
principalTable: "users",
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),
people_id = table.Column<int>(type: "integer", nullable: false),
show_id = table.Column<int>(type: "integer", nullable: true),
movie_id = table.Column<int>(type: "integer", nullable: true),
type = table.Column<string>(type: "text", nullable: false),
role = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_people_roles", x => x.id);
table.ForeignKey(
name: "fk_people_roles_movies_movie_id",
column: x => x.movie_id,
principalTable: "movies",
principalColumn: "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");
});
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: "character varying(256)", maxLength: 256, nullable: false),
show_id = table.Column<int>(type: "integer", nullable: false),
season_number = table.Column<int>(type: "integer", nullable: false),
name = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true),
start_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
end_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
external_id = table.Column<string>(type: "json", nullable: false)
},
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: "character varying(256)", maxLength: 256, nullable: false),
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: false),
name = table.Column<string>(type: "text", nullable: true),
overview = table.Column<string>(type: "text", nullable: true),
release_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
poster_source = table.Column<string>(type: "text", nullable: true),
poster_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
thumbnail_source = table.Column<string>(type: "text", nullable: true),
thumbnail_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
logo_source = table.Column<string>(type: "text", nullable: true),
logo_blurhash = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
external_id = table.Column<string>(type: "json", nullable: false)
},
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: "watched_episode",
columns: table => new
{
user_id = table.Column<int>(type: "integer", nullable: false),
episode_id = table.Column<int>(type: "integer", nullable: false),
watched_percentage = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_watched_episode", x => new { x.user_id, x.episode_id });
table.ForeignKey(
name: "fk_watched_episode_episodes_episode_id",
column: x => x.episode_id,
principalTable: "episodes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_watched_episode_users_user_id",
column: x => x.user_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_link_collection_movie_movie_id",
table: "link_collection_movie",
column: "movie_id");
migrationBuilder.CreateIndex(
name: "ix_link_collection_show_show_id",
table: "link_collection_show",
column: "show_id");
migrationBuilder.CreateIndex(
name: "ix_link_user_show_watched_id",
table: "link_user_show",
column: "watched_id");
migrationBuilder.CreateIndex(
name: "ix_movies_slug",
table: "movies",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_movies_studio_id",
table: "movies",
column: "studio_id");
migrationBuilder.CreateIndex(
name: "ix_people_slug",
table: "people",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_people_roles_movie_id",
table: "people_roles",
column: "movie_id");
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_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_users_slug",
table: "users",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_watched_episode_episode_id",
table: "watched_episode",
column: "episode_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "link_collection_movie");
migrationBuilder.DropTable(
name: "link_collection_show");
migrationBuilder.DropTable(
name: "link_user_show");
migrationBuilder.DropTable(
name: "people_roles");
migrationBuilder.DropTable(
name: "watched_episode");
migrationBuilder.DropTable(
name: "collections");
migrationBuilder.DropTable(
name: "movies");
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,64 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
public partial class Items : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// language=PostgreSQL
migrationBuilder.Sql(@"
CREATE VIEW library_items AS
SELECT
s.id, s.slug, s.name, s.tagline, s.aliases, s.overview, s.tags, s.genres, s.status,
s.start_air, s.end_air, s.poster_source, s.poster_blurhash, s.thumbnail_source, s.thumbnail_blurhash,
s.logo_source, s.logo_blurhash, s.trailer, s.external_id, s.start_air AS air_date, NULL as path,
'show'::item_kind AS kind
FROM shows AS s
UNION ALL
SELECT
-m.id, m.slug, m.name, m.tagline, m.aliases, m.overview, m.tags, m.genres, m.status,
m.air_date as start_air, m.air_date as end_air, m.poster_source, m.poster_blurhash, m.thumbnail_source,
m.thumbnail_blurhash, m.logo_source, m.logo_blurhash, m.trailer, m.external_id, m.air_date, m.path,
'movie'::item_kind AS kind
FROM movies AS m
UNION ALL
SELECT
c.id + 10000 AS id, c.slug, c.name, NULL as tagline, NULL as alises, c.overview, NULL AS tags, NULL AS genres, 'unknown'::status AS status,
NULL AS start_air, NULL AS end_air, c.poster_source, c.poster_blurhash, c.thumbnail_source,
c.thumbnail_blurhash, c.logo_source, c.logo_blurhash, NULL as trailer, c.external_id, NULL AS air_date, NULL as path,
'collection'::item_kind AS kind
FROM collections AS c
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// language=PostgreSQL
migrationBuilder.Sql(@"DROP VIEW library_items");
}
}
}

View File

@ -33,11 +33,6 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
public class PostgresContext : DatabaseContext public class PostgresContext : DatabaseContext
{ {
/// <summary>
/// The connection string to use.
/// </summary>
private readonly string _connection;
/// <summary> /// <summary>
/// Is this instance in debug mode? /// Is this instance in debug mode?
/// </summary> /// </summary>
@ -48,12 +43,13 @@ namespace Kyoo.Postgresql
/// </summary> /// </summary>
private readonly bool _skipConfigure; private readonly bool _skipConfigure;
// TOOD: This needs ot be updated but ef-core still does not offer a way to use this. // TODO: This needs ot be updated but ef-core still does not offer a way to use this.
[Obsolete] [Obsolete]
static PostgresContext() static PostgresContext()
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemType>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Genre>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<ItemKind>();
} }
/// <summary> /// <summary>
@ -78,7 +74,6 @@ namespace Kyoo.Postgresql
/// <param name="debugMode">Is this instance in debug mode?</param> /// <param name="debugMode">Is this instance in debug mode?</param>
public PostgresContext(string connection, bool debugMode) public PostgresContext(string connection, bool debugMode)
{ {
_connection = connection;
_debugMode = debugMode; _debugMode = debugMode;
} }
@ -106,51 +101,12 @@ namespace Kyoo.Postgresql
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.HasPostgresEnum<Status>(); modelBuilder.HasPostgresEnum<Status>();
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<Genre>();
modelBuilder.HasPostgresEnum<ItemKind>();
modelBuilder.Entity<LibraryItem>()
.ToView("library_items")
.HasKey(x => x.ID);
modelBuilder.Entity<User>()
.Property(x => x.ExtraData)
.HasColumnType("jsonb");
modelBuilder.Entity<LibraryItem>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Collection>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Show>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Season>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Episode>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<People>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<Provider>()
.Property(x => x.Images)
.HasColumnType("jsonb");
modelBuilder.Entity<User>()
.Property(x => x.Images)
.HasColumnType("jsonb");
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }
/// <inheritdoc />
protected override string MetadataName<T>()
{
SnakeCaseNameRewriter rewriter = new(CultureInfo.InvariantCulture);
return rewriter.RewriteName(typeof(T).Name + nameof(MetadataID));
}
/// <inheritdoc /> /// <inheritdoc />
protected override string LinkName<T, T2>() protected override string LinkName<T, T2>()
{ {

View File

@ -38,7 +38,7 @@ namespace Kyoo.Swagger
{ {
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>(); context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>() OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
.Aggregate(new OpenApiSecurityRequirement(), (agg, cur) => .Aggregate(new OpenApiSecurityRequirement(), (agg, _) =>
{ {
agg[nameof(Kyoo)] = Array.Empty<string>(); agg[nameof(Kyoo)] = Array.Empty<string>();
return agg; return agg;
@ -60,15 +60,15 @@ namespace Kyoo.Swagger
perms = context.MethodInfo.GetCustomAttributes<PartialPermissionAttribute>() perms = context.MethodInfo.GetCustomAttributes<PartialPermissionAttribute>()
.Aggregate(perms, (agg, cur) => .Aggregate(perms, (agg, cur) =>
{ {
Group group = controller.Group != Group.Overall Group? group = controller.Group != Group.Overall
? controller.Group ? controller.Group
: cur.Group; : cur.Group;
string type = controller.Type ?? cur.Type; string type = controller.Type ?? cur.Type;
Kind kind = controller.Type == null Kind? kind = controller.Type == null
? controller.Kind ? controller.Kind
: cur.Kind; : cur.Kind;
ICollection<string> permissions = _GetPermissionsList(agg, group); ICollection<string> permissions = _GetPermissionsList(agg, group ?? Group.Overall);
permissions.Add($"{type}.{kind.ToString().ToLower()}"); permissions.Add($"{type}.{kind!.Value.ToString().ToLower()}");
agg[nameof(Kyoo)] = permissions; agg[nameof(Kyoo)] = permissions;
return agg; return agg;
}); });

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
@ -87,7 +86,6 @@ namespace Kyoo.Swagger
x.IsNullableRaw = false; x.IsNullableRaw = false;
x.Type = JsonObjectType.String | JsonObjectType.Integer; x.Type = JsonObjectType.String | JsonObjectType.Integer;
})); }));
document.SchemaProcessors.Add(new ThumbnailProcessor());
document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
{ {

View File

@ -1,49 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models;
using NJsonSchema;
using NJsonSchema.Generation;
namespace Kyoo.Swagger
{
/// <summary>
/// An operation processor to add computed fields of <see cref="IThumbnails"/>.
/// </summary>
public class ThumbnailProcessor : ISchemaProcessor
{
/// <inheritdoc />
public void Process(SchemaProcessorContext context)
{
if (!context.ContextualType.OriginalType.IsAssignableTo(typeof(IThumbnails)))
return;
foreach ((int _, string imageP) in Images.ImageName)
{
string image = imageP.ToLower()[0] + imageP[1..];
context.Schema.Properties.Add(image, new JsonSchemaProperty
{
Type = JsonObjectType.String,
IsNullableRaw = true,
Description = $"An url to the {image} of this resource. If this resource does not have an image, " +
$"the link will be null. If the kyoo's instance is not capable of handling this kind of image " +
$"for the specific resource, this field won't be present."
});
}
}
}
}

View File

@ -22,6 +22,7 @@ using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Core.Controllers; using Kyoo.Core.Controllers;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Moq;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Kyoo.Tests.Database namespace Kyoo.Tests.Database
@ -37,31 +38,28 @@ namespace Kyoo.Tests.Database
{ {
Context = new PostgresTestContext(postgres, output); Context = new PostgresTestContext(postgres, output);
ProviderRepository provider = new(_NewContext()); Mock<IThumbnailsManager> thumbs = new();
LibraryRepository library = new(_NewContext(), provider); CollectionRepository collection = new(_NewContext(), thumbs.Object);
CollectionRepository collection = new(_NewContext(), provider); StudioRepository studio = new(_NewContext(), thumbs.Object);
GenreRepository genre = new(_NewContext()); PeopleRepository people = new(_NewContext(),
StudioRepository studio = new(_NewContext(), provider); new Lazy<IShowRepository>(() => LibraryManager.ShowRepository),
PeopleRepository people = new(_NewContext(), provider, thumbs.Object);
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository)); MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object);
ShowRepository show = new(_NewContext(), studio, people, genre, provider); ShowRepository show = new(_NewContext(), studio, people, thumbs.Object);
SeasonRepository season = new(_NewContext(), show, provider); SeasonRepository season = new(_NewContext(), show, thumbs.Object);
LibraryItemRepository libraryItem = new(_NewContext(), LibraryItemRepository libraryItem = new(_NewContext(), thumbs.Object);
new Lazy<ILibraryRepository>(() => LibraryManager.LibraryRepository)); EpisodeRepository episode = new(_NewContext(), show, thumbs.Object);
EpisodeRepository episode = new(_NewContext(), show, provider); UserRepository user = new(_NewContext(), thumbs.Object);
UserRepository user = new(_NewContext());
LibraryManager = new LibraryManager(new IBaseRepository[] { LibraryManager = new LibraryManager(new IBaseRepository[] {
provider,
library,
libraryItem, libraryItem,
collection, collection,
movies,
show, show,
season, season,
episode, episode,
people, people,
studio, studio,
genre,
user user
}); });
} }

View File

@ -19,7 +19,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
@ -30,7 +29,7 @@ using Xunit;
namespace Kyoo.Tests.Database namespace Kyoo.Tests.Database
{ {
public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable
where T : class, IResource, new() where T : class, IResource
{ {
protected readonly RepositoryActivator Repositories; protected readonly RepositoryActivator Repositories;
private readonly IRepository<T> _repository; private readonly IRepository<T> _repository;
@ -63,7 +62,7 @@ namespace Kyoo.Tests.Database
[Fact] [Fact]
public async Task GetByIdTest() public async Task GetByIdTest()
{ {
T value = await _repository.Get(TestSample.Get<T>().ID); T value = await _repository.Get(TestSample.Get<T>().Id);
KAssert.DeepEqual(TestSample.Get<T>(), value); KAssert.DeepEqual(TestSample.Get<T>(), value);
} }
@ -89,7 +88,7 @@ namespace Kyoo.Tests.Database
[Fact] [Fact]
public async Task DeleteByIdTest() public async Task DeleteByIdTest()
{ {
await _repository.Delete(TestSample.Get<T>().ID); await _repository.Delete(TestSample.Get<T>().Id);
Assert.Equal(0, await _repository.GetCount()); Assert.Equal(0, await _repository.GetCount());
} }
@ -114,23 +113,11 @@ namespace Kyoo.Tests.Database
await _repository.Delete(TestSample.Get<T>()); await _repository.Delete(TestSample.Get<T>());
T expected = TestSample.Get<T>(); T expected = TestSample.Get<T>();
expected.ID = 0; expected.Id = 0;
await _repository.Create(expected); await _repository.Create(expected);
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); 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] [Fact]
public virtual async Task CreateIfNotExistTest() public virtual async Task CreateIfNotExistTest()
{ {
@ -140,22 +127,16 @@ namespace Kyoo.Tests.Database
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>())); KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
} }
[Fact] // [Fact]
public async Task EditNullTest() // public async Task EditNonExistingTest()
{ // {
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false)); // await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { Id = 56 }));
} // }
[Fact]
public async Task EditNonExistingTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { ID = 56 }, false));
}
[Fact] [Fact]
public async Task GetExpressionIDTest() public async Task GetExpressionIDTest()
{ {
KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.ID == TestSample.Get<T>().ID)); KAssert.DeepEqual(TestSample.Get<T>(), await _repository.Get(x => x.Id == TestSample.Get<T>().Id));
} }
[Fact] [Fact]
@ -170,12 +151,6 @@ namespace Kyoo.Tests.Database
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing")); 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] [Fact]
public async Task GetOrDefaultTest() public async Task GetOrDefaultTest()
{ {

View File

@ -66,40 +66,29 @@ namespace Kyoo.Tests.Database
Assert.Equal("2!", ret.Slug); Assert.Equal("2!", ret.Slug);
} }
[Fact]
public async Task CreateWithoutNameTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.Name = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(collection));
}
[Fact] [Fact]
public async Task CreateWithExternalIdTest() public async Task CreateWithExternalIdTest()
{ {
Collection collection = TestSample.GetNew<Collection>(); Collection collection = TestSample.GetNew<Collection>();
collection.ExternalIDs = new[] collection.ExternalId = new Dictionary<string, MetadataId>
{ {
new MetadataID ["1"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
new MetadataID ["2"] = new()
{ {
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link", Link = "new-provider-link",
DataID = "new-id" DataId = "new-id"
} }
}; };
await _repository.Create(collection); await _repository.Create(collection);
Collection retrieved = await _repository.Get(2); Collection retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); Assert.Equal(2, retrieved.ExternalId.Count);
Assert.Equal(2, retrieved.ExternalIDs.Count); KAssert.DeepEqual(collection.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(collection.ExternalIDs.First(), retrieved.ExternalIDs.First()); KAssert.DeepEqual(collection.ExternalId.Last(), retrieved.ExternalId.Last());
KAssert.DeepEqual(collection.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
} }
[Fact] [Fact]
@ -107,11 +96,8 @@ namespace Kyoo.Tests.Database
{ {
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug); Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.Name = "New Title"; value.Name = "New Title";
value.Images = new Dictionary<int, string> value.Poster = new Image("new-poster");
{ await _repository.Edit(value);
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections.FirstAsync(); Collection retrieved = await database.Collections.FirstAsync();
@ -123,22 +109,18 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest() public async Task EditMetadataTest()
{ {
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug); Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new[] value.ExternalId = new Dictionary<string, MetadataId>
{ {
new MetadataID ["test"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
}; };
await _repository.Edit(value, false); await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections Collection retrieved = await database.Collections.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }
@ -147,41 +129,33 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest() public async Task AddMetadataTest()
{ {
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug); Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalIDs = new List<MetadataID> value.ExternalId = new Dictionary<string, MetadataId>
{ {
new() ["toto"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
}; };
await _repository.Edit(value, false); await _repository.Edit(value);
{ {
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections Collection retrieved = await database.Collections.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }
value.ExternalIDs.Add(new MetadataID value.ExternalId.Add("test", new MetadataId
{ {
Provider = TestSample.GetNew<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}); });
await _repository.Edit(value, false); await _repository.Edit(value);
{ {
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections Collection retrieved = await database.Collections.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }

View File

@ -55,12 +55,11 @@ namespace Kyoo.Tests.Database
{ {
Episode episode = await _repository.Get(1); Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
Show show = new() await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) =>
{ {
ID = episode.ShowID, x.Slug = "new-slug";
Slug = "new-slug" return Task.FromResult(true);
}; });
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
episode = await _repository.Get(1); episode = await _repository.Get(1);
Assert.Equal("new-slug-s1e1", episode.Slug); Assert.Equal("new-slug-s1e1", episode.Slug);
} }
@ -70,12 +69,11 @@ namespace Kyoo.Tests.Database
{ {
Episode episode = await _repository.Get(1); Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await _repository.Edit(new Episode episode = await _repository.Patch(1, (x) =>
{ {
ID = 1, x.SeasonNumber = 2;
SeasonNumber = 2, return Task.FromResult(true);
ShowID = 1 });
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
episode = await _repository.Get(1); episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
@ -86,12 +84,11 @@ namespace Kyoo.Tests.Database
{ {
Episode episode = await _repository.Get(1); Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await _repository.Edit(new Episode episode = await Repositories.LibraryManager.Patch<Episode>(episode.Id, (x) =>
{ {
ID = 1, x.EpisodeNumber = 2;
EpisodeNumber = 2, return Task.FromResult(true);
ShowID = 1 });
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(1); episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
@ -100,12 +97,12 @@ namespace Kyoo.Tests.Database
[Fact] [Fact]
public async Task EpisodeCreationSlugTest() public async Task EpisodeCreationSlugTest()
{ {
Episode episode = await _repository.Create(new Episode Episode model = TestSample.Get<Episode>();
{ model.Id = 0;
ShowID = TestSample.Get<Show>().ID, model.ShowId = TestSample.Get<Show>().Id;
SeasonNumber = 2, model.SeasonNumber = 2;
EpisodeNumber = 4 model.EpisodeNumber = 4;
}); Episode episode = await _repository.Create(model);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e4", episode.Slug);
} }
@ -127,12 +124,11 @@ namespace Kyoo.Tests.Database
public async Task SlugEditAbsoluteTest() public async Task SlugEditAbsoluteTest()
{ {
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode()); Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
Show show = new() await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) =>
{ {
ID = episode.ShowID, x.Slug = "new-slug";
Slug = "new-slug" return Task.FromResult(true);
}; });
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
episode = await _repository.Get(2); episode = await _repository.Get(2);
Assert.Equal($"new-slug-3", episode.Slug); Assert.Equal($"new-slug-3", episode.Slug);
} }
@ -141,12 +137,11 @@ namespace Kyoo.Tests.Database
public async Task AbsoluteNumberEditTest() public async Task AbsoluteNumberEditTest()
{ {
await _repository.Create(TestSample.GetAbsoluteEpisode()); await _repository.Create(TestSample.GetAbsoluteEpisode());
Episode episode = await _repository.Edit(new Episode Episode episode = await _repository.Patch(2, (x) =>
{ {
ID = 2, x.AbsoluteNumber = 56;
AbsoluteNumber = 56, return Task.FromResult(true);
ShowID = 1 });
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
episode = await _repository.Get(2); episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
@ -156,13 +151,12 @@ namespace Kyoo.Tests.Database
public async Task AbsoluteToNormalEditTest() public async Task AbsoluteToNormalEditTest()
{ {
await _repository.Create(TestSample.GetAbsoluteEpisode()); await _repository.Create(TestSample.GetAbsoluteEpisode());
Episode episode = await _repository.Edit(new Episode Episode episode = await _repository.Patch(2, (x) =>
{ {
ID = 2, x.SeasonNumber = 1;
SeasonNumber = 1, x.EpisodeNumber = 2;
EpisodeNumber = 2, return Task.FromResult(true);
ShowID = 1 });
}, false);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(2); episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
@ -174,72 +168,44 @@ namespace Kyoo.Tests.Database
Episode episode = await _repository.Get(1); Episode episode = await _repository.Get(1);
episode.SeasonNumber = null; episode.SeasonNumber = null;
episode.AbsoluteNumber = 12; episode.AbsoluteNumber = 12;
episode = await _repository.Edit(episode, true); episode = await _repository.Edit(episode);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug); Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
episode = await _repository.Get(1); episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug); 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);
}
[Fact] [Fact]
public async Task CreateWithExternalIdTest() public async Task CreateWithExternalIdTest()
{ {
Episode value = TestSample.GetNew<Episode>(); Episode value = TestSample.GetNew<Episode>();
value.ExternalIDs = new[] value.ExternalId = new Dictionary<string, MetadataId>
{ {
new MetadataID ["2"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
new MetadataID ["3"] = new()
{ {
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link", Link = "new-provider-link",
DataID = "new-id" DataId = "new-id"
} }
}; };
await _repository.Create(value); await _repository.Create(value);
Episode retrieved = await _repository.Get(2); Episode retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs); Assert.Equal(2, retrieved.ExternalId.Count);
Assert.Equal(2, retrieved.ExternalIDs.Count); KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First()); KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last());
KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
} }
[Fact] [Fact]
public async Task EditTest() public async Task EditTest()
{ {
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug); Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.Title = "New Title"; value.Name = "New Title";
value.Images = new Dictionary<int, string> value.Poster = new Image("poster");
{ await _repository.Edit(value);
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes.FirstAsync(); Episode retrieved = await database.Episodes.FirstAsync();
@ -251,22 +217,18 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest() public async Task EditMetadataTest()
{ {
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug); Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new[] value.ExternalId = new Dictionary<string, MetadataId>
{ {
new MetadataID ["1"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
}; };
await _repository.Edit(value, false); await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes Episode retrieved = await database.Episodes.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }
@ -275,41 +237,33 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest() public async Task AddMetadataTest()
{ {
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug); Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalIDs = new List<MetadataID> value.ExternalId = new Dictionary<string, MetadataId>
{ {
new() ["toto"] = new()
{ {
Provider = TestSample.Get<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}, },
}; };
await _repository.Edit(value, false); await _repository.Edit(value);
{ {
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes Episode retrieved = await database.Episodes.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }
value.ExternalIDs.Add(new MetadataID value.ExternalId.Add("test", new MetadataId
{ {
Provider = TestSample.GetNew<Provider>(),
Link = "link", Link = "link",
DataID = "id" DataId = "id"
}); });
await _repository.Edit(value, false); await _repository.Edit(value);
{ {
await using DatabaseContext database = Repositories.Context.New(); await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes Episode retrieved = await database.Episodes.FirstAsync();
.Include(x => x.ExternalIDs)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved); KAssert.DeepEqual(value, retrieved);
} }
@ -323,12 +277,10 @@ namespace Kyoo.Tests.Database
[InlineData("SuPeR")] [InlineData("SuPeR")]
public async Task SearchTest(string query) public async Task SearchTest(string query)
{ {
Episode value = new() Episode value = TestSample.Get<Episode>();
{ value.Id = 0;
Title = "This is a test super title", value.Name = "This is a test super title";
ShowID = 1, value.EpisodeNumber = 56;
AbsoluteNumber = 2
};
await _repository.Create(value); await _repository.Create(value);
ICollection<Episode> ret = await _repository.Search(query); ICollection<Episode> ret = await _repository.Search(query);
value.Show = TestSample.Get<Show>(); value.Show = TestSample.Get<Show>();
@ -342,9 +294,9 @@ namespace Kyoo.Tests.Database
await _repository.Delete(TestSample.Get<Episode>()); await _repository.Delete(TestSample.Get<Episode>());
Episode expected = TestSample.Get<Episode>(); Episode expected = TestSample.Get<Episode>();
expected.ID = 0; expected.Id = 0;
expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).ID; expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).ID; expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
await _repository.Create(expected); await _repository.Create(expected);
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug)); KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
} }
@ -355,8 +307,8 @@ namespace Kyoo.Tests.Database
Episode expected = TestSample.Get<Episode>(); Episode expected = TestSample.Get<Episode>();
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>())); KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>()));
await _repository.Delete(TestSample.Get<Episode>()); await _repository.Delete(TestSample.Get<Episode>());
expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).ID; expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).ID; expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected)); KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
} }
} }

Some files were not shown because too many files have changed in this diff Show More