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.
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)
- [ ] New public API added
- [ ] Non-breaking changes

View File

@ -34,11 +34,18 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: bash
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 \
-k:"AnonymusRaccoon_Kyoo" \
-o:"anonymus-raccoon" \
-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'

View File

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

View File

@ -5,284 +5,532 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
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; }
/// <summary>
/// The repository that handle libraries's items (a wrapper arround shows & collections).
/// </summary>
ILibraryItemRepository LibraryItemRepository { get; }
/// <summary>
/// The repository that handle collections.
/// </summary>
ICollectionRepository CollectionRepository { get; }
/// <summary>
/// The repository that handle shows.
/// </summary>
IShowRepository ShowRepository { get; }
/// <summary>
/// The repository that handle seasons.
/// </summary>
ISeasonRepository SeasonRepository { get; }
/// <summary>
/// The repository that handle episodes.
/// </summary>
IEpisodeRepository EpisodeRepository { get; }
/// <summary>
/// The repository that handle tracks.
/// </summary>
ITrackRepository TrackRepository { get; }
/// <summary>
/// The repository that handle people.
/// </summary>
IPeopleRepository PeopleRepository { get; }
/// <summary>
/// The repository that handle studios.
/// </summary>
IStudioRepository StudioRepository { get; }
/// <summary>
/// The repository that handle genres.
/// </summary>
IGenreRepository GenreRepository { get; }
/// <summary>
/// The repository that handle providers.
/// </summary>
IProviderRepository ProviderRepository { get; }
// Get by id
Task<Library> GetLibrary(int id);
Task<Collection> GetCollection(int id);
Task<Show> GetShow(int id);
Task<Season> GetSeason(int id);
Task<Season> GetSeason(int showID, int seasonNumber);
Task<Episode> GetEpisode(int id);
Task<Episode> GetEpisode(int showID, int seasonNumber, int episodeNumber);
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);
/// <summary>
/// Get the resource by it's ID
/// </summary>
/// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource found</returns>
Task<T> Get<T>(int id) where T : class, IResource;
// Get by slug
Task<Library> GetLibrary(string slug);
Task<Collection> GetCollection(string slug);
Task<Show> GetShow(string slug);
Task<Season> GetSeason(string slug);
Task<Season> GetSeason(string showSlug, int seasonNumber);
Task<Episode> GetEpisode(string slug);
Task<Episode> GetEpisode(string showSlug, int seasonNumber, int episodeNumber);
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);
/// <summary>
/// Get the resource by it's slug
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource found</returns>
Task<T> Get<T>(string slug) where T : class, IResource;
// Get by predicate
Task<Library> GetLibrary(Expression<Func<Library, bool>> where);
Task<Collection> GetCollection(Expression<Func<Collection, bool>> where);
Task<Show> GetShow(Expression<Func<Show, bool>> where);
Task<Season> GetSeason(Expression<Func<Season, bool>> where);
Task<Episode> GetEpisode(Expression<Func<Episode, bool>> where);
Task<Track> GetTrack(Expression<Func<Track, bool>> where);
Task<Genre> GetGenre(Expression<Func<Genre, bool>> where);
Task<Studio> GetStudio(Expression<Func<Studio, bool>> where);
Task<People> GetPerson(Expression<Func<People, bool>> where);
/// <summary>
/// Get the resource by a filter function.
/// </summary>
/// <param name="where">The filter function.</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The first resource found that match the where function</returns>
Task<T> Get<T>(Expression<Func<T, bool>> where) where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="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)
where T : class, IResource
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)
where T : class, IResource
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)
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);
// 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,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = 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,
[Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort,
Pagination limit = default
) => 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,
Sort<LibraryItem> sort = 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,
Expression<Func<LibraryItem, object>> sort,
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,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = 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,
[Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort,
Pagination limit = default
) => 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,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
Pagination limit = default);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">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,
[Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort,
Pagination limit = default
) => 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,
Sort<PeopleRole> sort = 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,
Expression<Func<PeopleRole, object>> sort,
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,
Sort<PeopleRole> sort = 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,
Expression<Func<PeopleRole, object>> sort,
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);
/// <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);
// Get all
Task<ICollection<Library>> GetLibraries(Expression<Func<Library, bool>> where = null,
Sort<Library> sort = default,
Pagination limit = default);
Task<ICollection<Collection>> GetCollections(Expression<Func<Collection, bool>> where = null,
Sort<Collection> sort = default,
Pagination limit = default);
Task<ICollection<Show>> GetShows(Expression<Func<Show, bool>> where = null,
Sort<Show> sort = default,
Pagination limit = default);
Task<ICollection<Season>> GetSeasons(Expression<Func<Season, bool>> where = null,
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);
/// <summary>
/// Get all resources with filters
/// </summary>
/// <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>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default) where T : class, IResource;
Task<ICollection<Library>> GetLibraries([Optional] Expression<Func<Library, bool>> where,
Expression<Func<Library, object>> sort,
Pagination limit = default
) => GetLibraries(where, new Sort<Library>(sort), limit);
Task<ICollection<Collection>> GetCollections([Optional] Expression<Func<Collection, bool>> where,
Expression<Func<Collection, object>> sort,
Pagination limit = default
) => GetCollections(where, new Sort<Collection>(sort), limit);
Task<ICollection<Show>> GetShows([Optional] Expression<Func<Show, bool>> where,
Expression<Func<Show, object>> sort,
Pagination limit = default
) => GetShows(where, new Sort<Show>(sort), limit);
Task<ICollection<Track>> GetTracks([Optional] Expression<Func<Track, bool>> where,
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 all resources with filters
/// </summary>
/// <param name="where">A filter function</param>
/// <param name="sort">A sort by function</param>
/// <param name="limit">How many items to return and where to start</param>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>([Optional] Expression<Func<T, bool>> where,
Expression<Func<T, object>> sort,
Pagination limit = default) where T : class, IResource
{
return GetAll(where, new Sort<T>(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
Task<int> GetLibrariesCount(Expression<Func<Library, bool>> where = null);
Task<int> GetCollectionsCount(Expression<Func<Collection, bool>> where = null);
Task<int> GetShowsCount(Expression<Func<Show, bool>> where = null);
Task<int> GetSeasonsCount(Expression<Func<Season, bool>> where = null);
Task<int> GetEpisodesCount(Expression<Func<Episode, bool>> where = null);
Task<int> GetTracksCount(Expression<Func<Track, bool>> where = null);
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);
/// <summary>
/// Search for a resource
/// </summary>
/// <param name="query">The search query</param>
/// <typeparam name="T">The type of resources</typeparam>
/// <returns>A list of 20 items that match the search query</returns>
Task<ICollection<T>> Search<T>(string query) where T : class, IResource;
// Search
Task<ICollection<Library>> SearchLibraries(string searchQuery);
Task<ICollection<Collection>> SearchCollections(string searchQuery);
Task<ICollection<Show>> SearchShows(string searchQuery);
Task<ICollection<Season>> SearchSeasons(string searchQuery);
Task<ICollection<Episode>> SearchEpisodes(string searchQuery);
Task<ICollection<Genre>> SearchGenres(string searchQuery);
Task<ICollection<Studio>> SearchStudios(string searchQuery);
Task<ICollection<People>> SearchPeople(string searchQuery);
/// <summary>
/// Create a new resource.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
Task<T> Create<T>([NotNull] T item) where T : class, IResource;
//Register values
Task<Library> RegisterLibrary(Library library);
Task<Collection> RegisterCollection(Collection collection);
Task<Show> RegisterShow(Show show);
Task<Season> RegisterSeason(Season season);
Task<Episode> RegisterEpisode(Episode episode);
Task<Track> RegisterTrack(Track track);
Task<Genre> RegisterGenre(Genre genre);
Task<Studio> RegisterStudio(Studio studio);
Task<People> RegisterPeople(People people);
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists<T>([NotNull] T item) where T : class, IResource;
// Edit values
Task<Library> EditLibrary(Library library, bool resetOld);
Task<Collection> EditCollection(Collection collection, bool resetOld);
Task<Show> EditShow(Show show, bool resetOld);
Task<Season> EditSeason(Season season, bool resetOld);
Task<Episode> EditEpisode(Episode episode, bool resetOld);
Task<Track> EditTrack(Track track, bool resetOld);
Task<Genre> EditGenre(Genre genre, bool resetOld);
Task<Studio> EditStudio(Studio studio, bool resetOld);
Task<People> EditPeople(People people, bool resetOld);
/// <summary>
/// Edit a resource
/// </summary>
/// <param name="item">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>
/// <typeparam name="T">The type of resources</typeparam>
/// <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<T>(T item, bool resetOld) where T : class, IResource;
// Delete values
Task DeleteLibrary(Library library);
Task DeleteCollection(Collection collection);
Task DeleteShow(Show show);
Task DeleteSeason(Season season);
Task DeleteEpisode(Episode episode);
Task DeleteTrack(Track track);
Task DeleteGenre(Genre genre);
Task DeleteStudio(Studio studio);
Task DeletePeople(People people);
/// <summary>
/// Delete a resource.
/// </summary>
/// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(T item) where T : class, IResource;
//Delete by slug
Task DeleteLibrary(string slug);
Task DeleteCollection(string slug);
Task DeleteShow(string slug);
Task DeleteSeason(string slug);
Task DeleteEpisode(string slug);
Task DeleteTrack(string slug);
Task DeleteGenre(string slug);
Task DeleteStudio(string slug);
Task DeletePeople(string slug);
/// <summary>
/// Delete a resource by it's ID.
/// </summary>
/// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(int id) where T : class, IResource;
//Delete by id
Task DeleteLibrary(int id);
Task DeleteCollection(int id);
Task DeleteShow(int id);
Task DeleteSeason(int id);
Task DeleteEpisode(int id);
Task DeleteTrack(int id);
Task DeleteGenre(int id);
Task DeleteStudio(int id);
Task DeletePeople(int id);
/// <summary>
/// Delete a resource by it's slug.
/// </summary>
/// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(string slug) where T : class, IResource;
}
}

