Merge pull request #25 from AnonymusRaccoon/library

Adding documentation to database's features (LibraryManager/Repositories)
This commit is contained in:
Zoe Roux 2021-04-23 12:37:55 -07:00 committed by GitHub
commit 0221cb7873
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2387 additions and 1552 deletions

View File

@ -3,7 +3,7 @@
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
If it fixes a bug or resolves a feature request, be sure to link to that issue. If it fixes a bug or resolves a feature request, be sure to link to that issue.
## Informations ## Information
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] New public API added - [ ] New public API added
- [ ] Non-breaking changes - [ ] Non-breaking changes

View File

@ -34,11 +34,18 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: bash shell: bash
run: | run: |
dotnet test \
'-p:CollectCoverage=true;CoverletOutputFormat=opencover' \
'-p:SkipTranscoder=true;SkipWebApp=true' || echo "Test failed. Skipping..."
dotnet build-server shutdown
./.sonar/scanner/dotnet-sonarscanner begin \ ./.sonar/scanner/dotnet-sonarscanner begin \
-k:"AnonymusRaccoon_Kyoo" \ -k:"AnonymusRaccoon_Kyoo" \
-o:"anonymus-raccoon" \ -o:"anonymus-raccoon" \
-d:sonar.login="${{ secrets.SONAR_TOKEN }}" \ -d:sonar.login="${{ secrets.SONAR_TOKEN }}" \
-d:sonar.host.url="https://sonarcloud.io" -d:sonar.host.url="https://sonarcloud.io" \
-d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml"
dotnet build --no-incremental '-p:SkipTranscoder=true;SkipWebApp=true' dotnet build --no-incremental '-p:SkipTranscoder=true;SkipWebApp=true'

View File

@ -21,8 +21,8 @@ jobs:
artifact: macos artifact: macos
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Checkout submodules with:
run: git submodule update --init --recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:

View File

@ -5,284 +5,532 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public interface ILibraryManager : IDisposable, IAsyncDisposable /// <summary>
/// An interface to interract with the database. Every repository is mapped through here.
/// </summary>
public interface ILibraryManager
{ {
// Repositories /// <summary>
/// Get the repository corresponding to the T item.
/// </summary>
/// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFound">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.
/// </summary>
ILibraryRepository LibraryRepository { get; } ILibraryRepository LibraryRepository { get; }
/// <summary>
/// The repository that handle libraries's items (a wrapper arround shows & collections).
/// </summary>
ILibraryItemRepository LibraryItemRepository { get; } ILibraryItemRepository LibraryItemRepository { get; }
/// <summary>
/// The repository that handle collections.
/// </summary>
ICollectionRepository CollectionRepository { get; } ICollectionRepository CollectionRepository { get; }
/// <summary>
/// The repository that handle shows.
/// </summary>
IShowRepository ShowRepository { get; } IShowRepository ShowRepository { get; }
/// <summary>
/// The repository that handle seasons.
/// </summary>
ISeasonRepository SeasonRepository { get; } ISeasonRepository SeasonRepository { get; }
/// <summary>
/// The repository that handle episodes.
/// </summary>
IEpisodeRepository EpisodeRepository { get; } IEpisodeRepository EpisodeRepository { get; }
/// <summary>
/// The repository that handle tracks.
/// </summary>
ITrackRepository TrackRepository { get; } ITrackRepository TrackRepository { get; }
/// <summary>
/// The repository that handle people.
/// </summary>
IPeopleRepository PeopleRepository { get; } IPeopleRepository PeopleRepository { get; }
/// <summary>
/// The repository that handle studios.
/// </summary>
IStudioRepository StudioRepository { get; } IStudioRepository StudioRepository { get; }
/// <summary>
/// The repository that handle genres.
/// </summary>
IGenreRepository GenreRepository { get; } IGenreRepository GenreRepository { get; }
/// <summary>
/// The repository that handle providers.
/// </summary>
IProviderRepository ProviderRepository { get; } IProviderRepository ProviderRepository { get; }
// Get by id /// <summary>
Task<Library> GetLibrary(int id); /// Get the resource by it's ID
Task<Collection> GetCollection(int id); /// </summary>
Task<Show> GetShow(int id); /// <param name="id">The id of the resource</param>
Task<Season> GetSeason(int id); /// <typeparam name="T">The type of the resource</typeparam>
Task<Season> GetSeason(int showID, int seasonNumber); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task<Episode> GetEpisode(int id); /// <returns>The resource found</returns>
Task<Episode> GetEpisode(int showID, int seasonNumber, int episodeNumber); Task<T> Get<T>(int id) where T : class, IResource;
Task<Genre> GetGenre(int id);
Task<Track> GetTrack(int id);
Task<Studio> GetStudio(int id);
Task<People> GetPeople(int id);
Task<ProviderID> GetProvider(int id);
// Get by slug /// <summary>
Task<Library> GetLibrary(string slug); /// Get the resource by it's slug
Task<Collection> GetCollection(string slug); /// </summary>
Task<Show> GetShow(string slug); /// <param name="slug">The slug of the resource</param>
Task<Season> GetSeason(string slug); /// <typeparam name="T">The type of the resource</typeparam>
Task<Season> GetSeason(string showSlug, int seasonNumber); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task<Episode> GetEpisode(string slug); /// <returns>The resource found</returns>
Task<Episode> GetEpisode(string showSlug, int seasonNumber, int episodeNumber); Task<T> Get<T>(string slug) where T : class, IResource;
Task<Episode> GetMovieEpisode(string movieSlug);
Task<Track> GetTrack(string slug, StreamType type = StreamType.Unknown);
Task<Genre> GetGenre(string slug);
Task<Studio> GetStudio(string slug);
Task<People> GetPeople(string slug);
Task<ProviderID> GetProvider(string slug);
// Get by predicate /// <summary>
Task<Library> GetLibrary(Expression<Func<Library, bool>> where); /// Get the resource by a filter function.
Task<Collection> GetCollection(Expression<Func<Collection, bool>> where); /// </summary>
Task<Show> GetShow(Expression<Func<Show, bool>> where); /// <param name="where">The filter function.</param>
Task<Season> GetSeason(Expression<Func<Season, bool>> where); /// <typeparam name="T">The type of the resource</typeparam>
Task<Episode> GetEpisode(Expression<Func<Episode, bool>> where); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task<Track> GetTrack(Expression<Func<Track, bool>> where); /// <returns>The first resource found that match the where function</returns>
Task<Genre> GetGenre(Expression<Func<Genre, bool>> where); Task<T> Get<T>(Expression<Func<T, bool>> where) where T : class, IResource;
Task<Studio> GetStudio(Expression<Func<Studio, bool>> where);
Task<People> GetPerson(Expression<Func<People, bool>> where);
/// <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="ItemNotFound">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="ItemNotFound">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="ItemNotFound">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="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a track from it's slug and it's type.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
/// Get the resource by it's ID or null if it is not found.
/// </summary>
/// <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>
/// <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) 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>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
/// <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>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
where T : class, IResource where T : class, IResource
where T2 : class, IResource, new(); where T2 : class, IResource, new();
/// <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>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member) Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource where T : class, IResource
where T2 : class, new(); where T2 : class, new();
/// <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>
/// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns>
Task<T> Load<T>([NotNull] T obj, string memberName) Task<T> Load<T>([NotNull] T obj, string memberName)
where T : class, IResource; where T : class, IResource;
/// <summary>
/// Load a related resource without specifing it's type.
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
Task Load([NotNull] IResource obj, string memberName); Task Load([NotNull] IResource obj, string memberName);
// Library Items relations /// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort, Expression<Func<LibraryItem, object>> sort,
Pagination limit = default Pagination limit = default
) => GetItemsFromLibrary(id, where, new Sort<LibraryItem>(sort), limit); ) => GetItemsFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string librarySlug, /// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string librarySlug,
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort, Expression<Func<LibraryItem, object>> sort,
Pagination limit = default Pagination limit = default
) => GetItemsFromLibrary(librarySlug, where, new Sort<LibraryItem>(sort), limit); ) => GetItemsFromLibrary(slug, where, new Sort<LibraryItem>(sort), limit);
// People Role relations
/// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// 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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetPeopleFromShow(showID, where, new Sort<PeopleRole>(sort), limit); ) => GetPeopleFromShow(showID, where, new Sort<PeopleRole>(sort), limit);
/// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// 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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetPeopleFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetPeopleFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit);
// Show Role relations
Task<ICollection<PeopleRole>> GetRolesFromPeople(int showID, /// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<PeopleRole>> GetRolesFromPeople(int showID,
/// <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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetRolesFromPeople(showID, where, new Sort<PeopleRole>(sort), limit); ) => GetRolesFromPeople(id, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<PeopleRole>> GetRolesFromPeople(string showSlug, /// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<PeopleRole>> GetRolesFromPeople(string showSlug,
/// <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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetRolesFromPeople(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetRolesFromPeople(slug, where, new Sort<PeopleRole>(sort), limit);
// Helpers
/// <summary>
/// Setup relations between a show, a library and a collection
/// </summary>
/// <param name="showID">The show's ID to setup relations with</param>
/// <param name="libraryID">The library's ID to setup relations with (optional)</param>
/// <param name="collectionID">The collection's ID to setup relations with (optional)</param>
Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary>
/// Setup relations between a show, a library and a collection
/// </summary>
/// <param name="show">The show to setup relations with</param>
/// <param name="library">The library to setup relations with (optional)</param>
/// <param name="collection">The collection to setup relations with (optional)</param>
Task AddShowLink([NotNull] Show show, Library library, Collection collection); Task AddShowLink([NotNull] Show show, Library library, Collection collection);
// Get all /// <summary>
Task<ICollection<Library>> GetLibraries(Expression<Func<Library, bool>> where = null, /// Get all resources with filters
Sort<Library> sort = default, /// </summary>
Pagination limit = default); /// <param name="where">A filter function</param>
Task<ICollection<Collection>> GetCollections(Expression<Func<Collection, bool>> where = null, /// <param name="sort">Sort informations (sort order & sort by)</param>
Sort<Collection> sort = default, /// <param name="limit">How many items to return and where to start</param>
Pagination limit = default); /// <typeparam name="T">The type of resources to load</typeparam>
Task<ICollection<Show>> GetShows(Expression<Func<Show, bool>> where = null, /// <returns>A list of resources that match every filters</returns>
Sort<Show> sort = default, Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
Pagination limit = default); Sort<T> sort = default,
Task<ICollection<Season>> GetSeasons(Expression<Func<Season, bool>> where = null, Pagination limit = default) where T : class, IResource;
Sort<Season> sort = default,
Pagination limit = default);
Task<ICollection<Episode>> GetEpisodes(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default,
Pagination limit = default);
Task<ICollection<Track>> GetTracks(Expression<Func<Track, bool>> where = null,
Sort<Track> sort = default,
Pagination limit = default);
Task<ICollection<Studio>> GetStudios(Expression<Func<Studio, bool>> where = null,
Sort<Studio> sort = default,
Pagination limit = default);
Task<ICollection<People>> GetPeople(Expression<Func<People, bool>> where = null,
Sort<People> sort = default,
Pagination limit = default);
Task<ICollection<Genre>> GetGenres(Expression<Func<Genre, bool>> where = null,
Sort<Genre> sort = default,
Pagination limit = default);
Task<ICollection<ProviderID>> GetProviders(Expression<Func<ProviderID, bool>> where = null,
Sort<ProviderID> sort = default,
Pagination limit = default);
Task<ICollection<Library>> GetLibraries([Optional] Expression<Func<Library, bool>> where, /// <summary>
Expression<Func<Library, object>> sort, /// Get all resources with filters
Pagination limit = default /// </summary>
) => GetLibraries(where, new Sort<Library>(sort), limit); /// <param name="where">A filter function</param>
Task<ICollection<Collection>> GetCollections([Optional] Expression<Func<Collection, bool>> where, /// <param name="sort">A sort by function</param>
Expression<Func<Collection, object>> sort, /// <param name="limit">How many items to return and where to start</param>
Pagination limit = default /// <typeparam name="T">The type of resources to load</typeparam>
) => GetCollections(where, new Sort<Collection>(sort), limit); /// <returns>A list of resources that match every filters</returns>
Task<ICollection<Show>> GetShows([Optional] Expression<Func<Show, bool>> where, Task<ICollection<T>> GetAll<T>([Optional] Expression<Func<T, bool>> where,
Expression<Func<Show, object>> sort, Expression<Func<T, object>> sort,
Pagination limit = default Pagination limit = default) where T : class, IResource
) => GetShows(where, new Sort<Show>(sort), limit); {
Task<ICollection<Track>> GetTracks([Optional] Expression<Func<Track, bool>> where, return GetAll(where, new Sort<T>(sort), limit);
Expression<Func<Track, object>> sort, }
Pagination limit = default
) => GetTracks(where, new Sort<Track>(sort), limit);
Task<ICollection<Studio>> GetStudios([Optional] Expression<Func<Studio, bool>> where,
Expression<Func<Studio, object>> sort,
Pagination limit = default
) => GetStudios(where, new Sort<Studio>(sort), limit);
Task<ICollection<People>> GetPeople([Optional] Expression<Func<People, bool>> where,
Expression<Func<People, object>> sort,
Pagination limit = default
) => GetPeople(where, new Sort<People>(sort), limit);
Task<ICollection<Genre>> GetGenres([Optional] Expression<Func<Genre, bool>> where,
Expression<Func<Genre, object>> sort,
Pagination limit = default
) => GetGenres(where, new Sort<Genre>(sort), limit);
Task<ICollection<ProviderID>> GetProviders([Optional] Expression<Func<ProviderID, bool>> where,
Expression<Func<ProviderID, object>> sort,
Pagination limit = default
) => GetProviders(where, new Sort<ProviderID>(sort), limit);
/// <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;
// Counts /// <summary>
Task<int> GetLibrariesCount(Expression<Func<Library, bool>> where = null); /// Search for a resource
Task<int> GetCollectionsCount(Expression<Func<Collection, bool>> where = null); /// </summary>
Task<int> GetShowsCount(Expression<Func<Show, bool>> where = null); /// <param name="query">The search query</param>
Task<int> GetSeasonsCount(Expression<Func<Season, bool>> where = null); /// <typeparam name="T">The type of resources</typeparam>
Task<int> GetEpisodesCount(Expression<Func<Episode, bool>> where = null); /// <returns>A list of 20 items that match the search query</returns>
Task<int> GetTracksCount(Expression<Func<Track, bool>> where = null); Task<ICollection<T>> Search<T>(string query) where T : class, IResource;
Task<int> GetGenresCount(Expression<Func<Genre, bool>> where = null);
Task<int> GetStudiosCount(Expression<Func<Studio, bool>> where = null);
Task<int> GetPeopleCount(Expression<Func<People, bool>> where = null);
// Search /// <summary>
Task<ICollection<Library>> SearchLibraries(string searchQuery); /// Create a new resource.
Task<ICollection<Collection>> SearchCollections(string searchQuery); /// </summary>
Task<ICollection<Show>> SearchShows(string searchQuery); /// <param name="item">The item to register</param>
Task<ICollection<Season>> SearchSeasons(string searchQuery); /// <typeparam name="T">The type of resource</typeparam>
Task<ICollection<Episode>> SearchEpisodes(string searchQuery); /// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
Task<ICollection<Genre>> SearchGenres(string searchQuery); Task<T> Create<T>([NotNull] T item) where T : class, IResource;
Task<ICollection<Studio>> SearchStudios(string searchQuery);
Task<ICollection<People>> SearchPeople(string searchQuery);
//Register values /// <summary>
Task<Library> RegisterLibrary(Library library); /// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
Task<Collection> RegisterCollection(Collection collection); /// </summary>
Task<Show> RegisterShow(Show show); /// <param name="item">The item to register</param>
Task<Season> RegisterSeason(Season season); /// <typeparam name="T">The type of resource</typeparam>
Task<Episode> RegisterEpisode(Episode episode); /// <returns>The newly created item or the existing value if it existed.</returns>
Task<Track> RegisterTrack(Track track); Task<T> CreateIfNotExists<T>([NotNull] T item) where T : class, IResource;
Task<Genre> RegisterGenre(Genre genre);
Task<Studio> RegisterStudio(Studio studio);
Task<People> RegisterPeople(People people);
// Edit values /// <summary>
Task<Library> EditLibrary(Library library, bool resetOld); /// Edit a resource
Task<Collection> EditCollection(Collection collection, bool resetOld); /// </summary>
Task<Show> EditShow(Show show, bool resetOld); /// <param name="item">The resourcce to edit, it's ID can't change.</param>
Task<Season> EditSeason(Season season, bool resetOld); /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
Task<Episode> EditEpisode(Episode episode, bool resetOld); /// <typeparam name="T">The type of resources</typeparam>
Task<Track> EditTrack(Track track, bool resetOld); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task<Genre> EditGenre(Genre genre, bool resetOld); /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<Studio> EditStudio(Studio studio, bool resetOld); Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource;
Task<People> EditPeople(People people, bool resetOld);
// Delete values /// <summary>
Task DeleteLibrary(Library library); /// Delete a resource.
Task DeleteCollection(Collection collection); /// </summary>
Task DeleteShow(Show show); /// <param name="item">The resource to delete</param>
Task DeleteSeason(Season season); /// <typeparam name="T">The type of resource to delete</typeparam>
Task DeleteEpisode(Episode episode); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteTrack(Track track); Task Delete<T>(T item) where T : class, IResource;
Task DeleteGenre(Genre genre);
Task DeleteStudio(Studio studio);
Task DeletePeople(People people);
//Delete by slug /// <summary>
Task DeleteLibrary(string slug); /// Delete a resource by it's ID.
Task DeleteCollection(string slug); /// </summary>
Task DeleteShow(string slug); /// <param name="id">The id of the resource to delete</param>
Task DeleteSeason(string slug); /// <typeparam name="T">The type of resource to delete</typeparam>
Task DeleteEpisode(string slug); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteTrack(string slug); Task Delete<T>(int id) where T : class, IResource;
Task DeleteGenre(string slug);
Task DeleteStudio(string slug);
Task DeletePeople(string slug);
//Delete by id /// <summary>
Task DeleteLibrary(int id); /// Delete a resource by it's slug.
Task DeleteCollection(int id); /// </summary>
Task DeleteShow(int id); /// <param name="slug">The slug of the resource to delete</param>
Task DeleteSeason(int id); /// <typeparam name="T">The type of resource to delete</typeparam>
Task DeleteEpisode(int id); /// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteTrack(int id); Task Delete<T>(string slug) where T : class, IResource;
Task DeleteGenre(int id);
Task DeleteStudio(int id);
Task DeletePeople(int id);
} }
} }

View File

