Add include as a first class param and remove library manager thing

This commit is contained in:
Zoe Roux 2023-10-27 20:19:40 +02:00
parent c621c45695
commit d3fbec1a9d
36 changed files with 480 additions and 1529 deletions

View File

@ -16,12 +16,7 @@
// 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.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Abstractions.Controllers
{
@ -30,413 +25,49 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
public interface ILibraryManager
{
/// <summary>
/// Get the repository corresponding to the T item.
/// </summary>
/// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The repository corresponding</returns>
IRepository<T> GetRepository<T>()
where T : class, IResource;
/// <summary>
/// The repository that handle libraries items (a wrapper around shows and collections).
/// </summary>
ILibraryItemRepository LibraryItemRepository { get; }
IRepository<LibraryItem> LibraryItems { get; }
/// <summary>
/// The repository that handle collections.
/// </summary>
ICollectionRepository CollectionRepository { get; }
IRepository<Collection> Collections { get; }
/// <summary>
/// The repository that handle shows.
/// </summary>
IMovieRepository MovieRepository { get; }
IRepository<Movie> Movies { get; }
/// <summary>
/// The repository that handle shows.
/// </summary>
IShowRepository ShowRepository { get; }
IRepository<Show> Shows { get; }
/// <summary>
/// The repository that handle seasons.
/// </summary>
ISeasonRepository SeasonRepository { get; }
IRepository<Season> Seasons { get; }
/// <summary>
/// The repository that handle episodes.
/// </summary>
IEpisodeRepository EpisodeRepository { get; }
IRepository<Episode> Episodes { get; }
/// <summary>
/// The repository that handle people.
/// </summary>
IPeopleRepository PeopleRepository { get; }
IRepository<People> People { get; }
/// <summary>
/// The repository that handle studios.
/// </summary>
IStudioRepository StudioRepository { get; }
IRepository<Studio> Studios { get; }
/// <summary>
/// The repository that handle users.
/// </summary>
IUserRepository UserRepository { get; }
/// <summary>
/// Get the resource by it's ID
/// </summary>
/// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns>
Task<T> Get<T>(int id)
where T : class, IResource;
/// <summary>
/// Get the resource by it's slug
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns>
Task<T> Get<T>(string slug)
where T : class, IResource;
/// <summary>
/// Get the resource by a filter function.
/// </summary>
/// <param name="where">The filter function.</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The first resource found that match the where function</returns>
Task<T> Get<T>(Expression<Func<T, bool>> where)
where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber);
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get the resource by it's ID or null if it is not found.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
Task<T?> GetOrDefault<T>(int id)
where T : class, IResource;
/// <summary>
/// Get the resource by it's slug or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
Task<T?> GetOrDefault<T>(string slug)
where T : class, IResource;
/// <summary>
/// Get the resource by a filter function or null if it is not found.
/// </summary>
/// <param name="where">The filter function.</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>
/// <returns>The first resource found that match the where function</returns>
Task<T?> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T>? sortBy = default)
where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season?> GetOrDefault(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season?> GetOrDefault(string showSlug, int seasonNumber);
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Load a related resource
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <paramref name="obj"/></returns>
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
where T2 : class, IResource;
/// <summary>
/// Load a collection of related resource
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <paramref name="obj"/></returns>
/// <seealso cref="Load{T,T2}(T, Expression{Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
where T2 : class;
/// <summary>
/// Load a related resource by it's name
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <paramref name="obj"/></returns>
/// <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(IResource, string, bool)"/>
Task<T> Load<T>(T obj, string memberName, bool force = false)
where T : class, IResource;
/// <summary>
/// Load a related resource without specifying it's type.
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <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}(T, string, bool)"/>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Load(IResource obj, string memberName, bool force = false);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showID">The ID of the show</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 <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showSlug">The slug of the show</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 <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a person.
/// </summary>
/// <param name="id">The id of the person</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 <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a person.
/// </summary>
/// <param name="slug">The slug of the person</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 <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get all resources with filters
/// </summary>
/// <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>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default,
Pagination? limit = default)
where T : class, IResource;
/// <summary>
/// Get the count of resources that match the filter
/// </summary>
/// <param name="where">A filter function</param>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<int> GetCount<T>(Expression<Func<T, bool>>? where = null)
where T : class, IResource;
/// <summary>
/// Search for a resource
/// </summary>
/// <param name="query">The search query</param>
/// <typeparam name="T">The type of resources</typeparam>
/// <returns>A list of 20 items that match the search query</returns>
Task<ICollection<T>> Search<T>(string query)
where T : class, IResource;
/// <summary>
/// Create a new resource.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
Task<T> Create<T>(T item)
where T : class, IResource;
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists<T>(T item)
where T : class, IResource;
/// <summary>
/// Edit a resource
/// </summary>
/// <param name="item">The resource to edit, it's ID can't change.</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> 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;
/// <summary>
/// Delete a resource.
/// </summary>
/// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(T item)
where T : class, IResource;
/// <summary>
/// Delete a resource by it's ID.
/// </summary>
/// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(int id)
where T : class, IResource;
/// <summary>
/// Delete a resource by it's slug.
/// </summary>
/// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(string slug)
where T : class, IResource;
IRepository<User> Users { get; }
}
}

View File

@ -22,6 +22,7 @@ using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
namespace Kyoo.Abstractions.Controllers
{
@ -42,47 +43,55 @@ namespace Kyoo.Abstractions.Controllers
/// Get a resource from it's ID.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <param name="include">The related fields to include.</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(int id);
Task<T> Get(int id, Include<T>? include = default);
/// <summary>
/// Get a resource from it's slug.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <param name="include">The related fields to include.</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(string slug);
Task<T> Get(string slug, Include<T>? include = default);
/// <summary>
/// Get the first resource that match the predicate.
/// </summary>
/// <param name="where">A predicate to filter the resource.</param>
/// <param name="include">The related fields to include.</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(Expression<Func<T, bool>> where);
Task<T> Get(Expression<Func<T, bool>> where, Include<T>? include = default);
/// <summary>
/// Get a resource from it's ID or null if it is not found.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <param name="include">The related fields to include.</param>
/// <returns>The resource found</returns>
Task<T?> GetOrDefault(int id);
Task<T?> GetOrDefault(int id, Include<T>? include = default);
/// <summary>
/// Get a resource from it's slug or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <param name="include">The related fields to include.</param>
/// <returns>The resource found</returns>
Task<T?> GetOrDefault(string slug);
Task<T?> GetOrDefault(string slug, Include<T>? include = default);
/// <summary>
/// Get the first resource that match the predicate or null if it is not found.
/// </summary>
/// <param name="where">A predicate to filter the resource.</param>
/// <param name="include">The related fields to include.</param>
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <returns>The resource found</returns>
Task<T?> GetOrDefault(Expression<Func<T, bool>> where, Sort<T>? sortBy = default);
Task<T?> GetOrDefault(Expression<Func<T, bool>> where,
Include<T>? include = default,
Sort<T>? sortBy = default);
/// <summary>
/// Search for resources.
@ -97,10 +106,12 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter predicate</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="include">The related fields to include.</param>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default,
Pagination? limit = default);
Pagination? limit = default,
Include<T>? include = default);
/// <summary>
/// Get the number of resources that match the filter's predicate.
@ -200,209 +211,4 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
Type RepositoryType { get; }
}
/// <summary>
/// A repository to handle shows.
/// </summary>
public interface IMovieRepository : IRepository<Movie> { }
/// <summary>
/// A repository to handle shows.
/// </summary>
public interface IShowRepository : IRepository<Show>
{
/// <summary>
/// Get a show's slug from it's ID.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <exception cref="ItemNotFoundException">If a show with the given ID is not found.</exception>
/// <returns>The show's slug</returns>
Task<string> GetSlug(int showID);
}
/// <summary>
/// A repository to handle seasons.
/// </summary>
public interface ISeasonRepository : IRepository<Season>
{
/// <summary>
/// Get a season from it's showID and it's seasonNumber
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber);
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season?> GetOrDefault(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season?> GetOrDefault(string showSlug, int seasonNumber);
}
/// <summary>
/// The repository to handle episodes
/// </summary>
public interface IEpisodeRepository : IRepository<Episode>
{
// TODO replace the next methods with extension methods.
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's showID and it's absolute number.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(int showID, int absoluteNumber);
/// <summary>
/// Get a episode from it's showID and it's absolute number.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
}
/// <summary>
/// A repository to handle library items (A wrapper around shows and collections).
/// </summary>
public interface ILibraryItemRepository : IRepository<LibraryItem> { }
/// <summary>
/// A repository for collections
/// </summary>
public interface ICollectionRepository : IRepository<Collection> { }
/// <summary>
/// A repository for studios.
/// </summary>
public interface IStudioRepository : IRepository<Studio> { }
/// <summary>
/// A repository for people.
/// </summary>
public interface IPeopleRepository : IRepository<People>
{
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showID">The ID of the show</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 <see cref="Show"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showSlug">The slug of the show</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 <see cref="Show"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a person.
/// </summary>
/// <param name="id">The id of the person</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 <see cref="People"/> exist with the given ID.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
/// <summary>
/// Get people's roles from a person.
/// </summary>
/// <param name="slug">The slug of the person</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 <see cref="People"/> exist with the given slug.</exception>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default);
}
/// <summary>
/// A repository to handle users.
/// </summary>
public interface IUserRepository : IRepository<User> { }
}

