diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 72c211ae..9abda1c4 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.InteropServices; using System.Threading.Tasks; using JetBrains.Annotations; using Kyoo.Models; @@ -8,6 +10,31 @@ namespace Kyoo.Controllers { public interface ILibraryManager : IDisposable, IAsyncDisposable { + // Repositories + ILibraryRepository LibraryRepository { get; } + ILibraryItemRepository LibraryItemRepository { get; } + ICollectionRepository CollectionRepository { get; } + IShowRepository ShowRepository { get; } + ISeasonRepository SeasonRepository { get; } + IEpisodeRepository EpisodeRepository { get; } + ITrackRepository TrackRepository { get; } + IPeopleRepository PeopleRepository { get; } + IStudioRepository StudioRepository { get; } + IGenreRepository GenreRepository { get; } + 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 GetStudio(int id); + Task GetPeople(int id); + // Get by slug Task GetLibrary(string slug); Task GetCollection(string slug); @@ -16,38 +43,408 @@ namespace Kyoo.Controllers Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber); Task GetMovieEpisode(string movieSlug); Task GetTrack(int id); - Task GetTrack(int episodeID, string language, bool isForced); Task GetGenre(string slug); Task GetStudio(string slug); Task GetPeople(string slug); // Get by relations - Task> GetSeasons(int showID); - Task> GetSeasons(string showSlug); + Task> GetSeasonsFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetSeasonsFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetSeasonsFromShow(showID, where, new Sort(sort), limit); + Task> GetSeasonsFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetSeasonsFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetSeasonsFromShow(showSlug, where, new Sort(sort), limit); - Task> GetEpisodes(int showID, int seasonNumber); - Task> GetEpisodes(string showSlug, int seasonNumber); - Task> GetEpisodes(int seasonID); + Task> GetEpisodesFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromShow(showID, where, new Sort(sort), limit); + Task> GetEpisodesFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromShow(showSlug, where, new Sort(sort), limit); + Task> GetEpisodesFromSeason(int seasonID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromSeason(int seasonID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromSeason(seasonID, where, new Sort(sort), limit); + + Task> GetEpisodesFromSeason(int showID, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromSeason(int showID, + int seasonNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromSeason(showID, seasonNumber, where, new Sort(sort), limit); + + Task> GetEpisodesFromSeason(string showSlug, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromSeason(string showSlug, + int seasonNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromSeason(showSlug, seasonNumber, where, new Sort(sort), limit); + + Task> GetPeopleFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetPeopleFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetPeopleFromShow(showID, where, new Sort(sort), limit); + + Task> GetPeopleFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetPeopleFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetPeopleFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetGenresFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetGenresFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetGenresFromShow(showID, where, new Sort(sort), limit); + + Task> GetGenresFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetGenresFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetGenresFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetTracksFromEpisode(int episodeID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetTracksFromEpisode(int episodeID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetTracksFromEpisode(episodeID, where, new Sort(sort), limit); + + Task> GetTracksFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetTracksFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetTracksFromEpisode(showID, seasonNumber, episodeNumber, where, new Sort(sort), limit); + + Task> GetTracksFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetTracksFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetTracksFromEpisode(showSlug, seasonNumber, episodeNumber, where, new Sort(sort), limit); + + Task GetStudioFromShow(int showID); + Task GetStudioFromShow(string showSlug); + Task GetShowFromSeason(int seasonID); + Task GetShowFromEpisode(int episodeID); + Task GetSeasonFromEpisode(int episodeID); + + Task> GetLibrariesFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetLibrariesFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetLibrariesFromShow(showID, where, new Sort(sort), limit); + + Task> GetLibrariesFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetLibrariesFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetLibrariesFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetCollectionsFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetCollectionsFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetCollectionsFromShow(showID, where, new Sort(sort), limit); + + Task> GetCollectionsFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetCollectionsFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetCollectionsFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetShowsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetShowsFromLibrary(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetShowsFromLibrary(id, where, new Sort(sort), limit); + + Task> GetShowsFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetShowsFromLibrary(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetShowsFromLibrary(slug, where, new Sort(sort), limit); + + Task> GetCollectionsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetCollectionsFromLibrary(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetCollectionsFromLibrary(id, where, new Sort(sort), limit); + + Task> GetCollectionsFromLibrary(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetCollectionsFromLibrary(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetCollectionsFromLibrary(showSlug, where, new Sort(sort), limit); + + Task> GetItemsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetItemsFromLibrary(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetItemsFromLibrary(id, where, new Sort(sort), limit); + + Task> GetItemsFromLibrary(string librarySlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetItemsFromLibrary(string librarySlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetItemsFromLibrary(librarySlug, where, new Sort(sort), limit); + + Task> GetShowsFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetShowsFromCollection(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetShowsFromCollection(id, where, new Sort(sort), limit); + + Task> GetShowsFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetShowsFromCollection(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetShowsFromCollection(slug, where, new Sort(sort), limit); + + Task> GetLibrariesFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetLibrariesFromCollection(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetLibrariesFromCollection(id, where, new Sort(sort), limit); + + Task> GetLibrariesFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetLibrariesFromCollection(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetLibrariesFromCollection(slug, where, new Sort(sort), limit); + + 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, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetRolesFromPeople(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetRolesFromPeople(showSlug, where, new Sort(sort), limit); + + // Helpers - Task GetShowByPath(string path); Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink([NotNull] Show show, Library library, Collection collection); // Get all - Task> GetLibraries(); - Task> GetCollections(); - Task> GetShows(); - Task> GetSeasons(); - Task> GetEpisodes(); - Task> GetTracks(); - Task> GetStudios(); - Task> GetPeoples(); - Task> GetGenres(); - Task> GetProviders(); + 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> GetSeasonsFromShow(Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetEpisodesFromShow(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> GetSeasonsFromShow([Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetSeasonsFromShow(where, new Sort(sort), limit); + Task> GetEpisodesFromShow([Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetEpisodesFromShow(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); - // Search + + // Search Task> SearchLibraries(string searchQuery); Task> SearchCollections(string searchQuery); Task> SearchShows(string searchQuery); @@ -58,26 +455,26 @@ namespace Kyoo.Controllers Task> SearchPeople(string searchQuery); //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); + 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 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); + 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 values @@ -90,5 +487,27 @@ namespace Kyoo.Controllers Task DeleteGenre(Genre genre); Task DeleteStudio(Studio studio); Task DeletePeople(People people); + + //Delete by slug + Task DelteLibrary(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 DelteLibrary(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); } } diff --git a/Kyoo.Common/Controllers/IRepository.cs b/Kyoo.Common/Controllers/IRepository.cs index caef30a6..01877446 100644 --- a/Kyoo.Common/Controllers/IRepository.cs +++ b/Kyoo.Common/Controllers/IRepository.cs @@ -1,21 +1,91 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Runtime.InteropServices; using System.Threading.Tasks; using JetBrains.Annotations; using Kyoo.Models; namespace Kyoo.Controllers { - public interface IRepository : IDisposable, IAsyncDisposable + public readonly struct Pagination + { + public int Count { get; } + public int AfterID { get; } + + public Pagination(int count, int afterID = 0) + { + Count = count; + AfterID = afterID; + } + + public static implicit operator Pagination(int limit) => new Pagination(limit); + } + + public struct Sort + { + public Expression> Key; + public bool Descendant; + + public Sort(Expression> key, bool descendant = false) + { + Key = key; + Descendant = descendant; + + if (Key.Body is MemberExpression || + Key.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)Key.Body).Operand is MemberExpression) + return; + + throw new ArgumentException("The given sort key is not valid."); + } + + public Sort(string sortBy) + { + if (string.IsNullOrEmpty(sortBy)) + { + Key = null; + Descendant = false; + return; + } + + string key = sortBy.Contains(':') ? sortBy.Substring(0, sortBy.IndexOf(':')) : sortBy; + string order = sortBy.Contains(':') ? sortBy.Substring(sortBy.IndexOf(':') + 1) : null; + + ParameterExpression param = Expression.Parameter(typeof(T), "x"); + MemberExpression property = Expression.Property(param, key); + Key = property.Type.IsValueType + ? Expression.Lambda>(Expression.Convert(property, typeof(object)), param) + : Expression.Lambda>(property, param); + + Descendant = order switch + { + "desc" => true, + "asc" => false, + null => false, + _ => throw new ArgumentException($"The sort order, if set, should be :asc or :desc but it was :{order}.") + }; + } + } + + public interface IRepository : IDisposable, IAsyncDisposable where T : IResource { Task Get(int id); Task Get(string slug); Task> Search(string query); - Task> GetAll(); - Task Create([NotNull] T obj); - Task CreateIfNotExists([NotNull] T obj); - Task Edit([NotNull] T edited, bool resetOld); + + Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default); + + Task> GetAll([Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetAll(where, new Sort(sort), limit); + + Task Create([NotNull] T obj); + Task CreateIfNotExists([NotNull] T obj); + Task Edit([NotNull] T edited, bool resetOld); Task Delete(int id); Task Delete(string slug); @@ -31,37 +101,365 @@ namespace Kyoo.Controllers public interface IShowRepository : IRepository { - Task GetByPath(string path); Task AddShowLink(int showID, int? libraryID, int? collectionID); + + Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromLibrary(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromLibrary(id, where, new Sort(sort), limit); + + Task> GetFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromLibrary(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromLibrary(slug, where, new Sort(sort), limit); + + Task> GetFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromCollection(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromCollection(id, where, new Sort(sort), limit); + + Task> GetFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromCollection(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromCollection(slug, where, new Sort(sort), limit); + + Task GetFromSeason(int seasonID); + Task GetFromEpisode(int episodeID); } public interface ISeasonRepository : IRepository { + Task Get(int showID, int seasonNumber); Task Get(string showSlug, int seasonNumber); Task Delete(string showSlug, int seasonNumber); - Task> GetSeasons(int showID); - Task> GetSeasons(string showSlug); + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + + Task GetFromEpisode(int episodeID); } public interface IEpisodeRepository : IRepository { + Task Get(int showID, int seasonNumber, int episodeNumber); Task Get(string showSlug, int seasonNumber, int episodeNumber); + Task Get(int seasonID, int episodeNumber); + Task GetAbsolute(int showID, int absoluteNumber); + Task GetAbsolute(string showSlug, int absoluteNumber); Task Delete(string showSlug, int seasonNumber, int episodeNumber); - Task> GetEpisodes(int showID, int seasonNumber); - Task> GetEpisodes(string showSlug, int seasonNumber); - Task> GetEpisodes(int seasonID); + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetFromSeason(int seasonID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromSeason(int seasonID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromSeason(seasonID, where, new Sort(sort), limit); + Task> GetFromSeason(int showID, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromSeason(int showID, + int seasonNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromSeason(showID, seasonNumber, where, new Sort(sort), limit); + Task> GetFromSeason(string showSlug, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromSeason(string showSlug, + int seasonNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromSeason(showSlug, seasonNumber, where, new Sort(sort), limit); } public interface ITrackRepository : IRepository { - Task Get(int episodeID, string languageTag, bool isForced); + Task> GetFromEpisode(int episodeID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromEpisode(int episodeID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromEpisode(episodeID, where, new Sort(sort), limit); + + Task> GetFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromEpisode(showID, seasonNumber, episodeNumber, where, new Sort(sort), limit); + + Task> GetFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromEpisode(showSlug, seasonNumber, episodeNumber, where, new Sort(sort), limit); + } + + public interface ILibraryRepository : IRepository + { + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromCollection(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromCollection(id, where, new Sort(sort), limit); + + Task> GetFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromCollection(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromCollection(slug, where, new Sort(sort), limit); + } + + public interface ILibraryItemRepository : IRepository + { + public Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + + 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, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + + public Task> GetFromLibrary(string librarySlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromLibrary(librarySlug, where, new Sort(sort), limit); + } + + public interface ICollectionRepository : IRepository + { + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromLibrary(int id, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromLibrary(id, where, new Sort(sort), limit); + + Task> GetFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromLibrary(string slug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromLibrary(slug, where, new Sort(sort), limit); + } + + public interface IGenreRepository : IRepository + { + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + } + + public interface IStudioRepository : IRepository + { + Task GetFromShow(int showID); + Task GetFromShow(string showSlug); + } + + public interface IPeopleRepository : IRepository + { + Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showID, where, new Sort(sort), limit); + + Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromShow(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromShow(showSlug, where, new Sort(sort), limit); + + Task> GetFromPeople(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromPeople(int showID, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromPeople(showID, where, new Sort(sort), limit); + + Task> GetFromPeople(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default); + Task> GetFromPeople(string showSlug, + [Optional] Expression> where, + Expression> sort, + Pagination limit = default + ) => GetFromPeople(showSlug, where, new Sort(sort), limit); } - public interface ILibraryRepository : IRepository {} - public interface ICollectionRepository : IRepository {} - public interface IGenreRepository : IRepository {} - public interface IStudioRepository : IRepository {} - public interface IPeopleRepository : IRepository {} public interface IProviderRepository : IRepository {} } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/Implementations/LibraryManager.cs b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs new file mode 100644 index 00000000..cb926f21 --- /dev/null +++ b/Kyoo.Common/Controllers/Implementations/LibraryManager.cs @@ -0,0 +1,796 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.Models; + +namespace Kyoo.Controllers +{ + public class LibraryManager : ILibraryManager + { + 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 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() + { + 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() + ); + } + + public Task GetLibrary(int id) + { + return LibraryRepository.Get(id); + } + + public Task GetCollection(int id) + { + return CollectionRepository.Get(id); + } + + public Task GetShow(int id) + { + return ShowRepository.Get(id); + } + + public Task GetSeason(int id) + { + return SeasonRepository.Get(id); + } + + public Task GetSeason(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 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 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 showSlug, int seasonNumber) + { + return SeasonRepository.Get(showSlug, seasonNumber); + } + + public Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber) + { + return EpisodeRepository.Get(showSlug, seasonNumber, episodeNumber); + } + + public Task GetMovieEpisode(string movieSlug) + { + return EpisodeRepository.Get(movieSlug); + } + + public Task GetTrack(int id) + { + 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); + } + + 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> GetSeasonsFromShow(Expression> where = null, + Sort sort = default, + Pagination page = default) + { + return SeasonRepository.GetAll(where, sort, page); + } + + public Task> GetEpisodesFromShow(Expression> where = null, + Sort sort = default, + Pagination page = default) + { + return EpisodeRepository.GetAll(where, sort, page); + } + + 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> GetSeasonsFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return SeasonRepository.GetFromShow(showID, where, sort, limit); + } + + public Task> GetSeasonsFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return SeasonRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetEpisodesFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return EpisodeRepository.GetFromShow(showID, where, sort, limit); + } + + public Task> GetEpisodesFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return EpisodeRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetEpisodesFromSeason(int seasonID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return EpisodeRepository.GetFromSeason(seasonID, where, sort, limit); + } + + public Task> GetEpisodesFromSeason(int showID, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return EpisodeRepository.GetFromSeason(showID, seasonNumber, where, sort, limit); + } + + public Task> GetEpisodesFromSeason(string showSlug, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return EpisodeRepository.GetFromSeason(showSlug, seasonNumber, where, sort, limit); + } + + 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, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return PeopleRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetGenresFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return GenreRepository.GetFromShow(showID, where, sort, limit); + } + + public Task> GetGenresFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return GenreRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetTracksFromEpisode(int episodeID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return TrackRepository.GetFromEpisode(episodeID, where, sort, limit); + } + + public Task> GetTracksFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return TrackRepository.GetFromEpisode(showID, seasonNumber, episodeNumber, where, sort, limit); + } + + public Task> GetTracksFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return TrackRepository.GetFromEpisode(showSlug, seasonNumber, episodeNumber, where, sort, limit); + } + + public Task GetStudioFromShow(int showID) + { + return StudioRepository.GetFromShow(showID); + } + + public Task GetStudioFromShow(string showSlug) + { + return StudioRepository.GetFromShow(showSlug); + } + + public Task GetShowFromSeason(int seasonID) + { + return ShowRepository.GetFromSeason(seasonID); + } + + public Task GetShowFromEpisode(int episodeID) + { + return ShowRepository.GetFromEpisode(episodeID); + } + + public Task GetSeasonFromEpisode(int episodeID) + { + return SeasonRepository.GetFromEpisode(episodeID); + } + + public Task> GetLibrariesFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryRepository.GetFromShow(showID, where, sort, limit); + } + + public Task> GetLibrariesFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetCollectionsFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return CollectionRepository.GetFromShow(showID, where, sort, limit); + } + + public Task> GetCollectionsFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return CollectionRepository.GetFromShow(showSlug, where, sort, limit); + } + + public Task> GetShowsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ShowRepository.GetFromLibrary(id, where, sort, limit); + } + + public Task> GetShowsFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ShowRepository.GetFromLibrary(slug, where, sort, limit); + } + + public Task> GetCollectionsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return CollectionRepository.GetFromLibrary(id, where, sort, limit); + } + + public Task> GetCollectionsFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return CollectionRepository.GetFromLibrary(slug, where, sort, limit); + } + + public Task> GetItemsFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryItemRepository.GetFromLibrary(id, where, sort, limit); + } + + public Task> GetItemsFromLibrary(string librarySlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryItemRepository.GetFromLibrary(librarySlug, where, sort, limit); + } + + public Task> GetShowsFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ShowRepository.GetFromCollection(id, where, sort, limit); + } + + public Task> GetShowsFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ShowRepository.GetFromCollection(slug, where, sort, limit); + } + + public Task> GetLibrariesFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryRepository.GetFromCollection(id, where, sort, limit); + } + + public Task> GetLibrariesFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return LibraryRepository.GetFromCollection(slug, where, sort, limit); + } + + public Task> GetRolesFromPeople(int id, + 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, + Pagination limit = default) + { + return PeopleRepository.GetFromPeople(slug, where, sort, limit); + } + + 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); + } + + public Task> SearchLibraries(string searchQuery) + { + return LibraryRepository.Search(searchQuery); + } + + public Task> SearchCollections(string searchQuery) + { + return CollectionRepository.Search(searchQuery); + } + + public Task> SearchShows(string searchQuery) + { + return ShowRepository.Search(searchQuery); + } + + public Task> SearchSeasons(string searchQuery) + { + return SeasonRepository.Search(searchQuery); + } + + public Task> SearchEpisodes(string searchQuery) + { + 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 DelteLibrary(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 DelteLibrary(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 DelteLibrary(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); + } + } +} diff --git a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs index c8836c0d..6b04c8b2 100644 --- a/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs +++ b/Kyoo.Common/Models/Exceptions/DuplicatedItemException.cs @@ -6,6 +6,8 @@ namespace Kyoo.Models.Exceptions { public override string Message { get; } + public DuplicatedItemException() {} + public DuplicatedItemException(string message) { Message = message; diff --git a/Kyoo.Common/Models/Exceptions/ItemNotFound.cs b/Kyoo.Common/Models/Exceptions/ItemNotFound.cs index c72427c6..f23cf363 100644 --- a/Kyoo.Common/Models/Exceptions/ItemNotFound.cs +++ b/Kyoo.Common/Models/Exceptions/ItemNotFound.cs @@ -6,6 +6,8 @@ namespace Kyoo.Models.Exceptions { public override string Message { get; } + public ItemNotFound() {} + public ItemNotFound(string message) { Message = message; diff --git a/Kyoo.Common/Models/LibraryItem.cs b/Kyoo.Common/Models/LibraryItem.cs new file mode 100644 index 00000000..4264a469 --- /dev/null +++ b/Kyoo.Common/Models/LibraryItem.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq.Expressions; + +namespace Kyoo.Models +{ + public enum ItemType + { + Show, + Movie, + Collection + } + + public class LibraryItem : IResource + { + public int ID { get; set; } + public string Slug { get; set; } + public string Title { get; set; } + public string Overview { get; set; } + public Status? Status { get; set; } + public string TrailerUrl { get; set; } + public int? StartYear { get; set; } + public int? EndYear { get; set; } + public string Poster { get; set; } + public ItemType Type { get; set; } + + public LibraryItem() {} + + public LibraryItem(Show show) + { + ID = show.ID; + Slug = show.Slug; + Title = show.Title; + Overview = show.Overview; + Status = show.Status; + TrailerUrl = show.TrailerUrl; + StartYear = show.StartYear; + EndYear = show.EndYear; + Poster = show.Poster; + Type = show.IsMovie ? ItemType.Movie : ItemType.Show; + } + + public LibraryItem(Collection collection) + { + ID = -collection.ID; + Slug = collection.Slug; + Title = collection.Name; + Overview = collection.Overview; + Status = Models.Status.Unknown; + TrailerUrl = null; + StartYear = null; + EndYear = null; + Poster = collection.Poster; + Type = ItemType.Collection; + } + + public static Expression> FromShow => x => new LibraryItem + { + ID = x.ID, + Slug = x.Slug, + Title = x.Title, + Overview = x.Overview, + Status = x.Status, + TrailerUrl = x.TrailerUrl, + StartYear = x.StartYear, + EndYear = x.EndYear, + Poster= x.Poster, + Type = x.IsMovie ? ItemType.Movie : ItemType.Show + }; + + public static Expression> FromCollection => x => new LibraryItem + { + ID = -x.ID, + Slug = x.Slug, + Title = x.Name, + Overview = x.Overview, + Status = Models.Status.Unknown, + TrailerUrl = null, + StartYear = null, + EndYear = null, + Poster= x.Poster, + Type = ItemType.Collection + }; + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Page.cs b/Kyoo.Common/Models/Page.cs new file mode 100644 index 00000000..f91e94d8 --- /dev/null +++ b/Kyoo.Common/Models/Page.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Kyoo.Models +{ + public class Page where T : IResource + { + public string This { get; set; } + public string First { get; set; } + public string Next { get; set; } + + public int Count => Items.Count; + public ICollection Items { get; set; } + + public Page() { } + + public Page(ICollection items) + { + Items = items; + } + + public Page(ICollection items, string @this, string next, string first) + { + Items = items; + This = @this; + Next = next; + First = first; + } + + public Page(ICollection items, + string url, + Dictionary query, + int limit) + { + Items = items; + This = url + query.ToQueryString(); + + if (items.Count == limit) + { + query["afterID"] = items.Last().ID.ToString(); + Next = url + query.ToQueryString(); + } + + query.Remove("afterID"); + First = url + query.ToQueryString(); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/PeopleLink.cs b/Kyoo.Common/Models/PeopleLink.cs index 509f2b93..04555298 100644 --- a/Kyoo.Common/Models/PeopleLink.cs +++ b/Kyoo.Common/Models/PeopleLink.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; +using System.Linq.Expressions; using Newtonsoft.Json; namespace Kyoo.Models { - public class PeopleLink + public class PeopleLink : IResource { [JsonIgnore] public int ID { get; set; } [JsonIgnore] public int PeopleID { get; set; } @@ -21,6 +23,12 @@ namespace Kyoo.Models set => People.Name = value; } + public string Poster + { + get => People.Poster; + set => People.Poster = value; + } + public IEnumerable ExternalIDs { get => People.ExternalIDs; @@ -42,11 +50,79 @@ namespace Kyoo.Models Type = type; } - public PeopleLink(string slug, string name, string role, string type, string imgPrimary, IEnumerable externalIDs) + public PeopleLink(string slug, + string name, + string role, + string type, + string poster, + IEnumerable externalIDs) { - People = new People(slug, name, imgPrimary, externalIDs); + People = new People(slug, name, poster, externalIDs); Role = role; Type = type; } } + + public class ShowRole : IResource + { + public int ID { get; set; } + public string Role { get; set; } + public string Type { get; set; } + + public string Slug { get; set; } + public string Title { get; set; } + public IEnumerable Aliases { get; set; } + [JsonIgnore] public string Path { get; set; } + public string Overview { get; set; } + public Status? Status { get; set; } + public string TrailerUrl { get; set; } + public int? StartYear { get; set; } + public int? EndYear { get; set; } + public string Poster { get; set; } + public string Logo { get; set; } + public string Backdrop { get; set; } + public bool IsMovie { get; set; } + + public ShowRole() {} + + public ShowRole(PeopleLink x) + { + ID = x.ID; + Role = x.Role; + Type = x.Type; + Slug = x.Show.Slug; + Title = x.Show.Title; + Aliases = x.Show.Aliases; + Path = x.Show.Path; + Overview = x.Show.Overview; + Status = x.Show.Status; + TrailerUrl = x.Show.TrailerUrl; + StartYear = x.Show.StartYear; + EndYear = x.Show.EndYear; + Poster = x.Show.Poster; + Logo = x.Show.Logo; + Backdrop = x.Show.Backdrop; + IsMovie = x.Show.IsMovie; + } + + public static Expression> FromPeopleRole => x => new ShowRole + { + ID = x.ID, + Role = x.Role, + Type = x.Type, + Slug = x.Show.Slug, + Title = x.Show.Title, + Aliases = x.Show.Aliases, + Path = x.Show.Path, + Overview = x.Show.Overview, + Status = x.Show.Status, + TrailerUrl = x.Show.TrailerUrl, + StartYear = x.Show.StartYear, + EndYear = x.Show.EndYear, + Poster = x.Show.Poster, + Logo = x.Show.Logo, + Backdrop = x.Show.Backdrop, + IsMovie = x.Show.IsMovie + }; + } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Collection.cs b/Kyoo.Common/Models/Resources/Collection.cs similarity index 75% rename from Kyoo.Common/Models/Collection.cs rename to Kyoo.Common/Models/Resources/Collection.cs index 5336c52c..07bae5bc 100644 --- a/Kyoo.Common/Models/Collection.cs +++ b/Kyoo.Common/Models/Resources/Collection.cs @@ -5,16 +5,15 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - public class Collection + public class Collection : IResource { [JsonIgnore] public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } public string Poster { get; set; } public string Overview { get; set; } - [JsonIgnore] public string ImgPrimary { get; set; } [NotMergable] [JsonIgnore] public virtual IEnumerable Links { get; set; } - public virtual IEnumerable Shows + [JsonIgnore] public virtual IEnumerable Shows { get => Links.Select(x => x.Show); set => Links = value.Select(x => new CollectionLink(this, x)); @@ -30,20 +29,12 @@ namespace Kyoo.Models public Collection() { } - public Collection(string slug, string name, string overview, string imgPrimary) + public Collection(string slug, string name, string overview, string poster) { Slug = slug; Name = name; Overview = overview; - ImgPrimary = imgPrimary; - } - - public Show AsShow() - { - return new Show(Slug, Name, null, null, Overview, null, null, null, null, null, null) - { - IsCollection = true - }; + Poster = poster; } } } diff --git a/Kyoo.Common/Models/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs similarity index 98% rename from Kyoo.Common/Models/Episode.cs rename to Kyoo.Common/Models/Resources/Episode.cs index a90dc5d9..a512ed75 100644 --- a/Kyoo.Common/Models/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -5,7 +5,7 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - public class Episode : IOnMerge + public class Episode : IResource, IOnMerge { [JsonIgnore] public int ID { get; set; } [JsonIgnore] public int ShowID { get; set; } diff --git a/Kyoo.Common/Models/Genre.cs b/Kyoo.Common/Models/Resources/Genre.cs similarity index 96% rename from Kyoo.Common/Models/Genre.cs rename to Kyoo.Common/Models/Resources/Genre.cs index 1f964eff..2fe7c3a3 100644 --- a/Kyoo.Common/Models/Genre.cs +++ b/Kyoo.Common/Models/Resources/Genre.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class Genre + public class Genre : IResource { [JsonIgnore] public int ID { get; set; } public string Slug { get; set; } diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs new file mode 100644 index 00000000..7ef6c4df --- /dev/null +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -0,0 +1,8 @@ +namespace Kyoo.Models +{ + public interface IResource + { + public int ID { get; set; } + public string Slug { get; } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Library.cs b/Kyoo.Common/Models/Resources/Library.cs similarity index 97% rename from Kyoo.Common/Models/Library.cs rename to Kyoo.Common/Models/Resources/Library.cs index 84441297..11526f43 100644 --- a/Kyoo.Common/Models/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class Library + public class Library : IResource { [JsonIgnore] public int ID { get; set; } public string Slug { get; set; } diff --git a/Kyoo.Common/Models/People.cs b/Kyoo.Common/Models/Resources/People.cs similarity index 69% rename from Kyoo.Common/Models/People.cs rename to Kyoo.Common/Models/Resources/People.cs index 6514dc9f..78a9c87d 100644 --- a/Kyoo.Common/Models/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -4,23 +4,23 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class People + public class People : IResource { public int ID { get; set; } public string Slug { get; set; } public string Name { get; set; } - [JsonIgnore] public string ImgPrimary { get; set; } + public string Poster { get; set; } public virtual IEnumerable ExternalIDs { get; set; } [JsonIgnore] public virtual IEnumerable Roles { get; set; } public People() {} - public People(string slug, string name, string imgPrimary, IEnumerable externalIDs) + public People(string slug, string name, string poster, IEnumerable externalIDs) { Slug = slug; Name = name; - ImgPrimary = imgPrimary; + Poster = poster; ExternalIDs = externalIDs; } } diff --git a/Kyoo.Common/Models/ProviderID.cs b/Kyoo.Common/Models/Resources/ProviderID.cs similarity index 57% rename from Kyoo.Common/Models/ProviderID.cs rename to Kyoo.Common/Models/Resources/ProviderID.cs index b68785b1..387376fa 100644 --- a/Kyoo.Common/Models/ProviderID.cs +++ b/Kyoo.Common/Models/Resources/ProviderID.cs @@ -2,17 +2,26 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class ProviderID + public class ProviderID : IResource { [JsonIgnore] public int ID { get; set; } + public string Slug { get; set; } public string Name { get; set; } public string Logo { get; set; } public ProviderID() { } + public ProviderID(string name, string logo) + { + Slug = Utility.ToSlug(name); + Name = name; + Logo = logo; + } + public ProviderID(int id, string name, string logo) { ID = id; + Slug = Utility.ToSlug(name); Name = name; Logo = logo; } diff --git a/Kyoo.Common/Models/Season.cs b/Kyoo.Common/Models/Resources/Season.cs similarity index 96% rename from Kyoo.Common/Models/Season.cs rename to Kyoo.Common/Models/Resources/Season.cs index 8d4d9052..2bd62a6a 100644 --- a/Kyoo.Common/Models/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class Season + public class Season : IResource { [JsonIgnore] public int ID { get; set; } [JsonIgnore] public int ShowID { get; set; } diff --git a/Kyoo.Common/Models/Show.cs b/Kyoo.Common/Models/Resources/Show.cs similarity index 95% rename from Kyoo.Common/Models/Show.cs rename to Kyoo.Common/Models/Resources/Show.cs index 959856ee..aabfc697 100644 --- a/Kyoo.Common/Models/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -5,7 +5,7 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { - public class Show : IOnMerge + public class Show : IResource, IOnMerge { [JsonIgnore] public int ID { get; set; } @@ -28,7 +28,6 @@ namespace Kyoo.Models public bool IsMovie { get; set; } - public bool IsCollection; public virtual IEnumerable Genres { @@ -82,7 +81,6 @@ namespace Kyoo.Models StartYear = startYear; EndYear = endYear; ExternalIDs = externalIDs; - IsCollection = false; } public Show(string slug, @@ -112,7 +110,6 @@ namespace Kyoo.Models Logo = logo; Backdrop = backdrop; ExternalIDs = externalIDs; - IsCollection = false; } public string GetID(string provider) @@ -140,5 +137,5 @@ namespace Kyoo.Models } } - public enum Status { Finished, Airing, Planned } + public enum Status { Finished, Airing, Planned, Unknown } } diff --git a/Kyoo.Common/Models/Studio.cs b/Kyoo.Common/Models/Resources/Studio.cs similarity index 94% rename from Kyoo.Common/Models/Studio.cs rename to Kyoo.Common/Models/Resources/Studio.cs index ec37121a..8936bf66 100644 --- a/Kyoo.Common/Models/Studio.cs +++ b/Kyoo.Common/Models/Resources/Studio.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Kyoo.Models { - public class Studio + public class Studio : IResource { [JsonIgnore] public int ID { get; set; } public string Slug { get; set; } diff --git a/Kyoo.Common/Models/Track.cs b/Kyoo.Common/Models/Resources/Track.cs similarity index 94% rename from Kyoo.Common/Models/Track.cs rename to Kyoo.Common/Models/Resources/Track.cs index 0e3549e0..88953eb2 100644 --- a/Kyoo.Common/Models/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -53,7 +53,7 @@ namespace Kyoo.Models } } - public class Track : Stream + public class Track : Stream, IResource { [JsonIgnore] public int ID { get; set; } [JsonIgnore] public int EpisodeID { get; set; } @@ -93,9 +93,10 @@ namespace Kyoo.Models { if (Type != StreamType.Subtitle) return null; - string slug = $"/subtitle/{Episode.Slug}.{Language ?? ID.ToString()}"; - if (IsForced) - slug += "-forced"; + + string slug = string.IsNullOrEmpty(Language) + ? ID.ToString() + : $"{Episode.Slug}.{Language}{(IsForced ? "-forced" : "")}"; switch (Codec) { case "ass": diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index a852c6f8..1e8b79cf 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -9,7 +9,6 @@ using System.Text.RegularExpressions; using JetBrains.Annotations; using Kyoo.Models; using Kyoo.Models.Attributes; -using Microsoft.VisualBasic; namespace Kyoo { @@ -224,5 +223,12 @@ namespace Kyoo yield return ret; } } + + public static string ToQueryString(this Dictionary query) + { + if (!query.Any()) + return string.Empty; + return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); + } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/ApiHelper.cs b/Kyoo.CommonAPI/ApiHelper.cs new file mode 100644 index 00000000..8258cbe6 --- /dev/null +++ b/Kyoo.CommonAPI/ApiHelper.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Kyoo.CommonApi +{ + public static class ApiHelper + { + public static Expression StringCompatibleExpression(Func operand, + Expression left, + Expression right) + { + if (left is MemberExpression member && ((PropertyInfo)member.Member).PropertyType == typeof(string)) + { + MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right); + return operand(call, Expression.Constant(0)); + } + return operand(left, right); + } + + public static Expression> ParseWhere(Dictionary where, + Expression> defaultWhere = null) + { + if (where == null || where.Count == 0) + return null; + + ParameterExpression param = Expression.Parameter(typeof(T)); + Expression expression = defaultWhere?.Body; + + foreach ((string key, string desired) in where) + { + string value = desired; + string operand = "eq"; + if (desired.Contains(':')) + { + operand = desired.Substring(0, desired.IndexOf(':')); + value = desired.Substring(desired.IndexOf(':') + 1); + } + + PropertyInfo property = typeof(T).GetProperty(key, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); + if (property == null) + throw new ArgumentException($"No filterable parameter with the name {key}."); + MemberExpression propertyExpr = Expression.Property(param, property); + + ConstantExpression valueExpr = null; + if (operand != "ctn") + { + Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + object val = string.IsNullOrEmpty(value) || value.Equals("null", StringComparison.OrdinalIgnoreCase) + ? null + : Convert.ChangeType(value, propertyType); + valueExpr = Expression.Constant(val, property.PropertyType); + } + + Expression condition = operand switch + { + "eq" => Expression.Equal(propertyExpr, valueExpr!), + "not" => Expression.NotEqual(propertyExpr, valueExpr!), + "lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr), + "lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr), + "gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr), + "gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr), + "ctn" => ContainsResourceExpression(propertyExpr, value), + _ => throw new ArgumentException($"Invalid operand: {operand}") + }; + + if (expression != null) + expression = Expression.AndAlso(expression, condition); + else + expression = condition; + } + + return Expression.Lambda>(expression!, param); + } + + private static Expression ContainsResourceExpression(MemberExpression xProperty, string value) + { + // x => x.PROPERTY.Any(y => y.Slug == value) + Expression ret = null; + ParameterExpression y = Expression.Parameter(xProperty.Type.GenericTypeArguments.First(), "y"); + foreach (string val in value.Split(',')) + { + MemberExpression yProperty; + ConstantExpression yValue; + if (int.TryParse(val, out int id)) + { + yProperty = Expression.Property(y, "ID"); + yValue = Expression.Constant(id); + } + else + { + yProperty = Expression.Property(y, "Slug"); + yValue = Expression.Constant(val); + } + + LambdaExpression lambda = Expression.Lambda(Expression.Equal(yProperty, yValue), y); + Expression iteration = Expression.Call(typeof(Enumerable), "Any", xProperty.Type.GenericTypeArguments, + xProperty, lambda); + + if (ret == null) + ret = iteration; + else + ret = Expression.AndAlso(ret, iteration); + } + return ret; + } + } +} \ No newline at end of file diff --git a/Kyoo.CommonAPI/CrudApi.cs b/Kyoo.CommonAPI/CrudApi.cs new file mode 100644 index 00000000..d029659e --- /dev/null +++ b/Kyoo.CommonAPI/CrudApi.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.CommonApi +{ + [ApiController] + public class CrudApi : ControllerBase where T : IResource + { + private readonly IRepository _repository; + private readonly string _baseURL; + + public CrudApi(IRepository repository, IConfiguration configuration) + { + _repository = repository; + _baseURL = configuration.GetValue("public_url").TrimEnd('/'); + } + + [HttpGet("{id:int}")] + [Authorize(Policy = "Read")] + [JsonDetailed] + public virtual async Task> Get(int id) + { + T ressource = await _repository.Get(id); + if (ressource == null) + return NotFound(); + + return ressource; + } + + [HttpGet("{slug}")] + [Authorize(Policy = "Read")] + [JsonDetailed] + public virtual async Task> Get(string slug) + { + T ressource = await _repository.Get(slug); + if (ressource == null) + return NotFound(); + + return ressource; + } + + [HttpGet] + [Authorize(Policy = "Read")] + public virtual async Task>> GetAll([FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _repository.GetAll(ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + protected Page Page(ICollection ressources, int limit) + where TResult : IResource + { + return new Page(ressources, + _baseURL + Request.Path, + Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), + limit); + } + + [HttpPost] + [Authorize(Policy = "Write")] + public virtual async Task> Create([FromBody] T ressource) + { + try + { + return await _repository.Create(ressource); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + catch (DuplicatedItemException) + { + T existing = await _repository.Get(ressource.Slug); + return Conflict(existing); + } + } + + [HttpPut] + [Authorize(Policy = "Write")] + public virtual async Task> Edit([FromQuery] bool resetOld, [FromBody] T ressource) + { + if (ressource.ID > 0) + return await _repository.Edit(ressource, resetOld); + + T old = await _repository.Get(ressource.Slug); + if (old == null) + return NotFound(); + + ressource.ID = old.ID; + return await _repository.Edit(ressource, resetOld); + } + + [HttpPut("{id:int}")] + [Authorize(Policy = "Write")] + public virtual async Task> Edit(int id, [FromQuery] bool resetOld, [FromBody] T ressource) + { + ressource.ID = id; + try + { + return await _repository.Edit(ressource, resetOld); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + + [HttpPut("{slug}")] + [Authorize(Policy = "Write")] + public virtual async Task> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T ressource) + { + T old = await _repository.Get(slug); + if (old == null) + return NotFound(); + ressource.ID = old.ID; + return await _repository.Edit(ressource, resetOld); + } + + [HttpDelete("{id:int}")] + [Authorize(Policy = "Write")] + public virtual async Task Delete(int id) + { + try + { + await _repository.Delete(id); + } + catch (ItemNotFound) + { + return NotFound(); + } + + return Ok(); + } + + [HttpDelete("{slug}")] + [Authorize(Policy = "Write")] + public virtual async Task Delete(string slug) + { + try + { + await _repository.Delete(slug); + } + catch (ItemNotFound) + { + return NotFound(); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/JsonSerializer.cs b/Kyoo.CommonAPI/JsonSerializer.cs similarity index 93% rename from Kyoo/Views/API/JsonSerializer.cs rename to Kyoo.CommonAPI/JsonSerializer.cs index 42c035f6..11c696fe 100644 --- a/Kyoo/Views/API/JsonSerializer.cs +++ b/Kyoo.CommonAPI/JsonSerializer.cs @@ -77,8 +77,9 @@ namespace Kyoo.Controllers { ContractResolver = new JsonPropertySelector(null, new Dictionary>() { - {typeof(Show), new HashSet {"genres", "studio", "people", "seasons"}}, - {typeof(Episode), new HashSet {"tracks"}} + {typeof(Show), new HashSet {"genres", "studio"}}, + {typeof(Episode), new HashSet {"tracks"}}, + {typeof(PeopleLink), new HashSet {"show"}} }) }, context.HttpContext.RequestServices.GetRequiredService>(), diff --git a/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj new file mode 100644 index 00000000..7bd6a5d4 --- /dev/null +++ b/Kyoo.CommonAPI/Kyoo.CommonAPI.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + Kyoo.CommonApi + Kyoo.CommonApi + Kyoo.CommonApi + AnonymusRaccoon + Library + + + + + + + + + + + + + + diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs new file mode 100644 index 00000000..2bb43812 --- /dev/null +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace Kyoo.Controllers +{ + public abstract class LocalRepository : IRepository where T : class, IResource + { + private readonly DbContext _database; + + protected abstract Expression> DefaultSort { get; } + + + protected LocalRepository(DbContext database) + { + _database = database; + } + + public virtual void Dispose() + { + _database.Dispose(); + } + + public virtual ValueTask DisposeAsync() + { + return _database.DisposeAsync(); + } + + public virtual Task Get(int id) + { + return _database.Set().FirstOrDefaultAsync(x => x.ID == id); + } + + public virtual Task Get(string slug) + { + return _database.Set().FirstOrDefaultAsync(x => x.Slug == slug); + } + + public abstract Task> Search(string query); + + public virtual Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ApplyFilters(_database.Set(), where, sort, limit); + } + + protected Task> ApplyFilters(IQueryable query, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ApplyFilters(query, Get, DefaultSort, where, sort, limit); + } + + protected async Task> ApplyFilters(IQueryable query, + Func> get, + Expression> defaultSort, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + if (where != null) + query = query.Where(where); + + Expression> sortKey = sort.Key ?? defaultSort; + Expression sortExpression = sortKey.Body.NodeType == ExpressionType.Convert + ? ((UnaryExpression)sortKey.Body).Operand + : sortKey.Body; + + if (typeof(Enum).IsAssignableFrom(sortExpression.Type)) + throw new ArgumentException("Invalid sort key."); + + query = sort.Descendant ? query.OrderByDescending(sortKey) : query.OrderBy(sortKey); + + if (limit.AfterID != 0) + { + TValue after = await get(limit.AfterID); + Expression key = Expression.Constant(sortKey.Compile()(after), sortExpression.Type); + query = query.Where(Expression.Lambda>( + ApiHelper.StringCompatibleExpression(Expression.GreaterThan, sortExpression, key), + sortKey.Parameters.First() + )); + } + if (limit.Count > 0) + query = query.Take(limit.Count); + + return await query.ToListAsync(); + } + + public abstract Task Create(T obj); + + public virtual async Task CreateIfNotExists(T obj) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + T old = await Get(obj.Slug); + if (old != null) + return old; + try + { + return await Create(obj); + } + catch (DuplicatedItemException) + { + old = await Get(obj.Slug); + if (old == null) + throw new SystemException("Unknown database state."); + return old; + } + } + + public virtual async Task Edit(T edited, bool resetOld) + { + if (edited == null) + throw new ArgumentNullException(nameof(edited)); + + T old = await Get(edited.Slug); + + if (old == null) + throw new ItemNotFound($"No ressource found with the slug {edited.Slug}."); + + if (resetOld) + Utility.Nullify(old); + Utility.Merge(old, edited); + await Validate(old); + await _database.SaveChangesAsync(); + return old; + } + + protected abstract Task Validate(T ressource); + + public virtual async Task Delete(int id) + { + T ressource = await Get(id); + await Delete(ressource); + } + + public virtual async Task Delete(string slug) + { + T ressource = await Get(slug); + await Delete(ressource); + } + + 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); + } + } +} \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index 12f6f2ad..cc3d4222 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -3,6 +3,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo", "Kyoo\Kyoo.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Common", "Kyoo.Common\Kyoo.Common.csproj", "{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.CommonAPI", "Kyoo.CommonAPI\Kyoo.CommonAPI.csproj", "{6F91B645-F785-46BB-9C4F-1EFC83E489B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,5 +19,9 @@ Global {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Debug|Any CPU.Build.0 = Debug|Any CPU {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.ActiveCfg = Release|Any CPU {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.Build.0 = Release|Any CPU + {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F91B645-F785-46BB-9C4F-1EFC83E489B6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs deleted file mode 100644 index 9b1a6590..00000000 --- a/Kyoo/Controllers/LibraryManager.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Kyoo.Models; - -namespace Kyoo.Controllers -{ - public class LibraryManager : ILibraryManager - { - private readonly ILibraryRepository _libraries; - private readonly ICollectionRepository _collections; - private readonly IShowRepository _shows; - private readonly ISeasonRepository _seasons; - private readonly IEpisodeRepository _episodes; - private readonly ITrackRepository _tracks; - private readonly IGenreRepository _genres; - private readonly IStudioRepository _studios; - private readonly IPeopleRepository _people; - private readonly IProviderRepository _providers; - - public LibraryManager(ILibraryRepository libraries, - ICollectionRepository collections, - IShowRepository shows, - ISeasonRepository seasons, - IEpisodeRepository episodes, - ITrackRepository tracks, - IGenreRepository genres, - IStudioRepository studios, - IProviderRepository providers, - IPeopleRepository people) - { - _libraries = libraries; - _collections = collections; - _shows = shows; - _seasons = seasons; - _episodes = episodes; - _tracks = tracks; - _genres = genres; - _studios = studios; - _providers = providers; - _people = people; - } - - public void Dispose() - { - _libraries.Dispose(); - _collections.Dispose(); - _shows.Dispose(); - _seasons.Dispose(); - _episodes.Dispose(); - _tracks.Dispose(); - _genres.Dispose(); - _studios.Dispose(); - _people.Dispose(); - _providers.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await Task.WhenAll( - _libraries.DisposeAsync().AsTask(), - _collections.DisposeAsync().AsTask(), - _shows.DisposeAsync().AsTask(), - _seasons.DisposeAsync().AsTask(), - _episodes.DisposeAsync().AsTask(), - _tracks.DisposeAsync().AsTask(), - _genres.DisposeAsync().AsTask(), - _studios.DisposeAsync().AsTask(), - _people.DisposeAsync().AsTask(), - _providers.DisposeAsync().AsTask() - ); - } - - public Task GetLibrary(string slug) - { - return _libraries.Get(slug); - } - - public Task GetCollection(string slug) - { - return _collections.Get(slug); - } - - public Task GetShow(string slug) - { - return _shows.Get(slug); - } - - public Task GetSeason(string showSlug, int seasonNumber) - { - return _seasons.Get(showSlug, seasonNumber); - } - - public Task GetEpisode(string showSlug, int seasonNumber, int episodeNumber) - { - return _episodes.Get(showSlug, seasonNumber, episodeNumber); - } - - public Task GetMovieEpisode(string movieSlug) - { - return _episodes.Get(movieSlug); - } - - public Task GetTrack(int id) - { - return _tracks.Get(id); - } - - public Task GetTrack(int episodeID, string language, bool isForced) - { - return _tracks.Get(episodeID, language, isForced); - } - - public Task GetGenre(string slug) - { - return _genres.Get(slug); - } - - public Task GetStudio(string slug) - { - return _studios.Get(slug); - } - - public Task GetPeople(string slug) - { - return _people.Get(slug); - } - - public Task> GetLibraries() - { - return _libraries.GetAll(); - } - - public Task> GetCollections() - { - return _collections.GetAll(); - } - - public Task> GetShows() - { - return _shows.GetAll(); - } - - public Task> GetSeasons() - { - return _seasons.GetAll(); - } - - public Task> GetEpisodes() - { - return _episodes.GetAll(); - } - - public Task> GetTracks() - { - return _tracks.GetAll(); - } - - public Task> GetStudios() - { - return _studios.GetAll(); - } - - public Task> GetPeoples() - { - return _people.GetAll(); - } - - public Task> GetGenres() - { - return _genres.GetAll(); - } - - public Task> GetProviders() - { - return _providers.GetAll(); - } - - public Task> GetSeasons(int showID) - { - return _seasons.GetSeasons(showID); - } - - public Task> GetSeasons(string showSlug) - { - return _seasons.GetSeasons(showSlug); - } - - public Task> GetEpisodes(int showID, int seasonNumber) - { - return _episodes.GetEpisodes(showID, seasonNumber); - } - - public Task> GetEpisodes(string showSlug, int seasonNumber) - { - return _episodes.GetEpisodes(showSlug, seasonNumber); - } - - public Task> GetEpisodes(int seasonID) - { - return _episodes.GetEpisodes(seasonID); - } - - public Task GetShowByPath(string path) - { - return _shows.GetByPath(path); - } - - public Task AddShowLink(int showID, int? libraryID, int? collectionID) - { - return _shows.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); - } - - public Task> SearchLibraries(string searchQuery) - { - return _libraries.Search(searchQuery); - } - - public Task> SearchCollections(string searchQuery) - { - return _collections.Search(searchQuery); - } - - public Task> SearchShows(string searchQuery) - { - return _shows.Search(searchQuery); - } - - public Task> SearchSeasons(string searchQuery) - { - return _seasons.Search(searchQuery); - } - - public Task> SearchEpisodes(string searchQuery) - { - return _episodes.Search(searchQuery); - } - - public Task> SearchGenres(string searchQuery) - { - return _genres.Search(searchQuery); - } - - public Task> SearchStudios(string searchQuery) - { - return _studios.Search(searchQuery); - } - - public Task> SearchPeople(string searchQuery) - { - return _people.Search(searchQuery); - } - - public Task RegisterLibrary(Library library) - { - return _libraries.Create(library); - } - - public Task RegisterCollection(Collection collection) - { - return _collections.Create(collection); - } - - public Task RegisterShow(Show show) - { - return _shows.Create(show); - } - - public Task RegisterSeason(Season season) - { - return _seasons.Create(season); - } - - public Task RegisterEpisode(Episode episode) - { - return _episodes.Create(episode); - } - - public Task RegisterTrack(Track track) - { - return _tracks.Create(track); - } - - public Task RegisterGenre(Genre genre) - { - return _genres.Create(genre); - } - - public Task RegisterStudio(Studio studio) - { - return _studios.Create(studio); - } - - public Task RegisterPeople(People people) - { - return _people.Create(people); - } - - public Task EditLibrary(Library library, bool resetOld) - { - return _libraries.Edit(library, resetOld); - } - - public Task EditCollection(Collection collection, bool resetOld) - { - return _collections.Edit(collection, resetOld); - } - - public Task EditShow(Show show, bool resetOld) - { - return _shows.Edit(show, resetOld); - } - - public Task EditSeason(Season season, bool resetOld) - { - return _seasons.Edit(season, resetOld); - } - - public Task EditEpisode(Episode episode, bool resetOld) - { - return _episodes.Edit(episode, resetOld); - } - - public Task EditTrack(Track track, bool resetOld) - { - return _tracks.Edit(track, resetOld); - } - - public Task EditGenre(Genre genre, bool resetOld) - { - return _genres.Edit(genre, resetOld); - } - - public Task EditStudio(Studio studio, bool resetOld) - { - return _studios.Edit(studio, resetOld); - } - - public Task EditPeople(People people, bool resetOld) - { - return _people.Edit(people, resetOld); - } - - public Task DelteLibrary(Library library) - { - return _libraries.Delete(library); - } - - public Task DeleteCollection(Collection collection) - { - return _collections.Delete(collection); - } - - public Task DeleteShow(Show show) - { - return _shows.Delete(show); - } - - public Task DeleteSeason(Season season) - { - return _seasons.Delete(season); - } - - public Task DeleteEpisode(Episode episode) - { - return _episodes.Delete(episode); - } - - public Task DeleteTrack(Track track) - { - return _tracks.Delete(track); - } - - public Task DeleteGenre(Genre genre) - { - return _genres.Delete(genre); - } - - public Task DeleteStudio(Studio studio) - { - return _studios.Delete(studio); - } - - public Task DeletePeople(People people) - { - return _people.Delete(people); - } - } -} diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 663898d1..a614e39a 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -21,7 +21,7 @@ namespace Kyoo.Controllers T ret = new T(); IEnumerable providers = library?.Providers - .Select(x => _providers.FirstOrDefault(y => y.Provider.Name == x.Name)) + .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) .Where(x => x != null) ?? _providers; @@ -47,7 +47,7 @@ namespace Kyoo.Controllers List ret = new List(); IEnumerable providers = library?.Providers - .Select(x => _providers.FirstOrDefault(y => y.Provider.Name == x.Name)) + .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug)) .Where(x => x != null) ?? _providers; diff --git a/Kyoo/Controllers/Repositories/CollectionRepository.cs b/Kyoo/Controllers/Repositories/CollectionRepository.cs index 776cc597..533b4a6f 100644 --- a/Kyoo/Controllers/Repositories/CollectionRepository.cs +++ b/Kyoo/Controllers/Repositories/CollectionRepository.cs @@ -1,129 +1,71 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class CollectionRepository : ICollectionRepository + public class CollectionRepository : LocalRepository, ICollectionRepository { private readonly DatabaseContext _database; + private readonly Lazy _shows; + private readonly Lazy _libraries; + protected override Expression> DefaultSort => x => x.Name; - - public CollectionRepository(DatabaseContext database) + public CollectionRepository(DatabaseContext database, IServiceProvider services) : base(database) { _database = database; - } - - public void Dispose() - { - _database.Dispose(); + _shows = new Lazy(services.GetRequiredService); + _libraries = new Lazy(services.GetRequiredService); } - public ValueTask DisposeAsync() + public override void Dispose() { - return _database.DisposeAsync(); - } - - public Task Get(int id) - { - return _database.Collections.FirstOrDefaultAsync(x => x.ID == id); + base.Dispose(); + if (_shows.IsValueCreated) + _shows.Value.Dispose(); + if (_libraries.IsValueCreated) + _libraries.Value.Dispose(); } - public Task Get(string slug) + public override async ValueTask DisposeAsync() { - return _database.Collections.FirstOrDefaultAsync(x => x.Slug == slug); + await _database.DisposeAsync(); + if (_shows.IsValueCreated) + await _shows.Value.DisposeAsync(); + if (_libraries.IsValueCreated) + await _libraries.Value.DisposeAsync(); } - - public async Task> Search(string query) + + public override async Task> Search(string query) { return await _database.Collections - .Where(x => EF.Functions.Like(x.Name, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Collections.ToListAsync(); - } - - public async Task Create(Collection obj) + public override async Task Create(Collection obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Added; - - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated collection (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated collection (slug {obj.Slug} already exists)."); + return obj; } - - public async Task CreateIfNotExists(Collection obj) + + protected override Task Validate(Collection ressource) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Collection old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + return Task.CompletedTask; } - public async Task Edit(Collection edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Collection old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No collection found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - Collection obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Collection obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(Collection obj) + public override async Task Delete(Collection obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -138,22 +80,70 @@ namespace Kyoo.Controllers await _database.SaveChangesAsync(); } - public async Task DeleteRange(IEnumerable objs) + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (Collection obj in objs) - await Delete(obj); + ICollection collections = await ApplyFilters(_database.CollectionLinks + .Where(x => x.ShowID == showID) + .Select(x => x.Collection), + where, + sort, + limit); + if (!collections.Any() & await _shows.Value.Get(showID) == null) + throw new ItemNotFound(); + return collections; } - - public async Task DeleteRange(IEnumerable ids) + + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (int id in ids) - await Delete(id); + ICollection collections = await ApplyFilters(_database.CollectionLinks + .Where(x => x.Show.Slug == showSlug) + .Select(x => x.Collection), + where, + sort, + limit); + if (!collections.Any() & await _shows.Value.Get(showSlug) == null) + throw new ItemNotFound(); + return collections; } - - public async Task DeleteRange(IEnumerable slugs) + + public async Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (string slug in slugs) - await Delete(slug); + ICollection collections = await ApplyFilters(_database.LibraryLinks + .Where(x => x.LibraryID == id && x.CollectionID != null) + .Select(x => x.Collection), + where, + sort, + limit); + if (!collections.Any() && await _libraries.Value.Get(id) == null) + throw new ItemNotFound(); + return collections; + } + + public async Task> GetFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection collections = await ApplyFilters(_database.LibraryLinks + .Where(x => x.Library.Slug == slug && x.CollectionID != null) + .Select(x => x.Collection), + where, + sort, + limit); + if (!collections.Any() && await _libraries.Value.Get(slug) == null) + throw new ItemNotFound(); + return collections; } } + + } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index e4d54134..9c0676e4 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -8,50 +10,49 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class EpisodeRepository : IEpisodeRepository + public class EpisodeRepository : LocalRepository, IEpisodeRepository { private readonly DatabaseContext _database; private readonly IProviderRepository _providers; - // private readonly ITrackRepository _tracks; + private readonly IShowRepository _shows; + private readonly ISeasonRepository _seasons; + protected override Expression> DefaultSort => x => x.EpisodeNumber; - public EpisodeRepository(DatabaseContext database, IProviderRepository providers) + public EpisodeRepository(DatabaseContext database, + IProviderRepository providers, + IShowRepository shows, + ISeasonRepository seasons) + : base(database) { _database = database; _providers = providers; + _shows = shows; + _seasons = seasons; } - - public void Dispose() + + + public override void Dispose() { _database.Dispose(); + _providers.Dispose(); } - public ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - return _database.DisposeAsync(); - } - - public Task Get(int id) - { - return _database.Episodes.FirstOrDefaultAsync(x => x.ID == id); + await _database.DisposeAsync(); + await _providers.DisposeAsync(); } - public Task Get(string slug) + public override Task Get(string slug) { - int sIndex = slug.IndexOf("-s", StringComparison.Ordinal); - int eIndex = slug.IndexOf("-e", StringComparison.Ordinal); - - if (sIndex == -1 && eIndex == -1) - return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); + Match match = Regex.Match(slug, @"(?.*)-s(?\d*)-e(?\d*)"); - if (sIndex == -1 || eIndex == -1 || eIndex < sIndex) - throw new InvalidOperationException("Invalid episode slug. Format: {showSlug}-s{seasonNumber}-e{episodeNumber}"); - string showSlug = slug.Substring(0, sIndex); - if (!int.TryParse(slug.Substring(sIndex + 2), out int seasonNumber)) - throw new InvalidOperationException("Invalid episode slug. Format: {showSlug}-s{seasonNumber}-e{episodeNumber}"); - if (!int.TryParse(slug.Substring(eIndex + 2), out int episodeNumber)) - throw new InvalidOperationException("Invalid episode slug. Format: {showSlug}-s{seasonNumber}-e{episodeNumber}"); - return Get(showSlug, seasonNumber, episodeNumber); + if (!match.Success) + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == slug); + return Get(match.Groups["show"].Value, + int.Parse(match.Groups["season"].Value), + int.Parse(match.Groups["episode"].Value)); } public Task Get(string showSlug, int seasonNumber, int episodeNumber) @@ -60,21 +61,41 @@ namespace Kyoo.Controllers && x.SeasonNumber == seasonNumber && x.EpisodeNumber == episodeNumber); } - - public async Task> Search(string query) + + public Task Get(int showID, int seasonNumber, int episodeNumber) + { + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber + && x.EpisodeNumber == episodeNumber); + } + + public Task Get(int seasonID, int episodeNumber) + { + return _database.Episodes.FirstOrDefaultAsync(x => x.SeasonID == seasonID + && x.EpisodeNumber == episodeNumber); + } + + public Task GetAbsolute(int showID, int absoluteNumber) + { + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID + && x.AbsoluteNumber == absoluteNumber); + } + + public Task GetAbsolute(string showSlug, int absoluteNumber) + { + return _database.Episodes.FirstOrDefaultAsync(x => x.Show.Slug == showSlug + && x.AbsoluteNumber == absoluteNumber); + } + + public override async Task> Search(string query) { return await _database.Episodes - .Where(x => EF.Functions.Like(x.Title, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Episodes.ToListAsync(); - } - - public async Task Create(Episode obj) + public override async Task Create(Episode obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -85,76 +106,15 @@ namespace Kyoo.Controllers foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Added; - // Since Episodes & Tracks are on the same DB, using a single commit is quicker. if (obj.Tracks != null) foreach (Track entry in obj.Tracks) _database.Entry(entry).State = EntityState.Added; - - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); - throw; - } - // Since Episodes & Tracks are on the same DB, using a single commit is quicker. - /*if (obj.Tracks != null) - * foreach (Track track in obj.Tracks) - * { - * track.EpisodeID = obj.ID; - * await _tracks.Create(track); - * } - */ - - return obj.ID; - } - - public async Task CreateIfNotExists(Episode obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Episode old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + await _database.SaveChangesAsync($"Trying to insert a duplicated episode (slug {obj.Slug} already exists)."); + return obj; } - public async Task Edit(Episode edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Episode old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No episode found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - - await Validate(old); - await _database.SaveChangesAsync(); - } - - private async Task Validate(Episode obj) + protected override async Task Validate(Episode obj) { if (obj.ShowID <= 0) throw new InvalidOperationException($"Can't store an episode not related to any show (showID: {obj.ShowID})."); @@ -162,37 +122,82 @@ namespace Kyoo.Controllers if (obj.ExternalIDs != null) { foreach (MetadataID link in obj.ExternalIDs) - link.ProviderID = await _providers.CreateIfNotExists(link.Provider); + link.Provider = await _providers.CreateIfNotExists(link.Provider); } } - public async Task> GetEpisodes(int showID, int seasonNumber) + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - return await _database.Episodes.Where(x => x.ShowID == showID - && x.SeasonNumber == seasonNumber).ToListAsync(); - } - - public async Task> GetEpisodes(string showSlug, int seasonNumber) - { - return await _database.Episodes.Where(x => x.Show.Slug == showSlug - && x.SeasonNumber == seasonNumber).ToListAsync(); - } - - public async Task> GetEpisodes(int seasonID) - { - return await _database.Episodes.Where(x => x.SeasonID == seasonID).ToListAsync(); + ICollection episodes = await ApplyFilters(_database.Episodes.Where(x => x.ShowID == showID), + where, + sort, + limit); + if (!episodes.Any() && await _shows.Get(showID) == null) + throw new ItemNotFound(); + return episodes; } - public async Task Delete(int id) + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - Episode obj = await Get(id); - await Delete(obj); + ICollection episodes = await ApplyFilters(_database.Episodes.Where(x => x.Show.Slug == showSlug), + where, + sort, + limit); + if (!episodes.Any() && await _shows.Get(showSlug) == null) + throw new ItemNotFound(); + return episodes; } - public async Task Delete(string slug) + public async Task> GetFromSeason(int seasonID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - Episode obj = await Get(slug); - await Delete(obj); + ICollection episodes = await ApplyFilters(_database.Episodes.Where(x => x.SeasonID == seasonID), + where, + sort, + limit); + if (!episodes.Any() && await _seasons.Get(seasonID) == null) + throw new ItemNotFound(); + return episodes; + } + + public async Task> GetFromSeason(int showID, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection episodes = await ApplyFilters(_database.Episodes.Where(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber), + where, + sort, + limit); + if (!episodes.Any() && await _seasons.Get(showID, seasonNumber) == null) + throw new ItemNotFound(); + return episodes; + } + + public async Task> GetFromSeason(string showSlug, + int seasonNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection episodes = await ApplyFilters(_database.Episodes.Where(x => x.Show.Slug == showSlug + && x.SeasonNumber == seasonNumber), + where, + sort, + limit); + if (!episodes.Any() && await _seasons.Get(showSlug, seasonNumber) == null) + throw new ItemNotFound(); + return episodes; } public async Task Delete(string showSlug, int seasonNumber, int episodeNumber) @@ -201,7 +206,7 @@ namespace Kyoo.Controllers await Delete(obj); } - public async Task Delete(Episode obj) + public override async Task Delete(Episode obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -213,23 +218,5 @@ namespace Kyoo.Controllers // Since Tracks & Episodes are on the same database and handled by dotnet-ef, we can't use the repository to delete them. await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) - { - foreach (Episode obj in objs) - await Delete(obj); - } - - public async Task DeleteRange(IEnumerable ids) - { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); - } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/GenreRepository.cs b/Kyoo/Controllers/Repositories/GenreRepository.cs index 9834e37e..f3a93873 100644 --- a/Kyoo/Controllers/Repositories/GenreRepository.cs +++ b/Kyoo/Controllers/Repositories/GenreRepository.cs @@ -1,129 +1,66 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class GenreRepository : IGenreRepository + public class GenreRepository : LocalRepository, IGenreRepository { private readonly DatabaseContext _database; + private readonly Lazy _shows; + protected override Expression> DefaultSort => x => x.Slug; - public GenreRepository(DatabaseContext database) + public GenreRepository(DatabaseContext database, IServiceProvider services) : base(database) { _database = database; - } - - public void Dispose() - { - _database.Dispose(); + _shows = new Lazy(services.GetRequiredService); } - public ValueTask DisposeAsync() + public override void Dispose() { - return _database.DisposeAsync(); + base.Dispose(); + if (_shows.IsValueCreated) + _shows.Value.Dispose(); } - public async Task Get(int id) + public override async ValueTask DisposeAsync() { - return await _database.Genres.FirstOrDefaultAsync(x => x.ID == id); + await _database.DisposeAsync(); + if (_shows.IsValueCreated) + await _shows.Value.DisposeAsync(); } - public async Task Get(string slug) - { - return await _database.Genres.FirstOrDefaultAsync(x => x.Slug == slug); - } - - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Genres - .Where(genre => EF.Functions.Like(genre.Name, $"%{query}%")) + .Where(genre => EF.Functions.ILike(genre.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Genres.ToListAsync(); - } - - public async Task Create(Genre obj) + public override async Task Create(Genre obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Added; - - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated genre (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated genre (slug {obj.Slug} already exists)."); + return obj; } - public async Task CreateIfNotExists(Genre obj) + protected override Task Validate(Genre ressource) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Genre old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + return Task.CompletedTask; } - public async Task Edit(Genre edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Genre old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No genre found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - Genre obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Genre obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(Genre obj) + public override async Task Delete(Genre obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -134,23 +71,36 @@ namespace Kyoo.Controllers _database.Entry(link).State = EntityState.Deleted; await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) + + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (Genre obj in objs) - await Delete(obj); + ICollection genres = await ApplyFilters(_database.GenreLinks.Where(x => x.ShowID == showID) + .Select(x => x.Genre), + where, + sort, + limit); + if (!genres.Any() && await _shows.Value.Get(showID) == null) + throw new ItemNotFound(); + return genres; } - - public async Task DeleteRange(IEnumerable ids) + + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); + ICollection genres = await ApplyFilters(_database.GenreLinks + .Where(x => x.Show.Slug == showSlug) + .Select(x => x.Genre), + where, + sort, + limit); + if (!genres.Any() && await _shows.Value.Get(showSlug) == null) + throw new ItemNotFound(); + return genres; } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/Helper.cs b/Kyoo/Controllers/Repositories/Helper.cs deleted file mode 100644 index 2881fe77..00000000 --- a/Kyoo/Controllers/Repositories/Helper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Npgsql; - -namespace Kyoo.Controllers -{ - public static class Helper - { - public static bool IsDuplicateException(DbUpdateException ex) - { - return ex.InnerException is PostgresException inner - && inner.SqlState == PostgresErrorCodes.UniqueViolation; - } - } -} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/LibraryItemRepository.cs b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs new file mode 100644 index 00000000..92a029a2 --- /dev/null +++ b/Kyoo/Controllers/Repositories/LibraryItemRepository.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Controllers +{ + public class LibraryItemRepository : LocalRepository, ILibraryItemRepository + { + private readonly DatabaseContext _database; + private readonly IProviderRepository _providers; + private readonly Lazy _libraries; + private readonly Lazy _shows; + private readonly Lazy _collections; + protected override Expression> DefaultSort => x => x.Title; + + + public LibraryItemRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) + : base(database) + { + _database = database; + _providers = providers; + _libraries = new Lazy(services.GetRequiredService); + _shows = new Lazy(services.GetRequiredService); + _collections = new Lazy(services.GetRequiredService); + } + + public override void Dispose() + { + _database.Dispose(); + _providers.Dispose(); + if (_shows.IsValueCreated) + _shows.Value.Dispose(); + if (_collections.IsValueCreated) + _collections.Value.Dispose(); + } + + public override async ValueTask DisposeAsync() + { + 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) + { + return id > 0 + ? new LibraryItem(await _shows.Value.Get(id)) + : new LibraryItem(await _collections.Value.Get(-id)); + } + + public override Task Get(string slug) + { + throw new InvalidOperationException(); + } + + private IQueryable ItemsQuery + => _database.Shows + .Where(x => !_database.CollectionLinks.Any(y => y.ShowID == x.ID)) + .Select(LibraryItem.FromShow) + .Concat(_database.Collections + .Select(LibraryItem.FromCollection)); + + public override Task> GetAll(Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + return ApplyFilters(ItemsQuery, where, sort, limit); + } + + public override async Task> Search(string query) + { + return await ItemsQuery + .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) + .Take(20) + .ToListAsync(); + } + + public override Task Create(LibraryItem obj) => throw new InvalidOperationException(); + public override Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); + public override Task Edit(LibraryItem obj, bool reset) => throw new InvalidOperationException(); + protected override Task Validate(LibraryItem obj) => 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(); + + private IQueryable LibraryRelatedQuery(Expression> selector) + => _database.LibraryLinks + .Where(selector) + .Select(x => x.Show) + .Where(x => x != null) + .Where(x => !_database.CollectionLinks.Any(y => y.ShowID == x.ID)) + .Select(LibraryItem.FromShow) + .Concat(_database.LibraryLinks + .Where(selector) + .Select(x => x.Collection) + .Where(x => x != null) + .Select(LibraryItem.FromCollection)); + + public async Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.LibraryID == id), + where, + sort, + limit); + if (!items.Any() && await _libraries.Value.Get(id) == null) + throw new ItemNotFound(); + return items; + } + + public async Task> GetFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection items = await ApplyFilters(LibraryRelatedQuery(x => x.Library.Slug == slug), + where, + sort, + limit); + if (!items.Any() && await _libraries.Value.Get(slug) == null) + throw new ItemNotFound(); + return items; + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/LibraryRepository.cs b/Kyoo/Controllers/Repositories/LibraryRepository.cs index 99e15a0f..db39e22d 100644 --- a/Kyoo/Controllers/Repositories/LibraryRepository.cs +++ b/Kyoo/Controllers/Repositories/LibraryRepository.cs @@ -1,59 +1,56 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class LibraryRepository : ILibraryRepository + public class LibraryRepository : LocalRepository, ILibraryRepository { private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly Lazy _shows; + protected override Expression> DefaultSort => x => x.ID; - public LibraryRepository(DatabaseContext database, IProviderRepository providers) + public LibraryRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) + : base(database) { _database = database; _providers = providers; - } - - public void Dispose() - { - _database.Dispose(); - } - - public ValueTask DisposeAsync() - { - return _database.DisposeAsync(); - } - - public Task Get(int id) - { - return _database.Libraries.FirstOrDefaultAsync(x => x.ID == id); + _shows = new Lazy(services.GetRequiredService); } - public Task Get(string slug) + public override void Dispose() { - return _database.Libraries.FirstOrDefaultAsync(x => x.Slug == slug); + _database.Dispose(); + _providers.Dispose(); + if (_shows.IsValueCreated) + _shows.Value.Dispose(); } - public async Task> Search(string query) + public override async ValueTask DisposeAsync() + { + await _database.DisposeAsync(); + await _providers.DisposeAsync(); + if (_shows.IsValueCreated) + await _shows.Value.DisposeAsync(); + } + + public override async Task> Search(string query) { return await _database.Libraries - .Where(x => EF.Functions.Like(x.Name, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Libraries.ToListAsync(); - } - - public async Task Create(Library obj) + public override async Task Create(Library obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -64,79 +61,25 @@ namespace Kyoo.Controllers foreach (ProviderLink entry in obj.ProviderLinks) _database.Entry(entry).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; - } - - public async Task CreateIfNotExists(Library obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Library old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists)."); + return obj; } - public async Task Edit(Library edited, bool resetOld) + protected override async Task Validate(Library obj) { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); + if (string.IsNullOrEmpty(obj.Slug)) + throw new ArgumentException("The library's slug must be set and not empty"); + if (string.IsNullOrEmpty(obj.Name)) + throw new ArgumentException("The library's name must be set and not empty"); + if (obj.Paths == null || !obj.Paths.Any()) + throw new ArgumentException("The library should have a least one path."); - Library old = await Get(edited.Name); - - if (old == null) - throw new ItemNotFound($"No library found with the name {edited.Name}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await Validate(old); - await _database.SaveChangesAsync(); - } - - private async Task Validate(Library obj) - { if (obj.ProviderLinks != null) foreach (ProviderLink link in obj.ProviderLinks) - link.ProviderID = await _providers.CreateIfNotExists(link.Provider); + link.Provider = await _providers.CreateIfNotExists(link.Provider); } - public async Task Delete(int id) - { - Library obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Library obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(Library obj) + public override async Task Delete(Library obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -150,23 +93,69 @@ namespace Kyoo.Controllers _database.Entry(entry).State = EntityState.Deleted; await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) + + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (Library obj in objs) - await Delete(obj); + ICollection libraries = await ApplyFilters(_database.LibraryLinks + .Where(x => x.ShowID == showID) + .Select(x => x.Library), + where, + sort, + limit); + if (!libraries.Any() && await _shows.Value.Get(showID) == null) + throw new ItemNotFound(); + return libraries; + } + + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection libraries = await ApplyFilters(_database.LibraryLinks + .Where(x => x.Show.Slug == showSlug) + .Select(x => x.Library), + where, + sort, + limit); + if (!libraries.Any() && await _shows.Value.Get(showSlug) == null) + throw new ItemNotFound(); + return libraries; } - public async Task DeleteRange(IEnumerable ids) + public async Task> GetFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (int id in ids) - await Delete(id); + ICollection libraries = await ApplyFilters(_database.LibraryLinks + .Where(x => x.CollectionID == id) + .Select(x => x.Library), + where, + sort, + limit); + if (!libraries.Any() && await _shows.Value.Get(id) == null) + throw new ItemNotFound(); + return libraries; } - - public async Task DeleteRange(IEnumerable slugs) + + public async Task> GetFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (string slug in slugs) - await Delete(slug); + ICollection libraries = await ApplyFilters(_database.LibraryLinks + .Where(x => x.Collection.Slug == slug) + .Select(x => x.Library), + where, + sort, + limit); + if (!libraries.Any() && await _shows.Value.Get(slug) == null) + throw new ItemNotFound(); + return libraries; } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/PeopleRepository.cs b/Kyoo/Controllers/Repositories/PeopleRepository.cs index 85664970..a9f2e920 100644 --- a/Kyoo/Controllers/Repositories/PeopleRepository.cs +++ b/Kyoo/Controllers/Repositories/PeopleRepository.cs @@ -1,58 +1,56 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class PeopleRepository : IPeopleRepository + public class PeopleRepository : LocalRepository, IPeopleRepository { private readonly DatabaseContext _database; private readonly IProviderRepository _providers; + private readonly Lazy _shows; + protected override Expression> DefaultSort => x => x.Name; - public PeopleRepository(DatabaseContext database, IProviderRepository providers) + public PeopleRepository(DatabaseContext database, IProviderRepository providers, IServiceProvider services) + : base(database) { _database = database; _providers = providers; + _shows = new Lazy(services.GetRequiredService); } - - public void Dispose() + + + public override void Dispose() { _database.Dispose(); + _providers.Dispose(); + if (_shows.IsValueCreated) + _shows.Value.Dispose(); } - public ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - return _database.DisposeAsync(); + await _database.DisposeAsync(); + await _providers.DisposeAsync(); + if (_shows.IsValueCreated) + await _shows.Value.DisposeAsync(); } - public Task Get(int id) + public override async Task> Search(string query) { - return _database.Peoples.FirstOrDefaultAsync(x => x.ID == id); - } - - public Task Get(string slug) - { - return _database.Peoples.FirstOrDefaultAsync(x => x.Slug == slug); - } - - public async Task> Search(string query) - { - return await _database.Peoples - .Where(people => EF.Functions.Like(people.Name, $"%{query}%")) + return await _database.People + .Where(people => EF.Functions.ILike(people.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Peoples.ToListAsync(); - } - - public async Task Create(People obj) + public override async Task Create(People obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -63,79 +61,18 @@ namespace Kyoo.Controllers foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated people (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists)."); + return obj; } - public async Task CreateIfNotExists(People obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - People old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } - } - - public async Task Edit(People edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - People old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No people found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await Validate(old); - await _database.SaveChangesAsync(); - } - - private async Task Validate(People obj) + protected override async Task Validate(People obj) { if (obj.ExternalIDs != null) foreach (MetadataID link in obj.ExternalIDs) - link.ProviderID = await _providers.CreateIfNotExists(link.Provider); + link.Provider = await _providers.CreateIfNotExists(link.Provider); } - public async Task Delete(int id) - { - People obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - People obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(People obj) + public override async Task Delete(People obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -149,23 +86,91 @@ namespace Kyoo.Controllers _database.Entry(link).State = EntityState.Deleted; await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) + + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (People obj in objs) - await Delete(obj); + if (sort.Key?.Body is MemberExpression member) + { + sort.Key = member.Member.Name switch + { + "Name" => x => x.People.Name, + "Slug" => x => x.People.Slug, + _ => sort.Key + }; + } + + ICollection people = await ApplyFilters(_database.PeopleRoles.Where(x => x.ShowID == showID), + id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), + x => x.People.Name, + where, + sort, + limit); + if (!people.Any() && await _shows.Value.Get(showID) == null) + throw new ItemNotFound(); + return people; + } + + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + if (sort.Key?.Body is MemberExpression member) + { + sort.Key = member.Member.Name switch + { + "Name" => x => x.People.Name, + "Slug" => x => x.People.Slug, + _ => sort.Key + }; + } + + ICollection people = await ApplyFilters(_database.PeopleRoles.Where(x => x.Show.Slug == showSlug), + id => _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id), + x => x.People.Name, + where, + sort, + limit); + if (!people.Any() && await _shows.Value.Get(showSlug) == null) + throw new ItemNotFound(); + return people; } - public async Task DeleteRange(IEnumerable ids) + public async Task> GetFromPeople(int peopleID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (int id in ids) - await Delete(id); + ICollection roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.PeopleID == peopleID) + .Select(ShowRole.FromPeopleRole), + async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), + x => x.Title, + where, + sort, + limit); + if (!roles.Any() && await Get(peopleID) == null) + throw new ItemNotFound(); + return roles; } - public async Task DeleteRange(IEnumerable slugs) + public async Task> GetFromPeople(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (string slug in slugs) - await Delete(slug); + ICollection roles = await ApplyFilters(_database.PeopleRoles.Where(x => x.People.Slug == slug) + .Select(ShowRole.FromPeopleRole), + async id => new ShowRole(await _database.PeopleRoles.FirstOrDefaultAsync(x => x.ID == id)), + x => x.Title, + where, + sort, + limit); + if (!roles.Any() && await Get(slug) == null) + throw new ItemNotFound(); + return roles; } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ProviderRepository.cs b/Kyoo/Controllers/Repositories/ProviderRepository.cs index ec298411..79025b00 100644 --- a/Kyoo/Controllers/Repositories/ProviderRepository.cs +++ b/Kyoo/Controllers/Repositories/ProviderRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -8,121 +9,42 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class ProviderRepository : IProviderRepository + public class ProviderRepository : LocalRepository, IProviderRepository { private readonly DatabaseContext _database; + protected override Expression> DefaultSort => x => x.Slug; - public ProviderRepository(DatabaseContext database) + public ProviderRepository(DatabaseContext database) : base(database) { _database = database; } - - public void Dispose() - { - _database.Dispose(); - } - public ValueTask DisposeAsync() - { - return _database.DisposeAsync(); - } - - public async Task Get(int id) - { - return await _database.Providers.FirstOrDefaultAsync(x => x.ID == id); - } - - public async Task Get(string slug) - { - return await _database.Providers.FirstOrDefaultAsync(x => x.Name == slug); - } - - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Providers - .Where(x => EF.Functions.Like(x.Name, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Providers.ToListAsync(); - } - - public async Task Create(ProviderID obj) + public override async Task Create(ProviderID obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated provider (name {obj.Name} already exists)."); - throw; - } - - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated provider (slug {obj.Slug} already exists)."); + return obj; } - - public async Task CreateIfNotExists(ProviderID obj) + + protected override Task Validate(ProviderID ressource) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - ProviderID old = await Get(obj.Name); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Name); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + return Task.CompletedTask; } - public async Task Edit(ProviderID edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - ProviderID old = await Get(edited.Name); - - if (old == null) - throw new ItemNotFound($"No provider found with the name {edited.Name}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - ProviderID obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - ProviderID obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(ProviderID obj) + public override async Task Delete(ProviderID obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -131,23 +53,5 @@ namespace Kyoo.Controllers // TODO handle ExternalID deletion when they refer to this providerID. await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) - { - foreach (ProviderID obj in objs) - await Delete(obj); - } - - public async Task DeleteRange(IEnumerable ids) - { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); - } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/SeasonRepository.cs b/Kyoo/Controllers/Repositories/SeasonRepository.cs index c9433c72..4875ed85 100644 --- a/Kyoo/Controllers/Repositories/SeasonRepository.cs +++ b/Kyoo/Controllers/Repositories/SeasonRepository.cs @@ -1,51 +1,67 @@ using System; using System.Collections.Generic; using System.Linq; +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 { - public class SeasonRepository : ISeasonRepository + public class SeasonRepository : LocalRepository, ISeasonRepository { private readonly DatabaseContext _database; private readonly IProviderRepository _providers; - private readonly IEpisodeRepository _episodes; + private readonly Lazy _episodes; + private readonly IShowRepository _shows; + protected override Expression> DefaultSort => x => x.SeasonNumber; - public SeasonRepository(DatabaseContext database, IProviderRepository providers, IEpisodeRepository episodes) + public SeasonRepository(DatabaseContext database, + IProviderRepository providers, + IShowRepository shows, + IServiceProvider services) + : base(database) { _database = database; _providers = providers; - _episodes = episodes; + _episodes = new Lazy(services.GetRequiredService); + _shows = shows; } - - public void Dispose() + + + public override void Dispose() { _database.Dispose(); + _providers.Dispose(); + if (_episodes.IsValueCreated) + _episodes.Value.Dispose(); } - public ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - return _database.DisposeAsync(); + await _database.DisposeAsync(); + await _providers.DisposeAsync(); + if (_episodes.IsValueCreated) + await _episodes.Value.DisposeAsync(); + } + + public override Task Get(string slug) + { + Match match = Regex.Match(slug, @"(?.*)-s(?\d*)"); + + if (!match.Success) + throw new ArgumentException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); + return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); } - public Task Get(int id) + public Task Get(int showID, int seasonNumber) { - return _database.Seasons.FirstOrDefaultAsync(x => x.ID == id); - } - - public Task Get(string slug) - { - int index = slug.IndexOf("-s", StringComparison.Ordinal); - if (index == -1) - throw new InvalidOperationException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); - string showSlug = slug.Substring(0, index); - if (!int.TryParse(slug.Substring(index + 2), out int seasonNumber)) - throw new InvalidOperationException("Invalid season slug. Format: {showSlug}-s{seasonNumber}"); - return Get(showSlug, seasonNumber); + return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID + && x.SeasonNumber == seasonNumber); } public Task Get(string showSlug, int seasonNumber) @@ -54,20 +70,15 @@ namespace Kyoo.Controllers && x.SeasonNumber == seasonNumber); } - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Seasons - .Where(x => EF.Functions.Like(x.Title, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Title, $"%{query}%")) .Take(20) .ToListAsync(); } - - public async Task> GetAll() - { - return await _database.Seasons.ToListAsync(); - } - - public async Task Create(Season obj) + + public override async Task Create(Season obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -78,61 +89,11 @@ namespace Kyoo.Controllers foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated season (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; - } - - public async Task CreateIfNotExists(Season obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Season old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } + await _database.SaveChangesAsync($"Trying to insert a duplicated season (slug {obj.Slug} already exists)."); + return obj; } - public async Task Edit(Season edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Season old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No season found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - - await Validate(old); - await _database.SaveChangesAsync(); - } - - private async Task Validate(Season obj) + protected override async Task Validate(Season obj) { if (obj.ShowID <= 0) throw new InvalidOperationException($"Can't store a season not related to any show (showID: {obj.ShowID})."); @@ -140,39 +101,45 @@ namespace Kyoo.Controllers if (obj.ExternalIDs != null) { foreach (MetadataID link in obj.ExternalIDs) - link.ProviderID = await _providers.CreateIfNotExists(link.Provider); + link.Provider = await _providers.CreateIfNotExists(link.Provider); } } - public async Task> GetSeasons(int showID) + public async Task> GetFromShow(int showID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - return await _database.Seasons.Where(x => x.ShowID == showID).ToListAsync(); + ICollection seasons = await ApplyFilters(_database.Seasons.Where(x => x.ShowID == showID), + where, + sort, + limit); + if (!seasons.Any() && await _shows.Get(showID) == null) + throw new ItemNotFound(); + return seasons; } - public async Task> GetSeasons(string showSlug) + public async Task> GetFromShow(string showSlug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - return await _database.Seasons.Where(x => x.Show.Slug == showSlug).ToListAsync(); + ICollection seasons = await ApplyFilters(_database.Seasons.Where(x => x.Show.Slug == showSlug), + where, + sort, + limit); + if (!seasons.Any() && await _shows.Get(showSlug) == null) + throw new ItemNotFound(); + return seasons; } - public async Task Delete(int id) - { - Season obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Season obj = await Get(slug); - await Delete(obj); - } - public async Task Delete(string showSlug, int seasonNumber) { Season obj = await Get(showSlug, seasonNumber); await Delete(obj); } - public async Task Delete(Season obj) + public override async Task Delete(Season obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -186,25 +153,12 @@ namespace Kyoo.Controllers await _database.SaveChangesAsync(); if (obj.Episodes != null) - await _episodes.DeleteRange(obj.Episodes); + await _episodes.Value.DeleteRange(obj.Episodes); } - - public async Task DeleteRange(IEnumerable objs) + + public Task GetFromEpisode(int episodeID) { - foreach (Season obj in objs) - await Delete(obj); - } - - public async Task DeleteRange(IEnumerable ids) - { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); + return _database.Seasons.FirstOrDefaultAsync(x => x.Episodes.Any(y => y.ID == episodeID)); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index 20bc7f67..a23d6c35 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -1,81 +1,92 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Controllers { - public class ShowRepository : IShowRepository + public class ShowRepository : LocalRepository, IShowRepository { private readonly DatabaseContext _database; private readonly IStudioRepository _studios; private readonly IPeopleRepository _people; private readonly IGenreRepository _genres; private readonly IProviderRepository _providers; - private readonly ISeasonRepository _seasons; - private readonly IEpisodeRepository _episodes; + private readonly Lazy _seasons; + private readonly Lazy _episodes; + private readonly Lazy _libraries; + private readonly Lazy _collections; + protected override Expression> DefaultSort => x => x.Title; public ShowRepository(DatabaseContext database, IStudioRepository studios, IPeopleRepository people, IGenreRepository genres, - IProviderRepository providers, - ISeasonRepository seasons, - IEpisodeRepository episodes) + IProviderRepository providers, + IServiceProvider services) + : base(database) { _database = database; _studios = studios; _people = people; _genres = genres; _providers = providers; - _seasons = seasons; - _episodes = episodes; + _seasons = new Lazy(services.GetRequiredService); + _episodes = new Lazy(services.GetRequiredService); + _libraries = new Lazy(services.GetRequiredService); + _collections = new Lazy(services.GetRequiredService); } - - public void Dispose() + + public override void Dispose() { _database.Dispose(); _studios.Dispose(); + _people.Dispose(); + _genres.Dispose(); + _providers.Dispose(); + if (_seasons.IsValueCreated) + _seasons.Value.Dispose(); + if (_episodes.IsValueCreated) + _episodes.Value.Dispose(); + if (_libraries.IsValueCreated) + _libraries.Value.Dispose(); + if (_collections.IsValueCreated) + _collections.Value.Dispose(); } - public async ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - await Task.WhenAll(_database.DisposeAsync().AsTask(), _studios.DisposeAsync().AsTask()); - } - - public Task Get(int id) - { - return _database.Shows.FirstOrDefaultAsync(x => x.ID == id); - } - - public Task Get(string slug) - { - return _database.Shows.FirstOrDefaultAsync(x => x.Slug == slug); + 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(); + if (_libraries.IsValueCreated) + await _libraries.Value.DisposeAsync(); + if (_collections.IsValueCreated) + await _collections.Value.DisposeAsync(); } - public Task GetByPath(string path) - { - return _database.Shows.FirstOrDefaultAsync(x => x.Path == path); - } - - public async Task> Search(string query) + public override async Task> Search(string query) { + query = $"%{query}%"; return await _database.Shows - .FromSqlInterpolated($@"SELECT * FROM Shows WHERE 'Shows.Title' LIKE {$"%{query}%"} - OR 'Shows.Aliases' LIKE {$"%{query}%"}") + .Where(x => EF.Functions.ILike(x.Title, query) + /*|| x.Aliases.Any(y => EF.Functions.ILike(y, query))*/) // NOT TRANSLATABLE. .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Shows.ToListAsync(); - } - - public async Task Create(Show obj) + public override async Task Create(Show obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -92,113 +103,49 @@ namespace Kyoo.Controllers foreach (MetadataID entry in obj.ExternalIDs) _database.Entry(entry).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated show (slug {obj.Slug} already exists)."); - throw; - } - - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated show (slug {obj.Slug} already exists)."); + return obj; } - public async Task CreateIfNotExists(Show obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Show old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } - } - - public async Task Edit(Show edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Show old = await Get(edited.Slug); - - if (old == null) - throw new ItemNotFound($"No show found with the slug {edited.Slug}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await Validate(old); - await _database.SaveChangesAsync(); - } - - private async Task Validate(Show obj) + protected override async Task Validate(Show obj) { if (obj.Studio != null) - obj.StudioID = await _studios.CreateIfNotExists(obj.Studio); + obj.Studio = await _studios.CreateIfNotExists(obj.Studio); if (obj.GenreLinks != null) foreach (GenreLink link in obj.GenreLinks) - link.GenreID = await _genres.CreateIfNotExists(link.Genre); + link.Genre = await _genres.CreateIfNotExists(link.Genre); if (obj.People != null) foreach (PeopleLink link in obj.People) - link.PeopleID = await _people.CreateIfNotExists(link.People); + link.People = await _people.CreateIfNotExists(link.People); if (obj.ExternalIDs != null) foreach (MetadataID link in obj.ExternalIDs) - link.ProviderID = await _providers.CreateIfNotExists(link.Provider); + link.Provider = await _providers.CreateIfNotExists(link.Provider); } public async Task AddShowLink(int showID, int? libraryID, int? collectionID) { if (collectionID != null) { - _database.CollectionLinks.AddIfNotExist(new CollectionLink { CollectionID = collectionID, ShowID = showID}, - x => x.CollectionID == collectionID && x.ShowID == showID); + await _database.CollectionLinks.AddAsync(new CollectionLink {CollectionID = collectionID, ShowID = showID}); + await _database.SaveIfNoDuplicates(); } if (libraryID != null) { - _database.LibraryLinks.AddIfNotExist(new LibraryLink {LibraryID = libraryID.Value, ShowID = showID}, - x => x.LibraryID == libraryID.Value && x.CollectionID == null && x.ShowID == showID); + await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, ShowID = showID}); + await _database.SaveIfNoDuplicates(); } if (libraryID != null && collectionID != null) { - _database.LibraryLinks.AddIfNotExist( - new LibraryLink {LibraryID = libraryID.Value, CollectionID = collectionID.Value}, - x => x.LibraryID == libraryID && x.CollectionID == collectionID && x.ShowID == null); + await _database.LibraryLinks.AddAsync(new LibraryLink {LibraryID = libraryID.Value, CollectionID = collectionID.Value}); + await _database.SaveIfNoDuplicates(); } - - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - Show obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Show obj = await Get(slug); - await Delete(obj); } - public async Task Delete(Show obj) + public override async Task Delete(Show obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -224,32 +171,88 @@ namespace Kyoo.Controllers if (obj.LibraryLinks != null) foreach (LibraryLink entry in obj.LibraryLinks) _database.Entry(entry).State = EntityState.Deleted; - - await _database.SaveChangesAsync(); + await _database.SaveChangesAsync(); + if (obj.Seasons != null) - await _seasons.DeleteRange(obj.Seasons); + await _seasons.Value.DeleteRange(obj.Seasons); if (obj.Episodes != null) - await _episodes.DeleteRange(obj.Episodes); + await _episodes.Value.DeleteRange(obj.Episodes); + } + + public async Task> GetFromLibrary(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection shows = await ApplyFilters(_database.LibraryLinks + .Where(x => x.LibraryID == id && x.ShowID != null) + .Select(x => x.Show), + where, + sort, + limit); + if (!shows.Any() && await _libraries.Value.Get(id) == null) + throw new ItemNotFound(); + return shows; + } + + public async Task> GetFromLibrary(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection shows = await ApplyFilters(_database.LibraryLinks + .Where(x => x.Library.Slug == slug && x.ShowID != null) + .Select(x => x.Show), + where, + sort, + limit); + if (!shows.Any() && await _libraries.Value.Get(slug) == null) + throw new ItemNotFound(); + return shows; } - public async Task DeleteRange(IEnumerable objs) + public async Task> GetFromCollection(int id, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (Show obj in objs) - await Delete(obj); + ICollection shows = await ApplyFilters(_database.CollectionLinks + .Where(x => x.CollectionID== id) + .Select(x => x.Show), + where, + sort, + limit); + if (!shows.Any() && await _libraries.Value.Get(id) == null) + throw new ItemNotFound(); + return shows; + } + + public async Task> GetFromCollection(string slug, + Expression> where = null, + Sort sort = default, + Pagination limit = default) + { + ICollection shows = await ApplyFilters(_database.CollectionLinks + .Where(x => x.Collection.Slug == slug) + .Select(x => x.Show), + where, + sort, + limit); + if (!shows.Any() && await _libraries.Value.Get(slug) == null) + throw new ItemNotFound(); + return shows; + } + + public Task GetFromSeason(int seasonID) + { + return _database.Shows.FirstOrDefaultAsync(x => x.Seasons.Any(y => y.ID == seasonID)); } - public async Task DeleteRange(IEnumerable ids) + public Task GetFromEpisode(int episodeID) { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); + return _database.Shows.FirstOrDefaultAsync(x => x.Episodes.Any(y => y.ID == episodeID)); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/StudioRepository.cs b/Kyoo/Controllers/Repositories/StudioRepository.cs index 69e93e74..e2680341 100644 --- a/Kyoo/Controllers/Repositories/StudioRepository.cs +++ b/Kyoo/Controllers/Repositories/StudioRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Models; using Kyoo.Models.Exceptions; @@ -8,120 +9,41 @@ using Microsoft.EntityFrameworkCore; namespace Kyoo.Controllers { - public class StudioRepository : IStudioRepository + public class StudioRepository : LocalRepository, IStudioRepository { private readonly DatabaseContext _database; + protected override Expression> DefaultSort => x => x.Name; - public StudioRepository(DatabaseContext database) + public StudioRepository(DatabaseContext database) : base(database) { _database = database; } - public void Dispose() - { - _database.Dispose(); - } - - public ValueTask DisposeAsync() - { - return _database.DisposeAsync(); - } - - public async Task Get(int id) - { - return await _database.Studios.FirstOrDefaultAsync(x => x.ID == id); - } - - public async Task Get(string slug) - { - return await _database.Studios.FirstOrDefaultAsync(x => x.Slug == slug); - } - - public async Task> Search(string query) + public override async Task> Search(string query) { return await _database.Studios - .Where(x => EF.Functions.Like(x.Name, $"%{query}%")) + .Where(x => EF.Functions.ILike(x.Name, $"%{query}%")) .Take(20) .ToListAsync(); } - public async Task> GetAll() - { - return await _database.Studios.ToListAsync(); - } - - public async Task Create(Studio obj) + public override async Task Create(Studio obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); _database.Entry(obj).State = EntityState.Added; - - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated studio (slug {obj.Slug} already exists)."); - throw; - } - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated studio (slug {obj.Slug} already exists)."); + return obj; + } + + protected override Task Validate(Studio ressource) + { + return Task.CompletedTask; } - public async Task CreateIfNotExists(Studio obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - Studio old = await Get(obj.Slug); - if (old != null) - return old.ID; - try - { - return await Create(obj); - } - catch (DuplicatedItemException) - { - old = await Get(obj.Slug); - if (old == null) - throw new SystemException("Unknown database state."); - return old.ID; - } - } - - public async Task Edit(Studio edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Studio old = await Get(edited.Name); - - if (old == null) - throw new ItemNotFound($"No studio found with the name {edited.Name}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - Studio obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Studio obj = await Get(slug); - await Delete(obj); - } - - public async Task Delete(Studio obj) + public override async Task Delete(Studio obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -133,23 +55,27 @@ namespace Kyoo.Controllers show.StudioID = null; await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) + + public async Task GetFromShow(int showID) { - foreach (Studio obj in objs) - await Delete(obj); + Studio studio = await _database.Shows + .Where(x => x.ID == showID) + .Select(x => x.Studio) + .FirstOrDefaultAsync(); + if (studio == null && !_database.Shows.Any(x => x.ID == showID)) + throw new ItemNotFound(); + return studio; } - - public async Task DeleteRange(IEnumerable ids) + + public async Task GetFromShow(string showSlug) { - foreach (int id in ids) - await Delete(id); - } - - public async Task DeleteRange(IEnumerable slugs) - { - foreach (string slug in slugs) - await Delete(slug); + Studio studio = await _database.Shows + .Where(x => x.Slug == showSlug) + .Select(x => x.Studio) + .FirstOrDefaultAsync(); + if (studio == null && !_database.Shows.Any(x => x.Slug == showSlug)) + throw new ItemNotFound(); + return studio; } } } \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index dbc54b01..f6c81f70 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -1,60 +1,72 @@ using System; using System.Collections.Generic; +using System.Linq; +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 { - public class TrackRepository : ITrackRepository + public class TrackRepository : LocalRepository, ITrackRepository { private readonly DatabaseContext _database; + private readonly Lazy _episodes; + protected override Expression> DefaultSort => x => x.ID; - public TrackRepository(DatabaseContext database) + public TrackRepository(DatabaseContext database, IServiceProvider services) : base(database) { _database = database; + _episodes = new Lazy(services.GetRequiredService); } - - public void Dispose() + + public override void Dispose() { _database.Dispose(); + if (_episodes.IsValueCreated) + _episodes.Value.Dispose(); } - public ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { - return _database.DisposeAsync(); - } - - public async Task Get(int id) - { - return await _database.Tracks.FirstOrDefaultAsync(x => x.ID == id); - } - - public Task Get(string slug) - { - throw new InvalidOperationException("Tracks do not support the get by slug method."); + await _database.DisposeAsync(); + if (_episodes.IsValueCreated) + await _episodes.Value.DisposeAsync(); } - public Task Get(int episodeID, string languageTag, bool isForced) + public override Task Get(string slug) { - return _database.Tracks.FirstOrDefaultAsync(x => x.EpisodeID == episodeID - && x.Language == languageTag - && x.IsForced == isForced); - } + Match match = Regex.Match(slug, + @"(?.*)-s(?\d*)-e(?\d*).(?.{0,3})(?-forced)?(\..*)?"); - public Task> Search(string query) + if (!match.Success) + { + if (int.TryParse(slug, out int id)) + return Get(id); + throw new ArgumentException("Invalid track slug. Format: {episodeSlug}.{language}[-forced][.{extension}]"); + } + + string showSlug = match.Groups["show"].Value; + int seasonNumber = int.Parse(match.Groups["season"].Value); + int episodeNumber = int.Parse(match.Groups["episode"].Value); + string language = match.Groups["language"].Value; + bool forced = match.Groups["forced"].Success; + return _database.Tracks.FirstOrDefaultAsync(x => x.Episode.Show.Slug == showSlug + && x.Episode.SeasonNumber == seasonNumber + && x.Episode.EpisodeNumber == episodeNumber + && x.Language == language + && x.IsForced == forced); + } + public override Task> Search(string query) { throw new InvalidOperationException("Tracks do not support the search method."); } - public async Task> GetAll() - { - return await _database.Tracks.ToListAsync(); - } - - public async Task Create(Track obj) + public override async Task Create(Track obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -64,54 +76,16 @@ namespace Kyoo.Controllers _database.Entry(obj).State = EntityState.Added; - try - { - await _database.SaveChangesAsync(); - } - catch (DbUpdateException ex) - { - _database.DiscardChanges(); - if (Helper.IsDuplicateException(ex)) - throw new DuplicatedItemException($"Trying to insert a duplicated track (slug {obj.Slug} already exists)."); - throw; - } - return obj.ID; + await _database.SaveChangesAsync($"Trying to insert a duplicated track (slug {obj.Slug} already exists)."); + return obj; } - public Task CreateIfNotExists(Track obj) + protected override Task Validate(Track ressource) { - return Create(obj); - } - - public async Task Edit(Track edited, bool resetOld) - { - if (edited == null) - throw new ArgumentNullException(nameof(edited)); - - Track old = await Get(edited.ID); - - if (old == null) - throw new ItemNotFound($"No track found with the ID {edited.ID}."); - - if (resetOld) - Utility.Nullify(old); - Utility.Merge(old, edited); - await _database.SaveChangesAsync(); - } - - public async Task Delete(int id) - { - Track obj = await Get(id); - await Delete(obj); - } - - public async Task Delete(string slug) - { - Track obj = await Get(slug); - await Delete(obj); + return Task.CompletedTask; } - public async Task Delete(Track obj) + public override async Task Delete(Track obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -119,23 +93,55 @@ namespace Kyoo.Controllers _database.Entry(obj).State = EntityState.Deleted; await _database.SaveChangesAsync(); } - - public async Task DeleteRange(IEnumerable objs) + + public async Task> GetFromEpisode(int episodeID, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (Track obj in objs) - await Delete(obj); + ICollection tracks = await ApplyFilters(_database.Tracks.Where(x => x.EpisodeID == episodeID), + where, + sort, + limit); + if (!tracks.Any() && await _episodes.Value.Get(episodeID) == null) + throw new ItemNotFound(); + return tracks; } - - public async Task DeleteRange(IEnumerable ids) + + public async Task> GetFromEpisode(int showID, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (int id in ids) - await Delete(id); + ICollection tracks = await ApplyFilters(_database.Tracks.Where(x => x.Episode.ShowID == showID + && x.Episode.SeasonNumber == seasonNumber + && x.Episode.EpisodeNumber == episodeNumber), + where, + sort, + limit); + if (!tracks.Any() && await _episodes.Value.Get(showID, seasonNumber, episodeNumber) == null) + throw new ItemNotFound(); + return tracks; } - - public async Task DeleteRange(IEnumerable slugs) + + public async Task> GetFromEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + Expression> where = null, + Sort sort = default, + Pagination limit = default) { - foreach (string slug in slugs) - await Delete(slug); + ICollection tracks = await ApplyFilters(_database.Tracks.Where(x => x.Episode.Show.Slug == showSlug + && x.Episode.SeasonNumber == seasonNumber + && x.Episode.EpisodeNumber == episodeNumber), + where, + sort, + limit); + if (!tracks.Any() && await _episodes.Value.Get(showSlug, seasonNumber, episodeNumber) == null) + throw new ItemNotFound(); + return tracks; } } } \ No newline at end of file diff --git a/Kyoo/Controllers/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs index 75f8ee5c..ba40992a 100644 --- a/Kyoo/Controllers/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -68,10 +68,10 @@ namespace Kyoo.Controllers foreach (PeopleLink peop in people) { string localPath = Path.Combine(root, peop.People.Slug + ".jpg"); - if (peop.People.ImgPrimary == null) + if (peop.People.Poster == null) continue; if (alwaysDownload || !File.Exists(localPath)) - await DownloadImage(peop.People.ImgPrimary, localPath, $"The profile picture of {peop.People.Name}"); + await DownloadImage(peop.People.Poster, localPath, $"The profile picture of {peop.People.Name}"); } return people; diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 69d8d8df..76fff030 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -44,6 +44,7 @@ + diff --git a/Kyoo/Models/DatabaseContext.cs b/Kyoo/Models/DatabaseContext.cs index a6e657f7..e730ca69 100644 --- a/Kyoo/Models/DatabaseContext.cs +++ b/Kyoo/Models/DatabaseContext.cs @@ -1,18 +1,22 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using IdentityServer4.EntityFramework.Entities; using IdentityServer4.EntityFramework.Extensions; using IdentityServer4.EntityFramework.Interfaces; using IdentityServer4.EntityFramework.Options; using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Kyoo.Models.Watch; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Options; +using Npgsql; namespace Kyoo { @@ -60,42 +64,51 @@ namespace Kyoo public DbSet Episodes { get; set; } public DbSet Tracks { get; set; } public DbSet Genres { get; set; } - public DbSet Peoples { get; set; } + public DbSet People { get; set; } public DbSet Studios { get; set; } public DbSet Providers { get; set; } public DbSet MetadataIds { get; set; } - public DbSet LibraryLinks { get; set; } - public DbSet CollectionLinks { get; set; } - public DbSet PeopleLinks { get; set; } + public DbSet PeopleRoles { get; set; } + // This is used because EF doesn't support Many-To-Many relationships so for now we need to override the getter/setters to store this. + public DbSet LibraryLinks { get; set; } + public DbSet CollectionLinks { get; set; } public DbSet GenreLinks { get; set; } public DbSet ProviderLinks { get; set; } - - - private readonly ValueConverter, string> _stringArrayConverter = - new ValueConverter, string>( - arr => string.Join("|", arr), - str => str.Split("|", StringSplitOptions.None)); + public DatabaseContext() + { + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + } + private readonly ValueComparer> _stringArrayComparer = new ValueComparer>( - (l1, l2) => l1.SequenceEqual(l2), - arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); - + (l1, l2) => l1.SequenceEqual(l2), + arr => arr.Aggregate(0, (i, s) => s.GetHashCode())); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().Property(e => e.Paths) - .HasConversion(_stringArrayConverter).Metadata - .SetValueComparer(_stringArrayComparer); - modelBuilder.Entity().Property(e => e.Aliases) - .HasConversion(_stringArrayConverter).Metadata - .SetValueComparer(_stringArrayComparer); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + modelBuilder.Entity() + .Property(x => x.Paths) + .HasColumnType("text[]") + .Metadata.SetValueComparer(_stringArrayComparer); + + modelBuilder.Entity() + .Property(x => x.Aliases) + .HasColumnType("text[]") + .Metadata.SetValueComparer(_stringArrayComparer); + + modelBuilder.Entity() .Property(t => t.IsDefault) .ValueGeneratedNever(); @@ -124,6 +137,7 @@ namespace Kyoo modelBuilder.Entity() .Ignore(x => x.Slug) .Ignore(x => x.Name) + .Ignore(x => x.Poster) .Ignore(x => x.ExternalIDs); modelBuilder.Entity() @@ -167,7 +181,7 @@ namespace Kyoo .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() - .HasIndex(x => x.Name) + .HasIndex(x => x.Slug) .IsUnique(); modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber}) @@ -175,8 +189,127 @@ namespace Kyoo modelBuilder.Entity() .HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber}) .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => new {x.LibraryID, x.ShowID}) + .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => new {x.LibraryID, x.CollectionID}) + .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => new {x.CollectionID, x.ShowID}) + .IsUnique(); } - + + public override int SaveChanges() + { + try + { + return base.SaveChanges(); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(); + throw; + } + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + try + { + return base.SaveChanges(acceptAllChangesOnSuccess); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(); + throw; + } + } + + public int SaveChanges(string duplicateMessage) + { + try + { + return base.SaveChanges(); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(duplicateMessage); + throw; + } + } + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = new CancellationToken()) + { + try + { + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(); + throw; + } + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + try + { + return await base.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(); + throw; + } + } + + public async Task SaveChangesAsync(string duplicateMessage, + CancellationToken cancellationToken = new CancellationToken()) + { + try + { + return await base.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) + { + DiscardChanges(); + if (IsDuplicateException(ex)) + throw new DuplicatedItemException(duplicateMessage); + throw; + } + } + + public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new CancellationToken()) + { + try + { + return await SaveChangesAsync(cancellationToken); + } + catch (DuplicatedItemException) + { + return -1; + } + } + + public static bool IsDuplicateException(DbUpdateException ex) + { + return ex.InnerException is PostgresException inner + && inner.SqlState == PostgresErrorCodes.UniqueViolation; + } + public void DiscardChanges() { foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged @@ -186,13 +319,4 @@ namespace Kyoo } } } -} - -public static class DbSetExtension -{ - public static EntityEntry AddIfNotExist(this DbSet db, T entity, Func predicate) where T : class - { - bool exists = db.Any(predicate); - return exists ? null : db.Add(entity); - } } \ No newline at end of file diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.Designer.cs b/Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.Designer.cs similarity index 95% rename from Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.Designer.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.Designer.cs index a8f29bf5..0e398aef 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.Designer.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.Designer.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Kyoo; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -10,13 +11,16 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Kyoo.Models.DatabaseMigrations.Internal { [DbContext(typeof(DatabaseContext))] - [Migration("20200623233713_Initial")] + [Migration("20200804172021_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder + .HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") + .HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .HasAnnotation("Npgsql:Enum:stream_type", "unknow,video,audio,subtitle") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("Relational:MaxIdentifierLength", 63); @@ -28,9 +32,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ImgPrimary") - .HasColumnType("text"); - b.Property("Name") .HasColumnType("text"); @@ -66,10 +67,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasKey("ID"); - b.HasIndex("CollectionID"); - b.HasIndex("ShowID"); + b.HasIndex("CollectionID", "ShowID") + .IsUnique(); + b.ToTable("CollectionLinks"); }); @@ -169,8 +171,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); - b.Property("Paths") - .HasColumnType("text"); + b.Property>("Paths") + .HasColumnType("text[]"); b.Property("Slug") .HasColumnType("text"); @@ -203,10 +205,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("CollectionID"); - b.HasIndex("LibraryID"); - b.HasIndex("ShowID"); + b.HasIndex("LibraryID", "CollectionID") + .IsUnique(); + + b.HasIndex("LibraryID", "ShowID") + .IsUnique(); + b.ToTable("LibraryLinks"); }); @@ -260,10 +266,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ImgPrimary") + b.Property("Name") .HasColumnType("text"); - b.Property("Name") + b.Property("Poster") .HasColumnType("text"); b.Property("Slug") @@ -274,7 +280,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("Slug") .IsUnique(); - b.ToTable("Peoples"); + b.ToTable("People"); }); modelBuilder.Entity("Kyoo.Models.PeopleLink", b => @@ -302,7 +308,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("ShowID"); - b.ToTable("PeopleLinks"); + b.ToTable("PeopleRoles"); }); modelBuilder.Entity("Kyoo.Models.ProviderID", b => @@ -318,9 +324,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); + b.Property("Slug") + .HasColumnType("text"); + b.HasKey("ID"); - b.HasIndex("Name") + b.HasIndex("Slug") .IsUnique(); b.ToTable("Providers"); @@ -388,8 +397,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("Aliases") - .HasColumnType("text"); + b.Property>("Aliases") + .HasColumnType("text[]"); b.Property("Backdrop") .HasColumnType("text"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.cs b/Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.cs similarity index 92% rename from Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.cs rename to Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.cs index 4bdd56d9..d94bb616 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/20200623233713_Initial.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/20200804172021_Initial.cs @@ -8,6 +8,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:item_type", "show,movie,collection") + .Annotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .Annotation("Npgsql:Enum:stream_type", "unknow,video,audio,subtitle"); + migrationBuilder.CreateTable( name: "Collections", columns: table => new @@ -17,8 +22,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal Slug = table.Column(nullable: true), Name = table.Column(nullable: true), Poster = table.Column(nullable: true), - Overview = table.Column(nullable: true), - ImgPrimary = table.Column(nullable: true) + Overview = table.Column(nullable: true) }, constraints: table => { @@ -47,7 +51,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Slug = table.Column(nullable: true), Name = table.Column(nullable: true), - Paths = table.Column(nullable: true) + Paths = table.Column(type: "text[]", nullable: true) }, constraints: table => { @@ -55,18 +59,18 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }); migrationBuilder.CreateTable( - name: "Peoples", + name: "People", columns: table => new { ID = table.Column(nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Slug = table.Column(nullable: true), Name = table.Column(nullable: true), - ImgPrimary = table.Column(nullable: true) + Poster = table.Column(nullable: true) }, constraints: table => { - table.PrimaryKey("PK_Peoples", x => x.ID); + table.PrimaryKey("PK_People", x => x.ID); }); migrationBuilder.CreateTable( @@ -75,6 +79,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { ID = table.Column(nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Slug = table.Column(nullable: true), Name = table.Column(nullable: true), Logo = table.Column(nullable: true) }, @@ -131,7 +136,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Slug = table.Column(nullable: true), Title = table.Column(nullable: true), - Aliases = table.Column(nullable: true), + Aliases = table.Column(type: "text[]", nullable: true), Path = table.Column(nullable: true), Overview = table.Column(nullable: true), Status = table.Column(nullable: true), @@ -239,7 +244,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }); migrationBuilder.CreateTable( - name: "PeopleLinks", + name: "PeopleRoles", columns: table => new { ID = table.Column(nullable: false) @@ -251,15 +256,15 @@ namespace Kyoo.Models.DatabaseMigrations.Internal }, constraints: table => { - table.PrimaryKey("PK_PeopleLinks", x => x.ID); + table.PrimaryKey("PK_PeopleRoles", x => x.ID); table.ForeignKey( - name: "FK_PeopleLinks_Peoples_PeopleID", + name: "FK_PeopleRoles_People_PeopleID", column: x => x.PeopleID, - principalTable: "Peoples", + principalTable: "People", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_PeopleLinks_Shows_ShowID", + name: "FK_PeopleRoles_Shows_ShowID", column: x => x.ShowID, principalTable: "Shows", principalColumn: "ID", @@ -349,9 +354,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_MetadataIds_Peoples_PeopleID", + name: "FK_MetadataIds_People_PeopleID", column: x => x.PeopleID, - principalTable: "Peoples", + principalTable: "People", principalColumn: "ID", onDelete: ReferentialAction.Cascade); table.ForeignKey( @@ -401,16 +406,17 @@ namespace Kyoo.Models.DatabaseMigrations.Internal onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_CollectionLinks_CollectionID", - table: "CollectionLinks", - column: "CollectionID"); - migrationBuilder.CreateIndex( name: "IX_CollectionLinks_ShowID", table: "CollectionLinks", column: "ShowID"); + migrationBuilder.CreateIndex( + name: "IX_CollectionLinks_CollectionID_ShowID", + table: "CollectionLinks", + columns: new[] { "CollectionID", "ShowID" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_Collections_Slug", table: "Collections", @@ -450,16 +456,23 @@ namespace Kyoo.Models.DatabaseMigrations.Internal table: "LibraryLinks", column: "CollectionID"); - migrationBuilder.CreateIndex( - name: "IX_LibraryLinks_LibraryID", - table: "LibraryLinks", - column: "LibraryID"); - migrationBuilder.CreateIndex( name: "IX_LibraryLinks_ShowID", table: "LibraryLinks", column: "ShowID"); + migrationBuilder.CreateIndex( + name: "IX_LibraryLinks_LibraryID_CollectionID", + table: "LibraryLinks", + columns: new[] { "LibraryID", "CollectionID" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_LibraryLinks_LibraryID_ShowID", + table: "LibraryLinks", + columns: new[] { "LibraryID", "ShowID" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_MetadataIds_EpisodeID", table: "MetadataIds", @@ -486,21 +499,21 @@ namespace Kyoo.Models.DatabaseMigrations.Internal column: "ShowID"); migrationBuilder.CreateIndex( - name: "IX_PeopleLinks_PeopleID", - table: "PeopleLinks", + name: "IX_People_Slug", + table: "People", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PeopleRoles_PeopleID", + table: "PeopleRoles", column: "PeopleID"); migrationBuilder.CreateIndex( - name: "IX_PeopleLinks_ShowID", - table: "PeopleLinks", + name: "IX_PeopleRoles_ShowID", + table: "PeopleRoles", column: "ShowID"); - migrationBuilder.CreateIndex( - name: "IX_Peoples_Slug", - table: "Peoples", - column: "Slug", - unique: true); - migrationBuilder.CreateIndex( name: "IX_ProviderLinks_LibraryID", table: "ProviderLinks", @@ -512,9 +525,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal column: "ProviderID"); migrationBuilder.CreateIndex( - name: "IX_Providers_Name", + name: "IX_Providers_Slug", table: "Providers", - column: "Name", + column: "Slug", unique: true); migrationBuilder.CreateIndex( @@ -561,7 +574,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "MetadataIds"); migrationBuilder.DropTable( - name: "PeopleLinks"); + name: "PeopleRoles"); migrationBuilder.DropTable( name: "ProviderLinks"); @@ -576,7 +589,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal name: "Collections"); migrationBuilder.DropTable( - name: "Peoples"); + name: "People"); migrationBuilder.DropTable( name: "Libraries"); diff --git a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs index 2479152f..b7c0e3cf 100644 --- a/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs +++ b/Kyoo/Models/DatabaseMigrations/Internal/DatabaseContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Kyoo; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,6 +16,9 @@ namespace Kyoo.Models.DatabaseMigrations.Internal { #pragma warning disable 612, 618 modelBuilder + .HasAnnotation("Npgsql:Enum:item_type", "show,movie,collection") + .HasAnnotation("Npgsql:Enum:status", "finished,airing,planned,unknown") + .HasAnnotation("Npgsql:Enum:stream_type", "unknow,video,audio,subtitle") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("ProductVersion", "3.1.3") .HasAnnotation("Relational:MaxIdentifierLength", 63); @@ -26,9 +30,6 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ImgPrimary") - .HasColumnType("text"); - b.Property("Name") .HasColumnType("text"); @@ -64,10 +65,11 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasKey("ID"); - b.HasIndex("CollectionID"); - b.HasIndex("ShowID"); + b.HasIndex("CollectionID", "ShowID") + .IsUnique(); + b.ToTable("CollectionLinks"); }); @@ -167,8 +169,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); - b.Property("Paths") - .HasColumnType("text"); + b.Property>("Paths") + .HasColumnType("text[]"); b.Property("Slug") .HasColumnType("text"); @@ -201,10 +203,14 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("CollectionID"); - b.HasIndex("LibraryID"); - b.HasIndex("ShowID"); + b.HasIndex("LibraryID", "CollectionID") + .IsUnique(); + + b.HasIndex("LibraryID", "ShowID") + .IsUnique(); + b.ToTable("LibraryLinks"); }); @@ -258,10 +264,10 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ImgPrimary") + b.Property("Name") .HasColumnType("text"); - b.Property("Name") + b.Property("Poster") .HasColumnType("text"); b.Property("Slug") @@ -272,7 +278,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("Slug") .IsUnique(); - b.ToTable("Peoples"); + b.ToTable("People"); }); modelBuilder.Entity("Kyoo.Models.PeopleLink", b => @@ -300,7 +306,7 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.HasIndex("ShowID"); - b.ToTable("PeopleLinks"); + b.ToTable("PeopleRoles"); }); modelBuilder.Entity("Kyoo.Models.ProviderID", b => @@ -316,9 +322,12 @@ namespace Kyoo.Models.DatabaseMigrations.Internal b.Property("Name") .HasColumnType("text"); + b.Property("Slug") + .HasColumnType("text"); + b.HasKey("ID"); - b.HasIndex("Name") + b.HasIndex("Slug") .IsUnique(); b.ToTable("Providers"); @@ -386,8 +395,8 @@ namespace Kyoo.Models.DatabaseMigrations.Internal .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("Aliases") - .HasColumnType("text"); + b.Property>("Aliases") + .HasColumnType("text[]"); b.Property("Backdrop") .HasColumnType("text"); diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 3e45db4b..d6ff4004 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -39,7 +39,7 @@ namespace Kyoo { configuration.RootPath = "wwwroot"; }); - + services.AddControllers().AddNewtonsoftJson(); services.AddHttpClient(); @@ -47,8 +47,8 @@ namespace Kyoo { options.UseLazyLoadingProxies() .UseNpgsql(_configuration.GetConnectionString("Database")); - // .EnableSensitiveDataLogging() - // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); + // .EnableSensitiveDataLogging() + // .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole())); }, ServiceLifetime.Transient); services.AddDbContext(options => @@ -133,16 +133,17 @@ namespace Kyoo }); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + 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.AddSingleton(); diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 4b2a8f46..387bd3af 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -63,7 +63,7 @@ namespace Kyoo.Controllers if (!Directory.Exists(show.Path)) await libraryManager.DeleteShow(show); - ICollection episodes = await libraryManager.GetEpisodes(); + ICollection episodes = await libraryManager.GetEpisodesFromShow(); ICollection libraries = argument == null ? await libraryManager.GetLibraries() : new [] { await libraryManager.GetLibrary(argument)}; @@ -224,7 +224,8 @@ namespace Kyoo.Controllers bool isMovie, Library library) { - Show show = await libraryManager.GetShowByPath(showPath); + Show show = (await libraryManager.GetShows(x => x.Path == showPath, limit: 1)) + .FirstOrDefault(); if (show != null) return show; show = await _metadataProvider.SearchShow(showTitle, isMovie, library); @@ -308,7 +309,9 @@ namespace Kyoo.Controllers private async Task> GetTracks(Episode episode) { IEnumerable tracks = await _transcoder.GetTrackInfo(episode.Path); - List epTracks = tracks.Where(x => x.Type != StreamType.Subtitle).Concat(GetExtractedSubtitles(episode)).ToList(); + List epTracks = tracks.Where(x => x.Type != StreamType.Subtitle) + .Concat(GetExtractedSubtitles(episode)) + .ToList(); if (epTracks.Count(x => !x.IsExternal) < tracks.Count()) epTracks.AddRange(await _transcoder.ExtractSubtitles(episode.Path)); episode.Tracks = epTracks; @@ -328,26 +331,58 @@ namespace Kyoo.Controllers foreach (string sub in Directory.EnumerateFiles(path, "", SearchOption.AllDirectories)) { string episodeLink = Path.GetFileNameWithoutExtension(episode.Path); - - if (!sub.Contains(episodeLink!)) + string subName = Path.GetFileName(sub); + + if (episodeLink == null + || subName?.Contains(episodeLink) == false + || subName.Length < episodeLink.Length + 5) continue; - string language = sub.Substring(Path.GetDirectoryName(sub).Length + episodeLink.Length + 2, 3); + string language = subName.Substring(episodeLink.Length + 2, 3); bool isDefault = sub.Contains("default"); bool isForced = sub.Contains("forced"); - Track track = new Track(StreamType.Subtitle, null, language, isDefault, isForced, null, false, sub) { EpisodeID = episode.ID }; + Track track = new Track(StreamType.Subtitle, null, language, isDefault, isForced, null, false, sub) + { + EpisodeID = episode.ID, + Codec = Path.GetExtension(sub) switch + { + ".ass" => "ass", + ".srt" => "subrip", + _ => null + } + }; - if (Path.GetExtension(sub) == ".ass") - track.Codec = "ass"; - else if (Path.GetExtension(sub) == ".srt") - track.Codec = "subrip"; - else - track.Codec = null; tracks.Add(track); } return tracks; } - private static readonly string[] VideoExtensions = { ".webm", ".mkv", ".flv", ".vob", ".ogg", ".ogv", ".avi", ".mts", ".m2ts", ".ts", ".mov", ".qt", ".asf", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".3gp", ".3g2" }; + private static readonly string[] VideoExtensions = + { + ".webm", + ".mkv", + ".flv", + ".vob", + ".ogg", + ".ogv", + ".avi", + ".mts", + ".m2ts", + ".ts", + ".mov", + ".qt", + ".asf", + ".mp4", + ".m4p", + ".m4v", + ".mpg", + ".mp2", + ".mpeg", + ".mpe", + ".mpv", + ".m2v", + ".3gp", + ".3g2" + }; private static bool IsVideo(string filePath) { diff --git a/Kyoo/Tasks/MetadataLoader.cs b/Kyoo/Tasks/MetadataLoader.cs index c66702c5..1fb923d9 100644 --- a/Kyoo/Tasks/MetadataLoader.cs +++ b/Kyoo/Tasks/MetadataLoader.cs @@ -17,15 +17,13 @@ namespace Kyoo.Tasks public bool RunOnStartup => true; public int Priority => 1000; - public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) + public async Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) { using IServiceScope serviceScope = serviceProvider.CreateScope(); - DatabaseContext database = serviceScope.ServiceProvider.GetService(); + IProviderRepository providers = serviceScope.ServiceProvider.GetService(); IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); foreach (IMetadataProvider provider in pluginManager.GetPlugins()) - database.Providers.AddIfNotExist(provider.Provider, x => x.Name == provider.Provider.Name); - database.SaveChanges(); - return Task.CompletedTask; + await providers.CreateIfNotExists(provider.Provider); } public Task> GetPossibleParameters() diff --git a/Kyoo/Views/API/CollectionAPI.cs b/Kyoo/Views/API/CollectionAPI.cs deleted file mode 100644 index bd4658fa..00000000 --- a/Kyoo/Views/API/CollectionAPI.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - [Route("api/[controller]")] - [ApiController] - public class CollectionController : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public CollectionController(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - [HttpGet("{collectionSlug}")] - [Authorize(Policy="Read")] - public async Task> GetShows(string collectionSlug) - { - Collection collection = await _libraryManager.GetCollection(collectionSlug); - - if (collection == null) - return NotFound(); - - return collection; - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/CollectionApi.cs b/Kyoo/Views/API/CollectionApi.cs new file mode 100644 index 00000000..bb9675d2 --- /dev/null +++ b/Kyoo/Views/API/CollectionApi.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/collection")] + [Route("api/collections")] + [ApiController] + public class CollectionApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public CollectionApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.CollectionRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/show")] + [HttpGet("{id:int}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShowsFromCollection(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/show")] + [HttpGet("{slug}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShowsFromCollection(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{id:int}/library")] + [HttpGet("{id:int}/libraries")] + [Authorize(Policy = "Read")] + public async Task>> GetLibraries(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetLibrariesFromCollection(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/library")] + [HttpGet("{slug}/libraries")] + [Authorize(Policy = "Read")] + public async Task>> GetLibraries(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetLibrariesFromCollection(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/EpisodeApi.cs b/Kyoo/Views/API/EpisodeApi.cs new file mode 100644 index 00000000..af29a341 --- /dev/null +++ b/Kyoo/Views/API/EpisodeApi.cs @@ -0,0 +1,173 @@ +using System; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/episode")] + [Route("api/episodes")] + [ApiController] + public class EpisodeApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public EpisodeApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.EpisodeRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{episodeID:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(int episodeID) + { + return await _libraryManager.GetShowFromEpisode(episodeID); + } + + [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(string showSlug) + { + return await _libraryManager.GetShow(showSlug); + } + + [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(int showID, int _) + { + return await _libraryManager.GetShow(showID); + } + + [HttpGet("{episodeID:int}/season")] + [Authorize(Policy = "Read")] + public async Task> GetSeason(int episodeID) + { + return await _libraryManager.GetSeasonFromEpisode(episodeID); + } + + [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] + [Authorize(Policy = "Read")] + public async Task> GetSeason(string showSlug, int seasonNuber) + { + return await _libraryManager.GetSeason(showSlug, seasonNuber); + } + + [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] + [Authorize(Policy = "Read")] + public async Task> GetSeason(int showID, int seasonNumber) + { + return await _libraryManager.GetSeason(showID, seasonNumber); + } + + [HttpGet("{episodeID:int}/track")] + [HttpGet("{episodeID:int}/tracks")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(int episodeID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetTracksFromEpisode(episodeID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/track")] + [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(int showID, + int seasonNumber, + int episodeNumber, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetTracksFromEpisode(showID, + seasonNumber, + episodeNumber, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/track")] + [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(string showSlug, + int seasonNumber, + int episodeNumber, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetTracksFromEpisode(showSlug, + seasonNumber, + episodeNumber, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/EpisodesAPI.cs b/Kyoo/Views/API/EpisodesAPI.cs deleted file mode 100644 index abf0650d..00000000 --- a/Kyoo/Views/API/EpisodesAPI.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - [Route("api/[controller]")] - [ApiController] - public class EpisodesController : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public EpisodesController(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - [HttpGet("{showSlug}/season/{seasonNumber}")] - [Authorize(Policy="Read")] - public async Task>> GetEpisodesForSeason(string showSlug, int seasonNumber) - { - IEnumerable episodes = await _libraryManager.GetEpisodes(showSlug, seasonNumber); - - if(episodes == null) - return NotFound(); - - return episodes.ToList(); - } - - [HttpGet("{showSlug}/season/{seasonNumber}/episode/{episodeNumber}")] - [Authorize(Policy="Read")] - [JsonDetailed] - public async Task> GetEpisode(string showSlug, int seasonNumber, int episodeNumber) - { - Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - - if (episode == null) - return NotFound(); - - return episode; - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/GenreApi.cs b/Kyoo/Views/API/GenreApi.cs new file mode 100644 index 00000000..10f15922 --- /dev/null +++ b/Kyoo/Views/API/GenreApi.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/genre")] + [Route("api/genres")] + [ApiController] + public class GenreApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public GenreApi(ILibraryManager libraryManager, IConfiguration config) + : base(libraryManager.GenreRepository, config) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/show")] + [HttpGet("{id:int}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShows( + ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.ID == id)), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/show")] + [HttpGet("{slug}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShows( + ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.Slug == slug)), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/GenresAPI.cs b/Kyoo/Views/API/GenresAPI.cs deleted file mode 100644 index 98d27e7f..00000000 --- a/Kyoo/Views/API/GenresAPI.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.API -{ - [Route("api/genres")] - [Route("api/genre")] - [ApiController] - public class GenresAPI : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public GenresAPI(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - public async Task>> Index() - { - return (await _libraryManager.GetGenres()).ToList(); - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/LibrariesAPI.cs b/Kyoo/Views/API/LibrariesAPI.cs deleted file mode 100644 index eeeee468..00000000 --- a/Kyoo/Views/API/LibrariesAPI.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; - -namespace Kyoo.Api -{ - [Route("api/libraries")] - [Route("api/library")] - [ApiController] - public class LibrariesAPI : ControllerBase - { - private readonly ILibraryManager _libraryManager; - private readonly ITaskManager _taskManager; - - public LibrariesAPI(ILibraryManager libraryManager, ITaskManager taskManager) - { - _libraryManager = libraryManager; - _taskManager = taskManager; - } - - [HttpGet] - public async Task> GetLibraries() - { - return await _libraryManager.GetLibraries(); - } - - [Route("/api/library/create")] - [HttpPost] - [Authorize(Policy="Admin")] - public async Task CreateLibrary([FromBody] Library library) - { - if (!ModelState.IsValid) - return BadRequest(library); - if (string.IsNullOrEmpty(library.Slug)) - return BadRequest(new {error = "The library's slug must be set and not empty"}); - if (string.IsNullOrEmpty(library.Name)) - return BadRequest(new {error = "The library's name must be set and not empty"}); - if (library.Paths == null || !library.Paths.Any()) - return BadRequest(new {error = "The library should have a least one path."}); - if (await _libraryManager.GetLibrary(library.Slug) != null) - return BadRequest(new {error = "Duplicated library slug"}); - await _libraryManager.RegisterLibrary(library); - _taskManager.StartTask("scan", library.Slug); - return Ok(); - } - - [HttpGet("{librarySlug}")] - [Authorize(Policy="Read")] - public async Task>> GetShows(string librarySlug) - { - Library library = await _libraryManager.GetLibrary(librarySlug); - - if (library == null) - return NotFound(); - - return library.Shows.Concat(library.Collections.Select(x => x.AsShow())).ToList(); - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/LibraryApi.cs b/Kyoo/Views/API/LibraryApi.cs new file mode 100644 index 00000000..86b00a66 --- /dev/null +++ b/Kyoo/Views/API/LibraryApi.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/library")] + [Route("api/libraries")] + [ApiController] + public class LibraryAPI : CrudApi + { + private readonly ILibraryManager _libraryManager; + private readonly ITaskManager _taskManager; + + public LibraryAPI(ILibraryManager libraryManager, ITaskManager taskManager, IConfiguration configuration) + : base(libraryManager.LibraryRepository, configuration) + { + _libraryManager = libraryManager; + _taskManager = taskManager; + } + + [Authorize(Policy = "Admin")] + public override async Task> Create(Library ressource) + { + ActionResult result = await base.Create(ressource); + if (result.Value != null) + _taskManager.StartTask("scan", result.Value.Slug); + return result; + } + + [HttpGet("{id:int}/show")] + [HttpGet("{id:int}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShowsFromLibrary(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/show")] + [HttpGet("{slug}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShowsFromLibrary(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{id:int}/collection")] + [HttpGet("{id:int}/collections")] + [Authorize(Policy = "Read")] + public async Task>> GetCollections(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetCollectionsFromLibrary(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/collection")] + [HttpGet("{slug}/collections")] + [Authorize(Policy = "Read")] + public async Task>> GetCollections(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetCollectionsFromLibrary(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{id:int}/item")] + [HttpGet("{id:int}/items")] + [Authorize(Policy = "Read")] + public async Task>> GetItems(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetItemsFromLibrary(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/item")] + [HttpGet("{slug}/items")] + [Authorize(Policy = "Read")] + public async Task>> GetItems(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetItemsFromLibrary(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/LibraryItemApi.cs b/Kyoo/Views/API/LibraryItemApi.cs new file mode 100644 index 00000000..e5918a3c --- /dev/null +++ b/Kyoo/Views/API/LibraryItemApi.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/item")] + [Route("api/items")] + [ApiController] + public class LibraryItemApi : ControllerBase + { + private readonly ILibraryItemRepository _libraryItems; + private readonly string _baseURL; + + + public LibraryItemApi(ILibraryItemRepository libraryItems, IConfiguration configuration) + { + _libraryItems = libraryItems; + _baseURL = configuration.GetValue("public_url").TrimEnd('/'); + } + + [HttpGet] + [Authorize(Policy = "Read")] + public async Task>> GetAll([FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryItems.GetAll( + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return new Page(ressources, + _baseURL + Request.Path, + Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), + limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/PeopleAPI.cs b/Kyoo/Views/API/PeopleAPI.cs deleted file mode 100644 index ad27d8b9..00000000 --- a/Kyoo/Views/API/PeopleAPI.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Api -{ - [Route("api/[controller]")] - [ApiController] - public class PeopleController : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public PeopleController(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - [HttpGet("{peopleSlug}")] - [Authorize(Policy="Read")] - public async Task> GetPeople(string peopleSlug) - { - People people = await _libraryManager.GetPeople(peopleSlug); - - if (people == null) - return NotFound(); - return new Collection(people.Slug, people.Name, null, null) - { - Shows = people.Roles.Select(x => x.Show), - Poster = "peopleimg/" + people.Slug - }; - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/PeopleApi.cs b/Kyoo/Views/API/PeopleApi.cs new file mode 100644 index 00000000..2803ead9 --- /dev/null +++ b/Kyoo/Views/API/PeopleApi.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/people")] + [ApiController] + public class PeopleApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public PeopleApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.PeopleRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/role")] + [HttpGet("{id:int}/roles")] + [Authorize(Policy = "Read")] + [JsonDetailed] + public async Task>> GetRoles(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection resources = await _libraryManager.GetRolesFromPeople(id, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(resources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/role")] + [HttpGet("{slug}/roles")] + [Authorize(Policy = "Read")] + [JsonDetailed] + public async Task>> GetRoles(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetRolesFromPeople(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/ProviderAPI.cs b/Kyoo/Views/API/ProviderAPI.cs deleted file mode 100644 index 019e304c..00000000 --- a/Kyoo/Views/API/ProviderAPI.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Kyoo.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.API -{ - [Route("api/provider")] - [Route("api/providers")] - [ApiController] - public class ProviderAPI : ControllerBase - { - private readonly DatabaseContext _database; - - public ProviderAPI(DatabaseContext database) - { - _database = database; - } - - [HttpGet("")] - [Authorize(Policy="Read")] - public ActionResult> Index() - { - return _database.Providers; - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/ProviderApi.cs b/Kyoo/Views/API/ProviderApi.cs new file mode 100644 index 00000000..c2798434 --- /dev/null +++ b/Kyoo/Views/API/ProviderApi.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/provider")] + [Route("api/providers")] + [ApiController] + public class ProviderAPI : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public ProviderAPI(ILibraryManager libraryManager, IConfiguration config) + : base(libraryManager.ProviderRepository, config) + { + _libraryManager = libraryManager; + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/SearchAPI.cs b/Kyoo/Views/API/SearchApi.cs similarity index 80% rename from Kyoo/Views/API/SearchAPI.cs rename to Kyoo/Views/API/SearchApi.cs index 4efcf93a..20c4297d 100644 --- a/Kyoo/Views/API/SearchAPI.cs +++ b/Kyoo/Views/API/SearchApi.cs @@ -6,13 +6,13 @@ using Microsoft.AspNetCore.Mvc; namespace Kyoo.Api { - [Route("api/[controller]")] + [Route("api/search")] [ApiController] - public class SearchController : ControllerBase + public class SearchApi : ControllerBase { private readonly ILibraryManager _libraryManager; - public SearchController(ILibraryManager libraryManager) + public SearchApi(ILibraryManager libraryManager) { _libraryManager = libraryManager; } @@ -21,7 +21,7 @@ namespace Kyoo.Api [Authorize(Policy="Read")] public async Task> Search(string query) { - SearchResult result = new SearchResult + return new SearchResult { Query = query, Collections = await _libraryManager.SearchCollections(query), @@ -31,7 +31,6 @@ namespace Kyoo.Api Genres = await _libraryManager.SearchGenres(query), Studios = await _libraryManager.SearchStudios(query) }; - return result; } } } \ No newline at end of file diff --git a/Kyoo/Views/API/SeasonApi.cs b/Kyoo/Views/API/SeasonApi.cs new file mode 100644 index 00000000..5825b370 --- /dev/null +++ b/Kyoo/Views/API/SeasonApi.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/season")] + [Route("api/seasons")] + [ApiController] + public class SeasonApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public SeasonApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.SeasonRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{seasonID:int}/episode")] + [HttpGet("{seasonID:int}/episodes")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(int seasonID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetEpisodesFromSeason(seasonID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showSlug}-s{seasonNumber:int}/episode")] + [HttpGet("{showSlug}-s{seasonNumber:int}/episodes")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(string showSlug, + int seasonNumber, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetEpisodesFromSeason(showSlug, + seasonNumber, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}-s{seasonNumber:int}/episode")] + [HttpGet("{showID:int}-s{seasonNumber:int}/episodes")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisode(int showID, + int seasonNumber, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetEpisodesFromSeason(showID, + seasonNumber, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{seasonID:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(int seasonID) + { + return await _libraryManager.GetShowFromSeason(seasonID); + } + + [HttpGet("{showSlug}-s{seasonNumber:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(string showSlug, int _) + { + return await _libraryManager.GetShow(showSlug); + } + + [HttpGet("{showID:int}-s{seasonNumber:int}/show")] + [Authorize(Policy = "Read")] + public async Task> GetShow(int showID, int _) + { + return await _libraryManager.GetShow(showID); + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/ShowApi.cs b/Kyoo/Views/API/ShowApi.cs new file mode 100644 index 00000000..e5af30a7 --- /dev/null +++ b/Kyoo/Views/API/ShowApi.cs @@ -0,0 +1,437 @@ +using System; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/show")] + [Route("api/shows")] + [ApiController] + public class ShowApi : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public ShowApi(ILibraryManager libraryManager, IConfiguration configuration) + : base(libraryManager.ShowRepository, configuration) + { + _libraryManager = libraryManager; + } + + [HttpGet("{showID:int}/season")] + [HttpGet("{showID:int}/seasons")] + [Authorize(Policy = "Read")] + public async Task>> GetSeasons(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetSeasonsFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/season")] + [HttpGet("{slug}/seasons")] + [Authorize(Policy = "Read")] + public async Task>> GetSeasons(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetSeasonsFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}/episode")] + [HttpGet("{showID:int}/episodes")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisodes(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetEpisodesFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/episode")] + [HttpGet("{slug}/episodes")] + [Authorize(Policy = "Read")] + public async Task>> GetEpisodes(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 50) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetEpisodesFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}/people")] + [Authorize(Policy = "Read")] + public async Task>> GetPeople(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetPeopleFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/people")] + [Authorize(Policy = "Read")] + public async Task>> GetPeople(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetPeopleFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}/genre")] + [HttpGet("{showID:int}/genres")] + [Authorize(Policy = "Read")] + public async Task>> GetGenres(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetGenresFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/genre")] + [HttpGet("{slug}/genres")] + [Authorize(Policy = "Read")] + public async Task>> GetGenre(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetGenresFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}/studio")] + [Authorize(Policy = "Read")] + public async Task> GetStudio(int showID) + { + try + { + return await _libraryManager.GetStudioFromShow(showID); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + + [HttpGet("{slug}/studio")] + [Authorize(Policy = "Read")] + public async Task> GetStudio(string slug) + { + try + { + return await _libraryManager.GetStudioFromShow(slug); + } + catch (ItemNotFound) + { + return NotFound(); + } + } + + [HttpGet("{showID:int}/library")] + [HttpGet("{showID:int}/libraries")] + [Authorize(Policy = "Read")] + public async Task>> GetLibraries(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetLibrariesFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/library")] + [HttpGet("{slug}/libraries")] + [Authorize(Policy = "Read")] + public async Task>> GetLibraries(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetLibrariesFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{showID:int}/collection")] + [HttpGet("{showID:int}/collections")] + [Authorize(Policy = "Read")] + public async Task>> GetCollections(int showID, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetCollectionsFromShow(showID, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/collection")] + [HttpGet("{slug}/collections")] + [Authorize(Policy = "Read")] + public async Task>> GetCollections(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 30) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetCollectionsFromShow(slug, + ApiHelper.ParseWhere(where), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} diff --git a/Kyoo/Views/API/ShowsAPI.cs b/Kyoo/Views/API/ShowsAPI.cs deleted file mode 100644 index a22a485e..00000000 --- a/Kyoo/Views/API/ShowsAPI.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; - -namespace Kyoo.Api -{ - [Route("api/shows")] - [Route("api/show")] - [ApiController] - public class ShowsAPI : ControllerBase - { - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly DatabaseContext _database; - private readonly IThumbnailsManager _thumbnailsManager; - private readonly ITaskManager _taskManager; - - public ShowsAPI(ILibraryManager libraryManager, - IProviderManager providerManager, - DatabaseContext database, - IThumbnailsManager thumbnailsManager, - ITaskManager taskManager) - { - _libraryManager = libraryManager; - _providerManager = providerManager; - _database = database; - _thumbnailsManager = thumbnailsManager; - _taskManager = taskManager; - } - - [HttpGet] - [Authorize(Policy="Read")] - public IEnumerable GetShows() - { - return _database.LibraryLinks - .Include(x => x.Show) - .Include(x => x.Collection) - .AsEnumerable().Select(x => x.Show ?? x.Collection.AsShow()).ToList(); - } - - [HttpGet("{slug}")] - [Authorize(Policy="Read")] - [JsonDetailed] - public async Task> GetShow(string slug) - { - Show show = await _libraryManager.GetShow(slug); - - if (show == null) - return NotFound(); - - return show; - } - - [HttpPost("edit/{slug}")] - [Authorize(Policy="Write")] - public async Task EditShow(string slug, [FromBody] Show show) - { - if (!ModelState.IsValid) - return BadRequest(show); - - Show old = _database.Shows.AsNoTracking().FirstOrDefault(x => x.Slug == slug); - if (old == null) - return NotFound(); - show.ID = old.ID; - show.Slug = slug; - show.Path = old.Path; - await _libraryManager.EditShow(show, false); - return Ok(); - } - - [HttpPost("re-identify/{slug}")] - [Authorize(Policy = "Write")] - public IActionResult ReIdentityShow(string slug, [FromBody] IEnumerable externalIDs) - { - if (!ModelState.IsValid) - return BadRequest(externalIDs); - Show show = _database.Shows.Include(x => x.ExternalIDs).FirstOrDefault(x => x.Slug == slug); - if (show == null) - return NotFound(); - _database.SaveChanges(); - _taskManager.StartTask("re-scan", $"show/{slug}"); - return Ok(); - } - - [HttpGet("identify/{name}")] - [Authorize(Policy = "Read")] - public async Task> IdentityShow(string name, [FromQuery] bool isMovie) - { - return await _providerManager.SearchShows(name, isMovie, null); - } - - [HttpPost("download-images/{slug}")] - [Authorize(Policy = "Write")] - public async Task DownloadImages(string slug) - { - Show show = await _libraryManager.GetShow(slug); - if (show == null) - return NotFound(); - await _thumbnailsManager.Validate(show, true); - return Ok(); - } - } -} diff --git a/Kyoo/Views/API/StudioAPI.cs b/Kyoo/Views/API/StudioAPI.cs deleted file mode 100644 index 59228155..00000000 --- a/Kyoo/Views/API/StudioAPI.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Controllers; -using Kyoo.Models; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.API -{ - [Route("api/studios")] - [Route("api/studio")] - [ApiController] - public class StudioAPI : ControllerBase - { - private readonly ILibraryManager _libraryManager; - - public StudioAPI(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - public async Task>> Index() - { - return (await _libraryManager.GetStudios()).ToList(); - } - } -} \ No newline at end of file diff --git a/Kyoo/Views/API/StudioApi.cs b/Kyoo/Views/API/StudioApi.cs new file mode 100644 index 00000000..8c15be4a --- /dev/null +++ b/Kyoo/Views/API/StudioApi.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.CommonApi; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Kyoo.Api +{ + [Route("api/studio")] + [Route("api/studios")] + [ApiController] + public class StudioAPI : CrudApi + { + private readonly ILibraryManager _libraryManager; + + public StudioAPI(ILibraryManager libraryManager, IConfiguration config) + : base(libraryManager.StudioRepository, config) + { + _libraryManager = libraryManager; + } + + [HttpGet("{id:int}/show")] + [HttpGet("{id:int}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(int id, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShows( + ApiHelper.ParseWhere(where, x => x.StudioID == id), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + + [HttpGet("{slug}/show")] + [HttpGet("{slug}/shows")] + [Authorize(Policy = "Read")] + public async Task>> GetShows(string slug, + [FromQuery] string sortBy, + [FromQuery] int afterID, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20) + { + where.Remove("sortBy"); + where.Remove("limit"); + where.Remove("afterID"); + + try + { + ICollection ressources = await _libraryManager.GetShows( + ApiHelper.ParseWhere(where, x => x.Studio.Slug == slug), + new Sort(sortBy), + new Pagination(limit, afterID)); + + return Page(ressources, limit); + } + catch (ItemNotFound) + { + return NotFound(); + } + catch (ArgumentException ex) + { + return BadRequest(new {Error = ex.Message}); + } + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/SubtitleAPI.cs b/Kyoo/Views/API/SubtitleAPI.cs index 00d4d998..3ffd72c3 100644 --- a/Kyoo/Views/API/SubtitleAPI.cs +++ b/Kyoo/Views/API/SubtitleAPI.cs @@ -33,7 +33,7 @@ namespace Kyoo.Api string identifier, string extension) { - string languageTag = identifier.Length == 3 ? identifier.Substring(0, 3) : null; + string languageTag = identifier.Length >= 3 ? identifier.Substring(0, 3) : null; bool forced = identifier.Length > 4 && identifier.Substring(4) == "forced"; Track subtitle = null; diff --git a/Kyoo/Views/WebClient b/Kyoo/Views/WebClient index fffb6690..9e7e7a10 160000 --- a/Kyoo/Views/WebClient +++ b/Kyoo/Views/WebClient @@ -1 +1 @@ -Subproject commit fffb6690fc5db161767753d1fc554be04eb732d4 +Subproject commit 9e7e7a1093d85f8e980b8115e0514166701cbd6b diff --git a/Kyoo/appsettings.json b/Kyoo/appsettings.json index 10101c80..6463ba2a 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/appsettings.json @@ -33,5 +33,5 @@ "plugins": "plugins/", "defaultPermissions": "read,play,write,admin", "newUserPermissions": "read,play,write,admin", - "regex": "(?:\\/(?.*?))?\\/(?.*)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$" + "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$" } \ No newline at end of file