@ -6,7 +6,7 @@ namespace Kyoo.Controllers
{ {
public interface IMetadataProvider public interface IMetadataProvider
{ {
ProviderID Provider { get; } Provider Provider { get; }
Task<Collection> GetCollectionFromName(string name); Task<Collection> GetCollectionFromName(string name);

View File

@ -6,28 +6,64 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// Informations about the pagination. How many items should be displayed and where to start.
/// </summary>
public readonly struct Pagination public readonly struct Pagination
{ {
/// <summary>
/// The count of items to return.
/// </summary>
public int Count { get; } public int Count { get; }
/// <summary>
/// Where to start? Using the given sort
/// </summary>
public int AfterID { get; } public int AfterID { get; }
/// <summary>
/// Create a new <see cref="Pagination"/> instance.
/// </summary>
/// <param name="count">Set the <see cref="Count"/> value</param>
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
public Pagination(int count, int afterID = 0) public Pagination(int count, int afterID = 0)
{ {
Count = count; Count = count;
AfterID = afterID; AfterID = afterID;
} }
/// <summary>
/// Implicitly create a new pagination from a limit number.
/// </summary>
/// <param name="limit">Set the <see cref="Count"/> value</param>
/// <returns>A new <see cref="Pagination"/> instance</returns>
public static implicit operator Pagination(int limit) => new(limit); public static implicit operator Pagination(int limit) => new(limit);
} }
public struct Sort<T> /// <summary>
/// Informations about how a query should be sorted. What factor should decide the sort and in which order.
/// </summary>
/// <typeparam name="T">For witch type this sort applies</typeparam>
public readonly struct Sort<T>
{ {
public Expression<Func<T, object>> Key; /// <summary>
public bool Descendant; /// The sort key. This member will be used to sort the results.
/// </summary>
public Expression<Func<T, object>> Key { get; }
/// <summary>
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendent order.
/// </summary>
public bool Descendant { get; }
/// <summary>
/// Create a new <see cref="Sort{T}"/> instance.
/// </summary>
/// <param name="key">The sort key given. It is assigned to <see cref="Key"/>.</param>
/// <param name="descendant">Should this be in descendant order? The default is false.</param>
/// <exception cref="ArgumentException">If the given key is not a member.</exception>
public Sort(Expression<Func<T, object>> key, bool descendant = false) public Sort(Expression<Func<T, object>> key, bool descendant = false)
{ {
Key = key; Key = key;
@ -37,6 +73,11 @@ namespace Kyoo.Controllers
throw new ArgumentException("The given sort key is not valid."); throw new ArgumentException("The given sort key is not valid.");
} }
/// <summary>
/// Create a new <see cref="Sort{T}"/> instance from a key's name (case insensitive).
/// </summary>
/// <param name="sortBy">A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key".</param>
/// <exception cref="ArgumentException">An invalid key or sort specifier as been given.</exception>
public Sort(string sortBy) public Sort(string sortBy)
{ {
if (string.IsNullOrEmpty(sortBy)) if (string.IsNullOrEmpty(sortBy))
@ -46,8 +87,8 @@ namespace Kyoo.Controllers
return; return;
} }
string key = sortBy.Contains(':') ? sortBy.Substring(0, sortBy.IndexOf(':')) : sortBy; string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
string order = sortBy.Contains(':') ? sortBy.Substring(sortBy.IndexOf(':') + 1) : null; string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
ParameterExpression param = Expression.Parameter(typeof(T), "x"); ParameterExpression param = Expression.Parameter(typeof(T), "x");
MemberExpression property = Expression.Property(param, key); MemberExpression property = Expression.Property(param, key);
@ -65,151 +106,544 @@ namespace Kyoo.Controllers
} }
} }
public interface IRepository<T> : IDisposable, IAsyncDisposable where T : class, IResource /// <summary>
/// A base class for repositories. Every service implementing this will be handled by the <see cref="LibraryManager"/>.
/// </summary>
public interface IBaseRepository
{ {
/// <summary>
/// The type for witch this repository is responsible or null if non applicable.
/// </summary>
Type RepositoryType { get; }
}
/// <summary>
/// A common repository for every resources.
/// </summary>
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
public interface IRepository<T> : IBaseRepository where T : class, IResource
{
/// <summary>
/// Get a resource from it's ID.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(int id); Task<T> Get(int id);
/// <summary>
/// Get a resource from it's slug.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item could not be found.</exception>
/// <returns>The resource found</returns>
Task<T> Get(string slug); Task<T> Get(string slug);
/// <summary>
/// Get the first resource that match the predicate.
/// </summary>
/// <param name="where">A predicate to filter the resource.</param>
/// <exception cref="ItemNotFound">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);
/// <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>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(int id);
/// <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>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(string slug);
/// <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>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
/// <summary>
/// Search for resources.
/// </summary>
/// <param name="query">The query string.</param>
/// <returns>A list of resources found</returns>
Task<ICollection<T>> Search(string query); Task<ICollection<T>> Search(string query);
/// <summary>
/// Get every resources that match all filters
/// </summary>
/// <param name="where">A filter predicate</param>
/// <param name="sort">Sort informations 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>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// Get every resources that match all filters
/// </summary>
/// <param name="where">A filter predicate</param>
/// <param name="sort">A sort by predicate. The order is ascending.</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll([Optional] Expression<Func<T, bool>> where, Task<ICollection<T>> GetAll([Optional] Expression<Func<T, bool>> where,
Expression<Func<T, object>> sort, Expression<Func<T, object>> sort,
Pagination limit = default Pagination limit = default
) => GetAll(where, new Sort<T>(sort), limit); ) => GetAll(where, new Sort<T>(sort), limit);
/// <summary>
/// Get the number of resources that match the filter's predicate.
/// </summary>
/// <param name="where">A filter predicate</param>
/// <returns>How many resources matched that filter</returns>
Task<int> GetCount(Expression<Func<T, bool>> where = null); Task<int> GetCount(Expression<Func<T, bool>> where = null);
/// <summary>
/// Create a new resource.
/// </summary>
/// <param name="obj">The item to register</param>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
Task<T> Create([NotNull] T obj); Task<T> Create([NotNull] T obj);
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="obj">The object to create</param>
/// <param name="silentFail">Allow issues to occurs in this method. Every issue is catched and ignored.</param>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false); Task<T> CreateIfNotExists([NotNull] T obj, bool silentFail = false);
/// <summary>
/// Edit a resource
/// </summary>
/// <param name="edited">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit([NotNull] T edited, bool resetOld); Task<T> Edit([NotNull] T edited, bool resetOld);
/// <summary>
/// Delete a resource by it's ID
/// </summary>
/// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete(int id); Task Delete(int id);
/// <summary>
/// Delete a resource by it's slug
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete(string slug); Task Delete(string slug);
/// <summary>
/// Delete a resource
/// </summary>
/// <param name="obj">The resource to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete([NotNull] T obj); Task Delete([NotNull] T obj);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">One or multiple resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="objs">An enumerable of resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<T> objs); Task DeleteRange(IEnumerable<T> objs);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">One or multiple resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="ids">An enumearble of resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids); Task DeleteRange(IEnumerable<int> ids);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">One or multiple resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="slugs">An enumerable of resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs); Task DeleteRange(IEnumerable<string> slugs);
/// <summary>
/// Delete a list of resources.
/// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange([NotNull] Expression<Func<T, bool>> where); Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
} }
/// <summary>
/// A repository to handle shows.
/// </summary>
public interface IShowRepository : IRepository<Show> public interface IShowRepository : IRepository<Show>
{ {
/// <summary>
/// Link a show to a collection and/or a library. The given show is now part of thoses containers.
/// If both a library and a collection are given, the collection is added to the library too.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="libraryID">The ID of the library (optional)</param>
/// <param name="collectionID">The ID of the collection (optional)</param>
Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary>
/// Get a show's slug from it's ID.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <exception cref="ItemNotFound">If a show with the given ID is not found.</exception>
/// <returns>The show's slug</returns>
Task<string> GetSlug(int showID); Task<string> GetSlug(int showID);
} }
/// <summary>
/// A repository to handle seasons.
/// </summary>
public interface ISeasonRepository : IRepository<Season> 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="ItemNotFound">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber); 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="ItemNotFound">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber); Task<Season> Get(string showSlug, int seasonNumber);
Task Delete(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> public interface IEpisodeRepository : IRepository<Episode>
{ {
/// <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="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber); 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="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
Task<Episode> Get(int seasonID, 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="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(int showID, int absoluteNumber); 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="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber); Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
Task Delete(string showSlug, int seasonNumber, int episodeNumber);
} }
/// <summary>
/// A repository to handle tracks
/// </summary>
public interface ITrackRepository : IRepository<Track> public interface ITrackRepository : IRepository<Track>
{ {
/// <summary>
/// Get a track from it's slug and it's type.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown); Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
} }
/// <summary>
/// A repository to handle libraries.
/// </summary>
public interface ILibraryRepository : IRepository<Library> { } public interface ILibraryRepository : IRepository<Library> { }
/// <summary>
/// A repository to handle library items (A wrapper arround shows and collections).
/// </summary>
public interface ILibraryItemRepository : IRepository<LibraryItem> public interface ILibraryItemRepository : IRepository<LibraryItem>
{ {
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id, public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(int id, public Task<ICollection<LibraryItem>> GetFromLibrary(int id,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort, Expression<Func<LibraryItem, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit); ) => GetFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
public Task<ICollection<LibraryItem>> GetFromLibrary(string librarySlug, /// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
public Task<ICollection<LibraryItem>> GetFromLibrary(string librarySlug, /// Get items (A wrapper arround shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
public Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
[Optional] Expression<Func<LibraryItem, bool>> where, [Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort, Expression<Func<LibraryItem, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromLibrary(librarySlug, where, new Sort<LibraryItem>(sort), limit); ) => GetFromLibrary(slug, where, new Sort<LibraryItem>(sort), limit);
} }
/// <summary>
/// A repository for collections
/// </summary>
public interface ICollectionRepository : IRepository<Collection> { } public interface ICollectionRepository : IRepository<Collection> { }
/// <summary>
/// A repository for genres.
/// </summary>
public interface IGenreRepository : IRepository<Genre> { } public interface IGenreRepository : IRepository<Genre> { }
/// <summary>
/// A repository for studios.
/// </summary>
public interface IStudioRepository : IRepository<Studio> { } public interface IStudioRepository : IRepository<Studio> { }
/// <summary>
/// A repository for people.
/// </summary>
public interface IPeopleRepository : IRepository<People> 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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// 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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(int showID, Task<ICollection<PeopleRole>> GetFromShow(int showID,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromShow(showID, where, new Sort<PeopleRole>(sort), limit); ) => GetFromShow(showID, where, new Sort<PeopleRole>(sort), limit);
/// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// 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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromShow(string showSlug, Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<PeopleRole>> GetFromPeople(int showID, /// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<PeopleRole>> GetFromPeople(int showID, /// <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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(int id,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromPeople(showID, where, new Sort<PeopleRole>(sort), limit); ) => GetFromPeople(id, where, new Sort<PeopleRole>(sort), limit);
Task<ICollection<PeopleRole>> GetFromPeople(string showSlug, /// <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 informations (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default); Pagination limit = default);
Task<ICollection<PeopleRole>> GetFromPeople(string showSlug, /// <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">A sort by method</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetFromPeople(string slug,
[Optional] Expression<Func<PeopleRole, bool>> where, [Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort, Expression<Func<PeopleRole, object>> sort,
Pagination limit = default Pagination limit = default
) => GetFromPeople(showSlug, where, new Sort<PeopleRole>(sort), limit); ) => GetFromPeople(slug, where, new Sort<PeopleRole>(sort), limit);
} }
public interface IProviderRepository : IRepository<ProviderID> /// <summary>
/// A repository to handle providers.
/// </summary>
public interface IProviderRepository : IRepository<Provider>
{ {
/// <summary>
/// Get a list of external ids that match all filters
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null, Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default, Sort<MetadataID> sort = default,
Pagination limit = default); Pagination limit = default);
/// <summary>
/// Get a list of external ids that match all filters
/// </summary>
/// <param name="where">A predicate to add arbitrary filter</param>
/// <param name="sort">A sort by expression</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <returns>A filtered list of external ids.</returns>
Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where, Task<ICollection<MetadataID>> GetMetadataID([Optional] Expression<Func<MetadataID, bool>> where,
Expression<Func<MetadataID, object>> sort, Expression<Func<MetadataID, object>> sort,
Pagination limit = default Pagination limit = default

View File

@ -11,7 +11,7 @@ namespace Kyoo.Controllers
Task Validate(Season season, bool alwaysDownload = false); Task Validate(Season season, bool alwaysDownload = false);
Task Validate(Episode episode, bool alwaysDownload = false); Task Validate(Episode episode, bool alwaysDownload = false);
Task Validate(People actors, bool alwaysDownload = false); Task Validate(People actors, bool alwaysDownload = false);
Task Validate(ProviderID actors, bool alwaysDownload = false); Task Validate(Provider actors, bool alwaysDownload = false);
Task<string> GetShowPoster([NotNull] Show show); Task<string> GetShowPoster([NotNull] Show show);
Task<string> GetShowLogo([NotNull] Show show); Task<string> GetShowLogo([NotNull] Show show);
@ -19,6 +19,6 @@ namespace Kyoo.Controllers
Task<string> GetSeasonPoster([NotNull] Season season); Task<string> GetSeasonPoster([NotNull] Season season);
Task<string> GetEpisodeThumb([NotNull] Episode episode); Task<string> GetEpisodeThumb([NotNull] Episode episode);
Task<string> GetPeoplePoster([NotNull] People people); Task<string> GetPeoplePoster([NotNull] People people);
Task<string> GetProviderLogo([NotNull] ProviderID provider); Task<string> GetProviderLogo([NotNull] Provider provider);
} }
} }

View File

