Unifing ItemNotFound behaviors and adding GetOrDefault.

This commit is contained in:
Zoe Roux 2021-04-21 01:49:21 +02:00
parent 5dd79d59eb
commit 411eaa7aed
17 changed files with 810 additions and 207 deletions

View File

@ -143,13 +143,79 @@ namespace Kyoo.Controllers
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary> /// <summary>
/// Get a tracck from it's slug and it's type. /// Get a track from it's slug and it's type.
/// </summary> /// </summary>
/// <param name="slug">The slug of the track</param> /// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns> /// <returns>The tracl found</returns>
Task<Track> GetTrack(string slug, StreamType type = StreamType.Unknown); Task<Track> Get(string slug, StreamType type = StreamType.Unknown);
/// <summary>
/// Get the resource by it's ID or null if it is not found.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
Task<T> GetOrDefault<T>(int id) where T : class, IResource;
/// <summary>
/// Get the resource by it's slug or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
Task<T> GetOrDefault<T>(string slug) where T : class, IResource;
/// <summary>
/// Get the resource by a filter function or null if it is not found.
/// </summary>
/// <param name="where">The filter function.</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The first resource found that match the where function</returns>
Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where) where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season> GetOrDefault(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season> GetOrDefault(string showSlug, int seasonNumber);
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown);
/// <summary> /// <summary>
@ -423,7 +489,15 @@ namespace Kyoo.Controllers
/// <param name="item">The item to register</param> /// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam> /// <typeparam name="T">The type of resource</typeparam>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns> /// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
Task<T> Create<T>(T item) where T : class, IResource; Task<T> Create<T>([NotNull] T item) where T : class, IResource;
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists<T>([NotNull] T item) where T : class, IResource;
/// <summary> /// <summary>
/// Edit a resource /// Edit a resource
@ -431,6 +505,7 @@ namespace Kyoo.Controllers
/// <param name="item">The resourcce to edit, it's ID can't change.</param> /// <param name="item">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam> /// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource; Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource;
@ -439,6 +514,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="item">The resource to delete</param> /// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(T item) where T : class, IResource; Task Delete<T>(T item) where T : class, IResource;
/// <summary> /// <summary>
@ -446,6 +522,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the resource to delete</param> /// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(int id) where T : class, IResource; Task Delete<T>(int id) where T : class, IResource;
/// <summary> /// <summary>
@ -453,6 +530,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource to delete</param> /// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete<T>(string slug) where T : class, IResource; Task Delete<T>(string slug) where T : class, IResource;
} }
} }

View File