View File

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

View File

@ -6,28 +6,64 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
{
/// <summary>
/// Informations about the pagination. How many items should be displayed and where to start.
/// </summary>
public readonly struct Pagination
{
/// <summary>
/// The count of items to return.
/// </summary>
public int Count { get; }
/// <summary>
/// Where to start? Using the given sort
/// </summary>
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)
{
Count = count;
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 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;
public bool Descendant;
/// <summary>
/// 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)
{
Key = key;
@ -37,6 +73,11 @@ namespace Kyoo.Controllers
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)
{
if (string.IsNullOrEmpty(sortBy))
@ -46,8 +87,8 @@ namespace Kyoo.Controllers
return;
}
string key = sortBy.Contains(':') ? sortBy.Substring(0, sortBy.IndexOf(':')) : sortBy;
string order = sortBy.Contains(':') ? sortBy.Substring(sortBy.IndexOf(':') + 1) : null;
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
string order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
ParameterExpression param = Expression.Parameter(typeof(T), "x");
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);
/// <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);
/// <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);
/// <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);
/// <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,
Sort<T> sort = 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,
Expression<Func<T, object>> sort,
Pagination limit = default
) => 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);
/// <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);
/// <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);
/// <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);
/// <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);
/// <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);
/// <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);
/// <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());
/// <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);
/// <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());
/// <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);
/// <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());
/// <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);
/// <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);
}
/// <summary>
/// A repository to handle shows.
/// </summary>
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);
/// <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);
}
/// <summary>
/// A repository to handle seasons.
/// </summary>
public interface ISeasonRepository : IRepository<Season>
{
/// <summary>
/// Get a season from it's showID and it's seasonNumber
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="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);
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>
{
/// <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);
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);
/// <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 Delete(string showSlug, int seasonNumber, int episodeNumber);
}
/// <summary>
/// A repository to handle tracks
/// </summary>
public interface ITrackRepository : IRepository<Track>
{
/// <summary>
/// Get a track from it's slug and it's type.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns>
Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
}
/// <summary>
/// A repository to handle libraries.
/// </summary>
public interface ILibraryRepository : IRepository<Library> { }
/// <summary>
/// A repository to handle library items (A wrapper arround shows and collections).
/// </summary>
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,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = 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,
[Optional] Expression<Func<LibraryItem, bool>> where,
Expression<Func<LibraryItem, object>> sort,
Pagination limit = default
) => 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,
Sort<LibraryItem> sort = default,
Pagination limit = default);
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">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,
Expression<Func<LibraryItem, object>> sort,
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> { }
/// <summary>
/// A repository for genres.
/// </summary>
public interface IGenreRepository : IRepository<Genre> { }
/// <summary>
/// A repository for studios.
/// </summary>
public interface IStudioRepository : IRepository<Studio> { }
/// <summary>
/// A repository for people.
/// </summary>
public interface IPeopleRepository : IRepository<People>
{
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort 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,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = 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,
[Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort,
Pagination limit = default
) => 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,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
Pagination limit = default);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">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,
[Optional] Expression<Func<PeopleRole, bool>> where,
Expression<Func<PeopleRole, object>> sort,
Pagination limit = default
) => 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,
Sort<PeopleRole> sort = 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,
Expression<Func<PeopleRole, object>> sort,
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,
Sort<PeopleRole> sort = 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,
Expression<Func<PeopleRole, object>> sort,
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,
Sort<MetadataID> sort = 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,
Expression<Func<MetadataID, object>> sort,
Pagination limit = default

View File