View File

@ -0,0 +1,48 @@
// 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.ComponentModel.DataAnnotations;
using System.Reflection;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils;
public class Include<T>
where T : IResource
{
public string[] Fields { get; private init; }
public static Include<T> From(string fields)
{
if (string.IsNullOrEmpty(fields))
return new();
string[] values = fields.Split(',');
foreach (string field in values)
{
PropertyInfo? prop = typeof(T).GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (prop?.GetCustomAttribute<LoadableRelationAttribute>() == null)
throw new ValidationException($"No loadable relation with the name {field}.");
}
return new()
{
Fields = values,
};
}
}

View File

@ -47,7 +47,7 @@ namespace Kyoo.Authentication.Views
/// <summary>
/// The repository to handle users.
/// </summary>
private readonly IUserRepository _users;
private readonly IRepository<User> _users;
/// <summary>
/// The token generator.
@ -65,7 +65,7 @@ namespace Kyoo.Authentication.Views
/// <param name="users">The repository used to check if the user exists.</param>
/// <param name="token">The token generator.</param>
/// <param name="permissions">The permission opitons.</param>
public AuthApi(IUserRepository users, ITokenController token, PermissionOption permissions)
public AuthApi(IRepository<User> users, ITokenController token, PermissionOption permissions)
{
_users = users;
_token = token;

View File

@ -16,16 +16,8 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Utils;
namespace Kyoo.Core.Controllers
{
@ -34,438 +26,53 @@ namespace Kyoo.Core.Controllers
/// </summary>
public class LibraryManager : ILibraryManager
{
/// <summary>
/// The list of repositories
/// </summary>
private readonly IBaseRepository[] _repositories;
/// <inheritdoc />
public ILibraryItemRepository LibraryItemRepository { get; }
/// <inheritdoc />
public ICollectionRepository CollectionRepository { get; }
/// <inheritdoc />
public IMovieRepository MovieRepository { get; }
/// <inheritdoc />
public IShowRepository ShowRepository { get; }
/// <inheritdoc />
public ISeasonRepository SeasonRepository { get; }
/// <inheritdoc />
public IEpisodeRepository EpisodeRepository { get; }
/// <inheritdoc />
public IPeopleRepository PeopleRepository { get; }
/// <inheritdoc />
public IStudioRepository StudioRepository { get; }
/// <inheritdoc />
public IUserRepository UserRepository { get; }
/// <summary>
/// Create a new <see cref="LibraryManager"/> instance with every repository available.
/// </summary>
/// <param name="repositories">The list of repositories that this library manager should manage.
/// If a repository for every base type is not available, this instance won't be stable.</param>
public LibraryManager(IEnumerable<IBaseRepository> repositories)
public LibraryManager(
IRepository<LibraryItem> libraryItemRepository,
IRepository<Collection> collectionRepository,
IRepository<Movie> movieRepository,
IRepository<Show> showRepository,
IRepository<Season> seasonRepository,
IRepository<Episode> episodeRepository,
IRepository<People> peopleRepository,
IRepository<Studio> studioRepository,
IRepository<User> userRepository)
{
_repositories = repositories.ToArray();
LibraryItemRepository = (ILibraryItemRepository)GetRepository<LibraryItem>();
CollectionRepository = (ICollectionRepository)GetRepository<Collection>();
MovieRepository = (IMovieRepository)GetRepository<Movie>();
ShowRepository = (IShowRepository)GetRepository<Show>();
SeasonRepository = (ISeasonRepository)GetRepository<Season>();
EpisodeRepository = (IEpisodeRepository)GetRepository<Episode>();
PeopleRepository = (IPeopleRepository)GetRepository<People>();
StudioRepository = (IStudioRepository)GetRepository<Studio>();
UserRepository = (IUserRepository)GetRepository<User>();
LibraryItems = libraryItemRepository;
Collections = collectionRepository;
Movies = movieRepository;
Shows = showRepository;
Seasons = seasonRepository;
Episodes = episodeRepository;
People = peopleRepository;
Studios = studioRepository;
Users = userRepository;
}
/// <inheritdoc />
public IRepository<T> GetRepository<T>()
where T : class, IResource
{
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
return ret;
throw new ItemNotFoundException($"No repository found for the type {typeof(T).Name}.");
}
public IRepository<LibraryItem> LibraryItems { get; }
/// <inheritdoc />
public Task<T> Get<T>(int id)
where T : class, IResource
{
return GetRepository<T>().Get(id);
}
public IRepository<Collection> Collections { get; }
/// <inheritdoc />
public Task<T> Get<T>(string slug)
where T : class, IResource
{
return GetRepository<T>().Get(slug);
}
public IRepository<Movie> Movies { get; }
/// <inheritdoc />
public Task<T> Get<T>(Expression<Func<T, bool>> where)
where T : class, IResource
{
return GetRepository<T>().Get(where);
}
public IRepository<Show> Shows { get; }
/// <inheritdoc />
public Task<Season> Get(int showID, int seasonNumber)
{
return SeasonRepository.Get(showID, seasonNumber);
}
public IRepository<Season> Seasons { get; }
/// <inheritdoc />
public Task<Season> Get(string showSlug, int seasonNumber)
{
return SeasonRepository.Get(showSlug, seasonNumber);
}
public IRepository<Episode> Episodes { get; }
/// <inheritdoc />
public Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{
return EpisodeRepository.Get(showID, seasonNumber, episodeNumber);
}
public IRepository<People> People { get; }
/// <inheritdoc />
public Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber)
{
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
}
public IRepository<Studio> Studios { get; }
/// <inheritdoc />
public async Task<T?> GetOrDefault<T>(int id)
where T : class, IResource
{
return await GetRepository<T>().GetOrDefault(id);
}
/// <inheritdoc />
public async Task<T?> GetOrDefault<T>(string slug)
where T : class, IResource
{
return await GetRepository<T>().GetOrDefault(slug);
}
/// <inheritdoc />
public async Task<T?> GetOrDefault<T>(Expression<Func<T, bool>> where, Sort<T>? sortBy)
where T : class, IResource
{
return await GetRepository<T>().GetOrDefault(where, sortBy);
}
/// <inheritdoc />
public async Task<Season?> GetOrDefault(int showID, int seasonNumber)
{
return await SeasonRepository.GetOrDefault(showID, seasonNumber);
}
/// <inheritdoc />
public async Task<Season?> GetOrDefault(string showSlug, int seasonNumber)
{
return await SeasonRepository.GetOrDefault(showSlug, seasonNumber);
}
/// <inheritdoc />
public async Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{
return await EpisodeRepository.GetOrDefault(showID, seasonNumber, episodeNumber);
}
/// <inheritdoc />
public async Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
}
/// <summary>
/// Set relations between to objects.
/// </summary>
/// <param name="obj">The owner object</param>
/// <param name="loader">A Task to load a collection of related objects</param>
/// <param name="setter">A setter function to store the collection of related objects</param>
/// <param name="inverse">A setter function to store the owner of a releated object loaded</param>
/// <typeparam name="T1">The type of the owner object</typeparam>
/// <typeparam name="T2">The type of the related object</typeparam>
private static async Task _SetRelation<T1, T2>(T1 obj,
Task<ICollection<T2>> loader,
Action<T1, ICollection<T2>> setter,
Action<T2, T1> inverse)
{
ICollection<T2> loaded = await loader;
setter(obj, loaded);
foreach (T2 item in loaded)
inverse(item, obj);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
where T2 : class, IResource
{
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
where T2 : class
{
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName, bool force = false)
where T : class, IResource
{
await Load(obj as IResource, memberName, force);
return obj;
}
/// <inheritdoc />
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1507:CodeMustNotContainMultipleBlankLinesInARow",
Justification = "Separate the code by semantics and simplify the code read.")]
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1107:Code should not contain multiple statements on one line",
Justification = "Assing IDs and Values in the same line.")]
public Task Load(IResource obj, string memberName, bool force = false)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
object? existingValue = obj.GetType()
.GetProperties()
.FirstOrDefault(x => string.Equals(x.Name, memberName, StringComparison.InvariantCultureIgnoreCase))
?.GetValue(obj);
if (existingValue != null && !force)
return Task.CompletedTask;
return (obj, member: memberName) switch
{
(Collection c, nameof(Collection.Shows)) => ShowRepository
.GetAll(x => x.Collections!.Any(y => y.Id == obj.Id))
.Then(x => c.Shows = x),
(Collection c, nameof(Collection.Movies)) => MovieRepository
.GetAll(x => x.Collections!.Any(y => y.Id == obj.Id))
.Then(x => c.Movies = x),
(Movie m, nameof(Movie.People)) => PeopleRepository
.GetFromShow(obj.Id)
.Then(x => m.People = x),
(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.People)) => PeopleRepository
.GetFromShow(obj.Id)
.Then(x => s.People = x),
(Show s, nameof(Show.Seasons)) => _SetRelation(s,
SeasonRepository.GetAll(x => x.Show!.Id == obj.Id),
(x, y) => x.Seasons = y,
(x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Show!.Id == obj.Id),
(x, y) => x.Episodes = y,
(x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Collections)) => CollectionRepository
.GetAll(x => x.Shows!.Any(y => y.Id == obj.Id))
.Then(x => s.Collections = x),
(Show s, nameof(Show.Studio)) => StudioRepository
.GetOrDefault(x => x.Shows!.Any(y => y.Id == obj.Id))
.Then(x =>
{
s.Studio = x;
s.StudioId = x?.Id ?? 0;
}),
(Season s, nameof(Season.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Season!.Id == obj.Id),
(x, y) => x.Episodes = y,
(x, y) => { x.Season = y; x.SeasonId = y.Id; }),
(Season s, nameof(Season.Show)) => ShowRepository
.GetOrDefault(x => x.Seasons!.Any(y => y.Id == obj.Id))
.Then(x =>
{
s.Show = x;
s.ShowId = x?.Id ?? 0;
}),
(Episode e, nameof(Episode.Show)) => ShowRepository
.GetOrDefault(x => x.Episodes!.Any(y => y.Id == obj.Id))
.Then(x =>
{
e.Show = x;
e.ShowId = x?.Id ?? 0;
}),
(Episode e, nameof(Episode.Season)) => SeasonRepository
.GetOrDefault(x => x.Episodes!.Any(y => y.Id == e.Id))
.Then(x =>
{
e.Season = x;
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()),
(Episode e, nameof(Episode.NextEpisode)) => EpisodeRepository
.GetAll(
where: x => x.ShowId == e.ShowId,
limit: new Pagination(1, e.Id)
).Then(x => e.NextEpisode = x.FirstOrDefault()),
(Studio s, nameof(Studio.Shows)) => ShowRepository
.GetAll(x => x.Studio!.Id == obj.Id)
.Then(x => s.Shows = x),
(Studio s, nameof(Studio.Movies)) => MovieRepository
.GetAll(x => x.Studio!.Id == obj.Id)
.Then(x => s.Movies = x),
(People p, nameof(People.Roles)) => PeopleRepository
.GetFromPeople(obj.Id)
.Then(x => p.Roles = x),
_ => throw new ArgumentException($"Couldn't find a way to load {memberName} of {obj.Slug}.")
};
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return PeopleRepository.GetFromShow(showID, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return PeopleRepository.GetFromShow(showSlug, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return PeopleRepository.GetFromPeople(id, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return PeopleRepository.GetFromPeople(slug, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default,
Pagination? limit = default)
where T : class, IResource
{
return GetRepository<T>().GetAll(where, sort, limit);
}
/// <inheritdoc />
public Task<int> GetCount<T>(Expression<Func<T, bool>>? where = null)
where T : class, IResource
{
return GetRepository<T>().GetCount(where);
}
/// <inheritdoc />
public Task<ICollection<T>> Search<T>(string query)
where T : class, IResource
{
return GetRepository<T>().Search(query);
}
/// <inheritdoc />
public Task<T> Create<T>(T item)
where T : class, IResource
{
return GetRepository<T>().Create(item);
}
/// <inheritdoc />
public Task<T> CreateIfNotExists<T>(T item)
where T : class, IResource
{
return GetRepository<T>().CreateIfNotExists(item);
}
/// <inheritdoc />
public Task<T> Edit<T>(T item)
where T : class, IResource
{
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 />
public Task Delete<T>(T item)
where T : class, IResource
{
return GetRepository<T>().Delete(item);
}
/// <inheritdoc />
public Task Delete<T>(int id)
where T : class, IResource
{
return GetRepository<T>().Delete(id);
}
/// <inheritdoc />
public Task Delete<T>(string slug)
where T : class, IResource
{
return GetRepository<T>().Delete(slug);
}
public IRepository<User> Users { get; }
}
}

View File

@ -30,7 +30,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle collections
/// </summary>
public class CollectionRepository : LocalRepository<Collection>, ICollectionRepository
public class CollectionRepository : LocalRepository<Collection>
{
/// <summary>
/// The database handle

View File

@ -22,9 +22,7 @@ using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Core.Controllers
@ -32,14 +30,14 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle episodes.
/// </summary>
public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository
public class EpisodeRepository : LocalRepository<Episode>
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
private readonly IShowRepository _shows;
private readonly IRepository<Show> _shows;
/// <inheritdoc />
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
@ -56,7 +54,7 @@ namespace Kyoo.Core.Controllers
/// <param name="shows">A show repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public EpisodeRepository(DatabaseContext database,
IShowRepository shows,
IRepository<Show> shows,
IThumbnailsManager thumbs)
: base(database, thumbs)
{
@ -76,60 +74,6 @@ namespace Kyoo.Core.Controllers
};
}
/// <inheritdoc />
public Task<Episode?> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber).Then(SetBackingImage);
}
/// <inheritdoc />
public Task<Episode?> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{
return _database.Episodes.FirstOrDefaultAsync(x => x.Show!.Slug == showSlug
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber).Then(SetBackingImage);
}
/// <inheritdoc />
public async Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{
Episode? ret = await GetOrDefault(showID, seasonNumber, episodeNumber);
if (ret == null)
throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showID}.");
return ret;
}
/// <inheritdoc />
public async Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber)
{
Episode? ret = await GetOrDefault(showSlug, seasonNumber, episodeNumber);
if (ret == null)
throw new ItemNotFoundException($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}.");
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{
Episode? ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage);
if (ret == null)
throw new ItemNotFoundException();
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{
Episode? ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show!.Slug == showSlug
&& x.AbsoluteNumber == absoluteNumber).Then(SetBackingImage);
if (ret == null)
throw new ItemNotFoundException();
return ret;
}
/// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query)
{
@ -156,9 +100,9 @@ namespace Kyoo.Core.Controllers
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() =>
obj.SeasonNumber != null && obj.EpisodeNumber != null
? Get(obj.ShowId, obj.SeasonNumber.Value, obj.EpisodeNumber.Value)
: GetAbsolute(obj.ShowId, obj.AbsoluteNumber!.Value));
obj is { SeasonNumber: not null, EpisodeNumber: not null }
? Get(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber && x.EpisodeNumber == obj.EpisodeNumber)
: Get(x => x.ShowId == obj.ShowId && x.AbsoluteNumber == obj.AbsoluteNumber));
OnResourceCreated(obj);
return obj;
}