@ -146,6 +146,25 @@ namespace Kyoo.Controllers
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get(Expression<Func<T, bool>> where); Task<T> Get(Expression<Func<T, bool>> where);
/// <summary>
/// Get a resource from it's ID or null if it is not found.
/// </summary>
/// <param name="id">The id of the resource</param>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(int id);
/// <summary>
/// Get a resource from it's slug or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(string slug);
/// <summary>
/// Get the first resource that match the predicate or null if it is not found.
/// </summary>
/// <param name="where">A predicate to filter the resource.</param>
/// <returns>The resource found</returns>
Task<T> GetOrDefault(Expression<Func<T, bool>> where);
/// <summary> /// <summary>
/// Search for resources. /// Search for resources.
/// </summary> /// </summary>
@ -203,6 +222,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="edited">The resourcce to edit, it's ID can't change.</param> /// <param name="edited">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit([NotNull] T edited, bool resetOld); Task<T> Edit([NotNull] T edited, bool resetOld);
@ -210,77 +230,193 @@ namespace Kyoo.Controllers
/// Delete a resource by it's ID /// Delete a resource by it's ID
/// </summary> /// </summary>
/// <param name="id">The ID of the resource</param> /// <param name="id">The ID of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete(int id); Task Delete(int id);
/// <summary> /// <summary>
/// Delete a resource by it's slug /// Delete a resource by it's slug
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource</param> /// <param name="slug">The slug of the resource</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete(string slug); Task Delete(string slug);
/// <summary> /// <summary>
/// Delete a resource /// Delete a resource
/// </summary> /// </summary>
/// <param name="obj">The resource to delete</param> /// <param name="obj">The resource to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task Delete([NotNull] T obj); Task Delete([NotNull] T obj);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="objs">One or multiple resources to delete</param> /// <param name="objs">One or multiple resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable()); Task DeleteRange(params T[] objs) => DeleteRange(objs.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="objs">An enumerable of resources to delete</param> /// <param name="objs">An enumerable of resources to delete</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<T> objs); Task DeleteRange(IEnumerable<T> objs);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">One or multiple resources's id</param> /// <param name="ids">One or multiple resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable()); Task DeleteRange(params int[] ids) => DeleteRange(ids.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="ids">An enumearble of resources's id</param> /// <param name="ids">An enumearble of resources's id</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<int> ids); Task DeleteRange(IEnumerable<int> ids);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">One or multiple resources's slug</param> /// <param name="slugs">One or multiple resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable()); Task DeleteRange(params string[] slugs) => DeleteRange(slugs.AsEnumerable());
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="slugs">An enumerable of resources's slug</param> /// <param name="slugs">An enumerable of resources's slug</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange(IEnumerable<string> slugs); Task DeleteRange(IEnumerable<string> slugs);
/// <summary> /// <summary>
/// Delete a list of resources. /// Delete a list of resources.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
Task DeleteRange([NotNull] Expression<Func<T, bool>> where); Task DeleteRange([NotNull] Expression<Func<T, bool>> where);
} }
/// <summary>
/// A repository to handle shows.
/// </summary>
public interface IShowRepository : IRepository<Show> public interface IShowRepository : IRepository<Show>
{ {
/// <summary>
/// Link a show to a collection and/or a library. The given show is now part of thoses containers.
/// If both a library and a collection are given, the collection is added to the library too.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="libraryID">The ID of the library (optional)</param>
/// <param name="collectionID">The ID of the collection (optional)</param>
Task AddShowLink(int showID, int? libraryID, int? collectionID); Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary>
/// Get a show's slug from it's ID.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <exception cref="ItemNotFound">If a show with the given ID is not found.</exception>
/// <returns>The show's slug</returns>
Task<string> GetSlug(int showID); Task<string> GetSlug(int showID);
} }
/// <summary>
/// A repository to handle seasons.
/// </summary>
public interface ISeasonRepository : IRepository<Season> public interface ISeasonRepository : IRepository<Season>
{ {
/// <summary>
/// Get a season from it's showID and it's seasonNumber
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(int showID, int seasonNumber); Task<Season> Get(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The season found</returns>
Task<Season> Get(string showSlug, int seasonNumber); Task<Season> Get(string showSlug, int seasonNumber);
Task Delete(string showSlug, int seasonNumber);
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season> GetOrDefault(int showID, int seasonNumber);
/// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
Task<Season> GetOrDefault(string showSlug, int seasonNumber);
} }
/// <summary>
/// The repository to handle episodes
/// </summary>
public interface IEpisodeRepository : IRepository<Episode> public interface IEpisodeRepository : IRepository<Episode>
{ {
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int showID, int seasonNumber, int episodeNumber); Task<Episode> Get(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber); Task<Episode> Get(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's season ID and it's episode number.
/// </summary>
/// <param name="seasonID">The ID of the season</param>
/// <param name="episodeNumber">The episode number</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> Get(int seasonID, int episodeNumber); Task<Episode> Get(int seasonID, int episodeNumber);
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Get a episode from it's showID and it's absolute number.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(int showID, int absoluteNumber); Task<Episode> GetAbsolute(int showID, int absoluteNumber);
/// <summary>
/// Get a episode from it's showID and it's absolute number.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="absoluteNumber">The episode's absolute number (The episode number does not reset to 1 after the end of a season.</param>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The episode found</returns>
Task<Episode> GetAbsolute(string showSlug, int absoluteNumber); Task<Episode> GetAbsolute(string showSlug, int absoluteNumber);
Task Delete(string showSlug, int seasonNumber, int episodeNumber);
} }
public interface ITrackRepository : IRepository<Track> public interface ITrackRepository : IRepository<Track>

View File

@ -15,27 +15,6 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
private readonly IBaseRepository[] _repositories; private readonly IBaseRepository[] _repositories;
/// <summary>
/// Create a new <see cref="LibraryManager"/> instancce with every repository available.
/// </summary>
/// <param name="repositories">The list of repositories that this library manager should manage. If a repository for every base type is not available, this instance won't be stable.</param>
public LibraryManager(IEnumerable<IBaseRepository> repositories)
{
_repositories = repositories.ToArray();
LibraryRepository = GetRepository<Library>() as ILibraryRepository;
LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
ShowRepository = GetRepository<Show>() as IShowRepository;
SeasonRepository = GetRepository<Season>() as ISeasonRepository;
EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
TrackRepository = GetRepository<Track>() as ITrackRepository;
PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
}
/// <summary> /// <summary>
/// The repository that handle libraries. /// The repository that handle libraries.
/// </summary> /// </summary>
@ -90,7 +69,60 @@ namespace Kyoo.Controllers
/// The repository that handle providers. /// The repository that handle providers.
/// </summary> /// </summary>
public IProviderRepository ProviderRepository { get; } public IProviderRepository ProviderRepository { get; }
/// <summary>
/// Create a new <see cref="LibraryManager"/> instancce with every repository available.
/// </summary>
/// <param name="repositories">The list of repositories that this library manager should manage. If a repository for every base type is not available, this instance won't be stable.</param>
public LibraryManager(IEnumerable<IBaseRepository> repositories)
{
_repositories = repositories.ToArray();
LibraryRepository = GetRepository<Library>() as ILibraryRepository;
LibraryItemRepository = GetRepository<LibraryItem>() as ILibraryItemRepository;
CollectionRepository = GetRepository<Collection>() as ICollectionRepository;
ShowRepository = GetRepository<Show>() as IShowRepository;
SeasonRepository = GetRepository<Season>() as ISeasonRepository;
EpisodeRepository = GetRepository<Episode>() as IEpisodeRepository;
TrackRepository = GetRepository<Track>() as ITrackRepository;
PeopleRepository = GetRepository<People>() as IPeopleRepository;
StudioRepository = GetRepository<Studio>() as IStudioRepository;
GenreRepository = GetRepository<Genre>() as IGenreRepository;
ProviderRepository = GetRepository<Provider>() as IProviderRepository;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
foreach (IBaseRepository repo in _repositories)
repo.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync()
{
await Task.WhenAll(_repositories.Select(x => x.DisposeAsync().AsTask()));
}
/// <summary>
/// Get the repository corresponding to the T item.
/// </summary>
/// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The repository corresponding</returns>
public IRepository<T> GetRepository<T>()
where T : class, IResource
{
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret)
return ret;
throw new ItemNotFound();
}
/// <summary> /// <summary>
/// Get the resource by it's ID /// Get the resource by it's ID
@ -188,44 +220,104 @@ namespace Kyoo.Controllers
/// <param name="type">The type (Video, Audio or Subtitle)</param> /// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <exception cref="ItemNotFound">If the item is not found</exception> /// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The tracl found</returns> /// <returns>The tracl found</returns>
public Task<Track> GetTrack(string slug, StreamType type = StreamType.Unknown) public Task<Track> Get(string slug, StreamType type = StreamType.Unknown)
{ {
return TrackRepository.Get(slug, type); return TrackRepository.Get(slug, type);
} }
/// <summary> /// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// Get the resource by it's ID or null if it is not found.
/// </summary> /// </summary>
public void Dispose() /// <param name="id">The id of the resource</param>
{ /// <typeparam name="T">The type of the resource</typeparam>
foreach (IBaseRepository repo in _repositories) /// <returns>The resource found</returns>
repo.Dispose(); public async Task<T> GetOrDefault<T>(int id)
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync()
{
await Task.WhenAll(_repositories.Select(x => x.DisposeAsync().AsTask()));
}
/// <summary>
/// Get the repository corresponding to the T item.
/// </summary>
/// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The repository corresponding</returns>
public IRepository<T> GetRepository<T>()
where T : class, IResource where T : class, IResource
{ {
if (_repositories.FirstOrDefault(x => x.RepositoryType == typeof(T)) is IRepository<T> ret) return await GetRepository<T>().GetOrDefault(id);
return ret; }
throw new ItemNotFound();
/// <summary>
/// Get the resource by it's slug or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the resource</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
public async Task<T> GetOrDefault<T>(string slug)
where T : class, IResource
{
return await GetRepository<T>().GetOrDefault(slug);
}
/// <summary>
/// Get the resource by a filter function or null if it is not found.
/// </summary>
/// <param name="where">The filter function.</param>
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The first resource found that match the where function</returns>
public async Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where)
where T : class, IResource
{
return await GetRepository<T>().GetOrDefault(where);
} }
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
public async Task<Season> GetOrDefault(int showID, int seasonNumber)
{
return await SeasonRepository.GetOrDefault(showID, seasonNumber);
}
/// <summary>
/// Get a season from it's show slug and it's seasonNumber or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <returns>The season found</returns>
public async Task<Season> GetOrDefault(string showSlug, int seasonNumber)
{
return await SeasonRepository.GetOrDefault(showSlug, seasonNumber);
}
/// <summary>
/// Get a episode from it's showID, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showID">The id of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
public async Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{
return await EpisodeRepository.GetOrDefault(showID, seasonNumber, episodeNumber);
}
/// <summary>
/// Get a episode from it's show slug, it's seasonNumber and it's episode number or null if it is not found.
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="seasonNumber">The season's number</param>
/// <param name="episodeNumber">The episode's number</param>
/// <returns>The episode found</returns>
public async Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber)
{
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
}
/// <summary>
/// Get a track from it's slug and it's type or null if it is not found.
/// </summary>
/// <param name="slug">The slug of the track</param>
/// <param name="type">The type (Video, Audio or Subtitle)</param>
/// <returns>The tracl found</returns>
public async Task<Track> GetOrDefault(string slug, StreamType type = StreamType.Unknown)
{
return await TrackRepository.GetOrDefault(slug, type);
}
/// <summary> /// <summary>
/// Load a related resource /// Load a related resource
/// </summary> /// </summary>
@ -360,7 +452,7 @@ namespace Kyoo.Controllers
.Then(x => s.Collections = x), .Then(x => s.Collections = x),
(Show s, nameof(Show.Studio)) => StudioRepository (Show s, nameof(Show.Studio)) => StudioRepository
.Get(x => x.Shows.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Shows.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
s.Studio = x; s.Studio = x;
@ -379,7 +471,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Season = y; x.SeasonID = y.ID; }), (x, y) => { x.Season = y; x.SeasonID = y.ID; }),
(Season s, nameof(Season.Show)) => ShowRepository (Season s, nameof(Season.Show)) => ShowRepository
.Get(x => x.Seasons.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Seasons.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
s.Show = x; s.Show = x;
@ -398,7 +490,7 @@ namespace Kyoo.Controllers
(x, y) => { x.Episode = y; x.EpisodeID = y.ID; }), (x, y) => { x.Episode = y; x.EpisodeID = y.ID; }),
(Episode e, nameof(Episode.Show)) => ShowRepository (Episode e, nameof(Episode.Show)) => ShowRepository
.Get(x => x.Episodes.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
e.Show = x; e.Show = x;
@ -406,7 +498,7 @@ namespace Kyoo.Controllers
}), }),
(Episode e, nameof(Episode.Season)) => SeasonRepository (Episode e, nameof(Episode.Season)) => SeasonRepository
.Get(x => x.Episodes.Any(y => y.ID == e.ID)) .GetOrDefault(x => x.Episodes.Any(y => y.ID == e.ID))
.Then(x => .Then(x =>
{ {
e.Season = x; e.Season = x;
@ -415,7 +507,7 @@ namespace Kyoo.Controllers
(Track t, nameof(Track.Episode)) => EpisodeRepository (Track t, nameof(Track.Episode)) => EpisodeRepository
.Get(x => x.Tracks.Any(y => y.ID == obj.ID)) .GetOrDefault(x => x.Tracks.Any(y => y.ID == obj.ID))
.Then(x => .Then(x =>
{ {
t.Episode = x; t.Episode = x;
@ -623,6 +715,18 @@ namespace Kyoo.Controllers
{ {
return GetRepository<T>().Create(item); return GetRepository<T>().Create(item);
} }
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
/// </summary>
/// <param name="item">The object to create</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns>
public Task<T> CreateIfNotExists<T>(T item)
where T : class, IResource
{
return GetRepository<T>().CreateIfNotExists(item);
}
/// <summary> /// <summary>
/// Edit a resource /// Edit a resource
@ -630,6 +734,7 @@ namespace Kyoo.Controllers
/// <param name="item">The resourcce to edit, it's ID can't change.</param> /// <param name="item">The resourcce to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param> /// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam> /// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns> /// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
public Task<T> Edit<T>(T item, bool resetOld) public Task<T> Edit<T>(T item, bool resetOld)
where T : class, IResource where T : class, IResource
@ -642,6 +747,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="item">The resource to delete</param> /// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
public Task Delete<T>(T item) public Task Delete<T>(T item)
where T : class, IResource where T : class, IResource
{ {
@ -653,6 +759,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="id">The id of the resource to delete</param> /// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
public Task Delete<T>(int id) public Task Delete<T>(int id)
where T : class, IResource where T : class, IResource
{ {
@ -664,6 +771,7 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="slug">The slug of the resource to delete</param> /// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam> /// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFound">If the item is not found</exception>
public Task Delete<T>(string slug) public Task Delete<T>(string slug)
where T : class, IResource where T : class, IResource
{ {

View File

@ -29,22 +29,28 @@ namespace Kyoo.CommonApi
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public virtual async Task<ActionResult<T>> Get(int id) public virtual async Task<ActionResult<T>> Get(int id)
{ {
T resource = await _repository.Get(id); try
if (resource == null) {
return await _repository.Get(id);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
}
return resource;
} }
[HttpGet("{slug}")] [HttpGet("{slug}")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public virtual async Task<ActionResult<T>> Get(string slug) public virtual async Task<ActionResult<T>> Get(string slug)
{ {
T resource = await _repository.Get(slug); try
if (resource == null) {
return await _repository.Get(slug);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
}
return resource;
} }
[HttpGet("count")] [HttpGet("count")]
@ -114,15 +120,19 @@ namespace Kyoo.CommonApi
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]
public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource) public virtual async Task<ActionResult<T>> Edit([FromQuery] bool resetOld, [FromBody] T resource)
{ {
if (resource.ID > 0) try
{
if (resource.ID > 0)
return await _repository.Edit(resource, resetOld);
T old = await _repository.Get(resource.Slug);
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld); return await _repository.Edit(resource, resetOld);
}
T old = await _repository.Get(resource.Slug); catch (ItemNotFound)
if (old == null) {
return NotFound(); return NotFound();
}
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
} }
[HttpPut("{id:int}")] [HttpPut("{id:int}")]
@ -144,11 +154,16 @@ namespace Kyoo.CommonApi
[Authorize(Policy = "Write")] [Authorize(Policy = "Write")]
public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource) public virtual async Task<ActionResult<T>> Edit(string slug, [FromQuery] bool resetOld, [FromBody] T resource)
{ {
T old = await _repository.Get(slug); try
if (old == null) {
T old = await _repository.Get(slug);
resource.ID = old.ID;
return await _repository.Edit(resource, resetOld);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
resource.ID = old.ID; }
return await _repository.Edit(resource, resetOld);
} }
[HttpDelete("{id:int}")] [HttpDelete("{id:int}")]

View File

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

View File

@ -0,0 +1,17 @@
using System.Linq;
using Xunit;
namespace Kyoo.Tests
{
public class RepositoryTests
{
[Fact]
public void Get_Test()
{
TestContext context = new();
using DatabaseContext database = context.New();
Assert.Equal(1, database.Shows.Count());
}
}
}

View File

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

View File

@ -210,13 +210,7 @@ namespace Kyoo.Controllers
return x; return x;
}).ToListAsync(); }).ToListAsync();
} }
public async Task Delete(string showSlug, int seasonNumber, int episodeNumber)
{
Episode obj = await Get(showSlug, seasonNumber, episodeNumber);
await Delete(obj);
}
public override async Task Delete(Episode obj) public override async Task Delete(Episode obj)
{ {
if (obj == null) if (obj == null)

View File

@ -5,22 +5,39 @@ using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
/// <summary>
/// A local repository to handle seasons.
/// </summary>
public class SeasonRepository : LocalRepository<Season>, ISeasonRepository public class SeasonRepository : LocalRepository<Season>, ISeasonRepository
{ {
/// <summary>
/// Has this instance been disposed and should not handle requests?
/// </summary>
private bool _disposed; private bool _disposed;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
private readonly IProviderRepository _providers; private readonly IProviderRepository _providers;
private readonly IShowRepository _shows; private readonly IShowRepository _shows;
private readonly Lazy<IEpisodeRepository> _episodes; private readonly Lazy<IEpisodeRepository> _episodes;
/// <inheritdoc/>
protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber; protected override Expression<Func<Season, object>> DefaultSort => x => x.SeasonNumber;
public SeasonRepository(DatabaseContext database, /// <summary>
/// Create a new <see cref="SeasonRepository"/> using the provided handle, a provider & a show repository and
/// a service provider to lazilly request an episode repository.
/// </summary>
/// <param name="database">The database handle that will be used</param>
/// <param name="providers">A provider repository</param>
/// <param name="shows">A show repository</param>
/// <param name="services">A service provider to lazilly request an episode repository.</param>
public SeasonRepository(DatabaseContext database,
IProviderRepository providers, IProviderRepository providers,
IShowRepository shows, IShowRepository shows,
IServiceProvider services) IServiceProvider services)
@ -33,6 +50,7 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc/>
public override void Dispose() public override void Dispose()
{ {
if (_disposed) if (_disposed)
@ -46,6 +64,7 @@ namespace Kyoo.Controllers
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
/// <inheritdoc/>
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed)
@ -58,22 +77,23 @@ namespace Kyoo.Controllers
await _episodes.Value.DisposeAsync(); await _episodes.Value.DisposeAsync();
} }
/// <inheritdoc/>
public override async Task<Season> Get(int id) public override async Task<Season> Get(int id)
{ {
Season ret = await base.Get(id); Season ret = await base.Get(id);
if (ret != null) ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
/// <inheritdoc/>
public override async Task<Season> Get(Expression<Func<Season, bool>> predicate) public override async Task<Season> Get(Expression<Func<Season, bool>> predicate)
{ {
Season ret = await base.Get(predicate); Season ret = await base.Get(predicate);
if (ret != null) ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
ret.ShowSlug = await _shows.GetSlug(ret.ShowID);
return ret; return ret;
} }
/// <inheritdoc/>
public override Task<Season> Get(string slug) public override Task<Season> Get(string slug)
{ {
Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)"); Match match = Regex.Match(slug, @"(?<show>.*)-s(?<season>\d*)");
@ -83,24 +103,41 @@ namespace Kyoo.Controllers
return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value)); return Get(match.Groups["show"].Value, int.Parse(match.Groups["season"].Value));
} }
/// <inheritdoc/>
public async Task<Season> Get(int showID, int seasonNumber) public async Task<Season> Get(int showID, int seasonNumber)
{ {
Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID Season ret = await GetOrDefault(showID, seasonNumber);
&& x.SeasonNumber == seasonNumber); if (ret == null)
if (ret != null) throw new ItemNotFound($"No season {seasonNumber} found for the show {showID}");
ret.ShowSlug = await _shows.GetSlug(showID); ret.ShowSlug = await _shows.GetSlug(showID);
return ret; return ret;
} }
/// <inheritdoc/>
public async Task<Season> Get(string showSlug, int seasonNumber) public async Task<Season> Get(string showSlug, int seasonNumber)
{ {
Season ret = await _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug Season ret = await GetOrDefault(showSlug, seasonNumber);
&& x.SeasonNumber == seasonNumber); if (ret == null)
if (ret != null) throw new ItemNotFound($"No season {seasonNumber} found for the show {showSlug}");
ret.ShowSlug = showSlug; ret.ShowSlug = showSlug;
return ret; return ret;
} }
/// <inheritdoc/>
public Task<Season> GetOrDefault(int showID, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID
&& x.SeasonNumber == seasonNumber);
}
/// <inheritdoc/>
public Task<Season> GetOrDefault(string showSlug, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.Show.Slug == showSlug
&& x.SeasonNumber == seasonNumber);
}
/// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query) public override async Task<ICollection<Season>> Search(string query)
{ {
List<Season> seasons = await _database.Seasons List<Season> seasons = await _database.Seasons
@ -113,6 +150,7 @@ namespace Kyoo.Controllers
return seasons; return seasons;
} }
/// <inheritdoc/>
public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null, public override async Task<ICollection<Season>> GetAll(Expression<Func<Season, bool>> where = null,
Sort<Season> sort = default, Sort<Season> sort = default,
Pagination limit = default) Pagination limit = default)
@ -123,6 +161,7 @@ namespace Kyoo.Controllers
return seasons; return seasons;
} }
/// <inheritdoc/>
public override async Task<Season> Create(Season obj) public override async Task<Season> Create(Season obj)
{ {
await base.Create(obj); await base.Create(obj);
@ -132,6 +171,7 @@ namespace Kyoo.Controllers
return obj; return obj;
} }
/// <inheritdoc/>
protected override async Task Validate(Season resource) protected override async Task Validate(Season resource)
{ {
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
@ -146,6 +186,7 @@ namespace Kyoo.Controllers
}); });
} }
/// <inheritdoc/>
protected override async Task EditRelations(Season resource, Season changed, bool resetOld) protected override async Task EditRelations(Season resource, Season changed, bool resetOld)
{ {
if (changed.ExternalIDs != null || resetOld) if (changed.ExternalIDs != null || resetOld)
@ -155,13 +196,8 @@ namespace Kyoo.Controllers
} }
await base.EditRelations(resource, changed, resetOld); await base.EditRelations(resource, changed, resetOld);
} }
public async Task Delete(string showSlug, int seasonNumber) /// <inheritdoc/>
{
Season obj = await Get(showSlug, seasonNumber);
await Delete(obj);
}
public override async Task Delete(Season obj) public override async Task Delete(Season obj)
{ {
if (obj == null) if (obj == null)

View File

@ -58,13 +58,13 @@ namespace Kyoo
modelBuilder.HasPostgresEnum<ItemType>(); modelBuilder.HasPostgresEnum<ItemType>();
modelBuilder.HasPostgresEnum<StreamType>(); modelBuilder.HasPostgresEnum<StreamType>();
modelBuilder.Entity<Library>() // modelBuilder.Entity<Library>()
.Property(x => x.Paths) // .Property(x => x.Paths)
.HasColumnType("text[]"); // .HasColumnType("text[]");
//
modelBuilder.Entity<Show>() // modelBuilder.Entity<Show>()
.Property(x => x.Aliases) // .Property(x => x.Aliases)
.HasColumnType("text[]"); // .HasColumnType("text[]");
modelBuilder.Entity<Track>() modelBuilder.Entity<Track>()
.Property(t => t.IsDefault) .Property(t => t.IsDefault)

View File

@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Kyoo.Models.DatabaseMigrations.Internal namespace Kyoo.Models.DatabaseMigrations.Internal
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20210417232515_Initial")] [Migration("20210420221509_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@ -144,44 +144,48 @@ namespace Kyoo.Controllers
private async Task RegisterExternalSubtitle(string path, CancellationToken token) private async Task RegisterExternalSubtitle(string path, CancellationToken token)
{ {
if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) try
return;
using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("subtitleRegex");
Regex regex = new(patern, RegexOptions.IgnoreCase);
Match match = regex.Match(path);
if (!match.Success)
{ {
await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex."); if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles"))
return; return;
using IServiceScope serviceScope = _serviceProvider.CreateScope();
await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = _config.GetValue<string>("subtitleRegex");
Regex regex = new(patern, RegexOptions.IgnoreCase);
Match match = regex.Match(path);
if (!match.Success)
{
await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex.");
return;
}
string episodePath = match.Groups["Episode"].Value;
Episode episode = await libraryManager!.Get<Episode>(x => x.Path.StartsWith(episodePath));
Track track = new()
{
Type = StreamType.Subtitle,
Language = match.Groups["Language"].Value,
IsDefault = match.Groups["Default"].Value.Length > 0,
IsForced = match.Groups["Forced"].Value.Length > 0,
Codec = SubtitleExtensions[Path.GetExtension(path)],
IsExternal = true,
Path = path,
Episode = episode
};
await libraryManager.Create(track);
Console.WriteLine($"Registering subtitle at: {path}.");
} }
catch (ItemNotFound)
string episodePath = match.Groups["Episode"].Value;
Episode episode = await libraryManager!.Get<Episode>(x => x.Path.StartsWith(episodePath));
if (episode == null)
{ {
await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}."); await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}.");
return;
} }
catch (Exception ex)
Track track = new(StreamType.Subtitle,
null,
match.Groups["Language"].Value,
match.Groups["Default"].Value.Length > 0,
match.Groups["Forced"].Value.Length > 0,
SubtitleExtensions[Path.GetExtension(path)],
true,
path)
{ {
Episode = episode await Console.Error.WriteLineAsync($"Unknown error while registering subtitle: {ex.Message}");
}; }
await libraryManager.Create(track);
Console.WriteLine($"Registering subtitle at: {path}.");
} }
private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token) private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token)
@ -308,22 +312,20 @@ namespace Kyoo.Controllers
{ {
if (seasonNumber == -1) if (seasonNumber == -1)
return default; return default;
Season season = await libraryManager.Get(show.Slug, seasonNumber); try
if (season == null)
{ {
season = await _metadataProvider.GetSeason(show, seasonNumber, library); Season season = await libraryManager.Get(show.Slug, seasonNumber);
try season.Show = show;
{ return season;
await libraryManager.Create(season); }
await _thumbnailsManager.Validate(season); catch (ItemNotFound)
} {
catch (DuplicatedItemException) Season season = await _metadataProvider.GetSeason(show, seasonNumber, library);
{ await libraryManager.CreateIfNotExists(season);
season = await libraryManager.Get(show.Slug, season.SeasonNumber); await _thumbnailsManager.Validate(season);
} season.Show = show;
return season;
} }
season.Show = show;
return season;
} }
private async Task<Episode> GetEpisode(ILibraryManager libraryManager, private async Task<Episode> GetEpisode(ILibraryManager libraryManager,

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.CommonApi; using Kyoo.CommonApi;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -163,20 +164,30 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(int id) public async Task<IActionResult> GetThumb(int id)
{ {
Episode episode = await _libraryManager.Get<Episode>(id); try
if (episode == null) {
Episode episode = await _libraryManager.Get<Episode>(id);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); }
} }
[HttpGet("{slug}/thumb")] [HttpGet("{slug}/thumb")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetThumb(string slug) public async Task<IActionResult> GetThumb(string slug)
{ {
Episode episode = await _libraryManager.Get<Episode>(slug); try
if (episode == null) {
Episode episode = await _libraryManager.Get<Episode>(slug);
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode));
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return _files.FileResult(await _thumbnails.GetEpisodeThumb(episode)); }
} }
} }
} }

