Improve duplicated error handling

This commit is contained in:
Zoe Roux 2023-11-28 02:52:19 +01:00
parent f59f9a7ba0
commit 9a5c4ab087
10 changed files with 75 additions and 17 deletions

View File

@ -26,7 +26,7 @@ namespace Kyoo.Abstractions.Controllers
public interface ILibraryManager
{
IRepository<T> Repository<T>()
where T : class, IResource, IQuery;
where T : IResource, IQuery;
/// <summary>
/// The repository that handle libraries items (a wrapper around shows and collections).

View File

@ -30,7 +30,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
public interface IRepository<T> : IBaseRepository
where T : class, IResource, IQuery
where T : IResource, IQuery
{
/// <summary>
/// The event handler type for all events of this repository.

View File

@ -36,7 +36,7 @@ namespace Kyoo.Abstractions.Controllers
/// The item to cache images.
/// </param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns><c>true</c> if an image has been downloaded, <c>false</c> otherwise.</returns>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DownloadImages<T>(T item)
where T : IThumbnails;
@ -50,5 +50,16 @@ namespace Kyoo.Abstractions.Controllers
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
string GetImagePath<T>(T item, string image, ImageQuality quality)
where T : IThumbnails;
/// <summary>
/// Delete images associated with the item.
/// </summary>
/// <param name="item">
/// The item with cached images.
/// </param>
/// <typeparam name="T">The type of the item</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteImages<T>(T item)
where T : IThumbnails;
}
}

View File

@ -25,7 +25,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// An interface to represent a resource that can be retrieved from the database.
/// </summary>
public interface IResource
public interface IResource : IQuery
{
/// <summary>
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.

View File

@ -92,7 +92,7 @@ namespace Kyoo.Core.Controllers
public IRepository<User> Users { get; }
public IRepository<T> Repository<T>()
where T : class, IResource, IQuery
where T : IResource, IQuery
{
return (IRepository<T>)_repositories.First(x => x.RepositoryType == typeof(T));
}

View File

@ -84,16 +84,20 @@ namespace Kyoo.Core.Controllers
.ToListAsync();
}
protected override Task<Episode?> 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);
}
/// <inheritdoc />
public override async Task<Episode> 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<Episode>.OnResourceCreated(obj);
return obj;
}

View File

@ -214,6 +214,11 @@ namespace Kyoo.Core.Controllers
return ret;
}
protected virtual Task<T?> GetDuplicated(T item)
{
return GetOrDefault(item.Slug);
}
/// <inheritdoc />
public virtual Task<T?> GetOrDefault(Guid id, Include<T>? 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<T>.OnResourceDeleted(obj);
if (obj is IThumbnails thumbs)
return _thumbs.DeleteImages(thumbs);
return Task.CompletedTask;
}

View File

@ -16,12 +16,13 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
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<Season?> GetDuplicated(Season item)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == item.ShowId && x.SeasonNumber == item.SeasonNumber);
}
/// <inheritdoc/>
public override async Task<ICollection<Season>> Search(string query, Include<Season>? include = default)
{
@ -83,11 +89,10 @@ namespace Kyoo.Core.Controllers
public override async Task<Season> 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<Season>.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;

View File

@ -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";
}
/// <inheritdoc />
public Task DeleteImages<T>(T item)
where T : IThumbnails
{
IEnumerable<string> 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;
}
}
}

View File

@ -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"));