diff --git a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs index cc3e2f5b..e1a2f299 100644 --- a/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/ILibraryManager.cs @@ -26,7 +26,7 @@ namespace Kyoo.Abstractions.Controllers public interface ILibraryManager { IRepository Repository() - where T : class, IResource, IQuery; + where T : IResource, IQuery; /// /// The repository that handle libraries items (a wrapper around shows and collections). diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs index 85161a55..b53d8c38 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs @@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Controllers /// /// The resource's type that this repository manage. public interface IRepository : IBaseRepository - where T : class, IResource, IQuery + where T : IResource, IQuery { /// /// The event handler type for all events of this repository. diff --git a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs index 8ef426c5..810998d8 100644 --- a/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs @@ -36,7 +36,7 @@ namespace Kyoo.Abstractions.Controllers /// The item to cache images. /// /// The type of the item - /// true if an image has been downloaded, false otherwise. + /// A representing the asynchronous operation. Task DownloadImages(T item) where T : IThumbnails; @@ -50,5 +50,16 @@ namespace Kyoo.Abstractions.Controllers /// The path of the image for the given resource or null if it does not exists. string GetImagePath(T item, string image, ImageQuality quality) where T : IThumbnails; + + /// + /// Delete images associated with the item. + /// + /// + /// The item with cached images. + /// + /// The type of the item + /// A representing the asynchronous operation. + Task DeleteImages(T item) + where T : IThumbnails; } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs index b7a7f4b6..2356965e 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IResource.cs @@ -25,7 +25,7 @@ namespace Kyoo.Abstractions.Models /// /// An interface to represent a resource that can be retrieved from the database. /// - public interface IResource + public interface IResource : IQuery { /// /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. diff --git a/back/src/Kyoo.Core/Controllers/LibraryManager.cs b/back/src/Kyoo.Core/Controllers/LibraryManager.cs index c9bc1b48..f9a1565f 100644 --- a/back/src/Kyoo.Core/Controllers/LibraryManager.cs +++ b/back/src/Kyoo.Core/Controllers/LibraryManager.cs @@ -92,7 +92,7 @@ namespace Kyoo.Core.Controllers public IRepository Users { get; } public IRepository Repository() - where T : class, IResource, IQuery + where T : IResource, IQuery { return (IRepository)_repositories.First(x => x.RepositoryType == typeof(T)); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 0f6bd9ea..c3b98c33 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -84,16 +84,20 @@ namespace Kyoo.Core.Controllers .ToListAsync(); } + protected override Task GetDuplicated(Episode item) + { + if (item is { SeasonNumber: not null, EpisodeNumber: not null }) + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber && x.EpisodeNumber == item.EpisodeNumber); + return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.AbsoluteNumber == item.AbsoluteNumber); + } + /// public override async Task Create(Episode obj) { obj.ShowSlug = obj.Show?.Slug ?? (await _database.Shows.FirstAsync(x => x.Id == obj.ShowId)).Slug; await base.Create(obj); _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => - obj is { SeasonNumber: not null, EpisodeNumber: not null } - ? _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber && x.EpisodeNumber == obj.EpisodeNumber) - : _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.AbsoluteNumber == obj.AbsoluteNumber)); + await _database.SaveChangesAsync(() => GetDuplicated(obj)); await IRepository.OnResourceCreated(obj); return obj; } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs index 4a8d60d3..64b76b5c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LocalRepository.cs @@ -214,6 +214,11 @@ namespace Kyoo.Core.Controllers return ret; } + protected virtual Task GetDuplicated(T item) + { + return GetOrDefault(item.Slug); + } + /// public virtual Task GetOrDefault(Guid id, Include? include = default) { @@ -324,7 +329,14 @@ namespace Kyoo.Core.Controllers await Validate(obj); if (obj is IThumbnails thumbs) { - await _thumbs.DownloadImages(thumbs); + try + { + await _thumbs.DownloadImages(thumbs); + } + catch (DuplicatedItemException e) when (e.Existing is null) + { + throw new DuplicatedItemException(await GetDuplicated(obj)); + } if (thumbs.Poster != null) Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry!.State = EntityState.Added; if (thumbs.Thumbnail != null) @@ -470,6 +482,8 @@ namespace Kyoo.Core.Controllers public virtual Task Delete(T obj) { IRepository.OnResourceDeleted(obj); + if (obj is IThumbnails thumbs) + return _thumbs.DeleteImages(thumbs); return Task.CompletedTask; } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 2ec418f8..19cd6e82 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -16,12 +16,13 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . -using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -70,6 +71,11 @@ namespace Kyoo.Core.Controllers _database = database; } + protected override Task GetDuplicated(Season item) + { + return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber); + } + /// public override async Task> Search(string query, Include? include = default) { @@ -83,11 +89,10 @@ namespace Kyoo.Core.Controllers public override async Task Create(Season obj) { await base.Create(obj); - obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug; + obj.ShowSlug = (await _database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug + ?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}"); _database.Entry(obj).State = EntityState.Added; - await _database.SaveChangesAsync(() => - _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber) - ); + await _database.SaveChangesAsync(() => GetDuplicated(obj)); await IRepository.OnResourceCreated(obj); return obj; } @@ -100,7 +105,7 @@ namespace Kyoo.Core.Controllers { if (resource.Show == null) { - throw new ArgumentException($"Can't store a season not related to any show " + + throw new ValidationException($"Can't store a season not related to any show " + $"(showID: {resource.ShowId})."); } resource.ShowId = resource.Show.Id; diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index 6e4720ce..8e7339de 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Blurhash.SkiaSharp; @@ -167,5 +168,24 @@ namespace Kyoo.Core.Controllers { return $"{_GetBaseImagePath(item, image)}.{quality.ToString().ToLowerInvariant()}.webp"; } + + /// + public Task DeleteImages(T item) + where T : IThumbnails + { + IEnumerable images = new[] { "poster", "thumbnail", "logo" } + .SelectMany(x => _GetBaseImagePath(item, x)) + .SelectMany(x => new[] + { + ImageQuality.High.ToString().ToLowerInvariant(), + ImageQuality.Medium.ToString().ToLowerInvariant(), + ImageQuality.Low.ToString().ToLowerInvariant(), + }.Select(quality => $"{x}.{quality}.webp") + ); + + foreach (string image in images) + File.Delete(image); + return Task.CompletedTask; + } } } diff --git a/back/src/Kyoo.Core/ExceptionFilter.cs b/back/src/Kyoo.Core/ExceptionFilter.cs index 372f9564..9a5bdb28 100644 --- a/back/src/Kyoo.Core/ExceptionFilter.cs +++ b/back/src/Kyoo.Core/ExceptionFilter.cs @@ -54,9 +54,13 @@ namespace Kyoo.Core case ItemNotFoundException ex: context.Result = new NotFoundObjectResult(new RequestError(ex.Message)); break; - case DuplicatedItemException ex: + case DuplicatedItemException ex when ex.Existing is not null: context.Result = new ConflictObjectResult(ex.Existing); break; + case DuplicatedItemException: + // Should not happen but if it does, it is better than returning a 409 with no body since clients expect json content + context.Result = new ConflictObjectResult(new RequestError("Duplicated item")); + break; case Exception ex: _logger.LogError(ex, "Unhandled error"); context.Result = new ServerErrorObjectResult(new RequestError("Internal Server Error"));