View File

@ -376,13 +376,18 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug) public async Task<ActionResult<Dictionary<string, string>>> GetFonts(string slug)
{ {
Show show = await _libraryManager.Get<Show>(slug); try
if (show == null) {
Show show = await _libraryManager.Get<Show>(slug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments");
return (await _files.ListFiles(path))
.ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}");
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments"); }
return (await _files.ListFiles(path))
.ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}/api/shows/{slug}/fonts/{Path.GetFileName(x)}");
} }
[HttpGet("{showSlug}/font/{slug}")] [HttpGet("{showSlug}/font/{slug}")]
@ -390,41 +395,61 @@ namespace Kyoo.Api
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<IActionResult> GetFont(string showSlug, string slug) public async Task<IActionResult> GetFont(string showSlug, string slug)
{ {
Show show = await _libraryManager.Get<Show>(showSlug); try
if (show == null) {
Show show = await _libraryManager.Get<Show>(showSlug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug);
return _files.FileResult(path);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug); }
return _files.FileResult(path);
} }
[HttpGet("{slug}/poster")] [HttpGet("{slug}/poster")]
[Authorize(Policy = "Read")] [Authorize(Policy = "Read")]
public async Task<IActionResult> GetPoster(string slug) public async Task<IActionResult> GetPoster(string slug)
{ {
Show show = await _libraryManager.Get<Show>(slug); try
if (show == null) {
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowPoster(show));
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowPoster(show)); }
} }
[HttpGet("{slug}/logo")] [HttpGet("{slug}/logo")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetLogo(string slug) public async Task<IActionResult> GetLogo(string slug)
{ {
Show show = await _libraryManager.Get<Show>(slug); try
if (show == null) {
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowLogo(show));
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowLogo(show)); }
} }
[HttpGet("{slug}/backdrop")] [HttpGet("{slug}/backdrop")]
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<IActionResult> GetBackdrop(string slug) public async Task<IActionResult> GetBackdrop(string slug)
{ {
Show show = await _libraryManager.Get<Show>(slug); try
if (show == null) {
Show show = await _libraryManager.Get<Show>(slug);
return _files.FileResult(await _thumbs.GetShowBackdrop(show));
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return _files.FileResult(await _thumbs.GetShowBackdrop(show)); }
} }
} }
} }

View File

@ -30,7 +30,7 @@ namespace Kyoo.Api
Track subtitle; Track subtitle;
try try
{ {
subtitle = await _libraryManager.GetTrack(slug, StreamType.Subtitle); subtitle = await _libraryManager.Get(slug, StreamType.Subtitle);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -21,10 +22,15 @@ namespace Kyoo.Api
[Authorize(Policy="Read")] [Authorize(Policy="Read")]
public async Task<ActionResult<WatchItem>> GetWatchItem(string slug) public async Task<ActionResult<WatchItem>> GetWatchItem(string slug)
{ {
Episode item = await _libraryManager.Get<Episode>(slug); try
if (item == null) {
Episode item = await _libraryManager.Get<Episode>(slug);
return await WatchItem.FromEpisode(item, _libraryManager);
}
catch (ItemNotFound)
{
return NotFound(); return NotFound();
return await WatchItem.FromEpisode(item, _libraryManager); }
} }
} }
} }