@ -4,248 +4,174 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
/// <summary>
/// The list of repositories
/// </summary>
private readonly IBaseRepository[] _repositories;
/// <inheritdoc />
public ILibraryRepository LibraryRepository { get; } public ILibraryRepository LibraryRepository { get; }
/// <inheritdoc />
public ILibraryItemRepository LibraryItemRepository { get; } public ILibraryItemRepository LibraryItemRepository { get; }
/// <inheritdoc />
public ICollectionRepository CollectionRepository { get; } public ICollectionRepository CollectionRepository { get; }
/// <inheritdoc />
public IShowRepository ShowRepository { get; } public IShowRepository ShowRepository { get; }
/// <inheritdoc />
public ISeasonRepository SeasonRepository { get; } public ISeasonRepository SeasonRepository { get; }
/// <inheritdoc />
public IEpisodeRepository EpisodeRepository { get; } public IEpisodeRepository EpisodeRepository { get; }
/// <inheritdoc />
public ITrackRepository TrackRepository { get; } public ITrackRepository TrackRepository { get; }
public IGenreRepository GenreRepository { get; } /// <inheritdoc />
public IStudioRepository StudioRepository { get; }
public IPeopleRepository PeopleRepository { get; } public IPeopleRepository PeopleRepository { get; }
/// <inheritdoc />
public IStudioRepository StudioRepository { get; }
/// <inheritdoc />
public IGenreRepository GenreRepository { get; }
/// <inheritdoc />
public IProviderRepository ProviderRepository { get; } public IProviderRepository ProviderRepository { get; }
public LibraryManager(ILibraryRepository libraryRepository,
ILibraryItemRepository libraryItemRepository, /// <summary>
ICollectionRepository collectionRepository, /// Create a new <see cref="LibraryManager"/> instancce with every repository available.
IShowRepository showRepository, /// </summary>
ISeasonRepository seasonRepository, /// <param name="repositories">The list of repositories that this library manager should manage.
IEpisodeRepository episodeRepository, /// If a repository for every base type is not available, this instance won't be stable.</param>
ITrackRepository trackRepository, public LibraryManager(IEnumerable<IBaseRepository> repositories)
IGenreRepository genreRepository,
IStudioRepository studioRepository,
IProviderRepository providerRepository,
IPeopleRepository peopleRepository)
{ {
LibraryRepository = libraryRepository; _repositories = repositories.ToArray();
LibraryItemRepository = libraryItemRepository; LibraryRepository = GetRepository<Library>() as ILibraryRepository;
CollectionRepository = collectionRepository; LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
ShowRepository = showRepository; CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
SeasonRepository = seasonRepository; ShowRepository = GetRepository<Show>() as IShowRepository;
EpisodeRepository = episodeRepository; SeasonRepository = GetRepository<Season>() as ISeasonRepository;
TrackRepository = trackRepository; EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
GenreRepository = genreRepository; TrackRepository = GetRepository<Track>() as ITrackRepository;
StudioRepository = studioRepository; PeopleRepository = GetRepository<People>() as IPeopleRepository;
ProviderRepository = providerRepository; StudioRepository = GetRepository<Studio>() as IStudioRepository;
PeopleRepository = peopleRepository; GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
} }
public void Dispose() /// <inheritdoc />
public IRepository<T> GetRepository<T>()
where T : class, IResource
{ {
LibraryRepository.Dispose(); if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
CollectionRepository.Dispose(); return ret;
ShowRepository.Dispose(); throw new ItemNotFound();
SeasonRepository.Dispose();
EpisodeRepository.Dispose();
TrackRepository.Dispose();
GenreRepository.Dispose();
StudioRepository.Dispose();
PeopleRepository.Dispose();
ProviderRepository.Dispose();
} }
public async ValueTask DisposeAsync() /// <inheritdoc />
public Task<T> Get<T>(int id)
where T : class, IResource
{ {
await Task.WhenAll( return GetRepository<T>().Get(id);
LibraryRepository.DisposeAsync().AsTask(),
CollectionRepository.DisposeAsync().AsTask(),
ShowRepository.DisposeAsync().AsTask(),
SeasonRepository.DisposeAsync().AsTask(),
EpisodeRepository.DisposeAsync().AsTask(),
TrackRepository.DisposeAsync().AsTask(),
GenreRepository.DisposeAsync().AsTask(),
StudioRepository.DisposeAsync().AsTask(),
PeopleRepository.DisposeAsync().AsTask(),
ProviderRepository.DisposeAsync().AsTask()
);
} }
public Task<Library> GetLibrary(int id) /// <inheritdoc />
public Task<T> Get<T>(string slug)
where T : class, IResource
{ {
return LibraryRepository.Get(id); return GetRepository<T>().Get(slug);
} }
public Task<Collection> GetCollection(int id) /// <inheritdoc />
public Task<T> Get<T>(Expression<Func<T, bool>> where)
where T : class, IResource
{ {
return CollectionRepository.Get(id); return GetRepository<T>().Get(where);
} }
public Task<Show> GetShow(int id) /// <inheritdoc />
{ public Task<Season> Get(int showID, int seasonNumber)
return ShowRepository.Get(id);
}
public Task<Season> GetSeason(int id)
{
return SeasonRepository.Get(id);
}
public Task<Season> GetSeason(int showID, int seasonNumber)
{ {
return SeasonRepository.Get(showID, seasonNumber); return SeasonRepository.Get(showID, seasonNumber);
} }
public Task<Episode> GetEpisode(int id) /// <inheritdoc />
{ public Task<Season> Get(string showSlug, int seasonNumber)
return EpisodeRepository.Get(id);
}
public Task<Episode> GetEpisode(int showID, int seasonNumber, int episodeNumber)
{
return EpisodeRepository.Get(showID, seasonNumber, episodeNumber);
}
public Task<Track> GetTrack(string slug, StreamType type = StreamType.Unknown)
{
return TrackRepository.Get(slug, type);
}
public Task<Genre> GetGenre(int id)
{
return GenreRepository.Get(id);
}
public Task<Studio> GetStudio(int id)
{
return StudioRepository.Get(id);
}
public Task<People> GetPeople(int id)
{
return PeopleRepository.Get(id);
}
public Task<ProviderID> GetProvider(int id)
{
return ProviderRepository.Get(id);
}
public Task<Library> GetLibrary(string slug)
{
return LibraryRepository.Get(slug);
}
public Task<Collection> GetCollection(string slug)
{
return CollectionRepository.Get(slug);
}
public Task<Show> GetShow(string slug)
{
return ShowRepository.Get(slug);
}
public Task<Season> GetSeason(string slug)
{
return SeasonRepository.Get(slug);
}
public Task<Season> GetSeason(string showSlug, int seasonNumber)
{ {
return SeasonRepository.Get(showSlug, seasonNumber); return SeasonRepository.Get(showSlug, seasonNumber);
} }
public Task<Episode> GetEpisode(string slug) /// <inheritdoc />
public Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{ {
return EpisodeRepository.Get(slug); return EpisodeRepository.Get(showID, seasonNumber, episodeNumber);
} }
public Task<Episode> GetEpisode(string showSlug, int seasonNumber, int episodeNumber) /// <inheritdoc />
public Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber)
{ {
return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber);
} }
public Task<Episode> GetMovieEpisode(string movieSlug) /// <inheritdoc />
public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
{ {
return EpisodeRepository.Get(movieSlug); return TrackRepository.Get(slug, type);
} }
public Task<Track> GetTrack(int id) /// <inheritdoc />
public async Task<T> GetOrDefault<T>(int id)
where T : class, IResource
{ {
return TrackRepository.Get(id); return await GetRepository<T>().GetOrDefault(id);
} }
public Task<Genre> GetGenre(string slug) /// <inheritdoc />
public async Task<T> GetOrDefault<T>(string slug)
where T : class, IResource
{ {
return GenreRepository.Get(slug); return await GetRepository<T>().GetOrDefault(slug);
} }
public Task<Studio> GetStudio(string slug) /// <inheritdoc />
public async Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where)
where T : class, IResource
{ {
return StudioRepository.Get(slug); return await GetRepository<T>().GetOrDefault(where);
} }
public Task<People> GetPeople(string slug) /// <inheritdoc />
public async Task<Season> GetOrDefault(int showID, int seasonNumber)
{ {
return PeopleRepository.Get(slug); return await SeasonRepository.GetOrDefault(showID, seasonNumber);
} }
public Task<ProviderID> GetProvider(string slug) /// <inheritdoc />
public async Task<Season> GetOrDefault(string showSlug, int seasonNumber)
{ {
return ProviderRepository.Get(slug); return await SeasonRepository.GetOrDefault(showSlug, seasonNumber);
} }
public Task<Library> GetLibrary(Expression<Func<Library, bool>> where) /// <inheritdoc />
public async Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{ {
return LibraryRepository.Get(where); return await EpisodeRepository.GetOrDefault(showID, seasonNumber, episodeNumber);
} }
public Task<Collection> GetCollection(Expression<Func<Collection, bool>> where) /// <inheritdoc />
public async Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{ {
return CollectionRepository.Get(where); return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
} }
public Task<Show> GetShow(Expression<Func<Show, bool>> where) /// <inheritdoc />
public async Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
{ {
return ShowRepository.Get(where); return await TrackRepository.GetOrDefault(slug, type);
}
public Task<Season> GetSeason(Expression<Func<Season, bool>> where)
{
return SeasonRepository.Get(where);
}
public Task<Episode> GetEpisode(Expression<Func<Episode, bool>> where)
{
return EpisodeRepository.Get(where);
}
public Task<Track> GetTrack(Expression<Func<Track, bool>> where)
{
return TrackRepository.Get(where);
}
public Task<Genre> GetGenre(Expression<Func<Genre, bool>> where)
{
return GenreRepository.Get(where);
}
public Task<Studio> GetStudio(Expression<Func<Studio, bool>> where)
{
return StudioRepository.Get(where);
}
public Task<People> GetPerson(Expression<Func<People, bool>> where)
{
return PeopleRepository.Get(where);
} }
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member) public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource where T : class, IResource
where T2 : class, IResource, new() where T2 : class, IResource, new()
@ -255,6 +181,7 @@ namespace Kyoo.Controllers
return Load(obj, Utility.GetPropertyName(member)); return Load(obj, Utility.GetPropertyName(member));
} }
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member) public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource where T : class, IResource
where T2 : class, new() where T2 : class, new()
@ -264,14 +191,24 @@ namespace Kyoo.Controllers
return Load(obj, Utility.GetPropertyName(member)); return Load(obj, Utility.GetPropertyName(member));
} }
public async Task<T> Load<T>(T obj, string member) /// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName)
where T : class, IResource where T : class, IResource
{ {
await Load(obj as IResource, member); await Load(obj as IResource, memberName);
return obj; return obj;
} }
private async Task SetRelation<T1, T2>(T1 obj, /// <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, Task<ICollection<T2>> loader,
Action<T1, ICollection<T2>> setter, Action<T1, ICollection<T2>> setter,
Action<T2, T1> inverse) Action<T2, T1> inverse)
@ -282,12 +219,13 @@ namespace Kyoo.Controllers
inverse(item, obj); inverse(item, obj);
} }
public Task Load(IResource obj, string member) /// <inheritdoc />
public Task Load(IResource obj, string memberName)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
return (obj, member) switch return (obj, member: memberName) switch
{ {
(Library l, nameof(Library.Providers)) => ProviderRepository (Library l, nameof(Library.Providers)) => ProviderRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID)) .GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
@ -343,7 +281,7 @@ namespace Kyoo.Controllers
.Then(x => s.Collections = x), .Then(x => s.Collections = x),
(Show s, nameof(Show.Studio)) => StudioRepository (Show s, nameof(Show.Studio)) => StudioRepository
.Get(x => x.Shows.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
s.Studio = x; s.Studio = x;
@ -362,7 +300,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Season = y; x.SeasonID = y.ID; }), (x, y) => { x.Season = y; x.SeasonID = y.ID; }),
(Season s, nameof(Season.Show)) => ShowRepository (Season s, nameof(Season.Show)) => ShowRepository
.Get(x => x.Seasons.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Seasons.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
s.Show = x; s.Show = x;
@ -381,7 +319,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository (Episode e, nameof(Episode.Show)) => ShowRepository
.Get(x => x.Episodes.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
e.Show = x; e.Show = x;
@ -389,7 +327,7 @@ namespace Kyoo.Controllers
}), }),
(Episode e, nameof(Episode.Season)) => SeasonRepository (Episode e, nameof(Episode.Season)) => SeasonRepository
.Get(x => x.Episodes.Any(y => y.ID == e.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.ID == e.ID))
.Then(x => .Then(x =>
{ {
e.Season = x; e.Season = x;
@ -398,7 +336,7 @@ namespace Kyoo.Controllers
(Track t, nameof(Track.Episode)) => EpisodeRepository (Track t, nameof(Track.Episode)) => EpisodeRepository
.Get(x => x.Tracks.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Tracks.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
t.Episode = x; t.Episode = x;
@ -426,85 +364,16 @@ namespace Kyoo.Controllers
.Then(x => p.Roles = x), .Then(x => p.Roles = x),
(ProviderID p, nameof(ProviderID.Libraries)) => LibraryRepository (Provider p, nameof(Provider.Libraries)) => LibraryRepository
.GetAll(x => x.Providers.Any(y => y.ID == obj.ID)) .GetAll(x => x.Providers.Any(y => y.ID == obj.ID))
.Then(x => p.Libraries = x), .Then(x => p.Libraries = x),
_ => throw new ArgumentException($"Couldn't find a way to load {member} of {obj.Slug}.") _ => throw new ArgumentException($"Couldn't find a way to load {memberName} of {obj.Slug}.")
}; };
} }
public Task<ICollection<Library>> GetLibraries(Expression<Func<Library, bool>> where = null, /// <inheritdoc />
Sort<Library> sort = default,
Pagination page = default)
{
return LibraryRepository.GetAll(where, sort, page);
}
public Task<ICollection<Collection>> GetCollections(Expression<Func<Collection, bool>> where = null,
Sort<Collection> sort = default,
Pagination page = default)
{
return CollectionRepository.GetAll(where, sort, page);
}
public Task<ICollection<Show>> GetShows(Expression<Func<Show, bool>> where = null,
Sort<Show> sort = default,
Pagination limit = default)
{
return ShowRepository.GetAll(where, sort, limit);
}
public Task<ICollection<Season>> GetSeasons(Expression<Func<Season, bool>> where = null,
Sort<Season> sort = default,
Pagination limit = default)
{
return SeasonRepository.GetAll(where, sort, limit);
}
public Task<ICollection<Episode>> GetEpisodes(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default,
Pagination limit = default)
{
return EpisodeRepository.GetAll(where, sort, limit);
}
public Task<ICollection<Track>> GetTracks(Expression<Func<Track, bool>> where = null,
Sort<Track> sort = default,
Pagination page = default)
{
return TrackRepository.GetAll(where, sort, page);
}
public Task<ICollection<Studio>> GetStudios(Expression<Func<Studio, bool>> where = null,
Sort<Studio> sort = default,
Pagination page = default)
{
return StudioRepository.GetAll(where, sort, page);
}
public Task<ICollection<People>> GetPeople(Expression<Func<People, bool>> where = null,
Sort<People> sort = default,
Pagination page = default)
{
return PeopleRepository.GetAll(where, sort, page);
}
public Task<ICollection<Genre>> GetGenres(Expression<Func<Genre, bool>> where = null,
Sort<Genre> sort = default,
Pagination page = default)
{
return GenreRepository.GetAll(where, sort, page);
}
public Task<ICollection<ProviderID>> GetProviders(Expression<Func<ProviderID, bool>> where = null,
Sort<ProviderID> sort = default,
Pagination page = default)
{
return ProviderRepository.GetAll(where, sort, page);
}
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id, public Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
@ -513,14 +382,16 @@ namespace Kyoo.Controllers
return LibraryItemRepository.GetFromLibrary(id, where, sort, limit); return LibraryItemRepository.GetFromLibrary(id, where, sort, limit);
} }
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(string librarySlug, /// <inheritdoc />
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
return LibraryItemRepository.GetFromLibrary(librarySlug, where, sort, limit); return LibraryItemRepository.GetFromLibrary(slug, where, sort, limit);
} }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID, public Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -529,6 +400,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromShow(showID, where, sort, limit); return PeopleRepository.GetFromShow(showID, where, sort, limit);
} }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug, public Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -537,6 +409,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromShow(showSlug, where, sort, limit); return PeopleRepository.GetFromShow(showSlug, where, sort, limit);
} }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(int id, public Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -545,6 +418,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromPeople(id, where, sort, limit); return PeopleRepository.GetFromPeople(id, where, sort, limit);
} }
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug, public Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -553,326 +427,83 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromPeople(slug, where, sort, limit); return PeopleRepository.GetFromPeople(slug, where, sort, limit);
} }
public Task<int> GetLibrariesCount(Expression<Func<Library, bool>> where = null) /// <inheritdoc />
{
return LibraryRepository.GetCount(where);
}
public Task<int> GetCollectionsCount(Expression<Func<Collection, bool>> where = null)
{
return CollectionRepository.GetCount(where);
}
public Task<int> GetShowsCount(Expression<Func<Show, bool>> where = null)
{
return ShowRepository.GetCount(where);
}
public Task<int> GetSeasonsCount(Expression<Func<Season, bool>> where = null)
{
return SeasonRepository.GetCount(where);
}
public Task<int> GetEpisodesCount(Expression<Func<Episode, bool>> where = null)
{
return EpisodeRepository.GetCount(where);
}
public Task<int> GetTracksCount(Expression<Func<Track, bool>> where = null)
{
return TrackRepository.GetCount(where);
}
public Task<int> GetGenresCount(Expression<Func<Genre, bool>> where = null)
{
return GenreRepository.GetCount(where);
}
public Task<int> GetStudiosCount(Expression<Func<Studio, bool>> where = null)
{
return StudioRepository.GetCount(where);
}
public Task<int> GetPeopleCount(Expression<Func<People, bool>> where = null)
{
return PeopleRepository.GetCount(where);
}
public Task AddShowLink(int showID, int? libraryID, int? collectionID) public Task AddShowLink(int showID, int? libraryID, int? collectionID)
{ {
return ShowRepository.AddShowLink(showID, libraryID, collectionID); return ShowRepository.AddShowLink(showID, libraryID, collectionID);
} }
/// <inheritdoc />
public Task AddShowLink(Show show, Library library, Collection collection) public Task AddShowLink(Show show, Library library, Collection collection)
{ {
if (show == null) if (show == null)
throw new ArgumentNullException(nameof(show)); throw new ArgumentNullException(nameof(show));
return AddShowLink(show.ID, library?.ID, collection?.ID); return ShowRepository.AddShowLink(show.ID, library?.ID, collection?.ID);
} }
public Task<ICollection<Library>> SearchLibraries(string searchQuery) /// <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 LibraryRepository.Search(searchQuery); return GetRepository<T>().GetAll(where, sort, limit);
} }
public Task<ICollection<Collection>> SearchCollections(string searchQuery) /// <inheritdoc />
public Task<int> GetCount<T>(Expression<Func<T, bool>> where = null)
where T : class, IResource
{ {
return CollectionRepository.Search(searchQuery); return GetRepository<T>().GetCount(where);
} }
public Task<ICollection<Show>> SearchShows(string searchQuery) /// <inheritdoc />
public Task<ICollection<T>> Search<T>(string query)
where T : class, IResource
{ {
return ShowRepository.Search(searchQuery); return GetRepository<T>().Search(query);
} }
public Task<ICollection<Season>> SearchSeasons(string searchQuery) /// <inheritdoc />
public Task<T> Create<T>(T item)
where T : class, IResource
{ {
return SeasonRepository.Search(searchQuery); return GetRepository<T>().Create(item);
} }
public Task<ICollection<Episode>> SearchEpisodes(string searchQuery) /// <inheritdoc />
public Task<T> CreateIfNotExists<T>(T item)
where T : class, IResource
{ {
return EpisodeRepository.Search(searchQuery); return GetRepository<T>().CreateIfNotExists(item);
} }
public Task<ICollection<Genre>> SearchGenres(string searchQuery) /// <inheritdoc />
public Task<T> Edit<T>(T item, bool resetOld)
where T : class, IResource
{ {
return GenreRepository.Search(searchQuery); return GetRepository<T>().Edit(item, resetOld);
} }
public Task<ICollection<Studio>> SearchStudios(string searchQuery) /// <inheritdoc />
public Task Delete<T>(T item)
where T : class, IResource
{ {
return StudioRepository.Search(searchQuery); return GetRepository<T>().Delete(item);
} }
public Task<ICollection<People>> SearchPeople(string searchQuery) /// <inheritdoc />
public Task Delete<T>(int id)
where T : class, IResource
{ {
return PeopleRepository.Search(searchQuery); return GetRepository<T>().Delete(id);
} }
public Task<Library> RegisterLibrary(Library library) /// <inheritdoc />
public Task Delete<T>(string slug)
where T : class, IResource
{ {
return LibraryRepository.Create(library); return GetRepository<T>().Delete(slug);
}
public Task<Collection> RegisterCollection(Collection collection)
{
return CollectionRepository.Create(collection);
}
public Task<Show> RegisterShow(Show show)
{
return ShowRepository.Create(show);
}
public Task<Season> RegisterSeason(Season season)
{
return SeasonRepository.Create(season);
}
public Task<Episode> RegisterEpisode(Episode episode)
{
return EpisodeRepository.Create(episode);
}
public Task<Track> RegisterTrack(Track track)
{
return TrackRepository.Create(track);
}
public Task<Genre> RegisterGenre(Genre genre)
{
return GenreRepository.Create(genre);
}
public Task<Studio> RegisterStudio(Studio studio)
{
return StudioRepository.Create(studio);
}
public Task<People> RegisterPeople(People people)
{
return PeopleRepository.Create(people);
}
public Task<Library> EditLibrary(Library library, bool resetOld)
{
return LibraryRepository.Edit(library, resetOld);
}
public Task<Collection> EditCollection(Collection collection, bool resetOld)
{
return CollectionRepository.Edit(collection, resetOld);
}
public Task<Show> EditShow(Show show, bool resetOld)
{
return ShowRepository.Edit(show, resetOld);
}
public Task<Season> EditSeason(Season season, bool resetOld)
{
return SeasonRepository.Edit(season, resetOld);
}
public Task<Episode> EditEpisode(Episode episode, bool resetOld)
{
return EpisodeRepository.Edit(episode, resetOld);
}
public Task<Track> EditTrack(Track track, bool resetOld)
{
return TrackRepository.Edit(track, resetOld);
}
public Task<Genre> EditGenre(Genre genre, bool resetOld)
{
return GenreRepository.Edit(genre, resetOld);
}
public Task<Studio> EditStudio(Studio studio, bool resetOld)
{
return StudioRepository.Edit(studio, resetOld);
}
public Task<People> EditPeople(People people, bool resetOld)
{
return PeopleRepository.Edit(people, resetOld);
}
public Task DeleteLibrary(Library library)
{
return LibraryRepository.Delete(library);
}
public Task DeleteCollection(Collection collection)
{
return CollectionRepository.Delete(collection);
}
public Task DeleteShow(Show show)
{
return ShowRepository.Delete(show);
}
public Task DeleteSeason(Season season)
{
return SeasonRepository.Delete(season);
}
public Task DeleteEpisode(Episode episode)
{
return EpisodeRepository.Delete(episode);
}
public Task DeleteTrack(Track track)
{
return TrackRepository.Delete(track);
}
public Task DeleteGenre(Genre genre)
{
return GenreRepository.Delete(genre);
}
public Task DeleteStudio(Studio studio)
{
return StudioRepository.Delete(studio);
}
public Task DeletePeople(People people)
{
return PeopleRepository.Delete(people);
}
public Task DeleteLibrary(string library)
{
return LibraryRepository.Delete(library);
}
public Task DeleteCollection(string collection)
{
return CollectionRepository.Delete(collection);
}
public Task DeleteShow(string show)
{
return ShowRepository.Delete(show);
}
public Task DeleteSeason(string season)
{
return SeasonRepository.Delete(season);
}
public Task DeleteEpisode(string episode)
{
return EpisodeRepository.Delete(episode);
}
public Task DeleteTrack(string track)
{
return TrackRepository.Delete(track);
}
public Task DeleteGenre(string genre)
{
return GenreRepository.Delete(genre);
}
public Task DeleteStudio(string studio)
{
return StudioRepository.Delete(studio);
}
public Task DeletePeople(string people)
{
return PeopleRepository.Delete(people);
}
public Task DeleteLibrary(int library)
{
return LibraryRepository.Delete(library);
}
public Task DeleteCollection(int collection)
{
return CollectionRepository.Delete(collection);
}
public Task DeleteShow(int show)
{
return ShowRepository.Delete(show);
}
public Task DeleteSeason(int season)
{
return SeasonRepository.Delete(season);
}
public Task DeleteEpisode(int episode)
{
return EpisodeRepository.Delete(episode);
}
public Task DeleteTrack(int track)
{
return TrackRepository.Delete(track);
}
public Task DeleteGenre(int genre)
{
return GenreRepository.Delete(genre);
}
public Task DeleteStudio(int studio)
{
return StudioRepository.Delete(studio);
}
public Task DeletePeople(int people)
{
return PeopleRepository.Delete(people);
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace Kyoo.Models
{ {
[SerializeIgnore] public int ID { get; set; } [SerializeIgnore] public int ID { get; set; }
[SerializeIgnore] public int ProviderID { get; set; } [SerializeIgnore] public int ProviderID { get; set; }
public virtual ProviderID Provider {get; set; } public virtual Provider Provider {get; set; }
[SerializeIgnore] public int? ShowID { get; set; } [SerializeIgnore] public int? ShowID { get; set; }
[SerializeIgnore] public virtual Show Show { get; set; } [SerializeIgnore] public virtual Show Show { get; set; }
@ -25,7 +25,7 @@ namespace Kyoo.Models
public MetadataID() { } public MetadataID() { }
public MetadataID(ProviderID provider, string dataID, string link) public MetadataID(Provider provider, string dataID, string link)
{ {
Provider = provider; Provider = provider;
DataID = dataID; DataID = dataID;

View File

@ -11,20 +11,20 @@ namespace Kyoo.Models
public string Name { get; set; } public string Name { get; set; }
public string[] Paths { get; set; } public string[] Paths { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<ProviderID> Providers { get; set; } [EditableRelation] [LoadableRelation] public virtual ICollection<Provider> Providers { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; } [LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; } [LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, ProviderID>> ProviderLinks { get; set; } [SerializeIgnore] public virtual ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; } [SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; } [SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif #endif
public Library() { } public Library() { }
public Library(string slug, string name, IEnumerable<string> paths, IEnumerable<ProviderID> providers) public Library(string slug, string name, IEnumerable<string> paths, IEnumerable<Provider> providers)
{ {
Slug = slug; Slug = slug;
Name = name; Name = name;

View File

@ -3,7 +3,7 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class ProviderID : IResource public class Provider : IResource
{ {
public int ID { get; set; } public int ID { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
@ -13,20 +13,20 @@ namespace Kyoo.Models
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; } [LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS #if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, ProviderID>> LibraryLinks { get; set; } [SerializeIgnore] public virtual ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; } [SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; }
#endif #endif
public ProviderID() { } public Provider() { }
public ProviderID(string name, string logo) public Provider(string name, string logo)
{ {
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);
Name = name; Name = name;
Logo = logo; Logo = logo;
} }
public ProviderID(int id, string name, string logo) public Provider(int id, string name, string logo)
{ {
ID = id; ID = id;
Slug = Utility.ToSlug(name); Slug = Utility.ToSlug(name);

View File

@ -109,18 +109,18 @@ namespace Kyoo.Models
if (!ep.Show.IsMovie) if (!ep.Show.IsMovie)
{ {
if (ep.EpisodeNumber > 1) if (ep.EpisodeNumber > 1)
previous = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1); previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber - 1);
else if (ep.SeasonNumber > 1) else if (ep.SeasonNumber > 1)
{ {
int count = await library.GetEpisodesCount(x => x.ShowID == ep.ShowID int count = await library.GetCount<Episode>(x => x.ShowID == ep.ShowID
&& x.SeasonNumber == ep.SeasonNumber - 1); && x.SeasonNumber == ep.SeasonNumber - 1);
previous = await library.GetEpisode(ep.ShowID, ep.SeasonNumber - 1, count); previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber - 1, count);
} }
if (ep.EpisodeNumber >= await library.GetEpisodesCount(x => x.SeasonID == ep.SeasonID)) if (ep.EpisodeNumber >= await library.GetCount<Episode>(x => x.SeasonID == ep.SeasonID))
next = await library.GetEpisode(ep.ShowID, ep.SeasonNumber + 1, 1); next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1);
else else
next = await library.GetEpisode(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1); next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber + 1);
} }
return new WatchItem(ep.ID, return new WatchItem(ep.ID,

View File

@ -29,22 +29,28 @@ namespace Kyoo.CommonApi
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public virtual async Task<ActionResult<T>> Get(int id) public virtual async Task<ActionResult<T>> Get(int id)
{ {
T resource = await _repository.Get(id); try
if (resource == null) {
return await _repository.Get(id);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
}
return resource;
} }
[HttpGet("{slug}")] [HttpGet("{slug}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public virtual async Task<ActionResult<T>> Get(string slug) public virtual async Task<ActionResult<T>> Get(string slug)
{ {
T resource = await _repository.Get(slug); try
if (resource == null) {
return await _repository.Get(slug);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
}
return resource;
} }
[HttpGet("count")] [HttpGet("count")]
@ -113,17 +119,21 @@ namespace Kyoo.CommonApi
[HttpPut] [HttpPut]
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]
public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource) public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource)
{
try
{ {
if (resource.ID > 0) if (resource.ID > 0)
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
T old = await _repository.Get(resource.Slug); T old = await _repository.Get(resource.Slug);
if (old == null)
return NotFound();
resource.ID = old.ID; resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpPut("{id:int}")] [HttpPut("{id:int}")]
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]
@ -143,13 +153,18 @@ namespace Kyoo.CommonApi
[HttpPut("{slug}")] [HttpPut("{slug}")]
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]
public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource)
{
try
{ {
T old = await _repository.Get(slug); T old = await _repository.Get(slug);
if (old == null)
return NotFound();
resource.ID = old.ID; resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpDelete("{id:int}")] [HttpDelete("{id:int}")]
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]

View File

@ -12,52 +12,99 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A base class to create repositories using Entity Framework.
/// </summary>
/// <typeparam name="T">The type of this repository</typeparam>
public abstract class LocalRepository<T> : IRepository<T> public abstract class LocalRepository<T> : IRepository<T>
where T : class, IResource where T : class, IResource
{ {
/// <summary>
/// The Entity Framework's Database handle.
/// </summary>
protected readonly DbContext Database; protected readonly DbContext Database;
/// <summary>
/// The default sort order that will be used for this resource's type.
/// </summary>
protected abstract Expression<Func<T, object>> DefaultSort { get; } protected abstract Expression<Func<T, object>> DefaultSort { get; }
/// <summary>
/// Create a new base <see cref="LocalRepository{T}"/> with the given database handle.
/// </summary>
/// <param name="database">A database connection to load resources of type <see cref="T"/></param>
protected LocalRepository(DbContext database) protected LocalRepository(DbContext database)
{ {
Database = database; Database = database;
} }
public virtual void Dispose() /// <inheritdoc/>
public Type RepositoryType => typeof(T);
/// <summary>
/// Get a resource from it's ID and make the <see cref="Database"/> instance track it.
/// </summary>
/// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracked resource with the given ID</returns>
protected virtual async Task<T> GetWithTracking(int id)
{ {
Database.Dispose(); T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
GC.SuppressFinalize(this); if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}");
return ret;
} }
public virtual ValueTask DisposeAsync() /// <inheritdoc/>
public virtual async Task<T> Get(int id)
{ {
return Database.DisposeAsync(); T ret = await GetOrDefault(id);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}");
return ret;
} }
public virtual Task<T> Get(int id) /// <inheritdoc/>
public virtual async Task<T> Get(string slug)
{
T ret = await GetOrDefault(slug);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the slug {slug}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<T> Get(Expression<Func<T, bool>> where)
{
T ret = await GetOrDefault(where);
if (ret == null)
throw new ItemNotFound($"No {typeof(T).Name} found with the given predicate.");
return ret;
}
/// <inheritdoc />
public virtual Task<T> GetOrDefault(int id)
{ {
return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id); return Database.Set<T>().FirstOrDefaultAsync(x => x.ID == id);
} }
public virtual Task<T> GetWithTracking(int id) /// <inheritdoc />
{ public virtual Task<T> GetOrDefault(string slug)
return Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
}
public virtual Task<T> Get(string slug)
{ {
return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug); return Database.Set<T>().FirstOrDefaultAsync(x => x.Slug == slug);
} }
public virtual Task<T> Get(Expression<Func<T, bool>> predicate) /// <inheritdoc />
public virtual Task<T> GetOrDefault(Expression<Func<T, bool>> where)
{ {
return Database.Set<T>().FirstOrDefaultAsync(predicate); return Database.Set<T>().FirstOrDefaultAsync(where);
} }
/// <inheritdoc/>
public abstract Task<ICollection<T>> Search(string query); public abstract Task<ICollection<T>> Search(string query);
/// <inheritdoc/>
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null, public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
Pagination limit = default) Pagination limit = default)
@ -65,6 +112,14 @@ namespace Kyoo.Controllers
return ApplyFilters(Database.Set<T>(), where, sort, limit); return ApplyFilters(Database.Set<T>(), where, sort, limit);
} }
/// <summary>
/// Apply filters to a query to ease sort, pagination & where queries for resources of this repository
/// </summary>
/// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <returns>The filtered query</returns>
protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query, protected Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
Expression<Func<T, bool>> where = null, Expression<Func<T, bool>> where = null,
Sort<T> sort = default, Sort<T> sort = default,
@ -73,6 +128,17 @@ namespace Kyoo.Controllers
return ApplyFilters(query, Get, DefaultSort, where, sort, limit); return ApplyFilters(query, Get, DefaultSort, where, sort, limit);
} }
/// <summary>
/// Apply filters to a query to ease sort, pagination & where queries for any resources types.
/// For resources of type <see cref="T"/>, see <see cref="ApplyFilters"/>
/// </summary>
/// <param name="get">A function to asynchronously get a resource from the database using it's ID.</param>
/// <param name="defaultSort">The default sort order of this resource's type.</param>
/// <param name="query">The base query to filter.</param>
/// <param name="where">An expression to filter based on arbitrary conditions</param>
/// <param name="sort">The sort settings (sort order & sort by)</param>
/// <param name="limit">Paginations information (where to start and how many to get)</param>
/// <returns>The filtered query</returns>
protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query, protected async Task<ICollection<TValue>> ApplyFilters<TValue>(IQueryable<TValue> query,
Func<int, Task<TValue>> get, Func<int, Task<TValue>> get,
Expression<Func<TValue, object>> defaultSort, Expression<Func<TValue, object>> defaultSort,
@ -108,6 +174,7 @@ namespace Kyoo.Controllers
return await query.ToListAsync(); return await query.ToListAsync();
} }
/// <inheritdoc/>
public virtual Task<int> GetCount(Expression<Func<T, bool>> where = null) public virtual Task<int> GetCount(Expression<Func<T, bool>> where = null)
{ {
IQueryable<T> query = Database.Set<T>(); IQueryable<T> query = Database.Set<T>();
@ -116,6 +183,7 @@ namespace Kyoo.Controllers
return query.CountAsync(); return query.CountAsync();
} }
/// <inheritdoc/>
public virtual async Task<T> Create(T obj) public virtual async Task<T> Create(T obj)
{ {
if (obj == null) if (obj == null)
@ -124,6 +192,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc/>
public virtual async Task<T> CreateIfNotExists(T obj, bool silentFail = false) public virtual async Task<T> CreateIfNotExists(T obj, bool silentFail = false)
{ {
try try
@ -139,10 +208,7 @@ namespace Kyoo.Controllers
} }
catch (DuplicatedItemException) catch (DuplicatedItemException)
{ {
T old = await Get(obj!.Slug); return await Get(obj.Slug);
if (old == null)
throw new SystemException("Unknown database state.");
return old;
} }
catch catch
{ {
@ -152,6 +218,7 @@ namespace Kyoo.Controllers
} }
} }
/// <inheritdoc/>
public virtual async Task<T> Edit(T edited, bool resetOld) public virtual async Task<T> Edit(T edited, bool resetOld)
{ {
if (edited == null) if (edited == null)
@ -162,8 +229,6 @@ namespace Kyoo.Controllers
try try
{ {
T old = await GetWithTracking(edited.ID); T old = await GetWithTracking(edited.ID);
if (old == null)
throw new ItemNotFound($"No resource found with the ID {edited.ID}.");
if (resetOld) if (resetOld)
Utility.Nullify(old); Utility.Nullify(old);
@ -178,11 +243,24 @@ namespace Kyoo.Controllers
} }
} }
/// <summary>
/// An overridable method to edit relatiosn of a resource.
/// </summary>
/// <param name="resource">The non edited resource</param>
/// <param name="changed">The new version of <see cref="resource"/>. This item will be saved on the databse and replace <see cref="resource"/></param>
/// <param name="resetOld">A boolean to indicate if all values of resource should be discarded or not.</param>
/// <returns></returns>
protected virtual Task EditRelations(T resource, T changed, bool resetOld) protected virtual Task EditRelations(T resource, T changed, bool resetOld)
{ {
return Validate(resource); return Validate(resource);
} }
/// <summary>
/// A method called just before saving a new resource to the database.
/// It is also called on the default implementation of <see cref="EditRelations"/>
/// </summary>
/// <param name="resource">The resource that will be saved</param>
/// <exception cref="ArgumentException">You can throw this if the resource is illegal and should not be saved.</exception>
protected virtual Task Validate(T resource) protected virtual Task Validate(T resource)
{ {
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
@ -205,38 +283,45 @@ namespace Kyoo.Controllers
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc/>
public virtual async Task Delete(int id) public virtual async Task Delete(int id)
{ {
T resource = await Get(id); T resource = await Get(id);
await Delete(resource); await Delete(resource);
} }
/// <inheritdoc/>
public virtual async Task Delete(string slug) public virtual async Task Delete(string slug)
{ {
T resource = await Get(slug); T resource = await Get(slug);
await Delete(resource); await Delete(resource);
} }
/// <inheritdoc/>
public abstract Task Delete(T obj); public abstract Task Delete(T obj);
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<T> objs) public virtual async Task DeleteRange(IEnumerable<T> objs)
{ {
foreach (T obj in objs) foreach (T obj in objs)
await Delete(obj); await Delete(obj);
} }
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<int> ids) public virtual async Task DeleteRange(IEnumerable<int> ids)
{ {
foreach (int id in ids) foreach (int id in ids)
await Delete(id); await Delete(id);
} }
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<string> slugs) public virtual async Task DeleteRange(IEnumerable<string> slugs)
{ {
foreach (string slug in slugs) foreach (string slug in slugs)
await Delete(slug); await Delete(slug);
} }
/// <inheritdoc/>
public async Task DeleteRange(Expression<Func<T, bool>> where) public async Task DeleteRange(Expression<Func<T, bool>> where)
{ {
ICollection<T> resources = await GetAll(where); ICollection<T> resources = await GetAll(where);

View File

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Kyoo.CommonApi namespace Kyoo.CommonApi
{ {
@ -77,7 +76,7 @@ namespace Kyoo.CommonApi
if (result.DeclaredType == null) if (result.DeclaredType == null)
return; return;
await using ILibraryManager library = context.HttpContext.RequestServices.GetService<ILibraryManager>(); ILibraryManager library = context.HttpContext.RequestServices.GetService<ILibraryManager>();
ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"]; ICollection<string> fields = (ICollection<string>)context.HttpContext.Items["fields"];
Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>)); Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>));

View File

@ -10,6 +10,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />

View File

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

View File

@ -0,0 +1,79 @@
using Kyoo.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Tests
{
/// <summary>
/// Class responsible to fill and create in memory databases for unit tests.
/// </summary>
public class TestContext
{
/// <summary>
/// The context's options that specify to use an in memory Sqlite database.
/// </summary>
private readonly DbContextOptions<DatabaseContext> _context;
/// <summary>
/// Create a new database and fill it with informations.
/// </summary>
public TestContext()
{
SqliteConnection connection = new("DataSource=:memory:");
connection.Open();
try
{
_context = new DbContextOptionsBuilder<DatabaseContext>()
.UseSqlite(connection)
.Options;
FillDatabase();
}
finally
{
connection.Close();
}
}
/// <summary>
/// Fill the database with pre defined values using a clean context.
/// </summary>
private void FillDatabase()
{
using DatabaseContext context = new(_context);
context.Shows.Add(new Show
{
ID = 67,
Slug = "anohana",
Title = "Anohana: The Flower We Saw That Day",
Aliases = new[]
{
"Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
"AnoHana",
"We Still Don't Know the Name of the Flower We Saw That Day."
},
Overview = "When Yadomi Jinta was a child, he was a central piece in a group of close friends. " +
"In time, however, these childhood friends drifted apart, and when they became high " +
"school students, they had long ceased to think of each other as friends.",
Status = Status.Finished,
TrailerUrl = null,
StartYear = 2011,
EndYear = 2011,
Poster = "poster",
Logo = "logo",
Backdrop = "backdrop",
IsMovie = false,
Studio = null
});
}
/// <summary>
/// Get a new databse context connected to a in memory Sqlite databse.
/// </summary>
/// <returns>A valid DatabaseContext</returns>
public DatabaseContext New()
{
return new(_context);
}
}
}

View File

@ -8,34 +8,30 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle collections
/// </summary>
public class CollectionRepository : LocalRepository<Collection>, ICollectionRepository public class CollectionRepository : LocalRepository<Collection>, ICollectionRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name; protected override Expression<Func<Collection, object>> DefaultSort => x => x.Name;
public CollectionRepository(DatabaseContext database) : base(database) /// <summary>
/// Create a new <see cref="CollectionRepository"/>.
/// </summary>
/// <param name="database">The database handle to use</param>
public CollectionRepository(DatabaseContext database)
: base(database)
{ {
_database = database; _database = database;
} }
public override void Dispose() /// <inheritdoc />
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
}
public override async Task<ICollection<Collection>> Search(string query) public override async Task<ICollection<Collection>> Search(string query)
{ {
return await _database.Collections return await _database.Collections
@ -45,6 +41,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<Collection> Create(Collection obj) public override async Task<Collection> Create(Collection obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -53,6 +50,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
public override async Task Delete(Collection obj) public override async Task Delete(Collection obj)
{ {
if (obj == null) if (obj == null)

View File

@ -5,20 +5,44 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle episodes.
/// </summary>
public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository
{ {
private bool _disposed; /// <summary>
/// The databse handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary>
/// A show repository to get show's slug from their ID and keep the slug in each episode.
/// </summary>
private readonly IShowRepository _shows; private readonly IShowRepository _shows;
/// <summary>
/// A track repository to handle creation and deletion of tracks related to the current episode.
/// </summary>
private readonly ITrackRepository _tracks; private readonly ITrackRepository _tracks;
/// <inheritdoc />
protected override Expression<Func<Episode, object>> DefaultSort => x => x.EpisodeNumber; protected override Expression<Func<Episode, object>> DefaultSort => x => x.EpisodeNumber;
/// <summary>
/// Create a new <see cref="EpisodeRepository"/>.
/// </summary>
/// <param name="database">The database handle to use.</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="tracks">A track repository</param>
public EpisodeRepository(DatabaseContext database, public EpisodeRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers,
IShowRepository shows, IShowRepository shows,
@ -32,60 +56,44 @@ namespace Kyoo.Controllers
} }
public override void Dispose() /// <inheritdoc />
public override async Task<Episode> GetOrDefault(int id)
{ {
if (_disposed) Episode ret = await base.GetOrDefault(id);
return;
_disposed = true;
_database.Dispose();
_providers.Dispose();
_shows.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _providers.DisposeAsync();
await _shows.DisposeAsync();
}
public override async Task<Episode> Get(int id)
{
Episode ret = await base.Get(id);
if (ret != null) if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID); ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
public override async Task<Episode> Get(string slug) /// <inheritdoc />
public override async Task<Episode> GetOrDefault(string slug)
{ {
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\d*)"); Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)e(?<episode>\d*)");
if (match.Success) if (match.Success)
{ {
return await Get(match.Groups["show"].Value, return await GetOrDefault(match.Groups["show"].Value,
int.Parse(match.Groups["season"].Value), int.Parse(match.Groups["season"].Value),
int.Parse(match.Groups["episode"].Value)); int.Parse(match.Groups["episode"].Value));
} }
Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug);
if (episode != null)
episode.ShowSlug = slug; episode.ShowSlug = slug;
return episode; return episode;
} }
public override async Task<Episode> Get(Expression<Func<Episode, bool>> predicate) /// <inheritdoc />
public override async Task<Episode> GetOrDefault(Expression<Func<Episode, bool>> where)
{ {
Episode ret = await base.Get(predicate); Episode ret = await base.GetOrDefault(where);
if (ret != null) if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID); ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
public async Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber) /// <inheritdoc />
public async Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{ {
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
@ -95,7 +103,26 @@ namespace Kyoo.Controllers
return ret; return ret;
} }
/// <inheritdoc />
public async Task<Episode> Get(int showID, int seasonNumber, int episodeNumber) public async Task<Episode> Get(int showID, int seasonNumber, int episodeNumber)
{
Episode ret = await GetOrDefault(showID, seasonNumber, episodeNumber);
if (ret == null)
throw new ItemNotFound($"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 ItemNotFound($"No episode S{seasonNumber}E{episodeNumber} found on the show {showSlug}.");
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{ {
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber && x.SeasonNumber == seasonNumber
@ -105,15 +132,7 @@ namespace Kyoo.Controllers
return ret; return ret;
} }
public async Task<Episode> Get(int seasonID, int episodeNumber) /// <inheritdoc />
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID
&& x.EpisodeNumber == episodeNumber);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret;
}
public async Task<Episode> GetAbsolute(int showID, int absoluteNumber) public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{ {
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
@ -123,6 +142,7 @@ namespace Kyoo.Controllers
return ret; return ret;
} }
/// <inheritdoc />
public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber) public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{ {
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
@ -132,6 +152,7 @@ namespace Kyoo.Controllers
return ret; return ret;
} }
/// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query) public override async Task<ICollection<Episode>> Search(string query)
{ {
List<Episode> episodes = await _database.Episodes List<Episode> episodes = await _database.Episodes
@ -144,6 +165,7 @@ namespace Kyoo.Controllers
return episodes; return episodes;
} }
/// <inheritdoc />
public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null, public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default, Sort<Episode> sort = default,
Pagination limit = default) Pagination limit = default)
@ -154,6 +176,7 @@ namespace Kyoo.Controllers
return episodes; return episodes;
} }
/// <inheritdoc />
public override async Task<Episode> Create(Episode obj) public override async Task<Episode> Create(Episode obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -163,6 +186,7 @@ namespace Kyoo.Controllers
return await ValidateTracks(obj); return await ValidateTracks(obj);
} }
/// <inheritdoc />
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld) protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{ {
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
@ -184,6 +208,11 @@ namespace Kyoo.Controllers
await Validate(resource); await Validate(resource);
} }
/// <summary>
/// Set track's index and ensure that every tracks is well-formed.
/// </summary>
/// <param name="resource">The resource to fix.</param>
/// <returns>The <see cref="resource"/> parameter is returnned.</returns>
private async Task<Episode> ValidateTracks(Episode resource) private async Task<Episode> ValidateTracks(Episode resource)
{ {
resource.Tracks = await resource.Tracks.MapAsync((x, i) => resource.Tracks = await resource.Tracks.MapAsync((x, i) =>
@ -198,6 +227,7 @@ namespace Kyoo.Controllers
return resource; return resource;
} }
/// <inheritdoc />
protected override async Task Validate(Episode resource) protected override async Task Validate(Episode resource)
{ {
await base.Validate(resource); await base.Validate(resource);
@ -210,12 +240,7 @@ namespace Kyoo.Controllers
}).ToListAsync(); }).ToListAsync();
} }
public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) /// <inheritdoc />
{
Episode obj = await Get(showSlug, seasonNumber, episodeNumber);
await Delete(obj);
}
public override async Task Delete(Episode obj) public override async Task Delete(Episode obj)
{ {
if (obj == null) if (obj == null)

View File

@ -8,35 +8,31 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository for genres.
/// </summary>
public class GenreRepository : LocalRepository<Genre>, IGenreRepository public class GenreRepository : LocalRepository<Genre>, IGenreRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<Genre, object>> DefaultSort => x => x.Slug; protected override Expression<Func<Genre, object>> DefaultSort => x => x.Slug;
public GenreRepository(DatabaseContext database) : base(database) /// <summary>
/// Create a new <see cref="GenreRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
public GenreRepository(DatabaseContext database)
: base(database)
{ {
_database = database; _database = database;
} }
public override void Dispose() /// <inheritdoc />
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
}
public override async Task<ICollection<Genre>> Search(string query) public override async Task<ICollection<Genre>> Search(string query)
{ {
return await _database.Genres return await _database.Genres
@ -46,6 +42,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<Genre> Create(Genre obj) public override async Task<Genre> Create(Genre obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -54,6 +51,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
public override async Task Delete(Genre obj) public override async Task Delete(Genre obj)
{ {
if (obj == null) if (obj == null)

View File

@ -10,18 +10,45 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle library items.
/// </summary>
public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary>
/// A lazy loaded library repository to validate queries (check if a library does exist)
/// </summary>
private readonly Lazy<ILibraryRepository> _libraries; private readonly Lazy<ILibraryRepository> _libraries;
/// <summary>
/// A lazy loaded show repository to get a show from it's id.
/// </summary>
private readonly Lazy<IShowRepository> _shows; private readonly Lazy<IShowRepository> _shows;
/// <summary>
/// A lazy loaded collection repository to get a collection from it's id.
/// </summary>
private readonly Lazy<ICollectionRepository> _collections; private readonly Lazy<ICollectionRepository> _collections;
/// <inheritdoc />
protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title; protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title;
public LibraryItemRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) /// <summary>
/// Create a new <see cref="LibraryItemRepository"/>.
/// </summary>
/// <param name="database">The databse instance</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazilly request a library, show or collection repository.</param>
public LibraryItemRepository(DatabaseContext database,
IProviderRepository providers,
IServiceProvider services)
: base(database) : base(database)
{ {
_database = database; _database = database;
@ -31,45 +58,25 @@ namespace Kyoo.Controllers
_collections = new Lazy<ICollectionRepository>(services.GetRequiredService<ICollectionRepository>); _collections = new Lazy<ICollectionRepository>(services.GetRequiredService<ICollectionRepository>);
} }
public override void Dispose()
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
_providers.Dispose();
if (_shows.IsValueCreated)
_shows.Value.Dispose();
if (_collections.IsValueCreated)
_collections.Value.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync() /// <inheritdoc />
{ public override async Task<LibraryItem> GetOrDefault(int id)
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _providers.DisposeAsync();
if (_shows.IsValueCreated)
await _shows.Value.DisposeAsync();
if (_collections.IsValueCreated)
await _collections.Value.DisposeAsync();
}
public override async Task<LibraryItem> Get(int id)
{ {
return id > 0 return id > 0
? new LibraryItem(await _shows.Value.Get(id)) ? new LibraryItem(await _shows.Value.GetOrDefault(id))
: new LibraryItem(await _collections.Value.Get(-id)); : new LibraryItem(await _collections.Value.GetOrDefault(-id));
} }
public override Task<LibraryItem> Get(string slug) /// <inheritdoc />
public override Task<LibraryItem> GetOrDefault(string slug)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException("You can't get a library item by a slug.");
} }
/// <summary>
/// Get a basic queryable with the right mapping from shows & collections.
/// Shows contained in a collection are excluded.
/// </summary>
private IQueryable<LibraryItem> ItemsQuery private IQueryable<LibraryItem> ItemsQuery
=> _database.Shows => _database.Shows
.Where(x => !x.Collections.Any()) .Where(x => !x.Collections.Any())
@ -77,6 +84,7 @@ namespace Kyoo.Controllers
.Concat(_database.Collections .Concat(_database.Collections
.Select(LibraryItem.FromCollection)); .Select(LibraryItem.FromCollection));
/// <inheritdoc />
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null, public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
Pagination limit = default) Pagination limit = default)
@ -84,6 +92,7 @@ namespace Kyoo.Controllers
return ApplyFilters(ItemsQuery, where, sort, limit); return ApplyFilters(ItemsQuery, where, sort, limit);
} }
/// <inheritdoc />
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null) public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
{ {
IQueryable<LibraryItem> query = ItemsQuery; IQueryable<LibraryItem> query = ItemsQuery;
@ -92,6 +101,7 @@ namespace Kyoo.Controllers
return query.CountAsync(); return query.CountAsync();
} }
/// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query) public override async Task<ICollection<LibraryItem>> Search(string query)
{ {
return await ItemsQuery return await ItemsQuery
@ -101,19 +111,31 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException(); public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj, bool silentFail = false) public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj, bool silentFail = false)
{ {
if (silentFail) if (silentFail)
return Task.FromResult<LibraryItem>(default); return Task.FromResult<LibraryItem>(default);
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
/// <inheritdoc />
public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(int id) => throw new InvalidOperationException(); public override Task Delete(int id) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(string slug) => throw new InvalidOperationException(); public override Task Delete(string slug) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(LibraryItem obj) => throw new InvalidOperationException(); public override Task Delete(LibraryItem obj) => throw new InvalidOperationException();
/// <summary>
/// Get a basic queryable for a library with the right mapping from shows & collections.
/// Shows contained in a collection are excluded.
/// </summary>
/// <param name="selector">Only items that are part of a library that match this predicate will be returned.</param>
/// <returns>A queryable containing items that are part of a library matching the selector.</returns>
private IQueryable<LibraryItem> LibraryRelatedQuery(Expression<Func<Library, bool>> selector) private IQueryable<LibraryItem> LibraryRelatedQuery(Expression<Func<Library, bool>> selector)
=> _database.Libraries => _database.Libraries
.Where(selector) .Where(selector)
@ -125,6 +147,7 @@ namespace Kyoo.Controllers
.SelectMany(x => x.Collections) .SelectMany(x => x.Collections)
.Select(LibraryItem.FromCollection)); .Select(LibraryItem.FromCollection));
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(int id, public async Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
@ -134,11 +157,12 @@ namespace Kyoo.Controllers
where, where,
sort, sort,
limit); limit);
if (!items.Any() && await _libraries.Value.Get(id) == null) if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null)
throw new ItemNotFound(); throw new ItemNotFound();
return items; return items;
} }
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(string slug, public async Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null, Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default, Sort<LibraryItem> sort = default,
@ -148,7 +172,7 @@ namespace Kyoo.Controllers
where, where,
sort, sort,
limit); limit);
if (!items.Any() && await _libraries.Value.Get(slug) == null) if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null)
throw new ItemNotFound(); throw new ItemNotFound();
return items; return items;
} }

