diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bccdb2b1..665f3f19 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 333672da..01ef1a22 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -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" + -k:"AnonymusRaccoon_Kyoo" \ + -o:"anonymus-raccoon" \ + -d:sonar.login="${{ secrets.SONAR_TOKEN }}" \ + -d:sonar.host.url="https://sonarcloud.io" \ + -d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" dotnet build --no-incremental '-p:SkipTranscoder=true;SkipWebApp=true' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5969ea76..bd06596b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index ae1d0ccb..d15987de 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -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 + /// + /// An interface to interract with the database. Every repository is mapped through here. + /// + public interface ILibraryManager { - // Repositories + /// + /// Get the repository corresponding to the T item. + /// + /// The type you want + /// If the item is not found + /// The repository corresponding + IRepository GetRepository() where T : class, IResource; + + /// + /// The repository that handle libraries. + /// ILibraryRepository LibraryRepository { get; } + + /// + /// The repository that handle libraries's items (a wrapper arround shows & collections). + /// ILibraryItemRepository LibraryItemRepository { get; } + + /// + /// The repository that handle collections. + /// ICollectionRepository CollectionRepository { get; } + + /// + /// The repository that handle shows. + /// IShowRepository ShowRepository { get; } + + /// + /// The repository that handle seasons. + /// ISeasonRepository SeasonRepository { get; } + + /// + /// The repository that handle episodes. + /// IEpisodeRepository EpisodeRepository { get; } + + /// + /// The repository that handle tracks. + /// ITrackRepository TrackRepository { get; } + + /// + /// The repository that handle people. + /// IPeopleRepository PeopleRepository { get; } + + /// + /// The repository that handle studios. + /// IStudioRepository StudioRepository { get; } + + /// + /// The repository that handle genres. + /// IGenreRepository GenreRepository { get; } + + /// + /// The repository that handle providers. + /// IProviderRepository ProviderRepository { get; } - // Get by id - Task GetLibrary(int id); - Task GetCollection(int id); - Task GetShow(int id); - Task GetSeason(int id); - Task GetSeason(int showID, int seasonNumber); - Task GetEpisode(int id); - Task GetEpisode(int showID, int seasonNumber, int episodeNumber); - Task GetGenre(int id); - Task GetTrack(int id); - Task GetStudio(int id); - Task GetPeople(int id); - Task GetProvider(int id); + /// + /// Get the resource by it's ID + /// + /// The id of the resource + /// The type of the resource + /// If the item is not found + /// The resource found + Task Get(int id) where T : class, IResource; - // Get by slug - Task GetLibrary(string slug); - Task GetCollection(string slug); - Task GetShow(string slug); - Task GetSeason(string slug); - Task GetSeason(string showSlug, int seasonNumber); - Task GetEpisode(string slug); - Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber); - Task GetMovieEpisode(string movieSlug); - Task GetTrack(string slug, StreamType type = StreamType.Unknown); - Task GetGenre(string slug); - Task GetStudio(string slug); - Task GetPeople(string slug); - Task GetProvider(string slug); + /// + /// Get the resource by it's slug + /// + /// The slug of the resource + /// The type of the resource + /// If the item is not found + /// The resource found + Task Get(string slug) where T : class, IResource; - // Get by predicate - Task GetLibrary(Expression> where); - Task GetCollection(Expression> where); - Task GetShow(Expression> where); - Task GetSeason(Expression> where); - Task GetEpisode(Expression> where); - Task GetTrack(Expression> where); - Task GetGenre(Expression> where); - Task GetStudio(Expression> where); - Task GetPerson(Expression> where); + /// + /// Get the resource by a filter function. + /// + /// The filter function. + /// The type of the resource + /// If the item is not found + /// The first resource found that match the where function + Task Get(Expression> where) where T : class, IResource; + /// + /// Get a season from it's showID and it's seasonNumber + /// + /// The id of the show + /// The season's number + /// If the item is not found + /// The season found + Task Get(int showID, int seasonNumber); + + /// + /// Get a season from it's show slug and it's seasonNumber + /// + /// The slug of the show + /// The season's number + /// If the item is not found + /// The season found + Task Get(string showSlug, int seasonNumber); + + /// + /// Get a episode from it's showID, it's seasonNumber and it's episode number. + /// + /// The id of the show + /// The season's number + /// The episode's number + /// If the item is not found + /// The episode found + Task Get(int showID, int seasonNumber, int episodeNumber); + + /// + /// Get a episode from it's show slug, it's seasonNumber and it's episode number. + /// + /// The slug of the show + /// The season's number + /// The episode's number + /// If the item is not found + /// The episode found + Task Get(string showSlug, int seasonNumber, int episodeNumber); + + /// + /// Get a track from it's slug and it's type. + /// + /// The slug of the track + /// The type (Video, Audio or Subtitle) + /// If the item is not found + /// The tracl found + Task Get(string slug, StreamType type = StreamType.Unknown); + + /// + /// Get the resource by it's ID or null if it is not found. + /// + /// The id of the resource + /// The type of the resource + /// The resource found + Task GetOrDefault(int id) where T : class, IResource; + + /// + /// Get the resource by it's slug or null if it is not found. + /// + /// The slug of the resource + /// The type of the resource + /// The resource found + Task GetOrDefault(string slug) where T : class, IResource; + + /// + /// Get the resource by a filter function or null if it is not found. + /// + /// The filter function. + /// The type of the resource + /// The first resource found that match the where function + Task GetOrDefault(Expression> where) where T : class, IResource; + + /// + /// Get a season from it's showID and it's seasonNumber or null if it is not found. + /// + /// The id of the show + /// The season's number + /// The season found + Task GetOrDefault(int showID, int seasonNumber); + + /// + /// Get a season from it's show slug and it's seasonNumber or null if it is not found. + /// + /// The slug of the show + /// The season's number + /// The season found + Task GetOrDefault(string showSlug, int seasonNumber); + + /// + /// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found. + /// + /// The id of the show + /// The season's number + /// The episode's number + /// The episode found + Task GetOrDefault(int showID, int seasonNumber, int episodeNumber); + + /// + /// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found. + /// + /// The slug of the show + /// The season's number + /// The episode's number + /// The episode found + Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); + + /// + /// Get a track from it's slug and it's type or null if it is not found. + /// + /// The slug of the track + /// The type (Video, Audio or Subtitle) + /// The tracl found + Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); + + + /// + /// Load a related resource + /// + /// The source object. + /// A getter function for the member to load + /// The type of the source object + /// The related resource's type + /// The param Task Load([NotNull] T obj, Expression> member) where T : class, IResource where T2 : class, IResource, new(); + /// + /// Load a collection of related resource + /// + /// The source object. + /// A getter function for the member to load + /// The type of the source object + /// The related resource's type + /// The param Task Load([NotNull] T obj, Expression>> member) where T : class, IResource where T2 : class, new(); + /// + /// Load a related resource by it's name + /// + /// The source object. + /// The name of the resource to load (case sensitive) + /// The type of the source object + /// The param Task Load([NotNull] T obj, string memberName) where T : class, IResource; + /// + /// Load a related resource without specifing it's type. + /// + /// The source object. + /// The name of the resource to load (case sensitive) Task Load([NotNull] IResource obj, string memberName); - - // Library Items relations + + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The ID of the library + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters Task> GetItemsFromLibrary(int id, Expression> where = null, Sort sort = default, Pagination limit = default); + + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The ID of the library + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters Task> GetItemsFromLibrary(int id, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetItemsFromLibrary(id, where, new Sort(sort), limit); - Task> GetItemsFromLibrary(string librarySlug, + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The slug of the library + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetItemsFromLibrary(string slug, Expression> where = null, Sort sort = default, Pagination limit = default); - Task> GetItemsFromLibrary(string librarySlug, + + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The slug of the library + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetItemsFromLibrary(string slug, [Optional] Expression> where, Expression> sort, Pagination limit = default - ) => GetItemsFromLibrary(librarySlug, where, new Sort(sort), limit); + ) => GetItemsFromLibrary(slug, where, new Sort(sort), limit); - // People Role relations + + /// + /// Get people's roles from a show. + /// + /// The ID of the show + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters Task> GetPeopleFromShow(int showID, Expression> where = null, Sort sort = default, Pagination limit = default); + + /// + /// Get people's roles from a show. + /// + /// The ID of the show + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters Task> GetPeopleFromShow(int showID, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetPeopleFromShow(showID, where, new Sort(sort), limit); + /// + /// Get people's roles from a show. + /// + /// The slug of the show + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters Task> GetPeopleFromShow(string showSlug, Expression> where = null, Sort sort = default, Pagination limit = default); + + /// + /// Get people's roles from a show. + /// + /// The slug of the show + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters Task> GetPeopleFromShow(string showSlug, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetPeopleFromShow(showSlug, where, new Sort(sort), limit); - // Show Role relations - Task> GetRolesFromPeople(int showID, - Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetRolesFromPeople(int showID, - [Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetRolesFromPeople(showID, where, new Sort(sort), limit); - Task> GetRolesFromPeople(string showSlug, + /// + /// Get people's roles from a person. + /// + /// The id of the person + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetRolesFromPeople(int id, Expression> where = null, Sort sort = default, Pagination limit = default); - Task> GetRolesFromPeople(string showSlug, + + /// + /// Get people's roles from a person. + /// + /// The id of the person + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetRolesFromPeople(int id, [Optional] Expression> where, Expression> sort, Pagination limit = default - ) => GetRolesFromPeople(showSlug, where, new Sort(sort), limit); + ) => GetRolesFromPeople(id, where, new Sort(sort), limit); + + /// + /// Get people's roles from a person. + /// + /// The slug of the person + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetRolesFromPeople(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + + /// + /// Get people's roles from a person. + /// + /// The slug of the person + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetRolesFromPeople(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetRolesFromPeople(slug, where, new Sort(sort), limit); - // Helpers + + /// + /// Setup relations between a show, a library and a collection + /// + /// The show's ID to setup relations with + /// The library's ID to setup relations with (optional) + /// The collection's ID to setup relations with (optional) Task AddShowLink(int showID, int? libraryID, int? collectionID); + + /// + /// Setup relations between a show, a library and a collection + /// + /// The show to setup relations with + /// The library to setup relations with (optional) + /// The collection to setup relations with (optional) Task AddShowLink([NotNull] Show show, Library library, Collection collection); - - // Get all - Task> GetLibraries(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetCollections(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetShows(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetSeasons(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetEpisodes(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetTracks(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetStudios(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetPeople(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetGenres(Expression> where = null, - Sort sort = default, - Pagination limit = default); - Task> GetProviders(Expression> where = null, - Sort sort = default, - Pagination limit = default); - - Task> GetLibraries([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetLibraries(where, new Sort(sort), limit); - Task> GetCollections([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetCollections(where, new Sort(sort), limit); - Task> GetShows([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetShows(where, new Sort(sort), limit); - Task> GetTracks([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetTracks(where, new Sort(sort), limit); - Task> GetStudios([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetStudios(where, new Sort(sort), limit); - Task> GetPeople([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetPeople(where, new Sort(sort), limit); - Task> GetGenres([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetGenres(where, new Sort(sort), limit); - Task> GetProviders([Optional] Expression> where, - Expression> sort, - Pagination limit = default - ) => GetProviders(where, new Sort(sort), limit); + /// + /// Get all resources with filters + /// + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// The type of resources to load + /// A list of resources that match every filters + Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) where T : class, IResource; - // Counts - Task GetLibrariesCount(Expression> where = null); - Task GetCollectionsCount(Expression> where = null); - Task GetShowsCount(Expression> where = null); - Task GetSeasonsCount(Expression> where = null); - Task GetEpisodesCount(Expression> where = null); - Task GetTracksCount(Expression> where = null); - Task GetGenresCount(Expression> where = null); - Task GetStudiosCount(Expression> where = null); - Task GetPeopleCount(Expression> where = null); + /// + /// Get all resources with filters + /// + /// A filter function + /// A sort by function + /// How many items to return and where to start + /// The type of resources to load + /// A list of resources that match every filters + Task> GetAll([Optional] Expression> where, + Expression> sort, + Pagination limit = default) where T : class, IResource + { + return GetAll(where, new Sort(sort), limit); + } + + /// + /// Get the count of resources that match the filter + /// + /// A filter function + /// The type of resources to load + /// A list of resources that match every filters + Task GetCount(Expression> where = null) where T : class, IResource; + + /// + /// Search for a resource + /// + /// The search query + /// The type of resources + /// A list of 20 items that match the search query + Task> Search(string query) where T : class, IResource; + + /// + /// Create a new resource. + /// + /// The item to register + /// The type of resource + /// The resource registers and completed by database's informations (related items & so on) + Task Create([NotNull] T item) where T : class, IResource; - // Search - Task> SearchLibraries(string searchQuery); - Task> SearchCollections(string searchQuery); - Task> SearchShows(string searchQuery); - Task> SearchSeasons(string searchQuery); - Task> SearchEpisodes(string searchQuery); - Task> SearchGenres(string searchQuery); - Task> SearchStudios(string searchQuery); - Task> SearchPeople(string searchQuery); + /// + /// Create a new resource if it does not exist already. If it does, the existing value is returned instead. + /// + /// The item to register + /// The type of resource + /// The newly created item or the existing value if it existed. + Task CreateIfNotExists([NotNull] T item) where T : class, IResource; - //Register values - Task RegisterLibrary(Library library); - Task RegisterCollection(Collection collection); - Task RegisterShow(Show show); - Task RegisterSeason(Season season); - Task RegisterEpisode(Episode episode); - Task RegisterTrack(Track track); - Task RegisterGenre(Genre genre); - Task RegisterStudio(Studio studio); - Task RegisterPeople(People people); + /// + /// Edit a resource + /// + /// The resourcce to edit, it's ID can't change. + /// Should old properties of the resource be discarded or should null values considered as not changed? + /// The type of resources + /// If the item is not found + /// The resource edited and completed by database's informations (related items & so on) + Task Edit(T item, bool resetOld) where T : class, IResource; + + /// + /// Delete a resource. + /// + /// The resource to delete + /// The type of resource to delete + /// If the item is not found + Task Delete(T item) where T : class, IResource; - // Edit values - Task EditLibrary(Library library, bool resetOld); - Task EditCollection(Collection collection, bool resetOld); - Task EditShow(Show show, bool resetOld); - Task EditSeason(Season season, bool resetOld); - Task EditEpisode(Episode episode, bool resetOld); - Task EditTrack(Track track, bool resetOld); - Task EditGenre(Genre genre, bool resetOld); - Task EditStudio(Studio studio, bool resetOld); - Task EditPeople(People people, bool resetOld); + /// + /// Delete a resource by it's ID. + /// + /// The id of the resource to delete + /// The type of resource to delete + /// If the item is not found + Task Delete(int id) 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); - - //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); - - //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); + /// + /// Delete a resource by it's slug. + /// + /// The slug of the resource to delete + /// The type of resource to delete + /// If the item is not found + Task Delete(string slug) where T : class, IResource; } } diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs index 6cf8a6ac..ce9592f9 100644 --- a/Kyoo.Common/Controllers/IMetadataProvider.cs +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -6,7 +6,7 @@ namespace Kyoo.Controllers { public interface IMetadataProvider { - ProviderID Provider { get; } + Provider Provider { get; } Task GetCollectionFromName(string name); diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index 5beee98b..2ed0b19a 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -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 { + /// + /// Informations about the pagination. How many items should be displayed and where to start. + /// public readonly struct Pagination { + /// + /// The count of items to return. + /// public int Count { get; } + /// + /// Where to start? Using the given sort + /// public int AfterID { get; } + /// + /// Create a new instance. + /// + /// Set the value + /// Set the value. If not specified, it will start from the start public Pagination(int count, int afterID = 0) { Count = count; AfterID = afterID; } + /// + /// Implicitly create a new pagination from a limit number. + /// + /// Set the value + /// A new instance public static implicit operator Pagination(int limit) => new(limit); } - public struct Sort + /// + /// Informations about how a query should be sorted. What factor should decide the sort and in which order. + /// + /// For witch type this sort applies + public readonly struct Sort { - public Expression> Key; - public bool Descendant; + /// + /// The sort key. This member will be used to sort the results. + /// + public Expression> Key { get; } + /// + /// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendent order. + /// + public bool Descendant { get; } + /// + /// Create a new instance. + /// + /// The sort key given. It is assigned to . + /// Should this be in descendant order? The default is false. + /// If the given key is not a member. public Sort(Expression> key, bool descendant = false) { Key = key; @@ -37,6 +73,11 @@ namespace Kyoo.Controllers throw new ArgumentException("The given sort key is not valid."); } + /// + /// Create a new instance from a key's name (case insensitive). + /// + /// A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key". + /// An invalid key or sort specifier as been given. 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); @@ -64,152 +105,545 @@ namespace Kyoo.Controllers }; } } - - public interface IRepository : IDisposable, IAsyncDisposable where T : class, IResource + + /// + /// A base class for repositories. Every service implementing this will be handled by the . + /// + public interface IBaseRepository { + /// + /// The type for witch this repository is responsible or null if non applicable. + /// + Type RepositoryType { get; } + } + + /// + /// A common repository for every resources. + /// + /// The resource's type that this repository manage. + public interface IRepository : IBaseRepository where T : class, IResource + { + /// + /// Get a resource from it's ID. + /// + /// The id of the resource + /// If the item could not be found. + /// The resource found Task Get(int id); + /// + /// Get a resource from it's slug. + /// + /// The slug of the resource + /// If the item could not be found. + /// The resource found Task Get(string slug); + /// + /// Get the first resource that match the predicate. + /// + /// A predicate to filter the resource. + /// If the item could not be found. + /// The resource found Task Get(Expression> where); + + /// + /// Get a resource from it's ID or null if it is not found. + /// + /// The id of the resource + /// The resource found + Task GetOrDefault(int id); + /// + /// Get a resource from it's slug or null if it is not found. + /// + /// The slug of the resource + /// The resource found + Task GetOrDefault(string slug); + /// + /// Get the first resource that match the predicate or null if it is not found. + /// + /// A predicate to filter the resource. + /// The resource found + Task GetOrDefault(Expression> where); + + /// + /// Search for resources. + /// + /// The query string. + /// A list of resources found Task> Search(string query); + /// + /// Get every resources that match all filters + /// + /// A filter predicate + /// Sort informations about the query (sort by, sort order) + /// How pagination should be done (where to start and how many to return) + /// A list of resources that match every filters Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default); - + /// + /// Get every resources that match all filters + /// + /// A filter predicate + /// A sort by predicate. The order is ascending. + /// How pagination should be done (where to start and how many to return) + /// A list of resources that match every filters Task> GetAll([Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetAll(where, new Sort(sort), limit); + /// + /// Get the number of resources that match the filter's predicate. + /// + /// A filter predicate + /// How many resources matched that filter Task GetCount(Expression> where = null); + /// + /// Create a new resource. + /// + /// The item to register + /// The resource registers and completed by database's informations (related items & so on) Task Create([NotNull] T obj); + + /// + /// Create a new resource if it does not exist already. If it does, the existing value is returned instead. + /// + /// The object to create + /// Allow issues to occurs in this method. Every issue is catched and ignored. + /// The newly created item or the existing value if it existed. Task CreateIfNotExists([NotNull] T obj, bool silentFail = false); + + /// + /// Edit a resource + /// + /// The resourcce to edit, it's ID can't change. + /// Should old properties of the resource be discarded or should null values considered as not changed? + /// If the item is not found + /// The resource edited and completed by database's informations (related items & so on) Task Edit([NotNull] T edited, bool resetOld); + /// + /// Delete a resource by it's ID + /// + /// The ID of the resource + /// If the item is not found Task Delete(int id); + /// + /// Delete a resource by it's slug + /// + /// The slug of the resource + /// If the item is not found Task Delete(string slug); + /// + /// Delete a resource + /// + /// The resource to delete + /// If the item is not found Task Delete([NotNull] T obj); + /// + /// Delete a list of resources. + /// + /// One or multiple resources to delete + /// If the item is not found Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); + /// + /// Delete a list of resources. + /// + /// An enumerable of resources to delete + /// If the item is not found Task DeleteRange(IEnumerable objs); + /// + /// Delete a list of resources. + /// + /// One or multiple resources's id + /// If the item is not found Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); + /// + /// Delete a list of resources. + /// + /// An enumearble of resources's id + /// If the item is not found Task DeleteRange(IEnumerable ids); + /// + /// Delete a list of resources. + /// + /// One or multiple resources's slug + /// If the item is not found Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); + /// + /// Delete a list of resources. + /// + /// An enumerable of resources's slug + /// If the item is not found Task DeleteRange(IEnumerable slugs); + /// + /// Delete a list of resources. + /// + /// A predicate to filter resources to delete. Every resource that match this will be deleted. + /// If the item is not found Task DeleteRange([NotNull] Expression> where); } + /// + /// A repository to handle shows. + /// public interface IShowRepository : IRepository { + /// + /// 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. + /// + /// The ID of the show + /// The ID of the library (optional) + /// The ID of the collection (optional) Task AddShowLink(int showID, int? libraryID, int? collectionID); + /// + /// Get a show's slug from it's ID. + /// + /// The ID of the show + /// If a show with the given ID is not found. + /// The show's slug Task GetSlug(int showID); } + /// + /// A repository to handle seasons. + /// public interface ISeasonRepository : IRepository { + /// + /// Get a season from it's showID and it's seasonNumber + /// + /// The id of the show + /// The season's number + /// If the item is not found + /// The season found Task Get(int showID, int seasonNumber); + + /// + /// Get a season from it's show slug and it's seasonNumber + /// + /// The slug of the show + /// The season's number + /// If the item is not found + /// The season found Task Get(string showSlug, int seasonNumber); - Task Delete(string showSlug, int seasonNumber); + + /// + /// Get a season from it's showID and it's seasonNumber or null if it is not found. + /// + /// The id of the show + /// The season's number + /// The season found + Task GetOrDefault(int showID, int seasonNumber); + + /// + /// Get a season from it's show slug and it's seasonNumber or null if it is not found. + /// + /// The slug of the show + /// The season's number + /// The season found + Task GetOrDefault(string showSlug, int seasonNumber); } + /// + /// The repository to handle episodes + /// public interface IEpisodeRepository : IRepository { + /// + /// Get a episode from it's showID, it's seasonNumber and it's episode number. + /// + /// The id of the show + /// The season's number + /// The episode's number + /// If the item is not found + /// The episode found Task Get(int showID, int seasonNumber, int episodeNumber); + /// + /// Get a episode from it's show slug, it's seasonNumber and it's episode number. + /// + /// The slug of the show + /// The season's number + /// The episode's number + /// If the item is not found + /// The episode found Task Get(string showSlug, int seasonNumber, int episodeNumber); - Task Get(int seasonID, int episodeNumber); + + /// + /// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found. + /// + /// The id of the show + /// The season's number + /// The episode's number + /// The episode found + Task GetOrDefault(int showID, int seasonNumber, int episodeNumber); + /// + /// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found. + /// + /// The slug of the show + /// The season's number + /// The episode's number + /// The episode found + Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber); + + /// + /// Get a episode from it's showID and it's absolute number. + /// + /// The id of the show + /// The episode's absolute number (The episode number does not reset to 1 after the end of a season. + /// If the item is not found + /// The episode found Task GetAbsolute(int showID, int absoluteNumber); + /// + /// Get a episode from it's showID and it's absolute number. + /// + /// The slug of the show + /// The episode's absolute number (The episode number does not reset to 1 after the end of a season. + /// If the item is not found + /// The episode found Task GetAbsolute(string showSlug, int absoluteNumber); - Task Delete(string showSlug, int seasonNumber, int episodeNumber); } + /// + /// A repository to handle tracks + /// public interface ITrackRepository : IRepository { + /// + /// Get a track from it's slug and it's type. + /// + /// The slug of the track + /// The type (Video, Audio or Subtitle) + /// If the item is not found + /// The tracl found Task Get(string slug, StreamType type = StreamType.Unknown); + + /// + /// Get a track from it's slug and it's type or null if it is not found. + /// + /// The slug of the track + /// The type (Video, Audio or Subtitle) + /// The tracl found + Task GetOrDefault(string slug, StreamType type = StreamType.Unknown); } + /// + /// A repository to handle libraries. + /// public interface ILibraryRepository : IRepository { } + /// + /// A repository to handle library items (A wrapper arround shows and collections). + /// public interface ILibraryItemRepository : IRepository { + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The ID of the library + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters public Task> GetFromLibrary(int id, Expression> where = null, Sort sort = default, Pagination limit = default); - + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The ID of the library + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters public Task> GetFromLibrary(int id, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetFromLibrary(id, where, new Sort(sort), limit); - public Task> GetFromLibrary(string librarySlug, + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The slug of the library + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + public Task> GetFromLibrary(string slug, Expression> where = null, Sort sort = default, Pagination limit = default); - - public Task> GetFromLibrary(string librarySlug, + /// + /// Get items (A wrapper arround shows or collections) from a library. + /// + /// The slug of the library + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + public Task> GetFromLibrary(string slug, [Optional] Expression> where, Expression> sort, Pagination limit = default - ) => GetFromLibrary(librarySlug, where, new Sort(sort), limit); + ) => GetFromLibrary(slug, where, new Sort(sort), limit); } + /// + /// A repository for collections + /// public interface ICollectionRepository : IRepository { } + + /// + /// A repository for genres. + /// public interface IGenreRepository : IRepository { } + + /// + /// A repository for studios. + /// public interface IStudioRepository : IRepository { } + /// + /// A repository for people. + /// public interface IPeopleRepository : IRepository { + /// + /// Get people's roles from a show. + /// + /// The ID of the show + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters Task> GetFromShow(int showID, Expression> where = null, Sort sort = default, Pagination limit = default); + /// + /// Get people's roles from a show. + /// + /// The ID of the show + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters Task> GetFromShow(int showID, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetFromShow(showID, where, new Sort(sort), limit); + /// + /// Get people's roles from a show. + /// + /// The slug of the show + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters Task> GetFromShow(string showSlug, Expression> where = null, Sort sort = default, Pagination limit = default); + /// + /// Get people's roles from a show. + /// + /// The slug of the show + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters Task> GetFromShow(string showSlug, [Optional] Expression> where, Expression> sort, Pagination limit = default ) => GetFromShow(showSlug, where, new Sort(sort), limit); - Task> GetFromPeople(int showID, + /// + /// Get people's roles from a person. + /// + /// The id of the person + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetFromPeople(int id, Expression> where = null, Sort sort = default, Pagination limit = default); - Task> GetFromPeople(int showID, + /// + /// Get people's roles from a person. + /// + /// The id of the person + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetFromPeople(int id, [Optional] Expression> where, Expression> sort, Pagination limit = default - ) => GetFromPeople(showID, where, new Sort(sort), limit); + ) => GetFromPeople(id, where, new Sort(sort), limit); - Task> GetFromPeople(string showSlug, + /// + /// Get people's roles from a person. + /// + /// The slug of the person + /// A filter function + /// Sort informations (sort order & sort by) + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetFromPeople(string slug, Expression> where = null, Sort sort = default, Pagination limit = default); - Task> GetFromPeople(string showSlug, + /// + /// Get people's roles from a person. + /// + /// The slug of the person + /// A filter function + /// A sort by method + /// How many items to return and where to start + /// A list of items that match every filters + Task> GetFromPeople(string slug, [Optional] Expression> where, Expression> sort, Pagination limit = default - ) => GetFromPeople(showSlug, where, new Sort(sort), limit); + ) => GetFromPeople(slug, where, new Sort(sort), limit); } - public interface IProviderRepository : IRepository + /// + /// A repository to handle providers. + /// + public interface IProviderRepository : IRepository { + /// + /// Get a list of external ids that match all filters + /// + /// A predicate to add arbitrary filter + /// Sort information (sort order & sort by) + /// Paginations information (where to start and how many to get) + /// A filtered list of external ids. Task> GetMetadataID(Expression> where = null, Sort sort = default, Pagination limit = default); + /// + /// Get a list of external ids that match all filters + /// + /// A predicate to add arbitrary filter + /// A sort by expression + /// Paginations information (where to start and how many to get) + /// A filtered list of external ids. Task> GetMetadataID([Optional] Expression> where, Expression> sort, Pagination limit = default diff --git a/Kyoo.Common/Controllers/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs index 5d2597cf..2282981a 100644 --- a/Kyoo.Common/Controllers/IThumbnailsManager.cs +++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs @@ -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 GetShowPoster([NotNull] Show show); Task GetShowLogo([NotNull] Show show); @@ -19,6 +19,6 @@ namespace Kyoo.Controllers Task GetSeasonPoster([NotNull] Season season); Task GetEpisodeThumb([NotNull] Episode episode); Task GetPeoplePoster([NotNull] People people); - Task GetProviderLogo([NotNull] ProviderID provider); + Task GetProviderLogo([NotNull] Provider provider); } } diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs index 7cae9b4d..2fb16735 100644 --- a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -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 { + /// + /// The list of repositories + /// + private readonly IBaseRepository[] _repositories; + + /// public ILibraryRepository LibraryRepository { get; } + /// public ILibraryItemRepository LibraryItemRepository { get; } + /// public ICollectionRepository CollectionRepository { get; } + /// public IShowRepository ShowRepository { get; } + /// public ISeasonRepository SeasonRepository { get; } + /// public IEpisodeRepository EpisodeRepository { get; } + /// public ITrackRepository TrackRepository { get; } - public IGenreRepository GenreRepository { get; } - public IStudioRepository StudioRepository { get; } + /// public IPeopleRepository PeopleRepository { get; } + /// + public IStudioRepository StudioRepository { get; } + /// + public IGenreRepository GenreRepository { get; } + /// 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) - { - LibraryRepository = libraryRepository; - LibraryItemRepository = libraryItemRepository; - CollectionRepository = collectionRepository; - ShowRepository = showRepository; - SeasonRepository = seasonRepository; - EpisodeRepository = episodeRepository; - TrackRepository = trackRepository; - GenreRepository = genreRepository; - StudioRepository = studioRepository; - ProviderRepository = providerRepository; - PeopleRepository = peopleRepository; - } - public void Dispose() - { - LibraryRepository.Dispose(); - CollectionRepository.Dispose(); - ShowRepository.Dispose(); - SeasonRepository.Dispose(); - EpisodeRepository.Dispose(); - TrackRepository.Dispose(); - GenreRepository.Dispose(); - StudioRepository.Dispose(); - PeopleRepository.Dispose(); - ProviderRepository.Dispose(); - } - public async ValueTask DisposeAsync() + /// + /// Create a new instancce with every repository available. + /// + /// 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. + public LibraryManager(IEnumerable repositories) { - 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() - ); + _repositories = repositories.ToArray(); + LibraryRepository = GetRepository() as ILibraryRepository; + LibraryItemRepository = GetRepository() as ILibraryItemRepository; + CollectionRepository = GetRepository() as ICollectionRepository; + ShowRepository = GetRepository() as IShowRepository; + SeasonRepository = GetRepository() as ISeasonRepository; + EpisodeRepository = GetRepository() as IEpisodeRepository; + TrackRepository = GetRepository() as ITrackRepository; + PeopleRepository = GetRepository() as IPeopleRepository; + StudioRepository = GetRepository() as IStudioRepository; + GenreRepository = GetRepository() as IGenreRepository; + ProviderRepository = GetRepository() as IProviderRepository; } - public Task GetLibrary(int id) + /// + public IRepository GetRepository() + where T : class, IResource { - return LibraryRepository.Get(id); + if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository ret) + return ret; + throw new ItemNotFound(); } - public Task GetCollection(int id) + /// + public Task Get(int id) + where T : class, IResource { - return CollectionRepository.Get(id); + return GetRepository().Get(id); } - public Task GetShow(int id) + /// + public Task Get(string slug) + where T : class, IResource { - return ShowRepository.Get(id); + return GetRepository().Get(slug); } - public Task GetSeason(int id) + /// + public Task Get(Expression> where) + where T : class, IResource { - return SeasonRepository.Get(id); + return GetRepository().Get(where); } - - public Task GetSeason(int showID, int seasonNumber) + + /// + public Task Get(int showID, int seasonNumber) { return SeasonRepository.Get(showID, seasonNumber); } - - public Task GetEpisode(int id) - { - return EpisodeRepository.Get(id); - } - public Task GetEpisode(int showID, int seasonNumber, int episodeNumber) - { - return EpisodeRepository.Get(showID, seasonNumber, episodeNumber); - } - - public Task GetTrack(string slug, StreamType type = StreamType.Unknown) - { - return TrackRepository.Get(slug, type); - } - - public Task GetGenre(int id) - { - return GenreRepository.Get(id); - } - - public Task GetStudio(int id) - { - return StudioRepository.Get(id); - } - - public Task GetPeople(int id) - { - return PeopleRepository.Get(id); - } - - public Task GetProvider(int id) - { - return ProviderRepository.Get(id); - } - - public Task GetLibrary(string slug) - { - return LibraryRepository.Get(slug); - } - - public Task GetCollection(string slug) - { - return CollectionRepository.Get(slug); - } - - public Task GetShow(string slug) - { - return ShowRepository.Get(slug); - } - - public Task GetSeason(string slug) - { - return SeasonRepository.Get(slug); - } - - public Task GetSeason(string showSlug, int seasonNumber) + /// + public Task Get(string showSlug, int seasonNumber) { return SeasonRepository.Get(showSlug, seasonNumber); } - public Task GetEpisode(string slug) + /// + public Task Get(int showID, int seasonNumber, int episodeNumber) { - return EpisodeRepository.Get(slug); + return EpisodeRepository.Get(showID, seasonNumber, episodeNumber); } - - public Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber) + + /// + public Task Get(string showSlug, int seasonNumber, int episodeNumber) { return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); } - public Task GetMovieEpisode(string movieSlug) + /// + public Task Get(string slug, StreamType type = StreamType.Unknown) { - return EpisodeRepository.Get(movieSlug); + return TrackRepository.Get(slug, type); } - public Task GetTrack(int id) + /// + public async Task GetOrDefault(int id) + where T : class, IResource { - return TrackRepository.Get(id); - } - - public Task GetGenre(string slug) - { - return GenreRepository.Get(slug); - } - - public Task GetStudio(string slug) - { - return StudioRepository.Get(slug); - } - - public Task GetPeople(string slug) - { - return PeopleRepository.Get(slug); + return await GetRepository().GetOrDefault(id); } - public Task GetProvider(string slug) + /// + public async Task GetOrDefault(string slug) + where T : class, IResource { - return ProviderRepository.Get(slug); + return await GetRepository().GetOrDefault(slug); + } + + /// + public async Task GetOrDefault(Expression> where) + where T : class, IResource + { + return await GetRepository().GetOrDefault(where); } - public Task GetLibrary(Expression> where) + /// + public async Task GetOrDefault(int showID, int seasonNumber) { - return LibraryRepository.Get(where); + return await SeasonRepository.GetOrDefault(showID, seasonNumber); + } + + /// + public async Task GetOrDefault(string showSlug, int seasonNumber) + { + return await SeasonRepository.GetOrDefault(showSlug, seasonNumber); + } + + /// + public async Task GetOrDefault(int showID, int seasonNumber, int episodeNumber) + { + return await EpisodeRepository.GetOrDefault(showID, seasonNumber, episodeNumber); + } + + /// + public async Task GetOrDefault(string showSlug, int seasonNumber, int episodeNumber) + { + return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber); } - public Task GetCollection(Expression> where) + /// + public async Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) { - return CollectionRepository.Get(where); + return await TrackRepository.GetOrDefault(slug, type); } - - public Task GetShow(Expression> where) - { - return ShowRepository.Get(where); - } - - public Task GetSeason(Expression> where) - { - return SeasonRepository.Get(where); - } - - public Task GetEpisode(Expression> where) - { - return EpisodeRepository.Get(where); - } - - public Task GetTrack(Expression> where) - { - return TrackRepository.Get(where); - } - - public Task GetGenre(Expression> where) - { - return GenreRepository.Get(where); - } - - public Task GetStudio(Expression> where) - { - return StudioRepository.Get(where); - } - - public Task GetPerson(Expression> where) - { - return PeopleRepository.Get(where); - } - + + /// public Task Load(T obj, Expression> member) where T : class, IResource where T2 : class, IResource, new() @@ -255,6 +181,7 @@ namespace Kyoo.Controllers return Load(obj, Utility.GetPropertyName(member)); } + /// public Task Load(T obj, Expression>> 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 Load(T obj, string member) + /// + public async Task Load(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 obj, + /// + /// Set relations between to objects. + /// + /// The owner object + /// A Task to load a collection of related objects + /// A setter function to store the collection of related objects + /// A setter function to store the owner of a releated object loaded + /// The type of the owner object + /// The type of the related object + private static async Task SetRelation(T1 obj, Task> loader, Action> setter, Action inverse) @@ -282,12 +219,13 @@ namespace Kyoo.Controllers inverse(item, obj); } - public Task Load(IResource obj, string member) + /// + 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> GetLibraries(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return LibraryRepository.GetAll(where, sort, page); - } - - public Task> GetCollections(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return CollectionRepository.GetAll(where, sort, page); - } - - public Task> GetShows(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return ShowRepository.GetAll(where, sort, limit); - } - - public Task> GetSeasons(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return SeasonRepository.GetAll(where, sort, limit); - } - - public Task> GetEpisodes(Expression> where = null, - Sort sort = default, - Pagination limit = default) - { - return EpisodeRepository.GetAll(where, sort, limit); - } - - public Task> GetTracks(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return TrackRepository.GetAll(where, sort, page); - } - - public Task> GetStudios(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return StudioRepository.GetAll(where, sort, page); - } - - public Task> GetPeople(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return PeopleRepository.GetAll(where, sort, page); - } - - public Task> GetGenres(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return GenreRepository.GetAll(where, sort, page); - } - - public Task> GetProviders(Expression> where = null, - Sort sort = default, - Pagination page = default) - { - return ProviderRepository.GetAll(where, sort, page); - } + /// public Task> GetItemsFromLibrary(int id, Expression> where = null, Sort sort = default, @@ -513,366 +382,128 @@ namespace Kyoo.Controllers return LibraryItemRepository.GetFromLibrary(id, where, sort, limit); } - public Task> GetItemsFromLibrary(string librarySlug, - Expression> where = null, + /// + public Task> GetItemsFromLibrary(string slug, + Expression> where = null, Sort sort = default, Pagination limit = default) { - return LibraryItemRepository.GetFromLibrary(librarySlug, where, sort, limit); + return LibraryItemRepository.GetFromLibrary(slug, where, sort, limit); } - - public Task> GetPeopleFromShow(int showID, + + /// + public Task> GetPeopleFromShow(int showID, Expression> where = null, Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromShow(showID, where, sort, limit); } - - public Task> GetPeopleFromShow(string showSlug, + + /// + public Task> GetPeopleFromShow(string showSlug, Expression> where = null, - Sort sort = default, + Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromShow(showSlug, where, sort, limit); } - + + /// public Task> GetRolesFromPeople(int id, - Expression> where = null, - Sort sort = default, + Expression> where = null, + Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromPeople(id, where, sort, limit); } - public Task> GetRolesFromPeople(string slug, - Expression> where = null, - Sort sort = default, + /// + public Task> GetRolesFromPeople(string slug, + Expression> where = null, + Sort sort = default, Pagination limit = default) { return PeopleRepository.GetFromPeople(slug, where, sort, limit); } - public Task GetLibrariesCount(Expression> where = null) - { - return LibraryRepository.GetCount(where); - } - - public Task GetCollectionsCount(Expression> where = null) - { - return CollectionRepository.GetCount(where); - } - - public Task GetShowsCount(Expression> where = null) - { - return ShowRepository.GetCount(where); - } - - public Task GetSeasonsCount(Expression> where = null) - { - return SeasonRepository.GetCount(where); - } - - public Task GetEpisodesCount(Expression> where = null) - { - return EpisodeRepository.GetCount(where); - } - - public Task GetTracksCount(Expression> where = null) - { - return TrackRepository.GetCount(where); - } - - public Task GetGenresCount(Expression> where = null) - { - return GenreRepository.GetCount(where); - } - - public Task GetStudiosCount(Expression> where = null) - { - return StudioRepository.GetCount(where); - } - - public Task GetPeopleCount(Expression> where = null) - { - return PeopleRepository.GetCount(where); - } - + /// public Task AddShowLink(int showID, int? libraryID, int? collectionID) { return ShowRepository.AddShowLink(showID, libraryID, collectionID); } + /// 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> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + where T : class, IResource + { + return GetRepository().GetAll(where, sort, limit); + } + + /// + public Task GetCount(Expression> where = null) + where T : class, IResource + { + return GetRepository().GetCount(where); + } + + /// + public Task> Search(string query) + where T : class, IResource + { + return GetRepository().Search(query); + } + + /// + public Task Create(T item) + where T : class, IResource + { + return GetRepository().Create(item); } - public Task> SearchLibraries(string searchQuery) + /// + public Task CreateIfNotExists(T item) + where T : class, IResource { - return LibraryRepository.Search(searchQuery); + return GetRepository().CreateIfNotExists(item); } - public Task> SearchCollections(string searchQuery) + /// + public Task Edit(T item, bool resetOld) + where T : class, IResource { - return CollectionRepository.Search(searchQuery); + return GetRepository().Edit(item, resetOld); } - public Task> SearchShows(string searchQuery) + /// + public Task Delete(T item) + where T : class, IResource { - return ShowRepository.Search(searchQuery); + return GetRepository().Delete(item); } - public Task> SearchSeasons(string searchQuery) + /// + public Task Delete(int id) + where T : class, IResource { - return SeasonRepository.Search(searchQuery); + return GetRepository().Delete(id); } - public Task> SearchEpisodes(string searchQuery) + /// + public Task Delete(string slug) + where T : class, IResource { - return EpisodeRepository.Search(searchQuery); - } - - public Task> SearchGenres(string searchQuery) - { - return GenreRepository.Search(searchQuery); - } - - public Task> SearchStudios(string searchQuery) - { - return StudioRepository.Search(searchQuery); - } - - public Task> SearchPeople(string searchQuery) - { - return PeopleRepository.Search(searchQuery); - } - - public Task RegisterLibrary(Library library) - { - return LibraryRepository.Create(library); - } - - public Task RegisterCollection(Collection collection) - { - return CollectionRepository.Create(collection); - } - - public Task RegisterShow(Show show) - { - return ShowRepository.Create(show); - } - - public Task RegisterSeason(Season season) - { - return SeasonRepository.Create(season); - } - - public Task RegisterEpisode(Episode episode) - { - return EpisodeRepository.Create(episode); - } - - public Task RegisterTrack(Track track) - { - return TrackRepository.Create(track); - } - - public Task RegisterGenre(Genre genre) - { - return GenreRepository.Create(genre); - } - - public Task RegisterStudio(Studio studio) - { - return StudioRepository.Create(studio); - } - - public Task RegisterPeople(People people) - { - return PeopleRepository.Create(people); - } - - public Task EditLibrary(Library library, bool resetOld) - { - return LibraryRepository.Edit(library, resetOld); - } - - public Task EditCollection(Collection collection, bool resetOld) - { - return CollectionRepository.Edit(collection, resetOld); - } - - public Task EditShow(Show show, bool resetOld) - { - return ShowRepository.Edit(show, resetOld); - } - - public Task EditSeason(Season season, bool resetOld) - { - return SeasonRepository.Edit(season, resetOld); - } - - public Task EditEpisode(Episode episode, bool resetOld) - { - return EpisodeRepository.Edit(episode, resetOld); - } - - public Task EditTrack(Track track, bool resetOld) - { - return TrackRepository.Edit(track, resetOld); - } - - public Task EditGenre(Genre genre, bool resetOld) - { - return GenreRepository.Edit(genre, resetOld); - } - - public Task EditStudio(Studio studio, bool resetOld) - { - return StudioRepository.Edit(studio, resetOld); - } - - public Task 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().Delete(slug); } } } diff --git a/Kyoo.Common/Models/MetadataID.cs b/Kyoo.Common/Models/MetadataID.cs index 71e1946a..cc7985ac 100644 --- a/Kyoo.Common/Models/MetadataID.cs +++ b/Kyoo.Common/Models/MetadataID.cs @@ -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; diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index c1e17b56..86cb324d 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -11,20 +11,20 @@ namespace Kyoo.Models public string Name { get; set; } public string[] Paths { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } + [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } [LoadableRelation] public virtual ICollection Shows { get; set; } [LoadableRelation] public virtual ICollection Collections { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ProviderLinks { get; set; } + [SerializeIgnore] public virtual ICollection> ProviderLinks { get; set; } [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } #endif public Library() { } - public Library(string slug, string name, IEnumerable paths, IEnumerable providers) + public Library(string slug, string name, IEnumerable paths, IEnumerable providers) { Slug = slug; Name = name; diff --git a/Kyoo.Common/Models/Resources/ProviderID.cs b/Kyoo.Common/Models/Resources/Provider.cs similarity index 79% rename from Kyoo.Common/Models/Resources/ProviderID.cs rename to Kyoo.Common/Models/Resources/Provider.cs index 2d1ec3d1..6a19f27c 100644 --- a/Kyoo.Common/Models/Resources/ProviderID.cs +++ b/Kyoo.Common/Models/Resources/Provider.cs @@ -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 Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } + [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } [SerializeIgnore] public virtual ICollection 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); diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index ccf5f602..71ec1993 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -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(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(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, diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs index 0bbf7733..50dcb588 100644 --- a/Kyoo.CommonAPI/CrudApi.cs +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -29,22 +29,28 @@ namespace Kyoo.CommonApi [Authorize(Policy = "Read")] public virtual async Task> 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> 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")] @@ -114,15 +120,19 @@ namespace Kyoo.CommonApi [Authorize(Policy = "Write")] public virtual async Task> Edit([FromQuery] bool resetOld, [FromBody] T resource) { - if (resource.ID > 0) + try + { + if (resource.ID > 0) + return await _repository.Edit(resource, resetOld); + + T old = await _repository.Get(resource.Slug); + resource.ID = old.ID; return await _repository.Edit(resource, resetOld); - - T old = await _repository.Get(resource.Slug); - if (old == null) + } + catch (ItemNotFound) + { return NotFound(); - - resource.ID = old.ID; - return await _repository.Edit(resource, resetOld); + } } [HttpPut("{id:int}")] @@ -144,11 +154,16 @@ namespace Kyoo.CommonApi [Authorize(Policy = "Write")] public virtual async Task> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) { - T old = await _repository.Get(slug); - if (old == null) + try + { + T old = await _repository.Get(slug); + resource.ID = old.ID; + return await _repository.Edit(resource, resetOld); + } + catch (ItemNotFound) + { return NotFound(); - resource.ID = old.ID; - return await _repository.Edit(resource, resetOld); + } } [HttpDelete("{id:int}")] diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 5da8fcc0..c1e14a6e 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -12,52 +12,99 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { + /// + /// A base class to create repositories using Entity Framework. + /// + /// The type of this repository public abstract class LocalRepository : IRepository where T : class, IResource { + /// + /// The Entity Framework's Database handle. + /// protected readonly DbContext Database; + /// + /// The default sort order that will be used for this resource's type. + /// protected abstract Expression> DefaultSort { get; } + /// + /// Create a new base with the given database handle. + /// + /// A database connection to load resources of type protected LocalRepository(DbContext database) { Database = database; } - - public virtual void Dispose() + + /// + public Type RepositoryType => typeof(T); + + /// + /// Get a resource from it's ID and make the instance track it. + /// + /// The ID of the resource + /// If the item is not found + /// The tracked resource with the given ID + protected virtual async Task GetWithTracking(int id) { - Database.Dispose(); - GC.SuppressFinalize(this); + T ret = await Database.Set().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 async Task Get(int id) + { + T ret = await GetOrDefault(id); + if (ret == null) + throw new ItemNotFound($"No {typeof(T).Name} found with the id {id}"); + return ret; } - public virtual ValueTask DisposeAsync() + /// + public virtual async Task Get(string slug) { - return Database.DisposeAsync(); + T ret = await GetOrDefault(slug); + if (ret == null) + throw new ItemNotFound($"No {typeof(T).Name} found with the slug {slug}"); + return ret; + } + + /// + public virtual async Task Get(Expression> where) + { + T ret = await GetOrDefault(where); + if (ret == null) + throw new ItemNotFound($"No {typeof(T).Name} found with the given predicate."); + return ret; } - public virtual Task Get(int id) + /// + public virtual Task GetOrDefault(int id) { return Database.Set().FirstOrDefaultAsync(x => x.ID == id); } - public virtual Task GetWithTracking(int id) - { - return Database.Set().AsTracking().FirstOrDefaultAsync(x => x.ID == id); - } - - public virtual Task Get(string slug) + /// + public virtual Task GetOrDefault(string slug) { return Database.Set().FirstOrDefaultAsync(x => x.Slug == slug); } - - public virtual Task Get(Expression> predicate) - { - return Database.Set().FirstOrDefaultAsync(predicate); - } + /// + public virtual Task GetOrDefault(Expression> where) + { + return Database.Set().FirstOrDefaultAsync(where); + } + + /// public abstract Task> Search(string query); + /// public virtual Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) @@ -65,6 +112,14 @@ namespace Kyoo.Controllers return ApplyFilters(Database.Set(), where, sort, limit); } + /// + /// Apply filters to a query to ease sort, pagination & where queries for resources of this repository + /// + /// The base query to filter. + /// An expression to filter based on arbitrary conditions + /// The sort settings (sort order & sort by) + /// Paginations information (where to start and how many to get) + /// The filtered query protected Task> ApplyFilters(IQueryable query, Expression> where = null, Sort sort = default, @@ -73,6 +128,17 @@ namespace Kyoo.Controllers return ApplyFilters(query, Get, DefaultSort, where, sort, limit); } + /// + /// Apply filters to a query to ease sort, pagination & where queries for any resources types. + /// For resources of type , see + /// + /// A function to asynchronously get a resource from the database using it's ID. + /// The default sort order of this resource's type. + /// The base query to filter. + /// An expression to filter based on arbitrary conditions + /// The sort settings (sort order & sort by) + /// Paginations information (where to start and how many to get) + /// The filtered query protected async Task> ApplyFilters(IQueryable query, Func> get, Expression> defaultSort, @@ -108,6 +174,7 @@ namespace Kyoo.Controllers return await query.ToListAsync(); } + /// public virtual Task GetCount(Expression> where = null) { IQueryable query = Database.Set(); @@ -116,6 +183,7 @@ namespace Kyoo.Controllers return query.CountAsync(); } + /// public virtual async Task Create(T obj) { if (obj == null) @@ -124,6 +192,7 @@ namespace Kyoo.Controllers return obj; } + /// public virtual async Task 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 } } + /// public virtual async Task Edit(T edited, bool resetOld) { if (edited == null) @@ -162,9 +229,7 @@ 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); Utility.Complete(old, edited, x => x.GetCustomAttribute() == null); @@ -178,11 +243,24 @@ namespace Kyoo.Controllers } } + /// + /// An overridable method to edit relatiosn of a resource. + /// + /// The non edited resource + /// The new version of . This item will be saved on the databse and replace + /// A boolean to indicate if all values of resource should be discarded or not. + /// protected virtual Task EditRelations(T resource, T changed, bool resetOld) { return Validate(resource); } + /// + /// A method called just before saving a new resource to the database. + /// It is also called on the default implementation of + /// + /// The resource that will be saved + /// You can throw this if the resource is illegal and should not be saved. protected virtual Task Validate(T resource) { if (string.IsNullOrEmpty(resource.Slug)) @@ -205,38 +283,45 @@ namespace Kyoo.Controllers return Task.CompletedTask; } + /// public virtual async Task Delete(int id) { T resource = await Get(id); await Delete(resource); } + /// public virtual async Task Delete(string slug) { T resource = await Get(slug); await Delete(resource); } + /// public abstract Task Delete(T obj); + /// public virtual async Task DeleteRange(IEnumerable objs) { foreach (T obj in objs) await Delete(obj); } + /// public virtual async Task DeleteRange(IEnumerable ids) { foreach (int id in ids) await Delete(id); } + /// public virtual async Task DeleteRange(IEnumerable slugs) { foreach (string slug in slugs) await Delete(slug); } + /// public async Task DeleteRange(Expression> where) { ICollection resources = await GetAll(where); diff --git a/Kyoo.CommonAPI/ResourceViewAttribute.cs b/Kyoo.CommonAPI/ResourceViewAttribute.cs index fa177342..06198663 100644 --- a/Kyoo.CommonAPI/ResourceViewAttribute.cs +++ b/Kyoo.CommonAPI/ResourceViewAttribute.cs @@ -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 library = context.HttpContext.RequestServices.GetService(); ICollection fields = (ICollection)context.HttpContext.Items["fields"]; Type pageType = Utility.GetGenericDefinition(result.DeclaredType, typeof(Page<>)); diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 0a5a4355..65e01c1f 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -10,6 +10,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/Kyoo.Tests/Library/SetupTests.cs b/Kyoo.Tests/Library/SetupTests.cs new file mode 100644 index 00000000..e1852a17 --- /dev/null +++ b/Kyoo.Tests/Library/SetupTests.cs @@ -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()); + // } + } +} \ No newline at end of file diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Library/TestContext.cs new file mode 100644 index 00000000..c9a83ad0 --- /dev/null +++ b/Kyoo.Tests/Library/TestContext.cs @@ -0,0 +1,79 @@ +using Kyoo.Models; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Kyoo.Tests +{ + /// + /// Class responsible to fill and create in memory databases for unit tests. + /// + public class TestContext + { + /// + /// The context's options that specify to use an in memory Sqlite database. + /// + private readonly DbContextOptions _context; + + /// + /// Create a new database and fill it with informations. + /// + public TestContext() + { + SqliteConnection connection = new("DataSource=:memory:"); + connection.Open(); + + try + { + _context = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + FillDatabase(); + } + finally + { + connection.Close(); + } + } + + /// + /// Fill the database with pre defined values using a clean context. + /// + 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 + }); + } + + /// + /// Get a new databse context connected to a in memory Sqlite databse. + /// + /// A valid DatabaseContext + public DatabaseContext New() + { + return new(_context); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/CollectionRepository.cs b/Kyoo/Controllers/Repositories/CollectionRepository.cs index ccebc298..bbef77af 100644 --- a/Kyoo/Controllers/Repositories/CollectionRepository.cs +++ b/Kyoo/Controllers/Repositories/CollectionRepository.cs @@ -8,34 +8,30 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { + /// + /// A local repository to handle collections + /// public class CollectionRepository : LocalRepository, ICollectionRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + + /// protected override Expression> DefaultSort => x => x.Name; - public CollectionRepository(DatabaseContext database) : base(database) + /// + /// Create a new . + /// + /// The database handle to use + 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(); - } - + /// public override async Task> Search(string query) { return await _database.Collections @@ -45,6 +41,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(Collection obj) { await base.Create(obj); @@ -53,6 +50,7 @@ namespace Kyoo.Controllers return obj; } + /// public override async Task Delete(Collection obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 121a1fed..1af6c485 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -5,21 +5,45 @@ 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 { + /// + /// A local repository to handle episodes. + /// public class EpisodeRepository : LocalRepository, IEpisodeRepository { - private bool _disposed; + /// + /// The databse handle + /// private readonly DatabaseContext _database; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + /// + /// A show repository to get show's slug from their ID and keep the slug in each episode. + /// private readonly IShowRepository _shows; + /// + /// A track repository to handle creation and deletion of tracks related to the current episode. + /// private readonly ITrackRepository _tracks; + + /// protected override Expression> DefaultSort => x => x.EpisodeNumber; - public EpisodeRepository(DatabaseContext database, + /// + /// Create a new . + /// + /// The database handle to use. + /// A provider repository + /// A show repository + /// A track repository + public EpisodeRepository(DatabaseContext database, IProviderRepository providers, IShowRepository shows, ITrackRepository tracks) @@ -30,62 +54,46 @@ namespace Kyoo.Controllers _shows = shows; _tracks = tracks; } + - - public override void Dispose() + /// + public override async Task 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 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 Get(string slug) + /// + public override async Task GetOrDefault(string slug) { Match match = Regex.Match(slug, @"(?.*)-s(?\d*)e(?\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); - episode.ShowSlug = slug; + if (episode != null) + episode.ShowSlug = slug; return episode; } - public override async Task Get(Expression> predicate) + /// + public override async Task GetOrDefault(Expression> 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 Get(string showSlug, int seasonNumber, int episodeNumber) + /// + public async Task 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; } + /// public async Task 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; + } + + /// + public async Task 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; + } + + /// + public async Task 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 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; - } - + /// public async Task GetAbsolute(int showID, int absoluteNumber) { Episode ret = await _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID @@ -123,6 +142,7 @@ namespace Kyoo.Controllers return ret; } + /// public async Task 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; } + /// public override async Task> Search(string query) { List episodes = await _database.Episodes @@ -144,6 +165,7 @@ namespace Kyoo.Controllers return episodes; } + /// public override async Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) @@ -154,6 +176,7 @@ namespace Kyoo.Controllers return episodes; } + /// public override async Task Create(Episode obj) { await base.Create(obj); @@ -163,6 +186,7 @@ namespace Kyoo.Controllers return await ValidateTracks(obj); } + /// 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); } + /// + /// Set track's index and ensure that every tracks is well-formed. + /// + /// The resource to fix. + /// The parameter is returnned. private async Task ValidateTracks(Episode resource) { resource.Tracks = await resource.Tracks.MapAsync((x, i) => @@ -198,6 +227,7 @@ namespace Kyoo.Controllers return resource; } + /// protected override async Task Validate(Episode resource) { await base.Validate(resource); @@ -209,13 +239,8 @@ namespace Kyoo.Controllers return x; }).ToListAsync(); } - - public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) - { - Episode obj = await Get(showSlug, seasonNumber, episodeNumber); - await Delete(obj); - } - + + /// public override async Task Delete(Episode obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/GenreRepository.cs b/Kyoo/Controllers/Repositories/GenreRepository.cs index bfbc1e3c..0ce1a155 100644 --- a/Kyoo/Controllers/Repositories/GenreRepository.cs +++ b/Kyoo/Controllers/Repositories/GenreRepository.cs @@ -8,35 +8,31 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { + /// + /// A local repository for genres. + /// public class GenreRepository : LocalRepository, IGenreRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + + /// protected override Expression> DefaultSort => x => x.Slug; - public GenreRepository(DatabaseContext database) : base(database) + /// + /// Create a new . + /// + /// The database handle + 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(); - } - + /// public override async Task> Search(string query) { return await _database.Genres @@ -46,6 +42,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(Genre obj) { await base.Create(obj); @@ -54,6 +51,7 @@ namespace Kyoo.Controllers return obj; } + /// public override async Task Delete(Genre obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs index 57f293b5..43afd407 100644 --- a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -10,18 +10,45 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { + /// + /// A local repository to handle library items. + /// public class LibraryItemRepository : LocalRepository, ILibraryItemRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + /// + /// A lazy loaded library repository to validate queries (check if a library does exist) + /// private readonly Lazy _libraries; + /// + /// A lazy loaded show repository to get a show from it's id. + /// private readonly Lazy _shows; + /// + /// A lazy loaded collection repository to get a collection from it's id. + /// private readonly Lazy _collections; + + /// protected override Expression> DefaultSort => x => x.Title; - public LibraryItemRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) + /// + /// Create a new . + /// + /// The databse instance + /// A provider repository + /// A service provider to lazilly request a library, show or collection repository. + public LibraryItemRepository(DatabaseContext database, + IProviderRepository providers, + IServiceProvider services) : base(database) { _database = database; @@ -30,53 +57,34 @@ namespace Kyoo.Controllers _shows = new Lazy(services.GetRequiredService); _collections = new Lazy(services.GetRequiredService); } - - 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 Get(int id) + /// + public override async Task 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 Get(string slug) + /// + public override Task GetOrDefault(string slug) { - throw new InvalidOperationException(); + throw new InvalidOperationException("You can't get a library item by a slug."); } - private IQueryable ItemsQuery + /// + /// Get a basic queryable with the right mapping from shows & collections. + /// Shows contained in a collection are excluded. + /// + private IQueryable ItemsQuery => _database.Shows .Where(x => !x.Collections.Any()) .Select(LibraryItem.FromShow) .Concat(_database.Collections .Select(LibraryItem.FromCollection)); + /// public override Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) @@ -84,6 +92,7 @@ namespace Kyoo.Controllers return ApplyFilters(ItemsQuery, where, sort, limit); } + /// public override Task GetCount(Expression> where = null) { IQueryable query = ItemsQuery; @@ -92,6 +101,7 @@ namespace Kyoo.Controllers return query.CountAsync(); } + /// public override async Task> Search(string query) { return await ItemsQuery @@ -101,19 +111,31 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); + /// public override Task CreateIfNotExists(LibraryItem obj, bool silentFail = false) { if (silentFail) return Task.FromResult(default); throw new InvalidOperationException(); } + /// public override Task Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); + /// public override Task Delete(int id) => throw new InvalidOperationException(); + /// public override Task Delete(string slug) => throw new InvalidOperationException(); + /// public override Task Delete(LibraryItem obj) => throw new InvalidOperationException(); + /// + /// Get a basic queryable for a library with the right mapping from shows & collections. + /// Shows contained in a collection are excluded. + /// + /// Only items that are part of a library that match this predicate will be returned. + /// A queryable containing items that are part of a library matching the selector. private IQueryable LibraryRelatedQuery(Expression> selector) => _database.Libraries .Where(selector) @@ -125,7 +147,8 @@ namespace Kyoo.Controllers .SelectMany(x => x.Collections) .Select(LibraryItem.FromCollection)); - public async Task> GetFromLibrary(int id, + /// + public async Task> GetFromLibrary(int id, Expression> where = null, Sort sort = default, Pagination limit = default) @@ -134,12 +157,13 @@ 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; } - public async Task> GetFromLibrary(string slug, + /// + public async Task> GetFromLibrary(string slug, Expression> where = null, Sort sort = default, Pagination limit = 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; } diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index b4d725c1..b4cab3b9 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -8,40 +8,38 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { + /// + /// A local repository to handle libraries. + /// public class LibraryRepository : LocalRepository, ILibraryRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + + /// protected override Expression> DefaultSort => x => x.ID; + /// + /// Create a new instance. + /// + /// The database handle + /// The providere repository public LibraryRepository(DatabaseContext database, IProviderRepository providers) : base(database) { _database = database; _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(); - } - + /// public override async Task> Search(string query) { return await _database.Libraries @@ -51,6 +49,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(Library obj) { await base.Create(obj); @@ -61,6 +60,7 @@ namespace Kyoo.Controllers return obj; } + /// protected override async Task Validate(Library resource) { await base.Validate(resource); @@ -69,6 +69,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// protected override async Task EditRelations(Library resource, Library changed, bool resetOld) { if (string.IsNullOrEmpty(resource.Slug)) @@ -86,6 +87,7 @@ namespace Kyoo.Controllers } } + /// public override async Task Delete(Library obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 9097bea8..526a3286 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -10,46 +10,45 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { + /// + /// A local repository to handle people. + /// public class PeopleRepository : LocalRepository, IPeopleRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + /// + /// A lazy loaded show repository to validate requests from shows. + /// private readonly Lazy _shows; + + /// protected override Expression> DefaultSort => x => x.Name; - public PeopleRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) + /// + /// Create a new + /// + /// The database handle + /// A provider repository + /// A service provider to lazy load a show repository + public PeopleRepository(DatabaseContext database, + IProviderRepository providers, + IServiceProvider services) : base(database) { _database = database; _providers = providers; _shows = new Lazy(services.GetRequiredService); } + - - 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(); - } - + /// public override async Task> Search(string query) { return await _database.People @@ -59,6 +58,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(People obj) { await base.Create(obj); @@ -68,6 +68,7 @@ namespace Kyoo.Controllers return obj; } + /// protected override async Task Validate(People resource) { await base.Validate(resource); @@ -85,6 +86,7 @@ namespace Kyoo.Controllers }); } + /// 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); } + /// public override async Task Delete(People obj) { if (obj == null) @@ -113,6 +116,7 @@ namespace Kyoo.Controllers await _database.SaveChangesAsync(); } + /// public async Task> GetFromShow(int showID, Expression> where = null, Sort sort = default, @@ -133,6 +137,7 @@ namespace Kyoo.Controllers return people; } + /// public async Task> GetFromShow(string showSlug, Expression> where = null, Sort sort = default, @@ -154,24 +159,26 @@ namespace Kyoo.Controllers return people; } - public async Task> GetFromPeople(int peopleID, + /// + public async Task> GetFromPeople(int id, Expression> where = null, Sort sort = default, Pagination limit = default) { ICollection 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; } + /// public async Task> GetFromPeople(string slug, Expression> where = null, Sort sort = default, diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index 79cc80b1..31c283d3 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -8,18 +8,32 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class ProviderRepository : LocalRepository, IProviderRepository + /// + /// A local repository to handle providers. + /// + public class ProviderRepository : LocalRepository, IProviderRepository { + /// + /// The database handle + /// private readonly DatabaseContext _database; - protected override Expression> DefaultSort => x => x.Slug; + + /// + protected override Expression> DefaultSort => x => x.Slug; - public ProviderRepository(DatabaseContext database) : base(database) + /// + /// Create a new . + /// + /// The database handle + public ProviderRepository(DatabaseContext database) + : base(database) { _database = database; } - public override async Task> Search(string query) + /// + public override async Task> 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 Create(ProviderID obj) + /// + public override async Task 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) + /// + 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(); } + /// public Task> GetMetadataID(Expression> where = null, Sort sort = default, Pagination limit = default) diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index 29c3e400..e35042ad 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -5,22 +5,47 @@ 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 { + /// + /// A local repository to handle seasons. + /// public class SeasonRepository : LocalRepository, ISeasonRepository { - private bool _disposed; + /// + /// The database handle + /// private readonly DatabaseContext _database; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + /// + /// A show repository to get show's slug from their ID and keep the slug in each episode. + /// private readonly IShowRepository _shows; + /// + /// A lazilly loaded episode repository to handle deletion of episodes with the season. + /// private readonly Lazy _episodes; + + /// protected override Expression> DefaultSort => x => x.SeasonNumber; - public SeasonRepository(DatabaseContext database, + /// + /// Create a new using the provided handle, a provider & a show repository and + /// a service provider to lazilly request an episode repository. + /// + /// The database handle that will be used + /// A provider repository + /// A show repository + /// A service provider to lazilly request an episode repository. + public SeasonRepository(DatabaseContext database, IProviderRepository providers, IShowRepository shows, IServiceProvider services) @@ -31,49 +56,25 @@ namespace Kyoo.Controllers _shows = shows; _episodes = new Lazy(services.GetRequiredService); } + - - 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(); - } - + /// public override async Task Get(int id) { Season ret = await base.Get(id); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); return ret; } - public override async Task Get(Expression> predicate) + /// + public override async Task Get(Expression> where) { - Season ret = await base.Get(predicate); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(ret.ShowID); + Season ret = await base.Get(where); + ret.ShowSlug = await _shows.GetSlug(ret.ShowID); return ret; } + /// public override Task Get(string slug) { Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); @@ -83,24 +84,41 @@ namespace Kyoo.Controllers return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); } + /// public async Task Get(int showID, int seasonNumber) { - Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber); - if (ret != null) - ret.ShowSlug = await _shows.GetSlug(showID); + 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; } + /// public async Task Get(string showSlug, int seasonNumber) { - Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber); - if (ret != null) - ret.ShowSlug = showSlug; + 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; } + /// + public Task GetOrDefault(int showID, int seasonNumber) + { + return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber); + } + + /// + public Task GetOrDefault(string showSlug, int seasonNumber) + { + return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber); + } + + /// public override async Task> Search(string query) { List seasons = await _database.Seasons @@ -113,6 +131,7 @@ namespace Kyoo.Controllers return seasons; } + /// public override async Task> GetAll(Expression> where = null, Sort sort = default, Pagination limit = default) @@ -123,6 +142,7 @@ namespace Kyoo.Controllers return seasons; } + /// public override async Task Create(Season obj) { await base.Create(obj); @@ -132,6 +152,7 @@ namespace Kyoo.Controllers return obj; } + /// protected override async Task Validate(Season resource) { if (resource.ShowID <= 0) @@ -146,6 +167,7 @@ namespace Kyoo.Controllers }); } + /// protected override async Task EditRelations(Season resource, Season changed, bool resetOld) { if (changed.ExternalIDs != null || resetOld) @@ -155,13 +177,8 @@ 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); - } - + + /// public override async Task Delete(Season obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index f07e4f5a..1129cd07 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -9,18 +9,52 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { + /// + /// A local repository to handle shows + /// public class ShowRepository : LocalRepository, IShowRepository { - private bool _disposed; + /// + /// The databse handle + /// private readonly DatabaseContext _database; + /// + /// A studio repository to handle creation/validation of related studios. + /// private readonly IStudioRepository _studios; + /// + /// A people repository to handle creation/validation of related people. + /// private readonly IPeopleRepository _people; + /// + /// A genres repository to handle creation/validation of related genres. + /// private readonly IGenreRepository _genres; + /// + /// A provider repository to handle externalID creation and deletion + /// private readonly IProviderRepository _providers; + /// + /// A lazy loaded season repository to handle cascade deletion (seasons deletion whith it's show) + /// private readonly Lazy _seasons; + /// + /// A lazy loaded episode repository to handle cascade deletion (episode deletion whith it's show) + /// private readonly Lazy _episodes; + + /// protected override Expression> DefaultSort => x => x.Title; + /// + /// Create a new . + /// + /// The database handle to use + /// A studio repository + /// A people repository + /// A genres repository + /// A provider repository + /// A service provider to lazilly request a season and an episode repository public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, @@ -37,40 +71,9 @@ namespace Kyoo.Controllers _seasons = new Lazy(services.GetRequiredService); _episodes = new Lazy(services.GetRequiredService); } + - 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(); - } - + /// public override async Task> Search(string query) { query = $"%{query}%"; @@ -83,6 +86,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(Show obj) { await base.Create(obj); @@ -94,6 +98,7 @@ namespace Kyoo.Controllers return obj; } + /// protected override async Task Validate(Show resource) { await base.Validate(resource); @@ -119,6 +124,7 @@ namespace Kyoo.Controllers }); } + /// protected override async Task EditRelations(Show resource, Show changed, bool resetOld) { await Validate(changed); @@ -145,6 +151,7 @@ namespace Kyoo.Controllers } } + /// public async Task AddShowLink(int showID, int? libraryID, int? collectionID) { if (collectionID != null) @@ -168,6 +175,7 @@ namespace Kyoo.Controllers } } + /// public Task GetSlug(int showID) { return _database.Shows.Where(x => x.ID == showID) @@ -175,6 +183,7 @@ namespace Kyoo.Controllers .FirstOrDefaultAsync(); } + /// public override async Task Delete(Show obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/StudioRepository.cs b/Kyoo/Controllers/Repositories/StudioRepository.cs index f8afc757..6c813f65 100644 --- a/Kyoo/Controllers/Repositories/StudioRepository.cs +++ b/Kyoo/Controllers/Repositories/StudioRepository.cs @@ -8,17 +8,31 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { + /// + /// A local repository to handle studios + /// public class StudioRepository : LocalRepository, IStudioRepository { + /// + /// The database handle + /// private readonly DatabaseContext _database; + + /// protected override Expression> DefaultSort => x => x.Name; - public StudioRepository(DatabaseContext database) : base(database) + /// + /// Create a new . + /// + /// The database handle + public StudioRepository(DatabaseContext database) + : base(database) { _database = database; } + /// public override async Task> Search(string query) { return await _database.Studios @@ -28,6 +42,7 @@ namespace Kyoo.Controllers .ToListAsync(); } + /// public override async Task Create(Studio obj) { await base.Create(obj); @@ -36,6 +51,7 @@ namespace Kyoo.Controllers return obj; } + /// public override async Task Delete(Studio obj) { if (obj == null) diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index d4e04376..55ddb427 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -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 { + /// + /// A local repository to handle tracks. + /// public class TrackRepository : LocalRepository, ITrackRepository { - private bool _disposed; + /// + /// The databse handle + /// private readonly DatabaseContext _database; + + /// protected override Expression> DefaultSort => x => x.TrackIndex; - public TrackRepository(DatabaseContext database) : base(database) + /// + /// Create a new . + /// + /// The datatabse handle + public TrackRepository(DatabaseContext database) + : base(database) { _database = database; } + - public override void Dispose() + /// + Task IRepository.Get(string slug) { - if (_disposed) - return; - _disposed = true; - _database.Dispose(); + return Get(slug); } - public override async ValueTask DisposeAsync() + /// + public async Task Get(string slug, StreamType type = StreamType.Unknown) { - if (_disposed) - return; - _disposed = true; - await _database.DisposeAsync(); - } - - public override Task Get(string slug) - { - return Get(slug, StreamType.Unknown); + 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 Task Get(string slug, StreamType type) + /// + public Task GetOrDefault(string slug, StreamType type = StreamType.Unknown) { Match match = Regex.Match(slug, @"(?.*)-s(?\d+)e(?\d+)(\.(?\w*))?\.(?.{0,3})(?-forced)?(\..*)?"); @@ -65,27 +74,23 @@ namespace Kyoo.Controllers if (match.Groups["type"].Success) type = Enum.Parse(match.Groups["type"].Value, true); - if (type == StreamType.Unknown) - { - return _database.Tracks.FirstOrDefaultAsync(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); + IQueryable query = _database.Tracks.Where(x => x.Episode.Show.Slug == showSlug + && x.Episode.SeasonNumber == seasonNumber + && x.Episode.EpisodeNumber == episodeNumber + && x.Language == language + && x.IsForced == forced); + if (type != StreamType.Unknown) + return query.FirstOrDefaultAsync(x => x.Type == type); + return query.FirstOrDefaultAsync(); } + /// public override Task> Search(string query) { throw new InvalidOperationException("Tracks do not support the search method."); } + /// public override async Task Create(Track obj) { if (obj.EpisodeID <= 0) @@ -107,6 +112,7 @@ namespace Kyoo.Controllers return obj; } + /// public override async Task Delete(Track obj) { if (obj == null) diff --git a/Kyoo/Controllers/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs index 68876274..b75e3231 100644 --- a/Kyoo/Controllers/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -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 GetProviderLogo(ProviderID provider) + public Task GetProviderLogo(Provider provider) { if (provider == null) throw new ArgumentNullException(nameof(provider)); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 713d807c..2d624b9c 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -113,10 +113,8 @@ - - + + diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index 435b01f7..577e9903 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -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 { + /// + /// The database handle used for all local repositories. + /// + /// + /// It should not be used directly, to access the database use a or repositories. + /// public class DatabaseContext : DbContext { - public DatabaseContext(DbContextOptions options) : base(options) - { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - ChangeTracker.LazyLoadingEnabled = false; - } - + /// + /// All libraries of Kyoo. See . + /// public DbSet Libraries { get; set; } + /// + /// All collections of Kyoo. See . + /// public DbSet Collections { get; set; } + /// + /// All shows of Kyoo. See . + /// public DbSet Shows { get; set; } + /// + /// All seasons of Kyoo. See . + /// public DbSet Seasons { get; set; } + /// + /// All episodes of Kyoo. See . + /// public DbSet Episodes { get; set; } + /// + /// All tracks of Kyoo. See . + /// public DbSet Tracks { get; set; } + /// + /// All genres of Kyoo. See . + /// public DbSet Genres { get; set; } + /// + /// All people of Kyoo. See . + /// public DbSet People { get; set; } + /// + /// All studios of Kyoo. See . + /// public DbSet Studios { get; set; } - public DbSet Providers { get; set; } + /// + /// All providers of Kyoo. See . + /// + public DbSet Providers { get; set; } + /// + /// All metadataIDs (ExternalIDs) of Kyoo. See . + /// public DbSet MetadataIds { get; set; } + /// + /// All people's role. See . + /// public DbSet PeopleRoles { get; set; } + /// + /// Get a generic link between two resource types. + /// + /// Types are order dependant. You can't inverse the order. Please always put the owner first. + /// The first resource type of the relation. It is the owner of the second + /// The second resource type of the relation. It is the contained resource. + /// All links between the two types. public DbSet> Links() where T1 : class, IResource where T2 : class, IResource @@ -39,7 +83,10 @@ namespace Kyoo return Set>(); } - + + /// + /// A basic constructor that set default values (query tracker behaviors, mapping enums...) + /// public DatabaseContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); @@ -50,6 +97,21 @@ namespace Kyoo ChangeTracker.LazyLoadingEnabled = false; } + /// + /// Create a new . + /// + /// Connection options to use (witch databse provider to use, connection strings...) + public DatabaseContext(DbContextOptions options) + : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.LazyLoadingEnabled = false; + } + + /// + /// Set database parameters to support every types of Kyoo. + /// + /// The database's model builder. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -58,14 +120,6 @@ namespace Kyoo modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); - modelBuilder.Entity() - .Property(x => x.Paths) - .HasColumnType("text[]"); - - modelBuilder.Entity() - .Property(x => x.Aliases) - .HasColumnType("text[]"); - modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); @@ -74,17 +128,17 @@ namespace Kyoo .Property(t => t.IsForced) .ValueGeneratedNever(); - modelBuilder.Entity() + modelBuilder.Entity() .HasMany(x => x.Libraries) .WithMany(x => x.Providers) - .UsingEntity>( + .UsingEntity>( y => y .HasOne(x => x.First) .WithMany(x => x.ProviderLinks), y => y .HasOne(x => x.Second) .WithMany(x => x.LibraryLinks), - y => y.HasKey(Link.PrimaryKey)); + y => y.HasKey(Link.PrimaryKey)); modelBuilder.Entity() .HasMany(x => x.Libraries) @@ -160,7 +214,7 @@ namespace Kyoo modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); - modelBuilder.Entity().Property(x => x.Slug).IsRequired(); + modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); modelBuilder.Entity().Property(x => x.Slug).IsRequired(); @@ -182,7 +236,7 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() @@ -196,6 +250,13 @@ namespace Kyoo .IsUnique(); } + /// + /// Return a new or an in cache temporary object wih the same ID as the one given + /// + /// If a resource with the same ID is found in the database, it will be used. + /// will be used overwise + /// The type of the resource + /// A resource that is now tracked by this context. public T GetTemporaryObject(T model) where T : class, IResource { @@ -206,6 +267,11 @@ namespace Kyoo return model; } + /// + /// Save changes that are applied to this context. + /// + /// A duplicated item has been found. + /// The number of state entries written to the database. public override int SaveChanges() { try @@ -221,6 +287,13 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// Indicates whether AcceptAllChanges() is called after the changes + /// have been sent successfully to the database. + /// A duplicated item has been found. + /// The number of state entries written to the database. public override int SaveChanges(bool acceptAllChangesOnSuccess) { try @@ -236,6 +309,13 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// The message that will have the + /// (if a duplicate is found). + /// A duplicated item has been found. + /// The number of state entries written to the database. public int SaveChanges(string duplicateMessage) { try @@ -251,6 +331,14 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// Indicates whether AcceptAllChanges() is called after the changes + /// have been sent successfully to the database. + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()) { @@ -267,6 +355,12 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { try @@ -282,6 +376,14 @@ namespace Kyoo } } + /// + /// Save changes that are applied to this context. + /// + /// The message that will have the + /// (if a duplicate is found). + /// A to observe while waiting for the task to complete + /// A duplicated item has been found. + /// The number of state entries written to the database. public async Task SaveChangesAsync(string duplicateMessage, CancellationToken cancellationToken = new()) { @@ -298,6 +400,12 @@ namespace Kyoo } } + /// + /// 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. + /// + /// A to observe while waiting for the task to complete + /// The number of state entries written to the database or -1 if a duplicate exist. public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new()) { try @@ -310,12 +418,31 @@ namespace Kyoo } } + /// + /// Save items or retry with a custom method if a duplicate is found. + /// + /// The item to save (other changes of this context will also be saved) + /// A function to run on fail, the param wil be mapped. + /// The second parameter is the current retry number. + /// A to observe while waiting for the task to complete + /// The type of the item to save + /// The number of state entries written to the database. public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new()) { return SaveOrRetry(obj, onFail, 0, cancellationToken); } - public async Task SaveOrRetry(T obj, + /// + /// Save items or retry with a custom method if a duplicate is found. + /// + /// The item to save (other changes of this context will also be saved) + /// A function to run on fail, the param wil be mapped. + /// The second parameter is the current retry number. + /// The current retry number. + /// A to observe while waiting for the task to complete + /// The type of the item to save + /// The number of state entries written to the database. + private async Task SaveOrRetry(T obj, Func onFail, int recurse, CancellationToken cancellationToken = new()) @@ -337,11 +464,20 @@ namespace Kyoo } } + /// + /// Check if the exception is a duplicated exception. + /// + /// WARNING: this only works for PostgreSQL + /// The exception to check + /// True if the exception is a duplicate exception. False otherwise private static bool IsDuplicateException(Exception ex) { return ex.InnerException is PostgresException {SqlState: PostgresErrorCodes.UniqueViolation}; } + /// + /// Delete every changes that are on this context. + /// private void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs similarity index 98% rename from Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs index b0c7add7..c27925c9 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.Designer.cs @@ -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"); }); - modelBuilder.Entity("Kyoo.Models.Link", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") .HasColumnType("integer"); @@ -191,7 +191,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("SecondID"); - b.ToTable("Link"); + b.ToTable("Link"); }); modelBuilder.Entity("Kyoo.Models.Link", 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("ID") .ValueGeneratedOnAdd() @@ -563,7 +563,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.Link", b => + modelBuilder.Entity("Kyoo.Models.Link", 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"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs similarity index 98% rename from Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs index f3825e97..56051cb1 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20210325184215_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20210420221509_Initial.cs @@ -128,7 +128,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }); migrationBuilder.CreateTable( - name: "Link", + name: "Link", columns: table => new { FirstID = table.Column(type: "integer", nullable: false), @@ -136,15 +136,15 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }, constraints: table => { - table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); + table.PrimaryKey("PK_Link", x => new { x.FirstID, x.SecondID }); table.ForeignKey( - name: "FK_Link_Libraries_FirstID", + name: "FK_Link_Libraries_FirstID", column: x => x.FirstID, principalTable: "Libraries", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_Link_Providers_SecondID", + name: "FK_Link_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_SecondID", - table: "Link", + name: "IX_Link_SecondID", + table: "Link", column: "SecondID"); migrationBuilder.CreateIndex( @@ -559,7 +559,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Link"); migrationBuilder.DropTable( - name: "Link"); + name: "Link"); migrationBuilder.DropTable( name: "Link"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs index 91eef22c..11d6b186 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs @@ -177,7 +177,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.ToTable("Link"); }); - modelBuilder.Entity("Kyoo.Models.Link", b => + modelBuilder.Entity("Kyoo.Models.Link", b => { b.Property("FirstID") .HasColumnType("integer"); @@ -189,7 +189,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("SecondID"); - b.ToTable("Link"); + b.ToTable("Link"); }); modelBuilder.Entity("Kyoo.Models.Link", 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("ID") .ValueGeneratedOnAdd() @@ -561,7 +561,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Navigation("Second"); }); - modelBuilder.Entity("Kyoo.Models.Link", b => + modelBuilder.Entity("Kyoo.Models.Link", 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"); diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 81e73def..1fa2b7c3 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -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,12 +22,9 @@ namespace Kyoo /// Command line arguments 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 { "d" => true, @@ -49,15 +50,50 @@ namespace Kyoo await host.Build().RunAsync(); } + /// + /// Register settings.json, environment variables and command lines arguments as configuration. + /// + /// The configuration builder to use + /// The command line arguments + /// The modified configuration builder + private static IConfigurationBuilder SetupConfig(IConfigurationBuilder builder, string[] args) + { + return builder.AddJsonFile("./settings.json", false, true) + .AddEnvironmentVariables() + .AddCommandLine(args); + } + /// /// Createa a web host /// /// Command line parameters that can be handled by kestrel /// A new web host instance - 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(); + } } } diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index ac05a36d..8e1485e3 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -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 and the IBaseRepository + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 41028ce9..47c931c9 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -32,8 +32,8 @@ namespace Kyoo.Controllers public async Task> GetPossibleParameters() { using IServiceScope serviceScope = _serviceProvider.CreateScope(); - await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - return (await libraryManager!.GetLibraries()).Select(x => x.Slug); + ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); + return (await libraryManager!.GetAll()).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 libraryManager = serviceScope.ServiceProvider.GetService(); - foreach (Show show in await libraryManager!.GetShows()) + foreach (Show show in await libraryManager!.GetAll()) if (!Directory.Exists(show.Path)) - await libraryManager.DeleteShow(show); + await libraryManager.Delete(show); - ICollection episodes = await libraryManager.GetEpisodes(); + ICollection episodes = await libraryManager.GetAll(); foreach (Episode episode in episodes) if (!File.Exists(episode.Path)) - await libraryManager.DeleteEpisode(episode); + await libraryManager.Delete(episode); - ICollection tracks = await libraryManager.GetTracks(); + ICollection tracks = await libraryManager.GetAll(); foreach (Track track in tracks) if (!File.Exists(track.Path)) - await libraryManager.DeleteTrack(track); + await libraryManager.Delete(track); ICollection libraries = argument == null - ? await libraryManager.GetLibraries() - : new [] { await libraryManager.GetLibrary(argument)}; + ? await libraryManager.GetAll() + : new [] { await libraryManager.Get(argument)}; if (argument != null && libraries.First() == null) throw new ArgumentException($"No library found with the name {argument}"); @@ -144,44 +144,48 @@ namespace Kyoo.Controllers private async Task RegisterExternalSubtitle(string path, CancellationToken token) { - if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) - return; - using IServiceScope serviceScope = _serviceProvider.CreateScope(); - await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - - string patern = _config.GetValue("subtitleRegex"); - Regex regex = new(patern, RegexOptions.IgnoreCase); - Match match = regex.Match(path); - - if (!match.Success) + try { - await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex."); - return; + if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) + return; + using IServiceScope serviceScope = _serviceProvider.CreateScope(); + ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); + + string patern = _config.GetValue("subtitleRegex"); + Regex regex = new(patern, RegexOptions.IgnoreCase); + Match match = regex.Match(path); + + if (!match.Success) + { + await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex."); + return; + } + + string episodePath = match.Groups["Episode"].Value; + Episode episode = await libraryManager!.Get(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.Create(track); + Console.WriteLine($"Registering subtitle at: {path}."); } - - string episodePath = match.Groups["Episode"].Value; - Episode episode = await libraryManager!.GetEpisode(x => x.Path.StartsWith(episodePath)); - - if (episode == null) + catch (ItemNotFound) { 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) + catch (Exception ex) { - Episode = episode - }; - - await libraryManager.RegisterTrack(track); - Console.WriteLine($"Registering subtitle at: {path}."); + 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 libraryManager = serviceScope.ServiceProvider.GetService(); string patern = _config.GetValue("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(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.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(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.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,22 +312,20 @@ namespace Kyoo.Controllers { if (seasonNumber == -1) return default; - Season season = await libraryManager.GetSeason(show.Slug, seasonNumber); - if (season == null) + try { - 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; } - season.Show = show; - return season; } private async Task GetEpisode(ILibraryManager libraryManager, diff --git a/Kyoo/Tasks/ExtractMetadata.cs b/Kyoo/Tasks/ExtractMetadata.cs index 73ed031b..d5513c97 100644 --- a/Kyoo/Tasks/ExtractMetadata.cs +++ b/Kyoo/Tasks/ExtractMetadata.cs @@ -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(id) + : _library!.Get(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(id) + : _library!.Get(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(id) + : _library!.Get(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); } } diff --git a/Kyoo/Views/CollectionApi.cs b/Kyoo/Views/CollectionApi.cs index 5bba0650..0d5b0be5 100644 --- a/Kyoo/Views/CollectionApi.cs +++ b/Kyoo/Views/CollectionApi.cs @@ -35,12 +35,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.ID == id)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetCollection(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -61,12 +61,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetCollection(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -87,12 +87,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetLibraries( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.ID == id)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetCollection(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -113,12 +113,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetLibraries( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Collections.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetCollection(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index c0c01a18..8dd28956 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -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> GetShow(int episodeID) { - return await _libraryManager.GetShow(x => x.Episodes.Any(y => y.ID == episodeID)); + return await _libraryManager.Get(x => x.Episodes.Any(y => y.ID == episodeID)); } [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] [Authorize(Policy = "Read")] - public async Task> GetShow(string showSlug) + public async Task> GetShow(string showSlug, int seasonNumber, int episodeNumber) { - return await _libraryManager.GetShow(showSlug); + return await _libraryManager.Get(showSlug); } [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] [Authorize(Policy = "Read")] - public async Task> GetShow(int showID, int _) + public async Task> GetShow(int showID, int seasonNumber, int episodeNumber) { - return await _libraryManager.GetShow(showID); + return await _libraryManager.Get(showID); } [HttpGet("{episodeID:int}/season")] [Authorize(Policy = "Read")] public async Task> GetSeason(int episodeID) { - return await _libraryManager.GetSeason(x => x.Episodes.Any(y => y.ID == episodeID)); + return await _libraryManager.Get(x => x.Episodes.Any(y => y.ID == episodeID)); } [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] [Authorize(Policy = "Read")] - public async Task> GetSeason(string showSlug, int seasonNuber) + public async Task> 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> GetSeason(int showID, int seasonNumber) + public async Task> 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 resources = await _libraryManager.GetTracks( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Episode.ID == episodeID), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetEpisode(episodeID) == null) + if (!resources.Any() && await _libraryManager.Get(episodeID) == null) return NotFound(); return Page(resources, limit); } @@ -112,14 +127,14 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetTracks( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Episode.ShowID == showID && x.Episode.SeasonNumber == seasonNumber && x.Episode.EpisodeNumber == episodeNumber), new Sort(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>> GetEpisode(string showSlug, + public async Task>> GetEpisode(string slug, int seasonNumber, int episodeNumber, [FromQuery] string sortBy, @@ -142,13 +157,14 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetTracks(ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == showSlug + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == slug && x.Episode.SeasonNumber == seasonNumber && x.Episode.EpisodeNumber == episodeNumber), new Sort(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 GetThumb(int id) { - Episode episode = await _libraryManager.GetEpisode(id); - if (episode == null) + try + { + Episode episode = await _libraryManager.Get(id); + return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); + } } [HttpGet("{slug}/thumb")] [Authorize(Policy="Read")] public async Task GetThumb(string slug) { - Episode episode = await _libraryManager.GetEpisode(slug); - if (episode == null) + try + { + Episode episode = await _libraryManager.Get(slug); + return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); + } } } } \ No newline at end of file diff --git a/Kyoo/Views/GenreApi.cs b/Kyoo/Views/GenreApi.cs index 29393e97..ab40f20b 100644 --- a/Kyoo/Views/GenreApi.cs +++ b/Kyoo/Views/GenreApi.cs @@ -36,12 +36,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.ID == id)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetGenre(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -62,12 +62,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetGenre(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/LibraryApi.cs b/Kyoo/Views/LibraryApi.cs index ba9ad3d0..93081db6 100644 --- a/Kyoo/Views/LibraryApi.cs +++ b/Kyoo/Views/LibraryApi.cs @@ -47,12 +47,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Libraries.Any(y => y.ID == id)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -73,12 +73,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Libraries.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -99,12 +99,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetCollections( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Libraries.Any(y => y.ID == id)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -125,12 +125,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetCollections( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Libraries.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -156,7 +156,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -182,7 +182,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetLibrary(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/PeopleApi.cs b/Kyoo/Views/PeopleApi.cs index bfcb8a29..8f80b6f2 100644 --- a/Kyoo/Views/PeopleApi.cs +++ b/Kyoo/Views/PeopleApi.cs @@ -90,7 +90,7 @@ namespace Kyoo.Api [Authorize(Policy="Read")] public async Task GetPeopleIcon(int id) { - People people = await _libraryManager.GetPeople(id); + People people = await _libraryManager.Get(id); return _files.FileResult(await _thumbs.GetPeoplePoster(people)); } @@ -98,7 +98,7 @@ namespace Kyoo.Api [Authorize(Policy="Read")] public async Task GetPeopleIcon(string slug) { - People people = await _libraryManager.GetPeople(slug); + People people = await _libraryManager.Get(slug); return _files.FileResult(await _thumbs.GetPeoplePoster(people)); } } diff --git a/Kyoo/Views/ProviderApi.cs b/Kyoo/Views/ProviderApi.cs index 2d3aab3d..050f2681 100644 --- a/Kyoo/Views/ProviderApi.cs +++ b/Kyoo/Views/ProviderApi.cs @@ -11,7 +11,7 @@ namespace Kyoo.Api [Route("api/provider")] [Route("api/providers")] [ApiController] - public class ProviderAPI : CrudApi + public class ProviderAPI : CrudApi { private readonly IThumbnailsManager _thumbnails; private readonly ILibraryManager _libraryManager; @@ -32,7 +32,7 @@ namespace Kyoo.Api [Authorize(Policy="Read")] public async Task GetLogo(int id) { - ProviderID provider = await _libraryManager.GetProvider(id); + Provider provider = await _libraryManager.Get(id); return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); } @@ -40,7 +40,7 @@ namespace Kyoo.Api [Authorize(Policy="Read")] public async Task GetLogo(string slug) { - ProviderID provider = await _libraryManager.GetProvider(slug); + Provider provider = await _libraryManager.Get(slug); return _files.FileResult(await _thumbnails.GetProviderLogo(provider)); } } diff --git a/Kyoo/Views/SearchApi.cs b/Kyoo/Views/SearchApi.cs index c253adfd..008bf0f5 100644 --- a/Kyoo/Views/SearchApi.cs +++ b/Kyoo/Views/SearchApi.cs @@ -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> 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(query), + Shows = await _libraryManager.Search(query), + Episodes = await _libraryManager.Search(query), + People = await _libraryManager.Search(query), + Genres = await _libraryManager.Search(query), + Studios = await _libraryManager.Search(query) }; } - [HttpGet("{query}/collection")] - [HttpGet("{query}/collections")] + [HttpGet("collection")] + [HttpGet("collections")] [Authorize(Policy="Read")] public Task> SearchCollections(string query) { - return _libraryManager.SearchCollections(query); + return _libraryManager.Search(query); } - [HttpGet("{query}/show")] - [HttpGet("{query}/shows")] + [HttpGet("show")] + [HttpGet("shows")] [Authorize(Policy="Read")] public Task> SearchShows(string query) { - return _libraryManager.SearchShows(query); + return _libraryManager.Search(query); } - [HttpGet("{query}/episode")] - [HttpGet("{query}/episodes")] + [HttpGet("episode")] + [HttpGet("episodes")] [Authorize(Policy="Read")] public Task> SearchEpisodes(string query) { - return _libraryManager.SearchEpisodes(query); + return _libraryManager.Search(query); } - [HttpGet("{query}/people")] + [HttpGet("people")] [Authorize(Policy="Read")] public Task> SearchPeople(string query) { - return _libraryManager.SearchPeople(query); + return _libraryManager.Search(query); } - [HttpGet("{query}/genre")] - [HttpGet("{query}/genres")] + [HttpGet("genre")] + [HttpGet("genres")] [Authorize(Policy="Read")] public Task> SearchGenres(string query) { - return _libraryManager.SearchGenres(query); + return _libraryManager.Search(query); } - [HttpGet("{query}/studio")] - [HttpGet("{query}/studios")] + [HttpGet("studio")] + [HttpGet("studios")] [Authorize(Policy="Read")] public Task> SearchStudios(string query) { - return _libraryManager.SearchStudios(query); + return _libraryManager.Search(query); } } } \ No newline at end of file diff --git a/Kyoo/Views/SeasonApi.cs b/Kyoo/Views/SeasonApi.cs index bc43cad5..9803f956 100644 --- a/Kyoo/Views/SeasonApi.cs +++ b/Kyoo/Views/SeasonApi.cs @@ -42,12 +42,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetEpisodes( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.SeasonID == seasonID), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetSeason(seasonID) == null) + if (!resources.Any() && await _libraryManager.Get(seasonID) == null) return NotFound(); return Page(resources, limit); } @@ -69,13 +69,13 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetEpisodes( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Show.Slug == showSlug && x.SeasonNumber == seasonNumber), new Sort(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 resources = await _libraryManager.GetEpisodes( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.ShowID == showID && x.SeasonNumber == seasonNumber), new Sort(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> GetShow(int seasonID) { - return await _libraryManager.GetShow(x => x.Seasons.Any(y => y.ID == seasonID)); + return await _libraryManager.Get(x => x.Seasons.Any(y => y.ID == seasonID)); } [HttpGet("{showSlug}-s{seasonNumber:int}/show")] [Authorize(Policy = "Read")] - public async Task> GetShow(string showSlug, int _) + public async Task> GetShow(string showSlug, int seasonNumber) { - return await _libraryManager.GetShow(showSlug); + return await _libraryManager.Get(showSlug); } [HttpGet("{showID:int}-s{seasonNumber:int}/show")] [Authorize(Policy = "Read")] - public async Task> GetShow(int showID, int _) + public async Task> GetShow(int showID, int seasonNumber) { - return await _libraryManager.GetShow(showID); + return await _libraryManager.Get(showID); } [HttpGet("{id:int}/thumb")] [Authorize(Policy="Read")] public async Task GetThumb(int id) { - Season season = await _libraryManager.GetSeason(id); + Season season = await _libraryManager.Get(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 GetThumb(string slug) { - Season season = await _libraryManager.GetSeason(slug); + Season season = await _libraryManager.Get(slug); await _libraryManager.Load(season, x => x.Show); return _files.FileResult(await _thumbs.GetSeasonPoster(season)); } diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index 703f8c45..de623916 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -44,12 +44,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetSeasons( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.ShowID == showID), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -70,12 +70,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetSeasons( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Show.Slug == slug), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -96,12 +96,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetEpisodes( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.ShowID == showID), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -122,12 +122,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetEpisodes( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Show.Slug == slug), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -152,7 +152,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -177,7 +177,7 @@ namespace Kyoo.Api new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -198,12 +198,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetGenres( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.ID == showID)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -224,12 +224,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetGenres( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(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(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(x => x.Shows.Any(y => y.Slug == slug)); } catch (ItemNotFound) { @@ -278,12 +278,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetLibraries( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.ID == showID)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -304,12 +304,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetLibraries( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -330,12 +330,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetCollections( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.ID == showID)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(showID) == null) + if (!resources.Any() && await _libraryManager.Get(showID) == null) return NotFound(); return Page(resources, limit); } @@ -356,12 +356,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetCollections( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Shows.Any(y => y.Slug == slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetShow(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } @@ -376,13 +376,18 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task>> GetFonts(string slug) { - Show show = await _libraryManager.GetShow(slug); - if (show == null) + try + { + Show show = await _libraryManager.Get(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(); - 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)}"); + } } [HttpGet("{showSlug}/font/{slug}")] @@ -390,41 +395,61 @@ namespace Kyoo.Api [Authorize(Policy = "Read")] public async Task GetFont(string showSlug, string slug) { - Show show = await _libraryManager.GetShow(showSlug); - if (show == null) + try + { + Show show = await _libraryManager.Get(showSlug); + string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); + return _files.FileResult(path); + } + catch (ItemNotFound) + { return NotFound(); - string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); - return _files.FileResult(path); + } } [HttpGet("{slug}/poster")] [Authorize(Policy = "Read")] public async Task GetPoster(string slug) { - Show show = await _libraryManager.GetShow(slug); - if (show == null) + try + { + Show show = await _libraryManager.Get(slug); + return _files.FileResult(await _thumbs.GetShowPoster(show)); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(await _thumbs.GetShowPoster(show)); + } } [HttpGet("{slug}/logo")] [Authorize(Policy="Read")] public async Task GetLogo(string slug) { - Show show = await _libraryManager.GetShow(slug); - if (show == null) + try + { + Show show = await _libraryManager.Get(slug); + return _files.FileResult(await _thumbs.GetShowLogo(show)); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(await _thumbs.GetShowLogo(show)); + } } [HttpGet("{slug}/backdrop")] [Authorize(Policy="Read")] public async Task GetBackdrop(string slug) { - Show show = await _libraryManager.GetShow(slug); - if (show == null) + try + { + Show show = await _libraryManager.Get(slug); + return _files.FileResult(await _thumbs.GetShowBackdrop(show)); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(await _thumbs.GetShowBackdrop(show)); + } } } } diff --git a/Kyoo/Views/StudioApi.cs b/Kyoo/Views/StudioApi.cs index a0cf872e..45f46829 100644 --- a/Kyoo/Views/StudioApi.cs +++ b/Kyoo/Views/StudioApi.cs @@ -35,12 +35,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.StudioID == id), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetStudio(id) == null) + if (!resources.Any() && await _libraryManager.Get(id) == null) return NotFound(); return Page(resources, limit); } @@ -61,12 +61,12 @@ namespace Kyoo.Api { try { - ICollection resources = await _libraryManager.GetShows( + ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, x => x.Studio.Slug == slug), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetStudio(slug) == null) + if (!resources.Any() && await _libraryManager.Get(slug) == null) return NotFound(); return Page(resources, limit); } diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 73f151ee..4ae053de 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -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") diff --git a/Kyoo/Views/TrackApi.cs b/Kyoo/Views/TrackApi.cs index 689e904b..79bced62 100644 --- a/Kyoo/Views/TrackApi.cs +++ b/Kyoo/Views/TrackApi.cs @@ -29,7 +29,7 @@ namespace Kyoo.Api { try { - return await _libraryManager.GetEpisode(x => x.Tracks.Any(y => y.ID == id)); + return await _libraryManager.Get(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(x => x.Tracks.Any(y => y.Slug == slug)); } catch (ItemNotFound) { diff --git a/Kyoo/Views/VideoApi.cs b/Kyoo/Views/VideoApi.cs index 1e6651b5..13c53e40 100644 --- a/Kyoo/Views/VideoApi.cs +++ b/Kyoo/Views/VideoApi.cs @@ -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; @@ -40,92 +41,59 @@ namespace Kyoo.Api ctx.HttpContext.Response.Headers.Add("Expires", "0"); } - - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] - [HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")] - [Authorize(Policy="Play")] - public async Task DirectEpisode(string showSlug, int seasonNumber, int episodeNumber) - { - 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(); - return _files.FileResult(episode.Path, true); - } - [HttpGet("{movieSlug}")] - [HttpGet("direct/{movieSlug}")] + [HttpGet("{slug}")] + [HttpGet("direct/{slug}")] [Authorize(Policy="Play")] - public async Task DirectMovie(string movieSlug) + public async Task Direct(string slug) { - Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); - - if (episode == null) + try + { + Episode episode = await _libraryManager.Get(slug); + return _files.FileResult(episode.Path, true); + } + catch (ItemNotFound) + { return NotFound(); - return _files.FileResult(episode.Path, true); - } - - - [HttpGet("transmux/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")] - [Authorize(Policy="Play")] - public async Task TransmuxEpisode(string showSlug, int seasonNumber, int episodeNumber) - { - 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(); - 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 TransmuxMovie(string movieSlug) - { - 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("transmux/{slug}/master.m3u8")] [Authorize(Policy="Play")] - public async Task TranscodeEpisode(string showSlug, int seasonNumber, int episodeNumber) + public async Task 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(); - 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 TranscodeMovie(string movieSlug) - { - Episode episode = await _libraryManager.GetMovieEpisode(movieSlug); + try + { + Episode episode = await _libraryManager.Get(slug); + string path = await _transcoder.Transmux(episode); - if (episode == null) + if (path == null) + return StatusCode(500); + return _files.FileResult(path, true); + } + catch (ItemNotFound) + { return NotFound(); - string path = await _transcoder.Transcode(episode); - if (path == null) - return StatusCode(500); - return _files.FileResult(path, true); + } + } + + [HttpGet("transcode/{slug}/master.m3u8")] + [Authorize(Policy="Play")] + public async Task Transcode(string slug) + { + try + { + Episode episode = await _libraryManager.Get(slug); + string path = await _transcoder.Transcode(episode); + + if (path == null) + return StatusCode(500); + return _files.FileResult(path, true); + } + catch (ItemNotFound) + { + return NotFound(); + } } diff --git a/Kyoo/Views/WatchApi.cs b/Kyoo/Views/WatchApi.cs index fec777bc..cd9327ae 100644 --- a/Kyoo/Views/WatchApi.cs +++ b/Kyoo/Views/WatchApi.cs @@ -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> GetWatchItem(string showSlug, int seasonNumber, int episodeNumber) + public async Task> GetWatchItem(string slug) { - Episode item = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - if (item == null) + try + { + Episode item = await _libraryManager.Get(slug); + return await WatchItem.FromEpisode(item, _libraryManager); + } + catch (ItemNotFound) + { return NotFound(); - return await WatchItem.FromEpisode(item, _libraryManager); - } - - [HttpGet("{movieSlug}")] - [Authorize(Policy="Read")] - public async Task> GetWatchItem(string movieSlug) - { - Episode item = await _libraryManager.GetMovieEpisode(movieSlug); - if (item == null) - return NotFound(); - return await WatchItem.FromEpisode(item, _libraryManager); + } } } } diff --git a/Kyoo/appsettings.json b/Kyoo/settings.json similarity index 56% rename from Kyoo/appsettings.json rename to Kyoo/settings.json index c49df121..bdd2f362 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/settings.json @@ -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", diff --git a/deployment/kyoo.service b/deployment/kyoo.service index 0eceef42..277c26ca 100644 --- a/deployment/kyoo.service +++ b/deployment/kyoo.service @@ -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