View File

@ -30,7 +30,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle library items.
/// </summary>
public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository
public class LibraryItemRepository : LocalRepository<LibraryItem>
{
/// <summary>
/// The database handle
@ -41,7 +41,7 @@ namespace Kyoo.Core.Controllers
protected override Sort<LibraryItem> DefaultSort => new Sort<LibraryItem>.By(x => x.Name);
/// <summary>
/// Create a new <see cref="ILibraryItemRepository"/>.
/// Create a new <see cref="LibraryItemRepository"/>.
/// </summary>
/// <param name="database">The database instance</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>

View File

@ -27,6 +27,7 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Api;
using Kyoo.Postgresql;
using Kyoo.Utils;
@ -83,7 +84,7 @@ namespace Kyoo.Core.Controllers
/// Sort the given query.
/// </summary>
/// <param name="query">The query to sort.</param>
/// <param name="sortBy">How to sort the query</param>
/// <param name="sortBy">How to sort the query.</param>
/// <returns>The newly sorted query.</returns>
protected IOrderedQueryable<T> Sort(IQueryable<T> query, Sort<T>? sortBy = null)
{
@ -268,6 +269,15 @@ namespace Kyoo.Core.Controllers
return Expression.Lambda<Func<T, bool>>(filter!, x);
}
protected IQueryable<T> AddIncludes(IQueryable<T> query, Include<T>? include)
{
if (include == null)
return query;
foreach (string field in include.Fields)
query = query.Include(field);
return query;
}
protected void SetBackingImage(T? obj)
{
if (obj is not IThumbnails thumbs)
@ -306,50 +316,66 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc/>
public virtual async Task<T> Get(int id)
public virtual async Task<T> Get(int id, Include<T>? include = default)
{
T? ret = await GetOrDefault(id);
T? ret = await GetOrDefault(id, include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<T> Get(string slug)
public virtual async Task<T> Get(string slug, Include<T>? include = default)
{
T? ret = await GetOrDefault(slug);
T? ret = await GetOrDefault(slug, include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<T> Get(Expression<Func<T, bool>> where)
public virtual async Task<T> Get(Expression<Func<T, bool>> where, Include<T>? include = default)
{
T? ret = await GetOrDefault(where);
T? ret = await GetOrDefault(where, include: include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret;
}
/// <inheritdoc />
public virtual Task<T?> GetOrDefault(int id)
public virtual Task<T?> GetOrDefault(int id, Include<T>? include = default)
{
return Database.Set<T>().FirstOrDefaultAsync(x => x.Id == id).Then(SetBackingImage);
return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Id == id)
.Then(SetBackingImage);
}
/// <inheritdoc />
public virtual Task<T?> GetOrDefault(string slug)
public virtual Task<T?> GetOrDefault(string slug, Include<T>? include = default)
{
if (slug == "random")
return Database.Set<T>().OrderBy(x => EF.Functions.Random()).FirstOrDefaultAsync().Then(SetBackingImage);
return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug).Then(SetBackingImage);
{
return AddIncludes(Database.Set<T>(), include)
.OrderBy(x => EF.Functions.Random())
.FirstOrDefaultAsync()
.Then(SetBackingImage);
}
return AddIncludes(Database.Set<T>(), include)
.FirstOrDefaultAsync(x => x.Slug == slug)
.Then(SetBackingImage);
}
/// <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,
Include<T>? include = default,
Sort<T>? sortBy = default)
{
return Sort(Database.Set<T>(), sortBy).FirstOrDefaultAsync(where).Then(SetBackingImage);
return Sort(
AddIncludes(Database.Set<T>(), include),
sortBy
)
.FirstOrDefaultAsync(where)
.Then(SetBackingImage);
}
/// <inheritdoc/>
@ -358,9 +384,10 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc/>
public virtual async Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default,
Pagination? limit = default)
Pagination? limit = default,
Include<T>? include = default)
{
return (await ApplyFilters(Database.Set<T>(), where, sort, limit))
return (await ApplyFilters(Database.Set<T>(), where, sort, limit, include))
.Select(SetBackingImageSelf).ToList();
}
@ -371,12 +398,15 @@ namespace Kyoo.Core.Controllers
/// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order and sort by)</param>
/// <param name="limit">Pagination information (where to start and how many to get)</param>
/// <param name="include">Related fields to also load with this query.</param>
/// <returns>The filtered query</returns>
protected async Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
Expression<Func<T, bool>>? where = null,
Sort<T>? sort = default,
Pagination? limit = default)
Pagination? limit = default,
Include<T>? include = default)
{
query = AddIncludes(query, include);
query = Sort(query, sort);
if (where != null)
query = query.Where(where);

View File

@ -29,7 +29,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle shows
/// </summary>
public class MovieRepository : LocalRepository<Movie>, IMovieRepository
public class MovieRepository : LocalRepository<Movie>
{
/// <summary>
/// The database handle
@ -39,12 +39,12 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A studio repository to handle creation/validation of related studios.
/// </summary>
private readonly IStudioRepository _studios;
private readonly IRepository<Studio> _studios;
/// <summary>
/// A people repository to handle creation/validation of related people.
/// </summary>
private readonly IPeopleRepository _people;
private readonly IRepository<People> _people;
/// <inheritdoc />
protected override Sort<Movie> DefaultSort => new Sort<Movie>.By(x => x.Name);
@ -57,8 +57,8 @@ namespace Kyoo.Core.Controllers
/// <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,
IRepository<Studio> studios,
IRepository<People> people,
IThumbnailsManager thumbs)
: base(database, thumbs)
{

View File

@ -32,7 +32,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle people.
/// </summary>
public class PeopleRepository : LocalRepository<People>, IPeopleRepository
public class PeopleRepository : LocalRepository<People>
{
/// <summary>
/// The database handle
@ -42,7 +42,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A lazy loaded show repository to validate requests from shows.
/// </summary>
private readonly Lazy<IShowRepository> _shows;
private readonly Lazy<IRepository<Show>> _shows;
/// <inheritdoc />
protected override Sort<People> DefaultSort => new Sort<People>.By(x => x.Name);
@ -54,7 +54,7 @@ namespace Kyoo.Core.Controllers
/// <param name="shows">A lazy loaded show repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public PeopleRepository(DatabaseContext database,
Lazy<IShowRepository> shows,
Lazy<IRepository<Show>> shows,
IThumbnailsManager thumbs)
: base(database, thumbs)
{
@ -123,13 +123,12 @@ namespace Kyoo.Core.Controllers
await base.Delete(obj);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return Task.FromResult<ICollection<PeopleRole>>(new List<PeopleRole>());
// /// <inheritdoc />
// public Task<ICollection<PeopleRole>> GetFromShow(int showID,
// Expression<Func<PeopleRole, bool>>? where = null,
// Sort<PeopleRole>? sort = default,
// Pagination? limit = default)
// {
// ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.ShowID == showID)
// .Include(x => x.People),
@ -142,69 +141,66 @@ namespace Kyoo.Core.Controllers
// foreach (PeopleRole role in people)
// role.ForPeople = true;
// return people;
}
// }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return Task.FromResult<ICollection<PeopleRole>>(new List<PeopleRole>());
// ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.Show.Slug == showSlug)
// .Include(x => x.People)
// .Include(x => x.Show),
// id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
// x => x.People.Name,
// where,
// sort,
// limit);
// if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null)
// throw new ItemNotFoundException();
// foreach (PeopleRole role in people)
// role.ForPeople = true;
// return people;
}
// /// <inheritdoc />
// public Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
// Expression<Func<PeopleRole, bool>>? where = null,
// Sort<PeopleRole>? sort = default,
// Pagination? limit = default)
// {
// ICollection<PeopleRole> people = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.Show.Slug == showSlug)
// .Include(x => x.People)
// .Include(x => x.Show),
// id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
// x => x.People.Name,
// where,
// sort,
// limit);
// if (!people.Any() && await _shows.Value.GetOrDefault(showSlug) == null)
// throw new ItemNotFoundException();
// foreach (PeopleRole role in people)
// role.ForPeople = true;
// return people;
// }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return Task.FromResult<ICollection<PeopleRole>>(new List<PeopleRole>());
// ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.PeopleID == id)
// .Include(x => x.Show),
// y => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == y),
// x => x.Show.Title,
// where,
// sort,
// limit);
// if (!roles.Any() && await GetOrDefault(id) == null)
// throw new ItemNotFoundException();
// return roles;
}
// /// <inheritdoc />
// public Task<ICollection<PeopleRole>> GetFromPeople(int id,
// Expression<Func<PeopleRole, bool>>? where = null,
// Sort<PeopleRole>? sort = default,
// Pagination? limit = default)
// {
// ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.PeopleID == id)
// .Include(x => x.Show),
// y => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == y),
// x => x.Show.Title,
// where,
// sort,
// limit);
// if (!roles.Any() && await GetOrDefault(id) == null)
// throw new ItemNotFoundException();
// return roles;
// }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>>? where = null,
Sort<PeopleRole>? sort = default,
Pagination? limit = default)
{
return Task.FromResult<ICollection<PeopleRole>>(new List<PeopleRole>());
// ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.People.Slug == slug)
// .Include(x => x.Show),
// id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
// x => x.Show.Title,
// where,
// sort,
// limit);
// if (!roles.Any() && await GetOrDefault(slug) == null)
// throw new ItemNotFoundException();
// return roles;
}
// /// <inheritdoc />
// public Task<ICollection<PeopleRole>> GetFromPeople(string slug,
// Expression<Func<PeopleRole, bool>>? where = null,
// Sort<PeopleRole>? sort = default,
// Pagination? limit = default)
// {
// ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
// .Where(x => x.People.Slug == slug)
// .Include(x => x.Show),
// id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id),
// x => x.Show.Title,
// where,
// sort,
// limit);
// if (!roles.Any() && await GetOrDefault(slug) == null)
// throw new ItemNotFoundException();
// return roles;
// }
}
}