@ -11,7 +11,7 @@ namespace Kyoo.Controllers
Task Validate(Season season, bool alwaysDownload = false);
Task Validate(Episode episode, 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> GetShowLogo([NotNull] Show show);
@ -19,6 +19,6 @@ namespace Kyoo.Controllers
Task<string> GetSeasonPoster([NotNull] Season season);
Task<string> GetEpisodeThumb([NotNull] Episode episode);
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.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
namespace Kyoo.Controllers
{
public class LibraryManager : ILibraryManager
{
/// <summary>
/// The list of repositories
/// </summary>
private readonly IBaseRepository[] _repositories;
/// <inheritdoc />
public ILibraryRepository LibraryRepository { get; }
/// <inheritdoc />
public ILibraryItemRepository LibraryItemRepository { get; }
/// <inheritdoc />
public ICollectionRepository CollectionRepository { get; }
/// <inheritdoc />
public IShowRepository ShowRepository { get; }
/// <inheritdoc />
public ISeasonRepository SeasonRepository { get; }
/// <inheritdoc />
public IEpisodeRepository EpisodeRepository { get; }
/// <inheritdoc />
public ITrackRepository TrackRepository { get; }
public IGenreRepository GenreRepository { get; }
public IStudioRepository StudioRepository { get; }
/// <inheritdoc />
public IPeopleRepository PeopleRepository { get; }
/// <inheritdoc />
public IStudioRepository StudioRepository { get; }
/// <inheritdoc />
public IGenreRepository GenreRepository { get; }
/// <inheritdoc />
public IProviderRepository ProviderRepository { get; }
public LibraryManager(ILibraryRepository libraryRepository,
ILibraryItemRepository libraryItemRepository,
ICollectionRepository collectionRepository,
IShowRepository showRepository,
ISeasonRepository seasonRepository,
IEpisodeRepository episodeRepository,
ITrackRepository trackRepository,
IGenreRepository genreRepository,
IStudioRepository studioRepository,
IProviderRepository providerRepository,
IPeopleRepository peopleRepository)
/// <summary>
/// Create a new <see cref="LibraryManager"/> instancce with every repository available.
/// </summary>
/// <param name="repositories">The list of repositories that this library manager should manage.
/// If a repository for every base type is not available, this instance won't be stable.</param>
public LibraryManager(IEnumerable<IBaseRepository> repositories)
{
LibraryRepository = libraryRepository;
LibraryItemRepository = libraryItemRepository;
CollectionRepository = collectionRepository;
ShowRepository = showRepository;
SeasonRepository = seasonRepository;
EpisodeRepository = episodeRepository;
TrackRepository = trackRepository;
GenreRepository = genreRepository;
StudioRepository = studioRepository;
ProviderRepository = providerRepository;
PeopleRepository = peopleRepository;
_repositories = repositories.ToArray();
LibraryRepository = GetRepository<Library>() as ILibraryRepository;
LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
ShowRepository = GetRepository<Show>() as IShowRepository;
SeasonRepository = GetRepository<Season>() as ISeasonRepository;
EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
TrackRepository = GetRepository<Track>() as ITrackRepository;
PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository;
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();
CollectionRepository.Dispose();
ShowRepository.Dispose();
SeasonRepository.Dispose();
EpisodeRepository.Dispose();
TrackRepository.Dispose();
GenreRepository.Dispose();
StudioRepository.Dispose();
PeopleRepository.Dispose();
ProviderRepository.Dispose();
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
return ret;
throw new ItemNotFound();
}
public async ValueTask DisposeAsync()
/// <inheritdoc />
public Task<T> Get<T>(int id)
where T : class, IResource
{
await Task.WhenAll(
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()
);
return GetRepository<T>().Get(id);
}
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)
{
return ShowRepository.Get(id);
}
public Task<Season> GetSeason(int id)
{
return SeasonRepository.Get(id);
}
public Task<Season> GetSeason(int showID, int seasonNumber)
/// <inheritdoc />
public Task<Season> Get(int showID, int seasonNumber)
{
return SeasonRepository.Get(showID, seasonNumber);
}
public Task<Episode> GetEpisode(int id)
{
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)
/// <inheritdoc />
public Task<Season> Get(string showSlug, int 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);
}
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);
}
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);
return await TrackRepository.GetOrDefault(slug, type);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new()
@ -255,6 +181,7 @@ namespace Kyoo.Controllers
return Load(obj, Utility.GetPropertyName(member));
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new()
@ -264,14 +191,24 @@ namespace Kyoo.Controllers
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
{
await Load(obj as IResource, member);
await Load(obj as IResource, memberName);
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,
Action<T1, ICollection<T2>> setter,
Action<T2, T1> inverse)
@ -282,12 +219,13 @@ namespace Kyoo.Controllers
inverse(item, obj);
}
public Task Load(IResource obj, string member)
/// <inheritdoc />
public Task Load(IResource obj, string memberName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return (obj, member) switch
return (obj, member: memberName) switch
{
(Library l, nameof(Library.Providers)) => ProviderRepository
.GetAll(x => x.Libraries.Any(y => y.ID == obj.ID))
@ -343,7 +281,7 @@ namespace Kyoo.Controllers
.Then(x => s.Collections = x),
(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 =>
{
s.Studio = x;
@ -362,7 +300,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Season = y; x.SeasonID = y.ID; }),
(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 =>
{
s.Show = x;
@ -381,7 +319,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(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 =>
{
e.Show = x;
@ -389,7 +327,7 @@ namespace Kyoo.Controllers
}),
(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 =>
{
e.Season = x;
@ -398,7 +336,7 @@ namespace Kyoo.Controllers
(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 =>
{
t.Episode = x;
@ -426,85 +364,16 @@ namespace Kyoo.Controllers
.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))
.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,
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);
}
/// <inheritdoc />
public Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
@ -513,14 +382,16 @@ namespace Kyoo.Controllers
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,
Sort<LibraryItem> sort = 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,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
@ -529,6 +400,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromShow(showID, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
@ -537,6 +409,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromShow(showSlug, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
@ -545,6 +418,7 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromPeople(id, where, sort, limit);
}
/// <inheritdoc />
public Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
Expression<Func<PeopleRole, bool>> where = null,
Sort<PeopleRole> sort = default,
@ -553,326 +427,83 @@ namespace Kyoo.Controllers
return PeopleRepository.GetFromPeople(slug, where, sort, limit);
}
public Task<int> GetLibrariesCount(Expression<Func<Library, bool>> where = null)
{
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);
}
/// <inheritdoc />
public Task AddShowLink(int showID, int? libraryID, int? collectionID)
{
return ShowRepository.AddShowLink(showID, libraryID, collectionID);
}
/// <inheritdoc />
public Task AddShowLink(Show show, Library library, Collection collection)
{
if (show == null)
throw new ArgumentNullException(nameof(show));
return 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);
}
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);
return GetRepository<T>().Delete(slug);
}
}
}

View File

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

View File

@ -11,20 +11,20 @@ namespace Kyoo.Models
public string Name { 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<Collection> Collections { get; set; }
#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, Collection>> CollectionLinks { get; set; }
#endif
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;
Name = name;

View File

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

View File

@ -109,18 +109,18 @@ namespace Kyoo.Models
if (!ep.Show.IsMovie)
{
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)
{
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);
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))
next = await library.GetEpisode(ep.ShowID, ep.SeasonNumber + 1, 1);
if (ep.EpisodeNumber >= await library.GetCount<Episode>(x => x.SeasonID == ep.SeasonID))
next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber + 1, 1);
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,