View File

@ -8,14 +8,29 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle libraries.
/// </summary>
public class LibraryRepository : LocalRepository<Library>, ILibraryRepository public class LibraryRepository : LocalRepository<Library>, ILibraryRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override Expression<Func<Library, object>> DefaultSort => x => x.ID; protected override Expression<Func<Library, object>> DefaultSort => x => x.ID;
/// <summary>
/// Create a new <see cref="LibraryRepository"/> instance.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">The providere repository</param>
public LibraryRepository(DatabaseContext database, IProviderRepository providers) public LibraryRepository(DatabaseContext database, IProviderRepository providers)
: base(database) : base(database)
{ {
@ -23,25 +38,8 @@ namespace Kyoo.Controllers
_providers = providers; _providers = providers;
} }
public override void Dispose()
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
_providers.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _providers.DisposeAsync();
}
/// <inheritdoc />
public override async Task<ICollection<Library>> Search(string query) public override async Task<ICollection<Library>> Search(string query)
{ {
return await _database.Libraries return await _database.Libraries
@ -51,6 +49,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<Library> Create(Library obj) public override async Task<Library> Create(Library obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -61,6 +60,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(Library resource) protected override async Task Validate(Library resource)
{ {
await base.Validate(resource); await base.Validate(resource);
@ -69,6 +69,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld) protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{ {
if (string.IsNullOrEmpty(resource.Slug)) if (string.IsNullOrEmpty(resource.Slug))
@ -86,6 +87,7 @@ namespace Kyoo.Controllers
} }
} }
/// <inheritdoc />
public override async Task Delete(Library obj) public override async Task Delete(Library obj)
{ {
if (obj == null) if (obj == null)

View File

@ -10,15 +10,36 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle people.
/// </summary>
public class PeopleRepository : LocalRepository<People>, IPeopleRepository public class PeopleRepository : LocalRepository<People>, IPeopleRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary>
/// A lazy loaded show repository to validate requests from shows.
/// </summary>
private readonly Lazy<IShowRepository> _shows; private readonly Lazy<IShowRepository> _shows;
/// <inheritdoc />
protected override Expression<Func<People, object>> DefaultSort => x => x.Name; protected override Expression<Func<People, object>> DefaultSort => x => x.Name;
public PeopleRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) /// <summary>
/// Create a new <see cref="PeopleRepository"/>
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazy load a show repository</param>
public PeopleRepository(DatabaseContext database,
IProviderRepository providers,
IServiceProvider services)
: base(database) : base(database)
{ {
_database = database; _database = database;
@ -27,29 +48,7 @@ namespace Kyoo.Controllers
} }
public override void Dispose() /// <inheritdoc />
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
_providers.Dispose();
if (_shows.IsValueCreated)
_shows.Value.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _providers.DisposeAsync();
if (_shows.IsValueCreated)
await _shows.Value.DisposeAsync();
}
public override async Task<ICollection<People>> Search(string query) public override async Task<ICollection<People>> Search(string query)
{ {
return await _database.People return await _database.People
@ -59,6 +58,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<People> Create(People obj) public override async Task<People> Create(People obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -68,6 +68,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(People resource) protected override async Task Validate(People resource)
{ {
await base.Validate(resource); await base.Validate(resource);
@ -85,6 +86,7 @@ namespace Kyoo.Controllers
}); });
} }
/// <inheritdoc />
protected override async Task EditRelations(People resource, People changed, bool resetOld) protected override async Task EditRelations(People resource, People changed, bool resetOld)
{ {
if (changed.Roles != null || resetOld) if (changed.Roles != null || resetOld)
@ -102,6 +104,7 @@ namespace Kyoo.Controllers
await base.EditRelations(resource, changed, resetOld); await base.EditRelations(resource, changed, resetOld);
} }
/// <inheritdoc />
public override async Task Delete(People obj) public override async Task Delete(People obj)
{ {
if (obj == null) if (obj == null)
@ -113,6 +116,7 @@ namespace Kyoo.Controllers
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
/// <inheritdoc />
public async Task<ICollection<PeopleRole>> GetFromShow(int showID, public async Task<ICollection<PeopleRole>> GetFromShow(int showID,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -133,6 +137,7 @@ namespace Kyoo.Controllers
return people; return people;
} }
/// <inheritdoc />
public async Task<ICollection<PeopleRole>> GetFromShow(string showSlug, public async Task<ICollection<PeopleRole>> GetFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
@ -154,24 +159,26 @@ namespace Kyoo.Controllers
return people; return people;
} }
public async Task<ICollection<PeopleRole>> GetFromPeople(int peopleID, /// <inheritdoc />
public async Task<ICollection<PeopleRole>> GetFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,
Pagination limit = default) Pagination limit = default)
{ {
ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles ICollection<PeopleRole> roles = await ApplyFilters(_database.PeopleRoles
.Where(x => x.PeopleID == peopleID) .Where(x => x.PeopleID == id)
.Include(x => x.Show), .Include(x => x.Show),
id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), y => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == y),
x => x.Show.Title, x => x.Show.Title,
where, where,
sort, sort,
limit); limit);
if (!roles.Any() && await Get(peopleID) == null) if (!roles.Any() && await Get(id) == null)
throw new ItemNotFound(); throw new ItemNotFound();
return roles; return roles;
} }
/// <inheritdoc />
public async Task<ICollection<PeopleRole>> GetFromPeople(string slug, public async Task<ICollection<PeopleRole>> GetFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null, Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default, Sort<PeopleRole> sort = default,

View File

@ -8,18 +8,32 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class ProviderRepository : LocalRepository<ProviderID>, IProviderRepository /// <summary>
/// A local repository to handle providers.
/// </summary>
public class ProviderRepository : LocalRepository<Provider>, IProviderRepository
{ {
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
protected override Expression<Func<ProviderID, object>> DefaultSort => x => x.Slug;
/// <inheritdoc />
protected override Expression<Func<Provider, object>> DefaultSort => x => x.Slug;
public ProviderRepository(DatabaseContext database) : base(database) /// <summary>
/// Create a new <see cref="ProviderRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
public ProviderRepository(DatabaseContext database)
: base(database)
{ {
_database = database; _database = database;
} }
public override async Task<ICollection<ProviderID>> Search(string query) /// <inheritdoc />
public override async Task<ICollection<Provider>> Search(string query)
{ {
return await _database.Providers return await _database.Providers
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
@ -28,7 +42,8 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
public override async Task<ProviderID> Create(ProviderID obj) /// <inheritdoc />
public override async Task<Provider> Create(Provider obj)
{ {
await base.Create(obj); await base.Create(obj);
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
@ -36,7 +51,8 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
public override async Task Delete(ProviderID obj) /// <inheritdoc />
public override async Task Delete(Provider obj)
{ {
if (obj == null) if (obj == null)
throw new ArgumentNullException(nameof(obj)); throw new ArgumentNullException(nameof(obj));
@ -46,6 +62,7 @@ namespace Kyoo.Controllers
await _database.SaveChangesAsync(); await _database.SaveChangesAsync();
} }
/// <inheritdoc />
public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null, public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default, Sort<MetadataID> sort = default,
Pagination limit = default) Pagination limit = default)

View File

@ -5,21 +5,46 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle seasons.
/// </summary>
public class SeasonRepository : LocalRepository<Season>, ISeasonRepository public class SeasonRepository : LocalRepository<Season>, ISeasonRepository
{ {
private bool _disposed; /// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary>
/// A show repository to get show's slug from their ID and keep the slug in each episode.
/// </summary>
private readonly IShowRepository _shows; private readonly IShowRepository _shows;
/// <summary>
/// A lazilly loaded episode repository to handle deletion of episodes with the season.
/// </summary>
private readonly Lazy<IEpisodeRepository> _episodes; private readonly Lazy<IEpisodeRepository> _episodes;
/// <inheritdoc/>
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber; protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
/// <summary>
/// Create a new <see cref="SeasonRepository"/> using the provided handle, a provider & a show repository and
/// a service provider to lazilly request an episode repository.
/// </summary>
/// <param name="database">The database handle that will be used</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="services">A service provider to lazilly request an episode repository.</param>
public SeasonRepository(DatabaseContext database, public SeasonRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers,
IShowRepository shows, IShowRepository shows,
@ -33,47 +58,23 @@ namespace Kyoo.Controllers
} }
public override void Dispose() /// <inheritdoc/>
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
_providers.Dispose();
_shows.Dispose();
if (_episodes.IsValueCreated)
_episodes.Value.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _providers.DisposeAsync();
await _shows.DisposeAsync();
if (_episodes.IsValueCreated)
await _episodes.Value.DisposeAsync();
}
public override async Task<Season> Get(int id) public override async Task<Season> Get(int id)
{ {
Season ret = await base.Get(id); Season ret = await base.Get(id);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID); ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
public override async Task<Season> Get(Expression<Func<Season, bool>> predicate) /// <inheritdoc/>
public override async Task<Season> Get(Expression<Func<Season, bool>> where)
{ {
Season ret = await base.Get(predicate); Season ret = await base.Get(where);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID); ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
/// <inheritdoc/>
public override Task<Season> Get(string slug) public override Task<Season> Get(string slug)
{ {
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)"); Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)");
@ -83,24 +84,41 @@ namespace Kyoo.Controllers
return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value));
} }
/// <inheritdoc/>
public async Task<Season> Get(int showID, int seasonNumber) public async Task<Season> Get(int showID, int seasonNumber)
{ {
Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID Season ret = await GetOrDefault(showID, seasonNumber);
&& x.SeasonNumber == seasonNumber); if (ret == null)
if (ret != null) throw new ItemNotFound($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID); ret.ShowSlug = await _shows.GetSlug(showID);
return ret; return ret;
} }
/// <inheritdoc/>
public async Task<Season> Get(string showSlug, int seasonNumber) public async Task<Season> Get(string showSlug, int seasonNumber)
{ {
Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Season ret = await GetOrDefault(showSlug, seasonNumber);
&& x.SeasonNumber == seasonNumber); if (ret == null)
if (ret != null) throw new ItemNotFound($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug; ret.ShowSlug = showSlug;
return ret; return ret;
} }
/// <inheritdoc/>
public Task<Season> GetOrDefault(int showID, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber);
}
/// <inheritdoc/>
public Task<Season> GetOrDefault(string showSlug, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber);
}
/// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query) public override async Task<ICollection<Season>> Search(string query)
{ {
List<Season> seasons = await _database.Seasons List<Season> seasons = await _database.Seasons
@ -113,6 +131,7 @@ namespace Kyoo.Controllers
return seasons; return seasons;
} }
/// <inheritdoc/>
public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null, public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null,
Sort<Season> sort = default, Sort<Season> sort = default,
Pagination limit = default) Pagination limit = default)
@ -123,6 +142,7 @@ namespace Kyoo.Controllers
return seasons; return seasons;
} }
/// <inheritdoc/>
public override async Task<Season> Create(Season obj) public override async Task<Season> Create(Season obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -132,6 +152,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc/>
protected override async Task Validate(Season resource) protected override async Task Validate(Season resource)
{ {
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
@ -146,6 +167,7 @@ namespace Kyoo.Controllers
}); });
} }
/// <inheritdoc/>
protected override async Task EditRelations(Season resource, Season changed, bool resetOld) protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{ {
if (changed.ExternalIDs != null || resetOld) if (changed.ExternalIDs != null || resetOld)
@ -156,12 +178,7 @@ namespace Kyoo.Controllers
await base.EditRelations(resource, changed, resetOld); await base.EditRelations(resource, changed, resetOld);
} }
public async Task Delete(string showSlug, int seasonNumber) /// <inheritdoc/>
{
Season obj = await Get(showSlug, seasonNumber);
await Delete(obj);
}
public override async Task Delete(Season obj) public override async Task Delete(Season obj)
{ {
if (obj == null) if (obj == null)

View File

@ -9,18 +9,52 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle shows
/// </summary>
public class ShowRepository : LocalRepository<Show>, IShowRepository public class ShowRepository : LocalRepository<Show>, IShowRepository
{ {
private bool _disposed; /// <summary>
/// The databse handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <summary>
/// A studio repository to handle creation/validation of related studios.
/// </summary>
private readonly IStudioRepository _studios; private readonly IStudioRepository _studios;
/// <summary>
/// A people repository to handle creation/validation of related people.
/// </summary>
private readonly IPeopleRepository _people; private readonly IPeopleRepository _people;
/// <summary>
/// A genres repository to handle creation/validation of related genres.
/// </summary>
private readonly IGenreRepository _genres; private readonly IGenreRepository _genres;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
/// <summary>
/// A lazy loaded season repository to handle cascade deletion (seasons deletion whith it's show)
/// </summary>
private readonly Lazy<ISeasonRepository> _seasons; private readonly Lazy<ISeasonRepository> _seasons;
/// <summary>
/// A lazy loaded episode repository to handle cascade deletion (episode deletion whith it's show)
/// </summary>
private readonly Lazy<IEpisodeRepository> _episodes; private readonly Lazy<IEpisodeRepository> _episodes;
/// <inheritdoc />
protected override Expression<Func<Show, object>> DefaultSort => x => x.Title; protected override Expression<Func<Show, object>> DefaultSort => x => x.Title;
/// <summary>
/// Create a new <see cref="ShowRepository"/>.
/// </summary>
/// <param name="database">The database handle to use</param>
/// <param name="studios">A studio repository</param>
/// <param name="people">A people repository</param>
/// <param name="genres">A genres repository</param>
/// <param name="providers">A provider repository</param>
/// <param name="services">A service provider to lazilly request a season and an episode repository</param>
public ShowRepository(DatabaseContext database, public ShowRepository(DatabaseContext database,
IStudioRepository studios, IStudioRepository studios,
IPeopleRepository people, IPeopleRepository people,
@ -38,39 +72,8 @@ namespace Kyoo.Controllers
_episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>); _episodes = new Lazy<IEpisodeRepository>(services.GetRequiredService<IEpisodeRepository>);
} }
public override void Dispose()
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
_studios.Dispose();
_people.Dispose();
_genres.Dispose();
_providers.Dispose();
if (_seasons.IsValueCreated)
_seasons.Value.Dispose();
if (_episodes.IsValueCreated)
_episodes.Value.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
await _studios.DisposeAsync();
await _people.DisposeAsync();
await _genres.DisposeAsync();
await _providers.DisposeAsync();
if (_seasons.IsValueCreated)
await _seasons.Value.DisposeAsync();
if (_episodes.IsValueCreated)
await _episodes.Value.DisposeAsync();
}
/// <inheritdoc />
public override async Task<ICollection<Show>> Search(string query) public override async Task<ICollection<Show>> Search(string query)
{ {
query = $"%{query}%"; query = $"%{query}%";
@ -83,6 +86,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<Show> Create(Show obj) public override async Task<Show> Create(Show obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -94,6 +98,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(Show resource) protected override async Task Validate(Show resource)
{ {
await base.Validate(resource); await base.Validate(resource);
@ -119,6 +124,7 @@ namespace Kyoo.Controllers
}); });
} }
/// <inheritdoc />
protected override async Task EditRelations(Show resource, Show changed, bool resetOld) protected override async Task EditRelations(Show resource, Show changed, bool resetOld)
{ {
await Validate(changed); await Validate(changed);
@ -145,6 +151,7 @@ namespace Kyoo.Controllers
} }
} }
/// <inheritdoc />
public async Task AddShowLink(int showID, int? libraryID, int? collectionID) public async Task AddShowLink(int showID, int? libraryID, int? collectionID)
{ {
if (collectionID != null) if (collectionID != null)
@ -168,6 +175,7 @@ namespace Kyoo.Controllers
} }
} }
/// <inheritdoc />
public Task<string> GetSlug(int showID) public Task<string> GetSlug(int showID)
{ {
return _database.Shows.Where(x => x.ID == showID) return _database.Shows.Where(x => x.ID == showID)
@ -175,6 +183,7 @@ namespace Kyoo.Controllers
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
/// <inheritdoc />
public override async Task Delete(Show obj) public override async Task Delete(Show obj)
{ {
if (obj == null) if (obj == null)

View File

@ -8,17 +8,31 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle studios
/// </summary>
public class StudioRepository : LocalRepository<Studio>, IStudioRepository public class StudioRepository : LocalRepository<Studio>, IStudioRepository
{ {
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<Studio, object>> DefaultSort => x => x.Name; protected override Expression<Func<Studio, object>> DefaultSort => x => x.Name;
public StudioRepository(DatabaseContext database) : base(database) /// <summary>
/// Create a new <see cref="StudioRepository"/>.
/// </summary>
/// <param name="database">The database handle</param>
public StudioRepository(DatabaseContext database)
: base(database)
{ {
_database = database; _database = database;
} }
/// <inheritdoc />
public override async Task<ICollection<Studio>> Search(string query) public override async Task<ICollection<Studio>> Search(string query)
{ {
return await _database.Studios return await _database.Studios
@ -28,6 +42,7 @@ namespace Kyoo.Controllers
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<Studio> Create(Studio obj) public override async Task<Studio> Create(Studio obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -36,6 +51,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
public override async Task Delete(Studio obj) public override async Task Delete(Studio obj)
{ {
if (obj == null) if (obj == null)

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,40 +10,48 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle tracks.
/// </summary>
public class TrackRepository : LocalRepository<Track>, ITrackRepository public class TrackRepository : LocalRepository<Track>, ITrackRepository
{ {
private bool _disposed; /// <summary>
/// The databse handle
/// </summary>
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
/// <inheritdoc />
protected override Expression<Func<Track, object>> DefaultSort => x => x.TrackIndex; protected override Expression<Func<Track, object>> DefaultSort => x => x.TrackIndex;
public TrackRepository(DatabaseContext database) : base(database) /// <summary>
/// Create a new <see cref="TrackRepository"/>.
/// </summary>
/// <param name="database">The datatabse handle</param>
public TrackRepository(DatabaseContext database)
: base(database)
{ {
_database = database; _database = database;
} }
public override void Dispose()
/// <inheritdoc />
Task<Track> IRepository<Track>.Get(string slug)
{ {
if (_disposed) return Get(slug);
return;
_disposed = true;
_database.Dispose();
} }
public override async ValueTask DisposeAsync() /// <inheritdoc />
public async Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
{ {
if (_disposed) Track ret = await GetOrDefault(slug, type);
return; if (ret == null)
_disposed = true; throw new ItemNotFound($"No track found with the slug {slug} and the type {type}.");
await _database.DisposeAsync(); return ret;
} }
public override Task<Track> Get(string slug) /// <inheritdoc />
{ public Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
return Get(slug, StreamType.Unknown);
}
public Task<Track> Get(string slug, StreamType type)
{ {
Match match = Regex.Match(slug, Match match = Regex.Match(slug,
@"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?"); @"(?<show>.*)-s(?<season>\d+)e(?<episode>\d+)(\.(?<type>\w*))?\.(?<language>.{0,3})(?<forced>-forced)?(\..*)?");
@ -65,27 +74,23 @@ namespace Kyoo.Controllers
if (match.Groups["type"].Success) if (match.Groups["type"].Success)
type = Enum.Parse<StreamType>(match.Groups["type"].Value, true); type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
if (type == StreamType.Unknown) IQueryable<Track> query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug
{
return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug
&& x.Episode.SeasonNumber == seasonNumber && x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber && x.Episode.EpisodeNumber == episodeNumber
&& x.Language == language && x.Language == language
&& x.IsForced == forced); && x.IsForced == forced);
} if (type != StreamType.Unknown)
return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug return query.FirstOrDefaultAsync(x => x.Type == type);
&& x.Episode.SeasonNumber == seasonNumber return query.FirstOrDefaultAsync();
&& x.Episode.EpisodeNumber == episodeNumber
&& x.Type == type
&& x.Language == language
&& x.IsForced == forced);
} }
/// <inheritdoc />
public override Task<ICollection<Track>> Search(string query) public override Task<ICollection<Track>> Search(string query)
{ {
throw new InvalidOperationException("Tracks do not support the search method."); throw new InvalidOperationException("Tracks do not support the search method.");
} }
/// <inheritdoc />
public override async Task<Track> Create(Track obj) public override async Task<Track> Create(Track obj)
{ {
if (obj.EpisodeID <= 0) if (obj.EpisodeID <= 0)
@ -107,6 +112,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc />
public override async Task Delete(Track obj) public override async Task Delete(Track obj)
{ {
if (obj == null) if (obj == null)

View File

@ -92,7 +92,7 @@ namespace Kyoo.Controllers
await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}"); await DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}");
} }
public async Task Validate(ProviderID provider, bool alwaysDownload) public async Task Validate(Provider provider, bool alwaysDownload)
{ {
if (provider.Logo == null) if (provider.Logo == null)
return; return;
@ -145,7 +145,7 @@ namespace Kyoo.Controllers
return Task.FromResult(thumbPath.StartsWith(_peoplePath) ? thumbPath : null); return Task.FromResult(thumbPath.StartsWith(_peoplePath) ? thumbPath : null);
} }
public Task<string> GetProviderLogo(ProviderID provider) public Task<string> GetProviderLogo(Provider provider)
{ {
if (provider == null) if (provider == null)
throw new ArgumentNullException(nameof(provider)); throw new ArgumentNullException(nameof(provider));

View File

@ -113,10 +113,8 @@
</Target> </Target>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true'"> <Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true'">
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" <Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" Command="mkdir -p build %26%26 cd build %26%26 cmake .. %26%26 make -j" />
Command="mkdir -p build %26%26 cd build %26%26 cmake .. %26%26 make -j" /> <Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' == 'true'" Command="(if not exist build mkdir build) %26%26 cd build %26%26 cmake .. -G &quot;NMake Makefiles&quot; %26%26 nmake" />
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' == 'true'"
Command='(if not exist build mkdir build) %26%26 cd build %26%26 cmake .. -G "NMake Makefiles" %26%26 nmake' />
<Copy SourceFiles="$(TranscoderRoot)/build/$(TranscoderBinary)" DestinationFolder="." /> <Copy SourceFiles="$(TranscoderRoot)/build/$(TranscoderBinary)" DestinationFolder="." />
</Target> </Target>

View File

@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -10,28 +11,71 @@ using Npgsql;
namespace Kyoo namespace Kyoo
{ {
/// <summary>
/// The database handle used for all local repositories.
/// </summary>
/// <remarks>
/// It should not be used directly, to access the database use a <see cref="ILibraryManager"/> or repositories.
/// </remarks>
public class DatabaseContext : DbContext public class DatabaseContext : DbContext
{ {
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) /// <summary>
{ /// All libraries of Kyoo. See <see cref="Library"/>.
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; /// </summary>
ChangeTracker.LazyLoadingEnabled = false;
}
public DbSet<Library> Libraries { get; set; } public DbSet<Library> Libraries { get; set; }
/// <summary>
/// All collections of Kyoo. See <see cref="Collection"/>.
/// </summary>
public DbSet<Collection> Collections { get; set; } public DbSet<Collection> Collections { get; set; }
/// <summary>
/// All shows of Kyoo. See <see cref="Show"/>.
/// </summary>
public DbSet<Show> Shows { get; set; } public DbSet<Show> Shows { get; set; }
/// <summary>
/// All seasons of Kyoo. See <see cref="Season"/>.
/// </summary>
public DbSet<Season> Seasons { get; set; } public DbSet<Season> Seasons { get; set; }
/// <summary>
/// All episodes of Kyoo. See <see cref="Episode"/>.
/// </summary>
public DbSet<Episode> Episodes { get; set; } public DbSet<Episode> Episodes { get; set; }
/// <summary>
/// All tracks of Kyoo. See <see cref="Track"/>.
/// </summary>
public DbSet<Track> Tracks { get; set; } public DbSet<Track> Tracks { get; set; }
/// <summary>
/// All genres of Kyoo. See <see cref="Genres"/>.
/// </summary>
public DbSet<Genre> Genres { get; set; } public DbSet<Genre> Genres { get; set; }
/// <summary>
/// All people of Kyoo. See <see cref="People"/>.
/// </summary>
public DbSet<People> People { get; set; } public DbSet<People> People { get; set; }
/// <summary>
/// All studios of Kyoo. See <see cref="Studio"/>.
/// </summary>
public DbSet<Studio> Studios { get; set; } public DbSet<Studio> Studios { get; set; }
public DbSet<ProviderID> Providers { get; set; } /// <summary>
/// All providers of Kyoo. See <see cref="Provider"/>.
/// </summary>
public DbSet<Provider> Providers { get; set; }
/// <summary>
/// All metadataIDs (ExternalIDs) of Kyoo. See <see cref="MetadataID"/>.
/// </summary>
public DbSet<MetadataID> MetadataIds { get; set; } public DbSet<MetadataID> MetadataIds { get; set; }
/// <summary>
/// All people's role. See <see cref="PeopleRole"/>.
/// </summary>
public DbSet<PeopleRole> PeopleRoles { get; set; } public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// Get a generic link between two resource types.
/// </summary>
/// <remarks>Types are order dependant. You can't inverse the order. Please always put the owner first.</remarks>
/// <typeparam name="T1">The first resource type of the relation. It is the owner of the second</typeparam>
/// <typeparam name="T2">The second resource type of the relation. It is the contained resource.</typeparam>
/// <returns>All links between the two types.</returns>
public DbSet<Link<T1, T2>> Links<T1, T2>() public DbSet<Link<T1, T2>> Links<T1, T2>()
where T1 : class, IResource where T1 : class, IResource
where T2 : class, IResource where T2 : class, IResource
@ -40,6 +84,9 @@ namespace Kyoo
} }
/// <summary>
/// A basic constructor that set default values (query tracker behaviors, mapping enums...)
/// </summary>
public DatabaseContext() public DatabaseContext()
{ {
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
@ -50,6 +97,21 @@ namespace Kyoo
ChangeTracker.LazyLoadingEnabled = false; ChangeTracker.LazyLoadingEnabled = false;
} }
/// <summary>
/// Create a new <see cref="DatabaseContext"/>.
/// </summary>
/// <param name="options">Connection options to use (witch databse provider to use, connection strings...)</param>
public DatabaseContext(DbContextOptions<DatabaseContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
}
/// <summary>
/// Set database parameters to support every types of Kyoo.
/// </summary>
/// <param name="modelBuilder">The database's model builder.</param>
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
@ -58,14 +120,6 @@ namespace Kyoo
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>(); modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<Library>()
.Property(x => x.Paths)
.HasColumnType("text[]");
modelBuilder.Entity<Show>()
.Property(x => x.Aliases)
.HasColumnType("text[]");
modelBuilder.Entity<Track>() modelBuilder.Entity<Track>()
.Property(t => t.IsDefault) .Property(t => t.IsDefault)
.ValueGeneratedNever(); .ValueGeneratedNever();
@ -74,17 +128,17 @@ namespace Kyoo
.Property(t => t.IsForced) .Property(t => t.IsForced)
.ValueGeneratedNever(); .ValueGeneratedNever();
modelBuilder.Entity<ProviderID>() modelBuilder.Entity<Provider>()
.HasMany(x => x.Libraries) .HasMany(x => x.Libraries)
.WithMany(x => x.Providers) .WithMany(x => x.Providers)
.UsingEntity<Link<Library, ProviderID>>( .UsingEntity<Link<Library, Provider>>(
y => y y => y
.HasOne(x => x.First) .HasOne(x => x.First)
.WithMany(x => x.ProviderLinks), .WithMany(x => x.ProviderLinks),
y => y y => y
.HasOne(x => x.Second) .HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks), .WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, ProviderID>.PrimaryKey)); y => y.HasKey(Link<Library, Provider>.PrimaryKey));
modelBuilder.Entity<Collection>() modelBuilder.Entity<Collection>()
.HasMany(x => x.Libraries) .HasMany(x => x.Libraries)
@ -160,7 +214,7 @@ namespace Kyoo
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<ProviderID>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired(); modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
@ -182,7 +236,7 @@ namespace Kyoo
modelBuilder.Entity<Studio>() modelBuilder.Entity<Studio>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<ProviderID>() modelBuilder.Entity<Provider>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Season>() modelBuilder.Entity<Season>()
@ -196,6 +250,13 @@ namespace Kyoo
.IsUnique(); .IsUnique();
} }
/// <summary>
/// Return a new or an in cache temporary object wih the same ID as the one given
/// </summary>
/// <param name="model">If a resource with the same ID is found in the database, it will be used.
/// <see cref="model"/> will be used overwise</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>A resource that is now tracked by this context.</returns>
public T GetTemporaryObject<T>(T model) public T GetTemporaryObject<T>(T model)
where T : class, IResource where T : class, IResource
{ {
@ -206,6 +267,11 @@ namespace Kyoo
return model; return model;
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public override int SaveChanges() public override int SaveChanges()
{ {
try try
@ -221,6 +287,13 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <param name="acceptAllChangesOnSuccess">Indicates whether AcceptAllChanges() is called after the changes
/// have been sent successfully to the database.</param>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public override int SaveChanges(bool acceptAllChangesOnSuccess) public override int SaveChanges(bool acceptAllChangesOnSuccess)
{ {
try try
@ -236,6 +309,13 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <param name="duplicateMessage">The message that will have the <see cref="DuplicatedItemException"/>
/// (if a duplicate is found).</param>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public int SaveChanges(string duplicateMessage) public int SaveChanges(string duplicateMessage)
{ {
try try
@ -251,6 +331,14 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <param name="acceptAllChangesOnSuccess">Indicates whether AcceptAllChanges() is called after the changes
/// have been sent successfully to the database.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = new()) CancellationToken cancellationToken = new())
{ {
@ -267,6 +355,12 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new()) public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{ {
try try
@ -282,6 +376,14 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes that are applied to this context.
/// </summary>
/// <param name="duplicateMessage">The message that will have the <see cref="DuplicatedItemException"/>
/// (if a duplicate is found).</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public async Task<int> SaveChangesAsync(string duplicateMessage, public async Task<int> SaveChangesAsync(string duplicateMessage,
CancellationToken cancellationToken = new()) CancellationToken cancellationToken = new())
{ {
@ -298,6 +400,12 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded.
/// The current context will still hold those invalid changes.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <returns>The number of state entries written to the database or -1 if a duplicate exist.</returns>
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new()) public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new())
{ {
try try
@ -310,12 +418,31 @@ namespace Kyoo
} }
} }
/// <summary>
/// Save items or retry with a custom method if a duplicate is found.
/// </summary>
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
/// The second parameter is the current retry number.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <typeparam name="T">The type of the item to save</typeparam>
/// <returns>The number of state entries written to the database.</returns>
public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new()) public Task<T> SaveOrRetry<T>(T obj, Func<T, int, T> onFail, CancellationToken cancellationToken = new())
{ {
return SaveOrRetry(obj, onFail, 0, cancellationToken); return SaveOrRetry(obj, onFail, 0, cancellationToken);
} }
public async Task<T> SaveOrRetry<T>(T obj, /// <summary>
/// Save items or retry with a custom method if a duplicate is found.
/// </summary>
/// <param name="obj">The item to save (other changes of this context will also be saved)</param>
/// <param name="onFail">A function to run on fail, the <see cref="obj"/> param wil be mapped.
/// The second parameter is the current retry number.</param>
/// <param name="recurse">The current retry number.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <typeparam name="T">The type of the item to save</typeparam>
/// <returns>The number of state entries written to the database.</returns>
private async Task<T> SaveOrRetry<T>(T obj,
Func<T, int, T> onFail, Func<T, int, T> onFail,
int recurse, int recurse,
CancellationToken cancellationToken = new()) CancellationToken cancellationToken = new())
@ -337,11 +464,20 @@ namespace Kyoo
} }
} }
/// <summary>
/// Check if the exception is a duplicated exception.
/// </summary>
/// <remarks>WARNING: this only works for PostgreSQL</remarks>
/// <param name="ex">The exception to check</param>
/// <returns>True if the exception is a duplicate exception. False otherwise</returns>
private static bool IsDuplicateException(Exception ex) private static bool IsDuplicateException(Exception ex)
{ {
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
} }
/// <summary>
/// Delete every changes that are on this context.
/// </summary>
private void DiscardChanges() private void DiscardChanges()
{ {
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged

View File

@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal namespace Kyoo.Models.DatabaseMigrations.Internal
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20210325184215_Initial")] [Migration("20210420221509_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -179,7 +179,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Link<Library, Collection>"); b.ToTable("Link<Library, Collection>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("integer");
@ -191,7 +191,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasIndex("SecondID"); b.HasIndex("SecondID");
b.ToTable("Link<Library, ProviderID>"); b.ToTable("Link<Library, Provider>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
@ -320,7 +320,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("PeopleRoles"); b.ToTable("PeopleRoles");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderID", b => modelBuilder.Entity("Kyoo.Models.Provider", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -563,7 +563,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Second"); b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{ {
b.HasOne("Kyoo.Models.Library", "First") b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks") .WithMany("ProviderLinks")
@ -571,7 +571,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ProviderID", "Second") b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany("LibraryLinks") .WithMany("LibraryLinks")
.HasForeignKey("SecondID") .HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -632,7 +632,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("PeopleID") .HasForeignKey("PeopleID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ProviderID", "Provider") b.HasOne("Kyoo.Models.Provider", "Provider")
.WithMany("MetadataLinks") .WithMany("MetadataLinks")
.HasForeignKey("ProviderID") .HasForeignKey("ProviderID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -744,7 +744,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Roles"); b.Navigation("Roles");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderID", b => modelBuilder.Entity("Kyoo.Models.Provider", b =>
{ {
b.Navigation("LibraryLinks"); b.Navigation("LibraryLinks");

View File

@ -128,7 +128,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Link<Library, ProviderID>", name: "Link<Library, Provider>",
columns: table => new columns: table => new
{ {
FirstID = table.Column<int>(type: "integer", nullable: false), FirstID = table.Column<int>(type: "integer", nullable: false),
@ -136,15 +136,15 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Link<Library, ProviderID>", x => new { x.FirstID, x.SecondID }); table.PrimaryKey("PK_Link<Library, Provider>", x => new { x.FirstID, x.SecondID });
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, ProviderID>_Libraries_FirstID", name: "FK_Link<Library, Provider>_Libraries_FirstID",
column: x => x.FirstID, column: x => x.FirstID,
principalTable: "Libraries", principalTable: "Libraries",
principalColumn: "ID", principalColumn: "ID",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Link<Library, ProviderID>_Providers_SecondID", name: "FK_Link<Library, Provider>_Providers_SecondID",
column: x => x.SecondID, column: x => x.SecondID,
principalTable: "Providers", principalTable: "Providers",
principalColumn: "ID", principalColumn: "ID",
@ -459,8 +459,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
column: "SecondID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Link<Library, ProviderID>_SecondID", name: "IX_Link<Library, Provider>_SecondID",
table: "Link<Library, ProviderID>", table: "Link<Library, Provider>",
column: "SecondID"); column: "SecondID");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
@ -559,7 +559,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
name: "Link<Library, Collection>"); name: "Link<Library, Collection>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Library, ProviderID>"); name: "Link<Library, Provider>");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link<Library, Show>"); name: "Link<Library, Show>");

View File

@ -177,7 +177,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("Link<Library, Collection>"); b.ToTable("Link<Library, Collection>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{ {
b.Property<int>("FirstID") b.Property<int>("FirstID")
.HasColumnType("integer"); .HasColumnType("integer");
@ -189,7 +189,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.HasIndex("SecondID"); b.HasIndex("SecondID");
b.ToTable("Link<Library, ProviderID>"); b.ToTable("Link<Library, Provider>");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Show>", b =>
@ -318,7 +318,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("PeopleRoles"); b.ToTable("PeopleRoles");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderID", b => modelBuilder.Entity("Kyoo.Models.Provider", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -561,7 +561,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Second"); b.Navigation("Second");
}); });
modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.ProviderID>", b => modelBuilder.Entity("Kyoo.Models.Link<Kyoo.Models.Library, Kyoo.Models.Provider>", b =>
{ {
b.HasOne("Kyoo.Models.Library", "First") b.HasOne("Kyoo.Models.Library", "First")
.WithMany("ProviderLinks") .WithMany("ProviderLinks")
@ -569,7 +569,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Kyoo.Models.ProviderID", "Second") b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany("LibraryLinks") .WithMany("LibraryLinks")
.HasForeignKey("SecondID") .HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -630,7 +630,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("PeopleID") .HasForeignKey("PeopleID")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ProviderID", "Provider") b.HasOne("Kyoo.Models.Provider", "Provider")
.WithMany("MetadataLinks") .WithMany("MetadataLinks")
.HasForeignKey("ProviderID") .HasForeignKey("ProviderID")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -742,7 +742,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Roles"); b.Navigation("Roles");
}); });
modelBuilder.Entity("Kyoo.Models.ProviderID", b => modelBuilder.Entity("Kyoo.Models.Provider", b =>
{ {
b.Navigation("LibraryLinks"); b.Navigation("LibraryLinks");

View File

@ -3,7 +3,11 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.VisualBasic.FileIO; using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Kyoo namespace Kyoo
{ {
@ -18,11 +22,8 @@ namespace Kyoo
/// <param name="args">Command line arguments</param> /// <param name="args">Command line arguments</param>
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
if (args.Length > 0) if (!File.Exists("./settings.json"))
FileSystem.CurrentDirectory = args[0]; File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json");
if (!File.Exists("./appsettings.json"))
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"), "appsettings.json");
bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch
{ {
@ -49,15 +50,50 @@ namespace Kyoo
await host.Build().RunAsync(); await host.Build().RunAsync();
} }
/// <summary>
/// Register settings.json, environment variables and command lines arguments as configuration.
/// </summary>
/// <param name="builder">The configuration builder to use</param>
/// <param name="args">The command line arguments</param>
/// <returns>The modified configuration builder</returns>
private static IConfigurationBuilder SetupConfig(IConfigurationBuilder builder, string[] args)
{
return builder.AddJsonFile("./settings.json", false, true)
.AddEnvironmentVariables()
.AddCommandLine(args);
}
/// <summary> /// <summary>
/// Createa a web host /// Createa a web host
/// </summary> /// </summary>
/// <param name="args">Command line parameters that can be handled by kestrel</param> /// <param name="args">Command line parameters that can be handled by kestrel</param>
/// <returns>A new web host instance</returns> /// <returns>A new web host instance</returns>
private static IWebHostBuilder CreateWebHostBuilder(string[] args) => private static IWebHostBuilder CreateWebHostBuilder(string[] args)
WebHost.CreateDefaultBuilder(args) {
.UseKestrel(config => { config.AddServerHeader = false; }) WebHost.CreateDefaultBuilder(args);
.UseUrls("http://*:5000")
return new WebHostBuilder()
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseConfiguration(SetupConfig(new ConfigurationBuilder(), args).Build())
.ConfigureAppConfiguration(x => SetupConfig(x, args))
.ConfigureLogging((context, builder) =>
{
builder.AddConfiguration(context.Configuration.GetSection("logging"))
.AddConsole()
.AddDebug()
.AddEventSourceLogger();
})
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
if (context.HostingEnvironment.IsDevelopment())
StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration);
})
.ConfigureServices(x => x.AddRouting())
.UseKestrel(options => { options.AddServerHeader = false; })
.UseIIS()
.UseIISIntegration()
.UseStartup<Startup>(); .UseStartup<Startup>();
} }
} }
}

View File

@ -147,6 +147,20 @@ namespace Kyoo
}); });
// TODO Add custom method to the service container and expose those methods to the plugin
// TODO Add for example a AddRepository that will automatically register the complex interface, the IRepository<T> and the IBaseRepository
services.AddScoped<IBaseRepository, LibraryRepository>();
services.AddScoped<IBaseRepository, LibraryItemRepository>();
services.AddScoped<IBaseRepository, CollectionRepository>();
services.AddScoped<IBaseRepository, ShowRepository>();
services.AddScoped<IBaseRepository, SeasonRepository>();
services.AddScoped<IBaseRepository, EpisodeRepository>();
services.AddScoped<IBaseRepository, TrackRepository>();
services.AddScoped<IBaseRepository, PeopleRepository>();
services.AddScoped<IBaseRepository, StudioRepository>();
services.AddScoped<IBaseRepository, GenreRepository>();
services.AddScoped<IBaseRepository, ProviderRepository>();
services.AddScoped<ILibraryRepository, LibraryRepository>(); services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<ILibraryItemRepository, LibraryItemRepository>(); services.AddScoped<ILibraryItemRepository, LibraryItemRepository>();
services.AddScoped<ICollectionRepository, CollectionRepository>(); services.AddScoped<ICollectionRepository, CollectionRepository>();

View File

@ -32,8 +32,8 @@ namespace Kyoo.Controllers
public async Task<IEnumerable<string>> GetPossibleParameters() public async Task<IEnumerable<string>> GetPossibleParameters()
{ {
using IServiceScope serviceScope = _serviceProvider.CreateScope(); using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
return (await libraryManager!.GetLibraries()).Select(x => x.Slug); return (await libraryManager!.GetAll<Library>()).Select(x => x.Slug);
} }
public int? Progress() public int? Progress()
@ -56,25 +56,25 @@ namespace Kyoo.Controllers
_parallelTasks = 30; _parallelTasks = 30;
using IServiceScope serviceScope = _serviceProvider.CreateScope(); using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
foreach (Show show in await libraryManager!.GetShows()) foreach (Show show in await libraryManager!.GetAll<Show>())
if (!Directory.Exists(show.Path)) if (!Directory.Exists(show.Path))
await libraryManager.DeleteShow(show); await libraryManager.Delete(show);
ICollection<Episode> episodes = await libraryManager.GetEpisodes(); ICollection<Episode> episodes = await libraryManager.GetAll<Episode>();
foreach (Episode episode in episodes) foreach (Episode episode in episodes)
if (!File.Exists(episode.Path)) if (!File.Exists(episode.Path))
await libraryManager.DeleteEpisode(episode); await libraryManager.Delete(episode);
ICollection<Track> tracks = await libraryManager.GetTracks(); ICollection<Track> tracks = await libraryManager.GetAll<Track>();
foreach (Track track in tracks) foreach (Track track in tracks)
if (!File.Exists(track.Path)) if (!File.Exists(track.Path))
await libraryManager.DeleteTrack(track); await libraryManager.Delete(track);
ICollection<Library> libraries = argument == null ICollection<Library> libraries = argument == null
? await libraryManager.GetLibraries() ? await libraryManager.GetAll<Library>()
: new [] { await libraryManager.GetLibrary(argument)}; : new [] { await libraryManager.Get<Library>(argument)};
if (argument != null && libraries.First() == null) if (argument != null && libraries.First() == null)
throw new ArgumentException($"No library found with the name {argument}"); throw new ArgumentException($"No library found with the name {argument}");
@ -143,11 +143,13 @@ namespace Kyoo.Controllers
} }
private async Task RegisterExternalSubtitle(string path, CancellationToken token) private async Task RegisterExternalSubtitle(string path, CancellationToken token)
{
try
{ {
if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles"))
return; return;
using IServiceScope serviceScope = _serviceProvider.CreateScope(); using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("subtitleRegex"); string patern = _config.GetValue<string>("subtitleRegex");
Regex regex = new(patern, RegexOptions.IgnoreCase); Regex regex = new(patern, RegexOptions.IgnoreCase);
@ -160,29 +162,31 @@ namespace Kyoo.Controllers
} }
string episodePath = match.Groups["Episode"].Value; string episodePath = match.Groups["Episode"].Value;
Episode episode = await libraryManager!.GetEpisode(x => x.Path.StartsWith(episodePath)); Episode episode = await libraryManager!.Get<Episode>(x => x.Path.StartsWith(episodePath));
Track track = new()
if (episode == null)
{
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
return;
}
Track track = new(StreamType.Subtitle,
null,
match.Groups["Language"].Value,
match.Groups["Default"].Value.Length > 0,
match.Groups["Forced"].Value.Length > 0,
SubtitleExtensions[Path.GetExtension(path)],
true,
path)
{ {
Type = StreamType.Subtitle,
Language = match.Groups["Language"].Value,
IsDefault = match.Groups["Default"].Value.Length > 0,
IsForced = match.Groups["Forced"].Value.Length > 0,
Codec = SubtitleExtensions[Path.GetExtension(path)],
IsExternal = true,
Path = path,
Episode = episode Episode = episode
}; };
await libraryManager.RegisterTrack(track); await libraryManager.Create(track);
Console.WriteLine($"Registering subtitle at: {path}."); Console.WriteLine($"Registering subtitle at: {path}.");
} }
catch (ItemNotFound)
{
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Unknown error while registering subtitle: {ex.Message}");
}
}
private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token) private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token)
{ {
@ -192,7 +196,7 @@ namespace Kyoo.Controllers
try try
{ {
using IServiceScope serviceScope = _serviceProvider.CreateScope(); using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>(); ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("regex"); string patern = _config.GetValue<string>("regex");
Regex regex = new(patern, RegexOptions.IgnoreCase); Regex regex = new(patern, RegexOptions.IgnoreCase);
@ -215,7 +219,7 @@ namespace Kyoo.Controllers
bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1; bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1;
Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
if (isMovie) if (isMovie)
await libraryManager!.RegisterEpisode(await GetMovie(show, path)); await libraryManager!.Create(await GetMovie(show, path));
else else
{ {
Season season = await GetSeason(libraryManager, show, seasonNumber, library); Season season = await GetSeason(libraryManager, show, seasonNumber, library);
@ -226,7 +230,7 @@ namespace Kyoo.Controllers
absoluteNumber, absoluteNumber,
path, path,
library); library);
await libraryManager!.RegisterEpisode(episode); await libraryManager!.Create(episode);
} }
await libraryManager.AddShowLink(show, library, collection); await libraryManager.AddShowLink(show, library, collection);
@ -250,19 +254,19 @@ namespace Kyoo.Controllers
{ {
if (string.IsNullOrEmpty(collectionName)) if (string.IsNullOrEmpty(collectionName))
return null; return null;
Collection collection = await libraryManager.GetCollection(Utility.ToSlug(collectionName)); Collection collection = await libraryManager.Get<Collection>(Utility.ToSlug(collectionName));
if (collection != null) if (collection != null)
return collection; return collection;
collection = await _metadataProvider.GetCollectionFromName(collectionName, library); collection = await _metadataProvider.GetCollectionFromName(collectionName, library);
try try
{ {
await libraryManager.RegisterCollection(collection); await libraryManager.Create(collection);
return collection; return collection;
} }
catch (DuplicatedItemException) catch (DuplicatedItemException)
{ {
return await libraryManager.GetCollection(collection.Slug); return await libraryManager.Get<Collection>(collection.Slug);
} }
} }
@ -272,7 +276,7 @@ namespace Kyoo.Controllers
bool isMovie, bool isMovie,
Library library) Library library)
{ {
Show old = await libraryManager.GetShow(x => x.Path == showPath); Show old = await libraryManager.Get<Show>(x => x.Path == showPath);
if (old != null) if (old != null)
{ {
await libraryManager.Load(old, x => x.ExternalIDs); await libraryManager.Load(old, x => x.ExternalIDs);
@ -284,18 +288,18 @@ namespace Kyoo.Controllers
try try
{ {
show = await libraryManager.RegisterShow(show); show = await libraryManager.Create(show);
} }
catch (DuplicatedItemException) catch (DuplicatedItemException)
{ {
old = await libraryManager.GetShow(show.Slug); old = await libraryManager.Get<Show>(show.Slug);
if (old.Path == showPath) if (old.Path == showPath)
{ {
await libraryManager.Load(old, x => x.ExternalIDs); await libraryManager.Load(old, x => x.ExternalIDs);
return old; return old;
} }
show.Slug += $"-{show.StartYear}"; show.Slug += $"-{show.StartYear}";
await libraryManager.RegisterShow(show); await libraryManager.Create(show);
} }
await _thumbnailsManager.Validate(show); await _thumbnailsManager.Validate(show);
return show; return show;
@ -308,23 +312,21 @@ namespace Kyoo.Controllers
{ {
if (seasonNumber == -1) if (seasonNumber == -1)
return default; return default;
Season season = await libraryManager.GetSeason(show.Slug, seasonNumber);
if (season == null)
{
season = await _metadataProvider.GetSeason(show, seasonNumber, library);
try try
{ {
await libraryManager.RegisterSeason(season); Season season = await libraryManager.Get(show.Slug, seasonNumber);
await _thumbnailsManager.Validate(season);
}
catch (DuplicatedItemException)
{
season = await libraryManager.GetSeason(show.Slug, season.SeasonNumber);
}
}
season.Show = show; season.Show = show;
return season; return season;
} }
catch (ItemNotFound)
{
Season season = await _metadataProvider.GetSeason(show, seasonNumber, library);
await libraryManager.CreateIfNotExists(season);
await _thumbnailsManager.Validate(season);
season.Show = show;
return season;
}
}
private async Task<Episode> GetEpisode(ILibraryManager libraryManager, private async Task<Episode> GetEpisode(ILibraryManager libraryManager,
Show show, Show show,

View File

@ -45,27 +45,25 @@ namespace Kyoo.Tasks
case "show": case "show":
case "shows": case "shows":
Show show = await (int.TryParse(slug, out id) Show show = await (int.TryParse(slug, out id)
? _library!.GetShow(id) ? _library!.Get<Show>(id)
: _library!.GetShow(slug)); : _library!.Get<Show>(slug));
await ExtractShow(show, thumbs, subs, token); await ExtractShow(show, thumbs, subs, token);
break; break;
case "season": case "season":
case "seasons": case "seasons":
Season season = await (int.TryParse(slug, out id) Season season = await (int.TryParse(slug, out id)
? _library!.GetSeason(id) ? _library!.Get<Season>(id)
: _library!.GetSeason(slug)); : _library!.Get<Season>(slug));
await ExtractSeason(season, thumbs, subs, token); await ExtractSeason(season, thumbs, subs, token);
break; break;
case "episode": case "episode":
case "episodes": case "episodes":
Episode episode = await (int.TryParse(slug, out id) Episode episode = await (int.TryParse(slug, out id)
? _library!.GetEpisode(id) ? _library!.Get<Episode>(id)
: _library!.GetEpisode(slug)); : _library!.Get<Episode>(slug));
await ExtractEpisode(episode, thumbs, subs); await ExtractEpisode(episode, thumbs, subs);
break; break;
} }
await _library!.DisposeAsync();
} }
private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token) private async Task ExtractShow(Show show, bool thumbs, bool subs, CancellationToken token)
@ -105,7 +103,7 @@ namespace Kyoo.Tasks
.Where(x => x.Type != StreamType.Attachment) .Where(x => x.Type != StreamType.Attachment)
.Concat(episode.Tracks.Where(x => x.IsExternal)) .Concat(episode.Tracks.Where(x => x.IsExternal))
.ToList(); .ToList();
await _library.EditEpisode(episode, false); await _library.Edit(episode, false);
} }
} }