View File

@ -32,7 +32,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle seasons.
/// </summary>
public class SeasonRepository : LocalRepository<Season>, ISeasonRepository
public class SeasonRepository : LocalRepository<Season>
{
/// <summary>
/// The database handle
@ -49,7 +49,7 @@ namespace Kyoo.Core.Controllers
/// <param name="shows">A shows repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public SeasonRepository(DatabaseContext database,
IShowRepository shows,
IRepository<Show> shows,
IThumbnailsManager thumbs)
: base(database, thumbs)
{
@ -68,38 +68,6 @@ namespace Kyoo.Core.Controllers
};
}
/// <inheritdoc/>
public async Task<Season> Get(int showID, int seasonNumber)
{
Season? ret = await GetOrDefault(showID, seasonNumber);
if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showID}");
return ret;
}
/// <inheritdoc/>
public async Task<Season> Get(string showSlug, int seasonNumber)
{
Season? ret = await GetOrDefault(showSlug, seasonNumber);
if (ret == null)
throw new ItemNotFoundException($"No season {seasonNumber} found for the show {showSlug}");
return ret;
}
/// <inheritdoc/>
public Task<Season?> GetOrDefault(int showID, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber).Then(SetBackingImage);
}
/// <inheritdoc/>
public Task<Season?> GetOrDefault(string showSlug, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.Show!.Slug == showSlug
&& x.SeasonNumber == seasonNumber).Then(SetBackingImage);
}
/// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query)
{
@ -119,7 +87,7 @@ namespace Kyoo.Core.Controllers
await base.Create(obj);
obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug;
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.ShowId, obj.SeasonNumber));
await _database.SaveChangesAsync(() => Get(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber));
OnResourceCreated(obj);
return obj;
}