View File

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

View File

@ -12,52 +12,99 @@ using Microsoft.EntityFrameworkCore;
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>
where T : class, IResource
{
/// <summary>
/// The Entity Framework's Database handle.
/// </summary>
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; }
/// <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)
{
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();
GC.SuppressFinalize(this);
T ret = await Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
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);
}
public virtual Task<T> GetWithTracking(int id)
{
return Database.Set<T>().AsTracking().FirstOrDefaultAsync(x => x.ID == id);
}
public virtual Task<T> Get(string slug)
/// <inheritdoc />
public virtual Task<T> GetOrDefault(string 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);
/// <inheritdoc/>
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default)
@ -65,6 +112,14 @@ namespace Kyoo.Controllers
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,
Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
@ -73,6 +128,17 @@ namespace Kyoo.Controllers
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,
Func<int, Task<TValue>> get,
Expression<Func<TValue, object>> defaultSort,
@ -108,6 +174,7 @@ namespace Kyoo.Controllers
return await query.ToListAsync();
}
/// <inheritdoc/>
public virtual Task<int> GetCount(Expression<Func<T, bool>> where = null)
{
IQueryable<T> query = Database.Set<T>();
@ -116,6 +183,7 @@ namespace Kyoo.Controllers
return query.CountAsync();
}
/// <inheritdoc/>
public virtual async Task<T> Create(T obj)
{
if (obj == null)
@ -124,6 +192,7 @@ namespace Kyoo.Controllers
return obj;
}
/// <inheritdoc/>
public virtual async Task<T> CreateIfNotExists(T obj, bool silentFail = false)
{
try
@ -139,10 +208,7 @@ namespace Kyoo.Controllers
}
catch (DuplicatedItemException)
{
T old = await Get(obj!.Slug);
if (old == null)
throw new SystemException("Unknown database state.");
return old;
return await Get(obj.Slug);
}
catch
{
@ -152,6 +218,7 @@ namespace Kyoo.Controllers
}
}
/// <inheritdoc/>
public virtual async Task<T> Edit(T edited, bool resetOld)
{
if (edited == null)
@ -162,8 +229,6 @@ namespace Kyoo.Controllers
try
{
T old = await GetWithTracking(edited.ID);
if (old == null)
throw new ItemNotFound($"No resource found with the ID {edited.ID}.");
if (resetOld)
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)
{
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)
{
if (string.IsNullOrEmpty(resource.Slug))
@ -205,38 +283,45 @@ namespace Kyoo.Controllers
return Task.CompletedTask;
}
/// <inheritdoc/>
public virtual async Task Delete(int id)
{
T resource = await Get(id);
await Delete(resource);
}
/// <inheritdoc/>
public virtual async Task Delete(string slug)
{
T resource = await Get(slug);
await Delete(resource);
}
/// <inheritdoc/>
public abstract Task Delete(T obj);
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<T> objs)
{
foreach (T obj in objs)
await Delete(obj);
}
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<int> ids)
{
foreach (int id in ids)
await Delete(id);
}
/// <inheritdoc/>
public virtual async Task DeleteRange(IEnumerable<string> slugs)
{
foreach (string slug in slugs)
await Delete(slug);
}
/// <inheritdoc/>
public async Task DeleteRange(Expression<Func<T, bool>> where)
{
ICollection<T> resources = await GetAll(where);

View File

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Kyoo.CommonApi
{
@ -77,7 +76,7 @@ namespace Kyoo.CommonApi
if (result.DeclaredType == null)
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"];
Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>));

View File