View File

@ -35,12 +35,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.ID == id)), ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.ID == id)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(id) == null) if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -61,12 +61,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Show>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(slug) == null) if (!resources.Any() && await _libraryManager.Get<Collection>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -87,12 +87,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.ID == id)), ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.ID == id)),
new Sort<Library>(sortBy), new Sort<Library>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(id) == null) if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -113,12 +113,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Library>(where, x => x.Collections.Any(y => y.Slug == slug)),
new Sort<Library>(sortBy), new Sort<Library>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(slug) == null) if (!resources.Any() && await _libraryManager.Get<Collection>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -35,42 +36,56 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(int episodeID) public async Task<ActionResult<Show>> GetShow(int episodeID)
{ {
return await _libraryManager.GetShow(x => x.Episodes.Any(y => y.ID == episodeID)); return await _libraryManager.Get<Show>(x => x.Episodes.Any(y => y.ID == episodeID));
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(string showSlug) public async Task<ActionResult<Show>> GetShow(string showSlug, int seasonNumber, int episodeNumber)
{ {
return await _libraryManager.GetShow(showSlug); return await _libraryManager.Get<Show>(showSlug);
} }
[HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(int showID, int _) public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber, int episodeNumber)
{ {
return await _libraryManager.GetShow(showID); return await _libraryManager.Get<Show>(showID);
} }
[HttpGet("{episodeID:int}/season")] [HttpGet("{episodeID:int}/season")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Season>> GetSeason(int episodeID) public async Task<ActionResult<Season>> GetSeason(int episodeID)
{ {
return await _libraryManager.GetSeason(x => x.Episodes.Any(y => y.ID == episodeID)); return await _libraryManager.Get<Season>(x => x.Episodes.Any(y => y.ID == episodeID));
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Season>> GetSeason(string showSlug, int seasonNuber) public async Task<ActionResult<Season>> GetSeason(string showSlug, int seasonNumber, int episodeNumber)
{ {
return await _libraryManager.GetSeason(showSlug, seasonNuber); try
{
return await _libraryManager.Get(showSlug, seasonNumber);
}
catch (ItemNotFound)
{
return NotFound();
}
} }
[HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Season>> GetSeason(int showID, int seasonNumber) public async Task<ActionResult<Season>> GetSeason(int showID, int seasonNumber, int episodeNumber)
{ {
return await _libraryManager.GetSeason(showID, seasonNumber); try
{
return await _libraryManager.Get(showID, seasonNumber);
}
catch (ItemNotFound)
{
return NotFound();
}
} }
[HttpGet("{episodeID:int}/track")] [HttpGet("{episodeID:int}/track")]
@ -84,12 +99,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks( ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.ID == episodeID), ApiHelper.ParseWhere<Track>(where, x => x.Episode.ID == episodeID),
new Sort<Track>(sortBy), new Sort<Track>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetEpisode(episodeID) == null) if (!resources.Any() && await _libraryManager.Get<Episode>(episodeID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -112,14 +127,14 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks( ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.ShowID == showID ApiHelper.ParseWhere<Track>(where, x => x.Episode.ShowID == showID
&& x.Episode.SeasonNumber == seasonNumber && x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber), && x.Episode.EpisodeNumber == episodeNumber),
new Sort<Track>(sortBy), new Sort<Track>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetEpisode(showID, seasonNumber, episodeNumber) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber, episodeNumber) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -129,10 +144,10 @@ namespace Kyoo.Api
} }
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/track")] [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/track")]
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Page<Track>>> GetEpisode(string showSlug, public async Task<ActionResult<Page<Track>>> GetEpisode(string slug,
int seasonNumber, int seasonNumber,
int episodeNumber, int episodeNumber,
[FromQuery] string sortBy, [FromQuery] string sortBy,
@ -142,13 +157,14 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Track> resources = await _libraryManager.GetTracks(ApiHelper.ParseWhere<Track>(where, x => x.Episode.Show.Slug == showSlug ICollection<Track> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Track>(where, x => x.Episode.Show.Slug == slug
&& x.Episode.SeasonNumber == seasonNumber && x.Episode.SeasonNumber == seasonNumber
&& x.Episode.EpisodeNumber == episodeNumber), && x.Episode.EpisodeNumber == episodeNumber),
new Sort<Track>(sortBy), new Sort<Track>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(slug, seasonNumber, episodeNumber) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -162,20 +178,30 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(int id) public async Task<IActionResult> GetThumb(int id)
{ {
Episode episode = await _libraryManager.GetEpisode(id); try
if (episode == null) {
return NotFound(); Episode episode = await _libraryManager.Get<Episode>(id);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{slug}/thumb")] [HttpGet("{slug}/thumb")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(string slug) public async Task<IActionResult> GetThumb(string slug)
{ {
Episode episode = await _libraryManager.GetEpisode(slug); try
if (episode == null) {
return NotFound(); Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
} }
catch (ItemNotFound)
{
return NotFound();
}
}
} }
} }

View File

@ -36,12 +36,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.ID == id)), ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.ID == id)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetGenre(id) == null) if (!resources.Any() && await _libraryManager.Get<Genre>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -62,12 +62,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetGenre(slug) == null) if (!resources.Any() && await _libraryManager.Get<Genre>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }

View File

@ -47,12 +47,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.ID == id)), ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.ID == id)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -73,12 +73,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -99,12 +99,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.ID == id)), ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.ID == id)),
new Sort<Collection>(sortBy), new Sort<Collection>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -125,12 +125,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.Slug == slug)),
new Sort<Collection>(sortBy), new Sort<Collection>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -156,7 +156,7 @@ namespace Kyoo.Api
new Sort<LibraryItem>(sortBy), new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -182,7 +182,7 @@ namespace Kyoo.Api
new Sort<LibraryItem>(sortBy), new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }

View File

@ -90,7 +90,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetPeopleIcon(int id) public async Task<IActionResult> GetPeopleIcon(int id)
{ {
People people = await _libraryManager.GetPeople(id); People people = await _libraryManager.Get<People>(id);
return _files.FileResult(await _thumbs.GetPeoplePoster(people)); return _files.FileResult(await _thumbs.GetPeoplePoster(people));
} }
@ -98,7 +98,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetPeopleIcon(string slug) public async Task<IActionResult> GetPeopleIcon(string slug)
{ {
People people = await _libraryManager.GetPeople(slug); People people = await _libraryManager.Get<People>(slug);
return _files.FileResult(await _thumbs.GetPeoplePoster(people)); return _files.FileResult(await _thumbs.GetPeoplePoster(people));
} }
} }

View File

@ -11,7 +11,7 @@ namespace Kyoo.Api
[Route("api/provider")] [Route("api/provider")]
[Route("api/providers")] [Route("api/providers")]
[ApiController] [ApiController]
public class ProviderAPI : CrudApi<ProviderID> public class ProviderAPI : CrudApi<Provider>
{ {
private readonly IThumbnailsManager _thumbnails; private readonly IThumbnailsManager _thumbnails;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -32,7 +32,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(int id) public async Task<IActionResult> GetLogo(int id)
{ {
ProviderID provider = await _libraryManager.GetProvider(id); Provider provider = await _libraryManager.Get<Provider>(id);
return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
} }
@ -40,7 +40,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(string slug) public async Task<IActionResult> GetLogo(string slug)
{ {
ProviderID provider = await _libraryManager.GetProvider(slug); Provider provider = await _libraryManager.Get<Provider>(slug);
return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); return _files.FileResult(await _thumbnails.GetProviderLogo(provider));
} }
} }

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Api namespace Kyoo.Api
{ {
[Route("api/search")] [Route("api/search/{query}")]
[ApiController] [ApiController]
public class SearchApi : ControllerBase public class SearchApi : ControllerBase
{ {
@ -18,67 +18,67 @@ namespace Kyoo.Api
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
[HttpGet("{query}")] [HttpGet]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<ActionResult<SearchResult>> Search(string query) public async Task<ActionResult<SearchResult>> Search(string query)
{ {
return new SearchResult return new SearchResult
{ {
Query = query, Query = query,
Collections = await _libraryManager.SearchCollections(query), Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.SearchShows(query), Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.SearchEpisodes(query), Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.SearchPeople(query), People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.SearchGenres(query), Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.SearchStudios(query) Studios = await _libraryManager.Search<Studio>(query)
}; };
} }
[HttpGet("{query}/collection")] [HttpGet("collection")]
[HttpGet("{query}/collections")] [HttpGet("collections")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<Collection>> SearchCollections(string query) public Task<ICollection<Collection>> SearchCollections(string query)
{ {
return _libraryManager.SearchCollections(query); return _libraryManager.Search<Collection>(query);
} }
[HttpGet("{query}/show")] [HttpGet("show")]
[HttpGet("{query}/shows")] [HttpGet("shows")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<Show>> SearchShows(string query) public Task<ICollection<Show>> SearchShows(string query)
{ {
return _libraryManager.SearchShows(query); return _libraryManager.Search<Show>(query);
} }
[HttpGet("{query}/episode")] [HttpGet("episode")]
[HttpGet("{query}/episodes")] [HttpGet("episodes")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<Episode>> SearchEpisodes(string query) public Task<ICollection<Episode>> SearchEpisodes(string query)
{ {
return _libraryManager.SearchEpisodes(query); return _libraryManager.Search<Episode>(query);
} }
[HttpGet("{query}/people")] [HttpGet("people")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<People>> SearchPeople(string query) public Task<ICollection<People>> SearchPeople(string query)
{ {
return _libraryManager.SearchPeople(query); return _libraryManager.Search<People>(query);
} }
[HttpGet("{query}/genre")] [HttpGet("genre")]
[HttpGet("{query}/genres")] [HttpGet("genres")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<Genre>> SearchGenres(string query) public Task<ICollection<Genre>> SearchGenres(string query)
{ {
return _libraryManager.SearchGenres(query); return _libraryManager.Search<Genre>(query);
} }
[HttpGet("{query}/studio")] [HttpGet("studio")]
[HttpGet("{query}/studios")] [HttpGet("studios")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public Task<ICollection<Studio>> SearchStudios(string query) public Task<ICollection<Studio>> SearchStudios(string query)
{ {
return _libraryManager.SearchStudios(query); return _libraryManager.Search<Studio>(query);
} }
} }
} }

View File

@ -42,12 +42,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.SeasonID == seasonID), ApiHelper.ParseWhere<Episode>(where, x => x.SeasonID == seasonID),
new Sort<Episode>(sortBy), new Sort<Episode>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetSeason(seasonID) == null) if (!resources.Any() && await _libraryManager.Get<Season>(seasonID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -69,13 +69,13 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == showSlug ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber), && x.SeasonNumber == seasonNumber),
new Sort<Episode>(sortBy), new Sort<Episode>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetSeason(showSlug, seasonNumber) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(showSlug, seasonNumber) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -97,12 +97,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber), ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber),
new Sort<Episode>(sortBy), new Sort<Episode>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetSeason(showID, seasonNumber) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -116,28 +116,28 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(int seasonID) public async Task<ActionResult<Show>> GetShow(int seasonID)
{ {
return await _libraryManager.GetShow(x => x.Seasons.Any(y => y.ID == seasonID)); return await _libraryManager.Get<Show>(x => x.Seasons.Any(y => y.ID == seasonID));
} }
[HttpGet("{showSlug}-s{seasonNumber:int}/show")] [HttpGet("{showSlug}-s{seasonNumber:int}/show")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(string showSlug, int _) public async Task<ActionResult<Show>> GetShow(string showSlug, int seasonNumber)
{ {
return await _libraryManager.GetShow(showSlug); return await _libraryManager.Get<Show>(showSlug);
} }
[HttpGet("{showID:int}-s{seasonNumber:int}/show")] [HttpGet("{showID:int}-s{seasonNumber:int}/show")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Show>> GetShow(int showID, int _) public async Task<ActionResult<Show>> GetShow(int showID, int seasonNumber)
{ {
return await _libraryManager.GetShow(showID); return await _libraryManager.Get<Show>(showID);
} }
[HttpGet("{id:int}/thumb")] [HttpGet("{id:int}/thumb")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(int id) public async Task<IActionResult> GetThumb(int id)
{ {
Season season = await _libraryManager.GetSeason(id); Season season = await _libraryManager.Get<Season>(id);
await _libraryManager.Load(season, x => x.Show); await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetSeasonPoster(season)); return _files.FileResult(await _thumbs.GetSeasonPoster(season));
} }
@ -146,7 +146,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(string slug) public async Task<IActionResult> GetThumb(string slug)
{ {
Season season = await _libraryManager.GetSeason(slug); Season season = await _libraryManager.Get<Season>(slug);
await _libraryManager.Load(season, x => x.Show); await _libraryManager.Load(season, x => x.Show);
return _files.FileResult(await _thumbs.GetSeasonPoster(season)); return _files.FileResult(await _thumbs.GetSeasonPoster(season));
} }

View File

@ -44,12 +44,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Season> resources = await _libraryManager.GetSeasons( ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Season>(where, x => x.ShowID == showID), ApiHelper.ParseWhere<Season>(where, x => x.ShowID == showID),
new Sort<Season>(sortBy), new Sort<Season>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -70,12 +70,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Season> resources = await _libraryManager.GetSeasons( ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Season>(where, x => x.Show.Slug == slug), ApiHelper.ParseWhere<Season>(where, x => x.Show.Slug == slug),
new Sort<Season>(sortBy), new Sort<Season>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -96,12 +96,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID), ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID),
new Sort<Episode>(sortBy), new Sort<Episode>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -122,12 +122,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Episode> resources = await _libraryManager.GetEpisodes( ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == slug), ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == slug),
new Sort<Episode>(sortBy), new Sort<Episode>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -152,7 +152,7 @@ namespace Kyoo.Api
new Sort<PeopleRole>(sortBy), new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -177,7 +177,7 @@ namespace Kyoo.Api
new Sort<PeopleRole>(sortBy), new Sort<PeopleRole>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -198,12 +198,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Genre> resources = await _libraryManager.GetGenres( ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.ID == showID)), ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Genre>(sortBy), new Sort<Genre>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -224,12 +224,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Genre> resources = await _libraryManager.GetGenres( ICollection<Genre> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Genre>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Genre>(sortBy), new Sort<Genre>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -245,7 +245,7 @@ namespace Kyoo.Api
{ {
try try
{ {
return await _libraryManager.GetStudio(x => x.Shows.Any(y => y.ID == showID)); return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.ID == showID));
} }
catch (ItemNotFound) catch (ItemNotFound)
{ {
@ -259,7 +259,7 @@ namespace Kyoo.Api
{ {
try try
{ {
return await _libraryManager.GetStudio(x => x.Shows.Any(y => y.Slug == slug)); return await _libraryManager.Get<Studio>(x => x.Shows.Any(y => y.Slug == slug));
} }
catch (ItemNotFound) catch (ItemNotFound)
{ {
@ -278,12 +278,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.ID == showID)), ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Library>(sortBy), new Sort<Library>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -304,12 +304,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Library> resources = await _libraryManager.GetLibraries( ICollection<Library> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Library>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Library>(sortBy), new Sort<Library>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -330,12 +330,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.ID == showID)), ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.ID == showID)),
new Sort<Collection>(sortBy), new Sort<Collection>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(showID) == null) if (!resources.Any() && await _libraryManager.Get<Show>(showID) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -356,12 +356,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetCollections( ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.Slug == slug)), ApiHelper.ParseWhere<Collection>(where, x => x.Shows.Any(y => y.Slug == slug)),
new Sort<Collection>(sortBy), new Sort<Collection>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetShow(slug) == null) if (!resources.Any() && await _libraryManager.Get<Show>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -376,55 +376,80 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug) public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug)
{ {
Show show = await _libraryManager.GetShow(slug); try
if (show == null) {
return NotFound(); Show show = await _libraryManager.Get<Show>(slug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments"); string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments");
return (await _files.ListFiles(path)) return (await _files.ListFiles(path))
.ToDictionary(Path.GetFileNameWithoutExtension, .ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}"); x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}");
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{showSlug}/font/{slug}")] [HttpGet("{showSlug}/font/{slug}")]
[HttpGet("{showSlug}/fonts/{slug}")] [HttpGet("{showSlug}/fonts/{slug}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<IActionResult> GetFont(string showSlug, string slug) public async Task<IActionResult> GetFont(string showSlug, string slug)
{ {
Show show = await _libraryManager.GetShow(showSlug); try
if (show == null) {
return NotFound(); Show show = await _libraryManager.Get<Show>(showSlug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug);
return _files.FileResult(path); return _files.FileResult(path);
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<IActionResult> GetPoster(string slug) public async Task<IActionResult> GetPoster(string slug)
{ {
Show show = await _libraryManager.GetShow(slug); try
if (show == null) {
return NotFound(); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowPoster(show)); return _files.FileResult(await _thumbs.GetShowPoster(show));
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{slug}/logo")] [HttpGet("{slug}/logo")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(string slug) public async Task<IActionResult> GetLogo(string slug)
{ {
Show show = await _libraryManager.GetShow(slug); try
if (show == null) {
return NotFound(); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowLogo(show)); return _files.FileResult(await _thumbs.GetShowLogo(show));
} }
catch (ItemNotFound)
{
return NotFound();
}
}
[HttpGet("{slug}/backdrop")] [HttpGet("{slug}/backdrop")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetBackdrop(string slug) public async Task<IActionResult> GetBackdrop(string slug)
{ {
Show show = await _libraryManager.GetShow(slug); try
if (show == null) {
return NotFound(); Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowBackdrop(show)); return _files.FileResult(await _thumbs.GetShowBackdrop(show));
} }
catch (ItemNotFound)
{
return NotFound();
}
}
} }
} }

View File

@ -35,12 +35,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.StudioID == id), ApiHelper.ParseWhere<Show>(where, x => x.StudioID == id),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetStudio(id) == null) if (!resources.Any() && await _libraryManager.Get<Studio>(id) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
@ -61,12 +61,12 @@ namespace Kyoo.Api
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetShows( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Studio.Slug == slug), ApiHelper.ParseWhere<Show>(where, x => x.Studio.Slug == slug),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetStudio(slug) == null) if (!resources.Any() && await _libraryManager.Get<Studio>(slug) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }

View File

@ -30,14 +30,14 @@ namespace Kyoo.Api
Track subtitle; Track subtitle;
try try
{ {
subtitle = await _libraryManager.GetTrack(slug, StreamType.Subtitle); subtitle = await _libraryManager.GetOrDefault(slug, StreamType.Subtitle);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new {error = ex.Message}); return BadRequest(new {error = ex.Message});
} }
if (subtitle == null || subtitle.Type != StreamType.Subtitle) if (subtitle is not {Type: StreamType.Subtitle})
return NotFound(); return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt") if (subtitle.Codec == "subrip" && extension == "vtt")

View File

@ -29,7 +29,7 @@ namespace Kyoo.Api
{ {
try try
{ {
return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.ID == id)); return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.ID == id));
} }
catch (ItemNotFound) catch (ItemNotFound)
{ {
@ -45,7 +45,7 @@ namespace Kyoo.Api
{ {
// TODO This won't work with the local repository implementation. // TODO This won't work with the local repository implementation.
// TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor // TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor
return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.Slug == slug)); return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.Slug == slug));
} }
catch (ItemNotFound) catch (ItemNotFound)
{ {

View File

@ -4,6 +4,7 @@ using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
@ -41,91 +42,58 @@ namespace Kyoo.Api
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("{slug}")]
[HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("direct/{slug}")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> DirectEpisode(string showSlug, int seasonNumber, int episodeNumber) public async Task<IActionResult> Direct(string slug)
{ {
if (seasonNumber < 0 || episodeNumber < 0) try
return BadRequest(new {error = "Season number or episode number can not be negative."}); {
Episode episode = await _libraryManager.Get<Episode>(slug);
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
return _files.FileResult(episode.Path, true); return _files.FileResult(episode.Path, true);
} }
catch (ItemNotFound)
[HttpGet("{movieSlug}")]
[HttpGet("direct/{movieSlug}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> DirectMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
return NotFound(); return NotFound();
return _files.FileResult(episode.Path, true); }
} }
[HttpGet("transmux/{slug}/master.m3u8")]
[HttpGet("transmux/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> TransmuxEpisode(string showSlug, int seasonNumber, int episodeNumber) public async Task<IActionResult> Transmux(string slug)
{ {
if (seasonNumber < 0 || episodeNumber < 0) try
return BadRequest(new {error = "Season number or episode number can not be negative."}); {
Episode episode = await _libraryManager.Get<Episode>(slug);
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
string path = await _transcoder.Transmux(episode); string path = await _transcoder.Transmux(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return _files.FileResult(path, true); return _files.FileResult(path, true);
} }
catch (ItemNotFound)
[HttpGet("transmux/{movieSlug}/master.m3u8")]
[Authorize(Policy="Play")]
public async Task<IActionResult> TransmuxMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transmux(episode); }
if (path == null)
return StatusCode(500);
return _files.FileResult(path, true);
} }
[HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")] [HttpGet("transcode/{slug}/master.m3u8")]
[Authorize(Policy="Play")] [Authorize(Policy="Play")]
public async Task<IActionResult> TranscodeEpisode(string showSlug, int seasonNumber, int episodeNumber) public async Task<IActionResult> Transcode(string slug)
{ {
if (seasonNumber < 0 || episodeNumber < 0) try
return BadRequest(new {error = "Season number or episode number can not be negative."}); {
Episode episode = await _libraryManager.Get<Episode>(slug);
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
string path = await _transcoder.Transcode(episode); string path = await _transcoder.Transcode(episode);
if (path == null) if (path == null)
return StatusCode(500); return StatusCode(500);
return _files.FileResult(path, true); return _files.FileResult(path, true);
} }
catch (ItemNotFound)
[HttpGet("transcode/{movieSlug}/master.m3u8")]
[Authorize(Policy="Play")]
public async Task<IActionResult> TranscodeMovie(string movieSlug)
{ {
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
return NotFound(); return NotFound();
string path = await _transcoder.Transcode(episode); }
if (path == null)
return StatusCode(500);
return _files.FileResult(path, true);
} }

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -17,24 +18,19 @@ namespace Kyoo.Api
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] [HttpGet("{slug}")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<ActionResult<WatchItem>> GetWatchItem(string showSlug, int seasonNumber, int episodeNumber) public async Task<ActionResult<WatchItem>> GetWatchItem(string slug)
{ {
Episode item = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); try
if (item == null) {
return NotFound(); Episode item = await _libraryManager.Get<Episode>(slug);
return await WatchItem.FromEpisode(item, _libraryManager); return await WatchItem.FromEpisode(item, _libraryManager);
} }
catch (ItemNotFound)
[HttpGet("{movieSlug}")]
[Authorize(Policy="Read")]
public async Task<ActionResult<WatchItem>> GetWatchItem(string movieSlug)
{ {
Episode item = await _libraryManager.GetMovieEpisode(movieSlug);
if (item == null)
return NotFound(); return NotFound();
return await WatchItem.FromEpisode(item, _libraryManager); }
} }
} }
} }