View File

@ -21,7 +21,6 @@ using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
@ -31,7 +30,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle shows
/// </summary>
public class ShowRepository : LocalRepository<Show>, IShowRepository
public class ShowRepository : LocalRepository<Show>
{
/// <summary>
/// The database handle
@ -41,12 +40,12 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A studio repository to handle creation/validation of related studios.
/// </summary>
private readonly IStudioRepository _studios;
private readonly IRepository<Studio> _studios;
/// <summary>
/// A people repository to handle creation/validation of related people.
/// </summary>
private readonly IPeopleRepository _people;
private readonly IRepository<People> _people;
/// <inheritdoc />
protected override Sort<Show> DefaultSort => new Sort<Show>.By(x => x.Name);
@ -59,8 +58,8 @@ namespace Kyoo.Core.Controllers
/// <param name="people">A people repository</param>
/// <param name="thumbs">The thumbnail manager used to store images.</param>
public ShowRepository(DatabaseContext database,
IStudioRepository studios,
IPeopleRepository people,
IRepository<Studio> studios,
IRepository<People> people,
IThumbnailsManager thumbs)
: base(database, thumbs)
{
@ -135,17 +134,6 @@ namespace Kyoo.Core.Controllers
}
}
/// <inheritdoc />
public async Task<string> GetSlug(int showID)
{
string? ret = await _database.Shows.Where(x => x.Id == showID)
.Select(x => x.Slug)
.FirstOrDefaultAsync();
if (ret == null)
throw new ItemNotFoundException();
return ret;
}
/// <inheritdoc />
public override async Task Delete(Show obj)
{

View File

@ -30,7 +30,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A local repository to handle studios
/// </summary>
public class StudioRepository : LocalRepository<Studio>, IStudioRepository
public class StudioRepository : LocalRepository<Studio>
{
/// <summary>
/// The database handle

View File

@ -29,7 +29,7 @@ namespace Kyoo.Core.Controllers
/// <summary>
/// A repository for users.
/// </summary>
public class UserRepository : LocalRepository<User>, IUserRepository
public class UserRepository : LocalRepository<User>
{
/// <summary>
/// The database handle

View File

@ -50,15 +50,15 @@ namespace Kyoo.Core
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
builder.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>();
builder.RegisterRepository<ICollectionRepository, CollectionRepository>();
builder.RegisterRepository<IMovieRepository, MovieRepository>();
builder.RegisterRepository<IShowRepository, ShowRepository>();
builder.RegisterRepository<ISeasonRepository, SeasonRepository>();
builder.RegisterRepository<IEpisodeRepository, EpisodeRepository>();
builder.RegisterRepository<IPeopleRepository, PeopleRepository>();
builder.RegisterRepository<IStudioRepository, StudioRepository>();
builder.RegisterRepository<IUserRepository, UserRepository>();
builder.RegisterRepository<LibraryItemRepository>();
builder.RegisterRepository<CollectionRepository>();
builder.RegisterRepository<MovieRepository>();
builder.RegisterRepository<ShowRepository>();
builder.RegisterRepository<SeasonRepository>();
builder.RegisterRepository<EpisodeRepository>();
builder.RegisterRepository<PeopleRepository>();
builder.RegisterRepository<StudioRepository>();
builder.RegisterRepository<UserRepository>();
}
/// <inheritdoc />

View File

@ -62,17 +62,18 @@ namespace Kyoo.Core.Api
/// Get a specific resource via it's ID or it's slug.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to retrieve.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>The retrieved resource.</returns>
/// <response code="404">A resource with the given ID or slug does not exist.</response>
[HttpGet("{identifier:id}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Get(Identifier identifier)
public async Task<ActionResult<T>> Get(Identifier identifier, [FromQuery] Include<T>? fields)
{
T? ret = await identifier.Match(
id => Repository.GetOrDefault(id),
slug => Repository.GetOrDefault(slug)
id => Repository.GetOrDefault(id, fields),
slug => Repository.GetOrDefault(slug, fields)
);
if (ret == null)
return NotFound();
@ -106,6 +107,7 @@ namespace Kyoo.Core.Api
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="where">Filter the returned items.</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of resources that match every filters.</returns>
/// <response code="400">Invalid filters or sort information.</response>
[HttpGet]
@ -115,12 +117,14 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] Sort<T> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<T>? fields)
{
ICollection<T> resources = await Repository.GetAll(
ApiHelper.ParseWhere<T>(where),
sortBy,
pagination
pagination,
fields
);
return Page(resources, pagination.Limit);

View File

@ -18,18 +18,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Core.Api
{
@ -54,80 +43,7 @@ namespace Kyoo.Core.Api
context.ActionArguments["where"] = nWhere;
}
List<string> fields = context.HttpContext.Request.Query["fields"]
.SelectMany(x => x!.Split(','))
.ToList();
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
Type type = descriptor.MethodInfo.ReturnType;
type = Utility.GetGenericDefinition(type, typeof(Task<>))?.GetGenericArguments()[0] ?? type;
type = Utility.GetGenericDefinition(type, typeof(ActionResult<>))?.GetGenericArguments()[0] ?? type;
type = Utility.GetGenericDefinition(type, typeof(Page<>))?.GetGenericArguments()[0] ?? type;
context.HttpContext.Items["ResourceType"] = type.Name;
PropertyInfo[] properties = type.GetProperties()
.Where(x => x.GetCustomAttribute<LoadableRelationAttribute>() != null)
.ToArray();
if (fields.Count == 1 && fields.Contains("all"))
fields = properties.Select(x => x.Name).ToList();
else
{
fields = fields
.Select(x =>
{
string? property = properties
.FirstOrDefault(y
=> string.Equals(x, y.Name, StringComparison.InvariantCultureIgnoreCase))
?.Name;
if (property != null)
return property;
context.Result = new BadRequestObjectResult(
new RequestError($"{x} does not exist on {type.Name}.")
);
return null;
})
.OfType<string>()
.ToList();
if (context.Result != null)
return;
}
}
context.HttpContext.Items["fields"] = fields;
base.OnActionExecuting(context);
}
/// <inheritdoc />
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ObjectResult result)
await _LoadResultRelations(context, result);
await base.OnResultExecutionAsync(context, next);
}
private static async Task _LoadResultRelations(ActionContext context, ObjectResult result)
{
if (result.DeclaredType == null)
return;
ILibraryManager library = context.HttpContext.RequestServices.GetRequiredService<ILibraryManager>();
ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"]!;
Type? pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>));
if (pageType != null)
{
foreach (IResource resource in ((dynamic)result.Value!).Items)
{
foreach (string field in fields!)
await library.Load(resource, field);
}
}
else if (result.DeclaredType.IsAssignableTo(typeof(IResource)))
{
foreach (string field in fields!)
await library.Load((IResource)result.Value!, field);
}
}
}
}