@ -10,6 +10,10 @@
</PropertyGroup>
<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.NET.Test.Sdk" Version="16.7.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
{
/// <summary>
/// A local repository to handle collections
/// </summary>
public class CollectionRepository : LocalRepository<Collection>, ICollectionRepository
{
private bool _disposed;
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
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;
}
public override void Dispose()
{
if (_disposed)
return;
_disposed = true;
_database.Dispose();
GC.SuppressFinalize(this);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
return;
_disposed = true;
await _database.DisposeAsync();
}
/// <inheritdoc />
public override async Task<ICollection<Collection>> Search(string query)
{
return await _database.Collections
@ -45,6 +41,7 @@ namespace Kyoo.Controllers
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Collection> Create(Collection obj)
{
await base.Create(obj);
@ -53,6 +50,7 @@ namespace Kyoo.Controllers
return obj;
}
/// <inheritdoc />
public override async Task Delete(Collection obj)
{
if (obj == null)

View File

@ -5,20 +5,44 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
{
/// <summary>
/// A local repository to handle episodes.
/// </summary>
public class EpisodeRepository : LocalRepository<Episode>, IEpisodeRepository
{
private bool _disposed;
/// <summary>
/// The databse handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <summary>
/// A show repository to get show's slug from their ID and keep the slug in each episode.
/// </summary>
private readonly IShowRepository _shows;
/// <summary>
/// A track repository to handle creation and deletion of tracks related to the current episode.
/// </summary>
private readonly ITrackRepository _tracks;
/// <inheritdoc />
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,
IProviderRepository providers,
IShowRepository shows,
@ -32,60 +56,44 @@ namespace Kyoo.Controllers
}
public override void Dispose()
/// <inheritdoc />
public override async Task<Episode> GetOrDefault(int id)
{
if (_disposed)
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);
Episode ret = await base.GetOrDefault(id);
if (ret != null)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
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*)");
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["episode"].Value));
}
Episode episode = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug);
if (episode != null)
episode.ShowSlug = slug;
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)
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
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
&& x.SeasonNumber == seasonNumber
@ -95,7 +103,26 @@ namespace Kyoo.Controllers
return ret;
}
/// <inheritdoc />
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
&& x.SeasonNumber == seasonNumber
@ -105,15 +132,7 @@ namespace Kyoo.Controllers
return ret;
}
public async Task<Episode> Get(int seasonID, int episodeNumber)
{
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;
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
@ -123,6 +142,7 @@ namespace Kyoo.Controllers
return ret;
}
/// <inheritdoc />
public async Task<Episode> GetAbsolute(string showSlug, int absoluteNumber)
{
Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
@ -132,6 +152,7 @@ namespace Kyoo.Controllers
return ret;
}
/// <inheritdoc />
public override async Task<ICollection<Episode>> Search(string query)
{
List<Episode> episodes = await _database.Episodes
@ -144,6 +165,7 @@ namespace Kyoo.Controllers
return episodes;
}
/// <inheritdoc />
public override async Task<ICollection<Episode>> GetAll(Expression<Func<Episode, bool>> where = null,
Sort<Episode> sort = default,
Pagination limit = default)
@ -154,6 +176,7 @@ namespace Kyoo.Controllers
return episodes;
}
/// <inheritdoc />
public override async Task<Episode> Create(Episode obj)
{
await base.Create(obj);
@ -163,6 +186,7 @@ namespace Kyoo.Controllers
return await ValidateTracks(obj);
}
/// <inheritdoc />
protected override async Task EditRelations(Episode resource, Episode changed, bool resetOld)
{
if (resource.ShowID <= 0)
@ -184,6 +208,11 @@ namespace Kyoo.Controllers
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)
{
resource.Tracks = await resource.Tracks.MapAsync((x, i) =>
@ -198,6 +227,7 @@ namespace Kyoo.Controllers
return resource;
}
/// <inheritdoc />
protected override async Task Validate(Episode resource)
{
await base.Validate(resource);
@ -210,12 +240,7 @@ namespace Kyoo.Controllers
}).ToListAsync();
}
public async Task Delete(string showSlug, int seasonNumber, int episodeNumber)
{
Episode obj = await Get(showSlug, seasonNumber, episodeNumber);
await Delete(obj);
}
/// <inheritdoc />
public override async Task Delete(Episode obj)
{
if (obj == null)

View File

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

View File

@ -10,18 +10,45 @@ using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers
{
/// <summary>
/// A local repository to handle library items.
/// </summary>
public class LibraryItemRepository : LocalRepository<LibraryItem>, ILibraryItemRepository
{
private bool _disposed;
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
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;
/// <summary>
/// A lazy loaded show repository to get a show from it's id.
/// </summary>
private readonly Lazy<IShowRepository> _shows;
/// <summary>
/// A lazy loaded collection repository to get a collection from it's id.
/// </summary>
private readonly Lazy<ICollectionRepository> _collections;
/// <inheritdoc />
protected override Expression<Func<LibraryItem, object>> DefaultSort => x => x.Title;
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)
{
_database = database;
@ -31,45 +58,25 @@ namespace Kyoo.Controllers
_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()
{
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)
/// <inheritdoc />
public override async Task<LibraryItem> GetOrDefault(int id)
{
return id > 0
? new LibraryItem(await _shows.Value.Get(id))
: new LibraryItem(await _collections.Value.Get(-id));
? new LibraryItem(await _shows.Value.GetOrDefault(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
=> _database.Shows
.Where(x => !x.Collections.Any())
@ -77,6 +84,7 @@ namespace Kyoo.Controllers
.Concat(_database.Collections
.Select(LibraryItem.FromCollection));
/// <inheritdoc />
public override Task<ICollection<LibraryItem>> GetAll(Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
Pagination limit = default)
@ -84,6 +92,7 @@ namespace Kyoo.Controllers
return ApplyFilters(ItemsQuery, where, sort, limit);
}
/// <inheritdoc />
public override Task<int> GetCount(Expression<Func<LibraryItem, bool>> where = null)
{
IQueryable<LibraryItem> query = ItemsQuery;
@ -92,6 +101,7 @@ namespace Kyoo.Controllers
return query.CountAsync();
}
/// <inheritdoc />
public override async Task<ICollection<LibraryItem>> Search(string query)
{
return await ItemsQuery
@ -101,19 +111,31 @@ namespace Kyoo.Controllers
.ToListAsync();
}
/// <inheritdoc />
public override Task<LibraryItem> Create(LibraryItem obj) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task<LibraryItem> CreateIfNotExists(LibraryItem obj, bool silentFail = false)
{
if (silentFail)
return Task.FromResult<LibraryItem>(default);
throw new InvalidOperationException();
}
/// <inheritdoc />
public override Task<LibraryItem> Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(int id) => throw new InvalidOperationException();
/// <inheritdoc />
public override Task Delete(string slug) => throw new InvalidOperationException();
/// <inheritdoc />
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)
=> _database.Libraries
.Where(selector)
@ -125,6 +147,7 @@ namespace Kyoo.Controllers
.SelectMany(x => x.Collections)
.Select(LibraryItem.FromCollection));
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(int id,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
@ -134,11 +157,12 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!items.Any() && await _libraries.Value.Get(id) == null)
if (!items.Any() && await _libraries.Value.GetOrDefault(id) == null)
throw new ItemNotFound();
return items;
}
/// <inheritdoc />
public async Task<ICollection<LibraryItem>> GetFromLibrary(string slug,
Expression<Func<LibraryItem, bool>> where = null,
Sort<LibraryItem> sort = default,
@ -148,7 +172,7 @@ namespace Kyoo.Controllers
where,
sort,
limit);
if (!items.Any() && await _libraries.Value.Get(slug) == null)
if (!items.Any() && await _libraries.Value.GetOrDefault(slug) == null)
throw new ItemNotFound();
return items;
}

View File

@ -8,14 +8,29 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
{
/// <summary>
/// A local repository to handle libraries.
/// </summary>
public class LibraryRepository : LocalRepository<Library>, ILibraryRepository
{
private bool _disposed;
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <summary>
/// A provider repository to handle externalID creation and deletion
/// </summary>
private readonly IProviderRepository _providers;
/// <inheritdoc />
protected override 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)
: base(database)
{
@ -23,25 +38,8 @@ namespace Kyoo.Controllers
_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)
{
return await _database.Libraries
@ -51,6 +49,7 @@ namespace Kyoo.Controllers
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Library> Create(Library obj)
{
await base.Create(obj);
@ -61,6 +60,7 @@ namespace Kyoo.Controllers
return obj;
}
/// <inheritdoc />
protected override async Task Validate(Library resource)
{
await base.Validate(resource);
@ -69,6 +69,7 @@ namespace Kyoo.Controllers
.ToListAsync();
}
/// <inheritdoc />
protected override async Task EditRelations(Library resource, Library changed, bool resetOld)
{
if (string.IsNullOrEmpty(resource.Slug))
@ -86,6 +87,7 @@ namespace Kyoo.Controllers
}
}
/// <inheritdoc />
public override async Task Delete(Library obj)
{
if (obj == null)

View File

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

View File

@ -8,18 +8,32 @@ using Microsoft.EntityFrameworkCore;
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;
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;
}
public override async Task<ICollection<ProviderID>> Search(string query)
/// <inheritdoc />
public override async Task<ICollection<Provider>> Search(string query)
{
return await _database.Providers
.Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
@ -28,7 +42,8 @@ namespace Kyoo.Controllers
.ToListAsync();
}
public override async Task<ProviderID> Create(ProviderID obj)
/// <inheritdoc />
public override async Task<Provider> Create(Provider obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
@ -36,7 +51,8 @@ namespace Kyoo.Controllers
return obj;
}
public override async Task Delete(ProviderID obj)
/// <inheritdoc />
public override async Task Delete(Provider obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
@ -46,6 +62,7 @@ namespace Kyoo.Controllers
await _database.SaveChangesAsync();
}
/// <inheritdoc />
public Task<ICollection<MetadataID>> GetMetadataID(Expression<Func<MetadataID, bool>> where = null,
Sort<MetadataID> sort = default,
Pagination limit = default)

View File

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

View File

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

View File

@ -8,17 +8,31 @@ using Microsoft.EntityFrameworkCore;
namespace Kyoo.Controllers
{
/// <summary>
/// A local repository to handle studios
/// </summary>
public class StudioRepository : LocalRepository<Studio>, IStudioRepository
{
/// <summary>
/// The database handle
/// </summary>
private readonly DatabaseContext _database;
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override async Task<ICollection<Studio>> Search(string query)
{
return await _database.Studios
@ -28,6 +42,7 @@ namespace Kyoo.Controllers
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Studio> Create(Studio obj)
{
await base.Create(obj);
@ -36,6 +51,7 @@ namespace Kyoo.Controllers
return obj;
}
/// <inheritdoc />
public override async Task Delete(Studio obj)
{
if (obj == null)

View File

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

View File

@ -92,7 +92,7 @@ namespace Kyoo.Controllers
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)
return;
@ -145,7 +145,7 @@ namespace Kyoo.Controllers
return Task.FromResult(thumbPath.StartsWith(_peoplePath) ? thumbPath : null);
}
public Task<string> GetProviderLogo(ProviderID provider)
public Task<string> GetProviderLogo(Provider provider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));

View File

@ -113,10 +113,8 @@
</Target>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true'">
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'"
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 "NMake Makefiles" %26%26 nmake' />
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" 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" />
<Copy SourceFiles="$(TranscoderRoot)/build/$(TranscoderBinary)" DestinationFolder="." />
</Target>

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
@ -10,28 +11,71 @@ using Npgsql;
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 DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
}
/// <summary>
/// All libraries of Kyoo. See <see cref="Library"/>.
/// </summary>
public DbSet<Library> Libraries { get; set; }
/// <summary>
/// All collections of Kyoo. See <see cref="Collection"/>.
/// </summary>
public DbSet<Collection> Collections { get; set; }
/// <summary>
/// All shows of Kyoo. See <see cref="Show"/>.
/// </summary>
public DbSet<Show> Shows { get; set; }
/// <summary>
/// All seasons of Kyoo. See <see cref="Season"/>.
/// </summary>
public DbSet<Season> Seasons { get; set; }
/// <summary>
/// All episodes of Kyoo. See <see cref="Episode"/>.
/// </summary>
public DbSet<Episode> Episodes { get; set; }
/// <summary>
/// All tracks of Kyoo. See <see cref="Track"/>.
/// </summary>
public DbSet<Track> Tracks { get; set; }
/// <summary>
/// All genres of Kyoo. See <see cref="Genres"/>.
/// </summary>
public DbSet<Genre> Genres { get; set; }
/// <summary>
/// All people of Kyoo. See <see cref="People"/>.
/// </summary>
public DbSet<People> People { get; set; }
/// <summary>
/// All studios of Kyoo. See <see cref="Studio"/>.
/// </summary>
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; }
/// <summary>
/// All people's role. See <see cref="PeopleRole"/>.
/// </summary>
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>()
where T1 : 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()
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<Status>();
@ -50,6 +97,21 @@ namespace Kyoo
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)
{
base.OnModelCreating(modelBuilder);
@ -58,14 +120,6 @@ namespace Kyoo
modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<Library>()
.Property(x => x.Paths)
.HasColumnType("text[]");
modelBuilder.Entity<Show>()
.Property(x => x.Aliases)
.HasColumnType("text[]");
modelBuilder.Entity<Track>()
.Property(t => t.IsDefault)
.ValueGeneratedNever();
@ -74,17 +128,17 @@ namespace Kyoo
.Property(t => t.IsForced)
.ValueGeneratedNever();
modelBuilder.Entity<ProviderID>()
modelBuilder.Entity<Provider>()
.HasMany(x => x.Libraries)
.WithMany(x => x.Providers)
.UsingEntity<Link<Library, ProviderID>>(
.UsingEntity<Link<Library, Provider>>(
y => y
.HasOne(x => x.First)
.WithMany(x => x.ProviderLinks),
y => y
.HasOne(x => x.Second)
.WithMany(x => x.LibraryLinks),
y => y.HasKey(Link<Library, ProviderID>.PrimaryKey));
y => y.HasKey(Link<Library, Provider>.PrimaryKey));
modelBuilder.Entity<Collection>()
.HasMany(x => x.Libraries)
@ -160,7 +214,7 @@ namespace Kyoo
modelBuilder.Entity<Genre>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Library>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<People>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<ProviderID>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Provider>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Show>().Property(x => x.Slug).IsRequired();
modelBuilder.Entity<Studio>().Property(x => x.Slug).IsRequired();
@ -182,7 +236,7 @@ namespace Kyoo
modelBuilder.Entity<Studio>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<ProviderID>()
modelBuilder.Entity<Provider>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Season>()
@ -196,6 +250,13 @@ namespace Kyoo
.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)
where T : class, IResource
{
@ -206,6 +267,11 @@ namespace Kyoo
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()
{
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)
{
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)
{
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,
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())
{
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,
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())
{
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())
{
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,
int recurse,
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)
{
return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation};
}
/// <summary>
/// Delete every changes that are on this context.
/// </summary>
private void DiscardChanges()
{
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
{
[DbContext(typeof(DatabaseContext))]
[Migration("20210325184215_Initial")]
[Migration("20210420221509_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -179,7 +179,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
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")
.HasColumnType("integer");
@ -191,7 +191,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
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 =>
@ -320,7 +320,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.ToTable("PeopleRoles");
});
modelBuilder.Entity("Kyoo.Models.ProviderID", b =>
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
@ -563,7 +563,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
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")
.WithMany("ProviderLinks")
@ -571,7 +571,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Kyoo.Models.ProviderID", "Second")
b.HasOne("Kyoo.Models.Provider", "Second")
.WithMany("LibraryLinks")
.HasForeignKey("SecondID")
.OnDelete(DeleteBehavior.Cascade)
@ -632,7 +632,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
.HasForeignKey("PeopleID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Kyoo.Models.ProviderID", "Provider")
b.HasOne("Kyoo.Models.Provider", "Provider")
.WithMany("MetadataLinks")
.HasForeignKey("ProviderID")
.OnDelete(DeleteBehavior.Cascade)
@ -744,7 +744,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal
b.Navigation("Roles");
});
modelBuilder.Entity("Kyoo.Models.ProviderID", b =>
modelBuilder.Entity("Kyoo.Models.Provider", b =>
{
b.Navigation("LibraryLinks");

View File

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

View File

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

View File

@ -3,7 +3,11 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
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
{
@ -18,11 +22,8 @@ namespace Kyoo
/// <param name="args">Command line arguments</param>
public static async Task Main(string[] args)
{
if (args.Length > 0)
FileSystem.CurrentDirectory = args[0];
if (!File.Exists("./appsettings.json"))
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"), "appsettings.json");
if (!File.Exists("./settings.json"))
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "settings.json"), "settings.json");
bool? debug = Environment.GetEnvironmentVariable("ENVIRONMENT")?.ToLowerInvariant() switch
{
@ -49,15 +50,50 @@ namespace Kyoo
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>
/// Createa a web host
/// </summary>
/// <param name="args">Command line parameters that can be handled by kestrel</param>
/// <returns>A new web host instance</returns>
private static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel(config => { config.AddServerHeader = false; })
.UseUrls("http://*:5000")
private static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
WebHost.CreateDefaultBuilder(args);
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>();
}
}
}

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<ILibraryItemRepository, LibraryItemRepository>();
services.AddScoped<ICollectionRepository, CollectionRepository>();

View File

@ -32,8 +32,8 @@ namespace Kyoo.Controllers
public async Task<IEnumerable<string>> GetPossibleParameters()
{
using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
return (await libraryManager!.GetLibraries()).Select(x => x.Slug);
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
return (await libraryManager!.GetAll<Library>()).Select(x => x.Slug);
}
public int? Progress()
@ -56,25 +56,25 @@ namespace Kyoo.Controllers
_parallelTasks = 30;
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))
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)
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)
if (!File.Exists(track.Path))
await libraryManager.DeleteTrack(track);
await libraryManager.Delete(track);
ICollection<Library> libraries = argument == null
? await libraryManager.GetLibraries()
: new [] { await libraryManager.GetLibrary(argument)};
? await libraryManager.GetAll<Library>()
: new [] { await libraryManager.Get<Library>(argument)};
if (argument != null && libraries.First() == null)
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)
{
try
{
if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles"))
return;
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");
Regex regex = new(patern, RegexOptions.IgnoreCase);
@ -160,29 +162,31 @@ namespace Kyoo.Controllers
}
string episodePath = match.Groups["Episode"].Value;
Episode episode = await libraryManager!.GetEpisode(x => x.Path.StartsWith(episodePath));
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)
Episode episode = await libraryManager!.Get<Episode>(x => x.Path.StartsWith(episodePath));
Track track = new()
{
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
};
await libraryManager.RegisterTrack(track);
await libraryManager.Create(track);
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)
{
@ -192,7 +196,7 @@ namespace Kyoo.Controllers
try
{
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");
Regex regex = new(patern, RegexOptions.IgnoreCase);
@ -215,7 +219,7 @@ namespace Kyoo.Controllers
bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1;
Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
if (isMovie)
await libraryManager!.RegisterEpisode(await GetMovie(show, path));
await libraryManager!.Create(await GetMovie(show, path));
else
{
Season season = await GetSeason(libraryManager, show, seasonNumber, library);
@ -226,7 +230,7 @@ namespace Kyoo.Controllers
absoluteNumber,
path,
library);
await libraryManager!.RegisterEpisode(episode);
await libraryManager!.Create(episode);
}
await libraryManager.AddShowLink(show, library, collection);
@ -250,19 +254,19 @@ namespace Kyoo.Controllers
{
if (string.IsNullOrEmpty(collectionName))
return null;
Collection collection = await libraryManager.GetCollection(Utility.ToSlug(collectionName));
Collection collection = await libraryManager.Get<Collection>(Utility.ToSlug(collectionName));
if (collection != null)
return collection;
collection = await _metadataProvider.GetCollectionFromName(collectionName, library);
try
{
await libraryManager.RegisterCollection(collection);
await libraryManager.Create(collection);
return collection;
}
catch (DuplicatedItemException)
{
return await libraryManager.GetCollection(collection.Slug);
return await libraryManager.Get<Collection>(collection.Slug);
}
}
@ -272,7 +276,7 @@ namespace Kyoo.Controllers
bool isMovie,
Library library)
{
Show old = await libraryManager.GetShow(x => x.Path == showPath);
Show old = await libraryManager.Get<Show>(x => x.Path == showPath);
if (old != null)
{
await libraryManager.Load(old, x => x.ExternalIDs);
@ -284,18 +288,18 @@ namespace Kyoo.Controllers
try
{
show = await libraryManager.RegisterShow(show);
show = await libraryManager.Create(show);
}
catch (DuplicatedItemException)
{
old = await libraryManager.GetShow(show.Slug);
old = await libraryManager.Get<Show>(show.Slug);
if (old.Path == showPath)
{
await libraryManager.Load(old, x => x.ExternalIDs);
return old;
}
show.Slug += $"-{show.StartYear}";
await libraryManager.RegisterShow(show);
await libraryManager.Create(show);
}
await _thumbnailsManager.Validate(show);
return show;
@ -308,23 +312,21 @@ namespace Kyoo.Controllers
{
if (seasonNumber == -1)
return default;
Season season = await libraryManager.GetSeason(show.Slug, seasonNumber);
if (season == null)
{
season = await _metadataProvider.GetSeason(show, seasonNumber, library);
try
{
await libraryManager.RegisterSeason(season);
await _thumbnailsManager.Validate(season);
}
catch (DuplicatedItemException)
{
season = await libraryManager.GetSeason(show.Slug, season.SeasonNumber);
}
}
Season season = await libraryManager.Get(show.Slug, seasonNumber);
season.Show = show;
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,
Show show,

View File

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

View File

@ -35,12 +35,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(id) == null)
if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -61,12 +61,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Collection>(slug) == null)
return NotFound();
return Page(resources, limit);
}
@ -87,12 +87,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(id) == null)
if (!resources.Any() && await _libraryManager.Get<Collection>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -113,12 +113,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Library>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetCollection(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Collection>(slug) == null)
return NotFound();
return Page(resources, limit);
}