View File

@ -1,31 +1,25 @@
{ {
"server.urls": "http://0.0.0.0:5000", "server.urls": "http://*:5000",
"public_url": "http://localhost:5000/", "public_url": "http://localhost:5000/",
"http_port": 5000,
"https_port": 44300,
"Database": { "database": {
"Server": "127.0.0.1", "server": "127.0.0.1",
"Port": "5432", "port": "5432",
"Database": "kyooDB", "database": "kyooDB",
"User Id": "kyoo", "user ID": "kyoo",
"Password": "kyooPassword", "password": "kyooPassword",
"Pooling": "true", "pooling": "true",
"MaxPoolSize": "95", "maxPoolSize": "95",
"Timeout": "30" "timeout": "30"
}, },
"Logging": { "logging": {
"LogLevel": { "logLevel": {
"Default": "Warning", "default": "Warning",
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information", "Microsoft.Hosting.Lifetime": "Information"
"Microsoft.EntityFrameworkCore.DbUpdateException": "None",
"Microsoft.EntityFrameworkCore.Update": "None",
"Microsoft.EntityFrameworkCore.Database.Command": "None"
} }
}, },
"AllowedHosts": "*",
"parallelTasks": "1", "parallelTasks": "1",

View File

@ -5,7 +5,8 @@ After=network.target
[Service] [Service]
User=kyoo User=kyoo
ExecStart=/usr/lib/kyoo/Kyoo /var/lib/kyoo WorkingDirectory=/var/lib/kyoo
ExecStart=/usr/lib/kyoo/Kyoo
Restart=on-abort Restart=on-abort
TimeoutSec=20 TimeoutSec=20