View File

@ -56,43 +56,43 @@ namespace Kyoo.Core.Api
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public StaffApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.PeopleRepository, thumbs)
: base(libraryManager.People, thumbs)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get roles
/// </summary>
/// <remarks>
/// List the roles in witch this person has played, written or worked in a way.
/// </remarks>
/// <param name="identifier">The ID or slug of the person.</param>
/// <param name="sortBy">A key to sort roles by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of roles to return.</param>
/// <returns>A page of roles.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No person with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/roles")]
[HttpGet("{identifier:id}/role", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<PeopleRole>>> GetRoles(Identifier identifier,
[FromQuery] Sort<PeopleRole> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
ICollection<PeopleRole> resources = await identifier.Match(
id => _libraryManager.GetRolesFromPeople(id, whereQuery, sortBy, pagination),
slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sortBy, pagination)
);
return Page(resources, pagination.Limit);
}
// /// <summary>
// /// Get roles
// /// </summary>
// /// <remarks>
// /// List the roles in witch this person has played, written or worked in a way.
// /// </remarks>
// /// <param name="identifier">The ID or slug of the person.</param>
// /// <param name="sortBy">A key to sort roles by.</param>
// /// <param name="where">An optional list of filters.</param>
// /// <param name="pagination">The number of roles to return.</param>
// /// <returns>A page of roles.</returns>
// /// <response code="400">The filters or the sort parameters are invalid.</response>
// /// <response code="404">No person with the given ID or slug could be found.</response>
// [HttpGet("{identifier:id}/roles")]
// [HttpGet("{identifier:id}/role", Order = AlternativeRoute)]
// [PartialPermission(Kind.Read)]
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<Page<PeopleRole>>> GetRoles(Identifier identifier,
// [FromQuery] Sort<PeopleRole> sortBy,
// [FromQuery] Dictionary<string, string> where,
// [FromQuery] Pagination pagination)
// {
// Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
//
// ICollection<PeopleRole> resources = await identifier.Match(
// id => _libraryManager.GetRolesFromPeople(id, whereQuery, sortBy, pagination),
// slug => _libraryManager.GetRolesFromPeople(slug, whereQuery, sortBy, pagination)
// );
//
// return Page(resources, pagination.Limit);
// }
}
}

View File

@ -16,7 +16,6 @@
// 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;
@ -53,7 +52,7 @@ namespace Kyoo.Core.Api
/// The library manager used to modify or retrieve information in the data store.
/// </param>
public StudioApi(ILibraryManager libraryManager)
: base(libraryManager.StudioRepository)
: base(libraryManager.Studios)
{
_libraryManager = libraryManager;
}
@ -68,6 +67,7 @@ namespace Kyoo.Core.Api
/// <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>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No studio with the given ID or slug could be found.</response>
@ -80,15 +80,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Show> fields)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ICollection<Show> resources = await _libraryManager.Shows.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Studio>()) == null)
if (!resources.Any() && await _libraryManager.Studios.GetOrDefault(identifier.IsSame<Studio>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}

View File

@ -54,7 +54,7 @@ namespace Kyoo.Core.Api
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public CollectionApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.CollectionRepository, thumbs)
: base(libraryManager.Collections, thumbs)
{
_libraryManager = libraryManager;
}
@ -69,6 +69,7 @@ namespace Kyoo.Core.Api
/// <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>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID could be found.</response>
@ -81,15 +82,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Show> fields)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ICollection<Show> resources = await _libraryManager.Shows.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Collection>(x => x.Collections!)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Collection>()) == null)
if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}

View File

@ -53,7 +53,7 @@ namespace Kyoo.Core.Api
/// <param name="thumbnails">The thumbnail manager used to retrieve images paths.</param>
public EpisodeApi(ILibraryManager libraryManager,
IThumbnailsManager thumbnails)
: base(libraryManager.EpisodeRepository, thumbnails)
: base(libraryManager.Episodes, thumbnails)
{
_libraryManager = libraryManager;
}
@ -65,15 +65,16 @@ namespace Kyoo.Core.Api
/// Get the show that this episode is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>The show that contains this episode.</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/show")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Show>> GetShow(Identifier identifier)
public async Task<ActionResult<Show>> GetShow(Identifier identifier, [FromQuery] Include<Show> fields)
{
return await _libraryManager.Get(identifier.IsContainedIn<Show, Episode>(x => x.Episodes!));
return await _libraryManager.Shows.Get(identifier.IsContainedIn<Show, Episode>(x => x.Episodes!), fields);
}
/// <summary>
@ -83,6 +84,7 @@ namespace Kyoo.Core.Api
/// Get the season that this episode is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>The season that contains this episode.</returns>
/// <response code="204">The episode is not part of a season.</response>
/// <response code="404">No episode with the given ID or slug could be found.</response>
@ -91,14 +93,17 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Season>> GetSeason(Identifier identifier)
public async Task<ActionResult<Season>> GetSeason(Identifier identifier, [FromQuery] Include<Season> fields)
{
Season? ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Season, Episode>(x => x.Episodes!));
Season? ret = await _libraryManager.Seasons.GetOrDefault(
identifier.IsContainedIn<Season, Episode>(x => x.Episodes!),
fields
);
if (ret != null)
return ret;
Episode? episode = await identifier.Match(
id => _libraryManager.GetOrDefault<Episode>(id),
slug => _libraryManager.GetOrDefault<Episode>(slug)
id => _libraryManager.Episodes.GetOrDefault(id),
slug => _libraryManager.Episodes.GetOrDefault(slug)
);
return episode == null
? NotFound()

View File