View File

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

View File

@ -36,12 +36,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetGenre(id) == null)
if (!resources.Any() && await _libraryManager.Get<Genre>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -62,12 +62,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetGenre(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Genre>(slug) == null)
return NotFound();
return Page(resources, limit);
}

View File

@ -47,12 +47,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -73,12 +73,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}
@ -99,12 +99,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -125,12 +125,12 @@ namespace Kyoo.Api
{
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)),
new Sort<Collection>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}
@ -156,7 +156,7 @@ namespace Kyoo.Api
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(id) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(id) == null)
return NotFound();
return Page(resources, limit);
}
@ -182,7 +182,7 @@ namespace Kyoo.Api
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null)
if (!resources.Any() && await _libraryManager.Get<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}

View File

@ -90,7 +90,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")]
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));
}
@ -98,7 +98,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")]
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));
}
}

View File

@ -11,7 +11,7 @@ namespace Kyoo.Api
[Route("api/provider")]
[Route("api/providers")]
[ApiController]
public class ProviderAPI : CrudApi<ProviderID>
public class ProviderAPI : CrudApi<Provider>
{
private readonly IThumbnailsManager _thumbnails;
private readonly ILibraryManager _libraryManager;
@ -32,7 +32,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")]
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));
}
@ -40,7 +40,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")]
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));
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Api
{
[Route("api/search")]
[Route("api/search/{query}")]
[ApiController]
public class SearchApi : ControllerBase
{
@ -18,67 +18,67 @@ namespace Kyoo.Api
_libraryManager = libraryManager;
}
[HttpGet("{query}")]
[HttpGet]
[Authorize(Policy="Read")]
public async Task<ActionResult<SearchResult>> Search(string query)
{
return new SearchResult
{
Query = query,
Collections = await _libraryManager.SearchCollections(query),
Shows = await _libraryManager.SearchShows(query),
Episodes = await _libraryManager.SearchEpisodes(query),
People = await _libraryManager.SearchPeople(query),
Genres = await _libraryManager.SearchGenres(query),
Studios = await _libraryManager.SearchStudios(query)
Collections = await _libraryManager.Search<Collection>(query),
Shows = await _libraryManager.Search<Show>(query),
Episodes = await _libraryManager.Search<Episode>(query),
People = await _libraryManager.Search<People>(query),
Genres = await _libraryManager.Search<Genre>(query),
Studios = await _libraryManager.Search<Studio>(query)
};
}
[HttpGet("{query}/collection")]
[HttpGet("{query}/collections")]
[HttpGet("collection")]
[HttpGet("collections")]
[Authorize(Policy="Read")]
public Task<ICollection<Collection>> SearchCollections(string query)
{
return _libraryManager.SearchCollections(query);
return _libraryManager.Search<Collection>(query);
}
[HttpGet("{query}/show")]
[HttpGet("{query}/shows")]
[HttpGet("show")]
[HttpGet("shows")]
[Authorize(Policy="Read")]
public Task<ICollection<Show>> SearchShows(string query)
{
return _libraryManager.SearchShows(query);
return _libraryManager.Search<Show>(query);
}
[HttpGet("{query}/episode")]
[HttpGet("{query}/episodes")]
[HttpGet("episode")]
[HttpGet("episodes")]
[Authorize(Policy="Read")]
public Task<ICollection<Episode>> SearchEpisodes(string query)
{
return _libraryManager.SearchEpisodes(query);
return _libraryManager.Search<Episode>(query);
}
[HttpGet("{query}/people")]
[HttpGet("people")]
[Authorize(Policy="Read")]
public Task<ICollection<People>> SearchPeople(string query)
{
return _libraryManager.SearchPeople(query);
return _libraryManager.Search<People>(query);
}
[HttpGet("{query}/genre")]
[HttpGet("{query}/genres")]
[HttpGet("genre")]
[HttpGet("genres")]
[Authorize(Policy="Read")]
public Task<ICollection<Genre>> SearchGenres(string query)
{
return _libraryManager.SearchGenres(query);
return _libraryManager.Search<Genre>(query);
}
[HttpGet("{query}/studio")]
[HttpGet("{query}/studios")]
[HttpGet("studio")]
[HttpGet("studios")]
[Authorize(Policy="Read")]
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
{
ICollection<Episode> resources = await _libraryManager.GetEpisodes(
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.SeasonID == seasonID),
new Sort<Episode>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetSeason(seasonID) == null)
if (!resources.Any() && await _libraryManager.Get<Season>(seasonID) == null)
return NotFound();
return Page(resources, limit);
}
@ -69,13 +69,13 @@ namespace Kyoo.Api
{
try
{
ICollection<Episode> resources = await _libraryManager.GetEpisodes(
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber),
new Sort<Episode>(sortBy),
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 Page(resources, limit);
}
@ -97,12 +97,12 @@ namespace Kyoo.Api
{
try
{
ICollection<Episode> resources = await _libraryManager.GetEpisodes(
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Episode>(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber),
new Sort<Episode>(sortBy),
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 Page(resources, limit);
}
@ -116,28 +116,28 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")]
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")]
[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")]
[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")]
[Authorize(Policy="Read")]
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);
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
}
@ -146,7 +146,7 @@ namespace Kyoo.Api
[Authorize(Policy="Read")]
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);
return _files.FileResult(await _thumbs.GetSeasonPoster(season));
}

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ namespace Kyoo.Api
{
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)
{
@ -45,7 +45,7 @@ namespace Kyoo.Api
{
// 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
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)
{

View File

@ -4,6 +4,7 @@ using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
@ -41,91 +42,58 @@ namespace Kyoo.Api
}
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
[HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
[HttpGet("{slug}")]
[HttpGet("direct/{slug}")]
[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)
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
try
{
Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(episode.Path, true);
}
[HttpGet("{movieSlug}")]
[HttpGet("direct/{movieSlug}")]
[Authorize(Policy="Play")]
public async Task<IActionResult> DirectMovie(string movieSlug)
catch (ItemNotFound)
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
return NotFound();
return _files.FileResult(episode.Path, true);
}
}
[HttpGet("transmux/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")]
[HttpGet("transmux/{slug}/master.m3u8")]
[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)
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
try
{
Episode episode = await _libraryManager.Get<Episode>(slug);
string path = await _transcoder.Transmux(episode);
if (path == null)
return StatusCode(500);
return _files.FileResult(path, true);
}
[HttpGet("transmux/{movieSlug}/master.m3u8")]
[Authorize(Policy="Play")]
public async Task<IActionResult> TransmuxMovie(string movieSlug)
catch (ItemNotFound)
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
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")]
public async Task<IActionResult> TranscodeEpisode(string showSlug, int seasonNumber, int episodeNumber)
public async Task<IActionResult> Transcode(string slug)
{
if (seasonNumber < 0 || episodeNumber < 0)
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
try
{
Episode episode = await _libraryManager.Get<Episode>(slug);
string path = await _transcoder.Transcode(episode);
if (path == null)
return StatusCode(500);
return _files.FileResult(path, true);
}
[HttpGet("transcode/{movieSlug}/master.m3u8")]
[Authorize(Policy="Play")]
public async Task<IActionResult> TranscodeMovie(string movieSlug)
catch (ItemNotFound)
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null)
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 Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -17,24 +18,19 @@ namespace Kyoo.Api
_libraryManager = libraryManager;
}
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
[HttpGet("{slug}")]
[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);
if (item == null)
return NotFound();
try
{
Episode item = await _libraryManager.Get<Episode>(slug);
return await WatchItem.FromEpisode(item, _libraryManager);
}
[HttpGet("{movieSlug}")]
[Authorize(Policy="Read")]
public async Task<ActionResult<WatchItem>> GetWatchItem(string movieSlug)
catch (ItemNotFound)
{
Episode item = await _libraryManager.GetMovieEpisode(movieSlug);
if (item == null)
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/",
"http_port": 5000,
"https_port": 44300,
"Database": {
"Server": "127.0.0.1",
"Port": "5432",
"Database": "kyooDB",
"User Id": "kyoo",
"Password": "kyooPassword",
"Pooling": "true",
"MaxPoolSize": "95",
"Timeout": "30"
"database": {
"server": "127.0.0.1",
"port": "5432",
"database": "kyooDB",
"user ID": "kyoo",
"password": "kyooPassword",
"pooling": "true",
"maxPoolSize": "95",
"timeout": "30"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"logging": {
"logLevel": {
"default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.DbUpdateException": "None",
"Microsoft.EntityFrameworkCore.Update": "None",
"Microsoft.EntityFrameworkCore.Database.Command": "None"
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"parallelTasks": "1",

View File

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