@ -16,14 +16,10 @@
// 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.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;
@ -44,7 +40,7 @@ namespace Kyoo.Core.Api
/// <summary>
/// The library item repository used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryItemRepository _libraryItems;
private readonly IRepository<LibraryItem> _libraryItems;
/// <summary>
/// Create a new <see cref="LibraryItemApi"/>.
@ -53,7 +49,7 @@ namespace Kyoo.Core.Api
/// The library item repository used to modify or retrieve information in the data store.
/// </param>
/// <param name="thumbs">Thumbnail manager to retrieve images.</param>
public LibraryItemApi(ILibraryItemRepository libraryItems, IThumbnailsManager thumbs)
public LibraryItemApi(IRepository<LibraryItem> libraryItems, IThumbnailsManager thumbs)
: base(libraryItems, thumbs)
{
_libraryItems = libraryItems;

View File

@ -54,7 +54,7 @@ namespace Kyoo.Core.Api
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public MovieApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.MovieRepository, thumbs)
: base(libraryManager.Movies, thumbs)
{
_libraryManager = libraryManager;
}
@ -100,15 +100,16 @@ namespace Kyoo.Core.Api
/// Get the studio that made the show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="fields">The aditional fields to include in the result.</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)
public async Task<ActionResult<Studio>> GetStudio(Identifier identifier, [FromQuery] Include<Studio> fields)
{
return await _libraryManager.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies!));
return await _libraryManager.Studios.Get(identifier.IsContainedIn<Studio, Movie>(x => x.Movies!), fields);
}
/// <summary>
@ -121,6 +122,7 @@ namespace Kyoo.Core.Api
/// <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>
/// <param name="fields">The aditional fields to include in the result.</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>
@ -133,15 +135,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Collection> fields)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Movie>(x => x.Movies!)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Movie>()) == null)
if (!resources.Any() && await _libraryManager.Movies.GetOrDefault(identifier.IsSame<Movie>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}

View File

@ -22,6 +22,7 @@ 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;
@ -52,39 +53,6 @@ namespace Kyoo.Core.Api
_libraryManager = libraryManager;
}
/// <summary>
/// Global search
/// </summary>
/// <remarks>
/// Search for collections, shows, episodes, staff, genre and studios at the same time
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <returns>A list of every resources found for the specified query.</returns>
[HttpGet]
[Permission(nameof(Collection), Kind.Read)]
[Permission(nameof(Show), Kind.Read)]
[Permission(nameof(Episode), Kind.Read)]
[Permission(nameof(People), Kind.Read)]
[Permission(nameof(Genre), Kind.Read)]
[Permission(nameof(Studio), Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<SearchResult>> Search(string query)
{
HttpContext.Items["ResourceType"] = nameof(Episode);
HttpContext.Items["fields"] = new[] { nameof(Episode.Show) };
return new SearchResult
{
Query = query,
Collections = await _libraryManager.Search<Collection>(query),
Items = await _libraryManager.Search<LibraryItem>(query),
Movies = await _libraryManager.Search<Movie>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
/// <summary>
/// Search collections
/// </summary>
@ -92,15 +60,16 @@ namespace Kyoo.Core.Api
/// Search for collections
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of collections found for the specified query.</returns>
[HttpGet("collections")]
[HttpGet("collection", Order = AlternativeRoute)]
[Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Collection>> SearchCollections(string query)
public Task<ICollection<Collection>> SearchCollections(string query, [FromQuery] Include<Collection> fields)
{
return _libraryManager.Search<Collection>(query);
return _libraryManager.Collections.Search(query);
}
/// <summary>
@ -110,15 +79,16 @@ namespace Kyoo.Core.Api
/// Search for shows
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of shows found for the specified query.</returns>
[HttpGet("shows")]
[HttpGet("show", Order = AlternativeRoute)]
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Shows")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Show>> SearchShows(string query)
public Task<ICollection<Show>> SearchShows(string query, [FromQuery] Include<Show> fields)
{
return _libraryManager.Search<Show>(query);
return _libraryManager.Shows.Search(query);
}
/// <summary>
@ -128,15 +98,16 @@ namespace Kyoo.Core.Api
/// Search for items
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of items found for the specified query.</returns>
[HttpGet("items")]
[HttpGet("item", Order = AlternativeRoute)]
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<LibraryItem>> SearchItems(string query)
public Task<ICollection<LibraryItem>> SearchItems(string query, [FromQuery] Include<LibraryItem> fields)
{
return _libraryManager.Search<LibraryItem>(query);
return _libraryManager.LibraryItems.Search(query);
}
/// <summary>
@ -146,15 +117,16 @@ namespace Kyoo.Core.Api
/// Search for episodes
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of episodes found for the specified query.</returns>
[HttpGet("episodes")]
[HttpGet("episode", Order = AlternativeRoute)]
[Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Episode>> SearchEpisodes(string query)
public Task<ICollection<Episode>> SearchEpisodes(string query, [FromQuery] Include<Episode> fields)
{
return _libraryManager.Search<Episode>(query);
return _libraryManager.Episodes.Search(query);
}
/// <summary>
@ -164,6 +136,7 @@ namespace Kyoo.Core.Api
/// Search for staff
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of staff members found for the specified query.</returns>
[HttpGet("staff")]
[HttpGet("person", Order = AlternativeRoute)]
@ -171,9 +144,9 @@ namespace Kyoo.Core.Api
[Permission(nameof(People), Kind.Read)]
[ApiDefinition("Staff")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<People>> SearchPeople(string query)
public Task<ICollection<People>> SearchPeople(string query, [FromQuery] Include<People> fields)
{
return _libraryManager.Search<People>(query);
return _libraryManager.People.Search(query);
}
/// <summary>
@ -183,15 +156,16 @@ namespace Kyoo.Core.Api
/// Search for studios
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of studios found for the specified query.</returns>
[HttpGet("studios")]
[HttpGet("studio", Order = AlternativeRoute)]
[Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ICollection<Studio>> SearchStudios(string query)
public Task<ICollection<Studio>> SearchStudios(string query, [FromQuery] Include<Studio> fields)
{
return _libraryManager.Search<Studio>(query);
return _libraryManager.Studios.Search(query);
}
}
}

View File

@ -54,7 +54,7 @@ namespace Kyoo.Core.Api
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public SeasonApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.SeasonRepository, thumbs)
: base(libraryManager.Seasons, thumbs)
{
_libraryManager = libraryManager;
}
@ -69,6 +69,7 @@ namespace Kyoo.Core.Api
/// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of episodes to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of episodes.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No season with the given ID or slug could be found.</response>
@ -81,15 +82,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier,
[FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Episode> fields)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Season>()) == null)
if (!resources.Any() && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
@ -101,15 +104,19 @@ namespace Kyoo.Core.Api
/// Get the show that this season is part of.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>The show that contains this season.</returns>
/// <response code="404">No season with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/show")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Show>> GetShow(Identifier identifier)
public async Task<ActionResult<Show>> GetShow(Identifier identifier, [FromQuery] Include<Show> fields)
{
Show? ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn<Show, Season>(x => x.Seasons!));
Show? ret = await _libraryManager.Shows.GetOrDefault(
identifier.IsContainedIn<Show, Season>(x => x.Seasons!),
fields
);
if (ret == null)
return NotFound();
return ret;

View File

@ -16,10 +16,8 @@
// 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;
@ -56,7 +54,7 @@ namespace Kyoo.Core.Api
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
public ShowApi(ILibraryManager libraryManager,
IThumbnailsManager thumbs)
: base(libraryManager.ShowRepository, thumbs)
: base(libraryManager.Shows, thumbs)
{
_libraryManager = libraryManager;
}
@ -71,6 +69,7 @@ namespace Kyoo.Core.Api
/// <param name="sortBy">A key to sort seasons by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of seasons to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of seasons.</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>
@ -83,15 +82,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Season>>> GetSeasons(Identifier identifier,
[FromQuery] Sort<Season> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Season> fields)
{
ICollection<Season> resources = await _libraryManager.GetAll(
ICollection<Season> resources = await _libraryManager.Seasons.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
@ -106,6 +107,7 @@ namespace Kyoo.Core.Api
/// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="pagination">The number of episodes to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of episodes.</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>
@ -118,51 +120,55 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier,
[FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Episode> fields)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}
/// <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] Sort<PeopleRole> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
ICollection<PeopleRole> resources = await identifier.Match(
id => _libraryManager.GetPeopleFromShow(id, whereQuery, sortBy, pagination),
slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sortBy, pagination)
);
return Page(resources, pagination.Limit);
}
// /// <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>
// /// <param name="fields">The aditional fields to include in the result.</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] Sort<PeopleRole> sortBy,
// [FromQuery] Dictionary<string, string> where,
// [FromQuery] Pagination pagination,
// [FromQuery] Include<PeopleRole> fields)
// {
// Expression<Func<PeopleRole, bool>>? whereQuery = ApiHelper.ParseWhere<PeopleRole>(where);
//
// ICollection<PeopleRole> resources = await identifier.Match(
// id => _libraryManager.GetPeopleFromShow(id, whereQuery, sortBy, pagination),
// slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sortBy, pagination)
// );
// return Page(resources, pagination.Limit);
// }
/// <summary>
/// Get studio that made the show
@ -171,15 +177,16 @@ namespace Kyoo.Core.Api
/// Get the studio that made the show.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="fields">The aditional fields to include in the result.</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)
public async Task<ActionResult<Studio>> GetStudio(Identifier identifier, [FromQuery] Include<Studio> fields)
{
return await _libraryManager.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows!));
return await _libraryManager.Studios.Get(identifier.IsContainedIn<Studio, Show>(x => x.Shows!), fields);
}
/// <summary>
@ -192,6 +199,7 @@ namespace Kyoo.Core.Api
/// <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>
/// <param name="fields">The aditional fields to include in the result.</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>
@ -204,15 +212,17 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
[FromQuery] Pagination pagination,
[FromQuery] Include<Collection> fields)
{
ICollection<Collection> resources = await _libraryManager.GetAll(
ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
sortBy,
pagination
pagination,
fields
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Show>()) == null)
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
return NotFound();
return Page(resources, pagination.Limit);
}

View File

@ -18,8 +18,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Core.Controllers;
using Kyoo.Postgresql;
using Moq;
@ -34,6 +36,8 @@ namespace Kyoo.Tests.Database
private readonly List<DatabaseContext> _databases = new();
private readonly IBaseRepository[] _repositories;
public RepositoryActivator(ITestOutputHelper output, PostgresFixture postgres = null)
{
Context = new PostgresTestContext(postgres, output);
@ -42,7 +46,7 @@ namespace Kyoo.Tests.Database
CollectionRepository collection = new(_NewContext(), thumbs.Object);
StudioRepository studio = new(_NewContext(), thumbs.Object);
PeopleRepository people = new(_NewContext(),
new Lazy<IShowRepository>(() => LibraryManager.ShowRepository),
new Lazy<IRepository<Show>>(() => LibraryManager.Shows),
thumbs.Object);
MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object);
ShowRepository show = new(_NewContext(), studio, people, thumbs.Object);
@ -51,7 +55,8 @@ namespace Kyoo.Tests.Database
EpisodeRepository episode = new(_NewContext(), show, thumbs.Object);
UserRepository user = new(_NewContext(), thumbs.Object);
LibraryManager = new LibraryManager(new IBaseRepository[] {
_repositories = new IBaseRepository[]
{
libraryItem,
collection,
movies,
@ -61,7 +66,25 @@ namespace Kyoo.Tests.Database
people,
studio,
user
});
};
LibraryManager = new LibraryManager(
libraryItem,
collection,
movies,
show,
season,
episode,
people,
studio,
user
);
}
public IRepository<T> GetRepository<T>()
where T: class, IResource
{
return _repositories.First(x => x.RepositoryType == typeof(T)) as IRepository<T>;
}
private DatabaseContext _NewContext()

View File

@ -37,7 +37,7 @@ namespace Kyoo.Tests.Database
protected RepositoryTests(RepositoryActivator repositories)
{
Repositories = repositories;
_repository = Repositories.LibraryManager.GetRepository<T>();
_repository = Repositories.GetRepository<T>();
}
public void Dispose()

View File

@ -41,12 +41,12 @@ namespace Kyoo.Tests.Database
public abstract class ACollectionTests : RepositoryTests<Collection>
{
private readonly ICollectionRepository _repository;
private readonly IRepository<Collection> _repository;
protected ACollectionTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.CollectionRepository;
_repository = Repositories.LibraryManager.Collections;
}
[Fact]

View File

@ -42,13 +42,13 @@ namespace Kyoo.Tests.Database
public abstract class AEpisodeTests : RepositoryTests<Episode>
{
private readonly IEpisodeRepository _repository;
private readonly IRepository<Episode> _repository;
protected AEpisodeTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = repositories.LibraryManager.EpisodeRepository;
_repository = repositories.LibraryManager.Episodes;
}
[Fact]
@ -56,7 +56,7 @@ namespace Kyoo.Tests.Database
{
Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) =>
await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) =>
{
x.Slug = "new-slug";
return Task.FromResult(true);
@ -85,7 +85,7 @@ namespace Kyoo.Tests.Database
{
Episode episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
episode = await Repositories.LibraryManager.Patch<Episode>(episode.Id, (x) =>
episode = await Repositories.LibraryManager.Episodes.Patch(episode.Id, (x) =>
{
x.EpisodeNumber = 2;
return Task.FromResult(true);
@ -125,7 +125,7 @@ namespace Kyoo.Tests.Database
public async Task SlugEditAbsoluteTest()
{
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
await Repositories.LibraryManager.ShowRepository.Patch(episode.ShowId, (x) =>
await Repositories.LibraryManager.Shows.Patch(episode.ShowId, (x) =>
{
x.Slug = "new-slug";
return Task.FromResult(true);
@ -298,8 +298,8 @@ namespace Kyoo.Tests.Database
Episode expected = TestSample.Get<Episode>();
expected.Id = 0;
expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())).Id;
await _repository.Create(expected);
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
}
@ -310,8 +310,8 @@ namespace Kyoo.Tests.Database
Episode expected = TestSample.Get<Episode>();
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>()));
await _repository.Delete(TestSample.Get<Episode>());
expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
expected.ShowId = (await Repositories.LibraryManager.Shows.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.Seasons.Create(TestSample.Get<Season>())).Id;
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
}
}

View File

@ -40,12 +40,12 @@ namespace Kyoo.Tests.Database
public abstract class APeopleTests : RepositoryTests<People>
{
private readonly IPeopleRepository _repository;
private readonly IRepository<People> _repository;
protected APeopleTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.PeopleRepository;
_repository = Repositories.LibraryManager.People;
}
[Fact]

View File

@ -40,12 +40,12 @@ namespace Kyoo.Tests.Database
public abstract class ASeasonTests : RepositoryTests<Season>
{
private readonly ISeasonRepository _repository;
private readonly IRepository<Season> _repository;
protected ASeasonTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.SeasonRepository;
_repository = Repositories.LibraryManager.Seasons;
}
[Fact]
@ -53,7 +53,7 @@ namespace Kyoo.Tests.Database
{
Season season = await _repository.Get(1);
Assert.Equal("anohana-s1", season.Slug);
await Repositories.LibraryManager.ShowRepository.Patch(season.ShowId, (x) =>
await Repositories.LibraryManager.Shows.Patch(season.ShowId, (x) =>
{
x.Slug = "new-slug";
return Task.FromResult(true);

View File

@ -42,12 +42,12 @@ namespace Kyoo.Tests.Database
public abstract class AShowTests : RepositoryTests<Show>
{
private readonly IShowRepository _repository;
private readonly IRepository<Show> _repository;
protected AShowTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.ShowRepository;
_repository = Repositories.LibraryManager.Shows;
}
[Fact]
@ -278,13 +278,6 @@ namespace Kyoo.Tests.Database
Assert.Equal("300!", created.Slug);
}
[Fact]
public async Task GetSlugTest()
{
Show reference = TestSample.Get<Show>();
Assert.Equal(reference.Slug, await _repository.GetSlug(reference.Id));
}
[Theory]
[InlineData("test")]
[InlineData("super")]
@ -307,15 +300,11 @@ namespace Kyoo.Tests.Database
public async Task DeleteShowWithEpisodeAndSeason()
{
Show show = TestSample.Get<Show>();
await Repositories.LibraryManager.Load(show, x => x.Seasons);
await Repositories.LibraryManager.Load(show, x => x.Episodes);
Assert.Equal(1, await _repository.GetCount());
Assert.Single(show.Seasons!);
Assert.Single(show.Episodes!);
await _repository.Delete(show);
Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.Shows.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.Seasons.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.Episodes.GetCount());
}
}
}

View File

@ -37,12 +37,12 @@ namespace Kyoo.Tests.Database
public abstract class AStudioTests : RepositoryTests<Studio>
{
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private readonly IStudioRepository _repository;
private readonly IRepository<Studio> _repository;
protected AStudioTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.StudioRepository;
_repository = Repositories.LibraryManager.Studios;
}
}
}

View File

@ -37,12 +37,12 @@ namespace Kyoo.Tests.Database
public abstract class AUserTests : RepositoryTests<User>
{
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private readonly IUserRepository _repository;
private readonly IRepository<User> _repository;
protected AUserTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.UserRepository;
_repository = Repositories.LibraryManager.Users;
}
}
}