Handling failed tasks and fixing library provider registration

This commit is contained in:
Zoe Roux 2021-07-19 16:17:51 +02:00
parent 5a480402e1
commit a4635866a7
28 changed files with 228 additions and 116 deletions

View File

@ -0,0 +1,45 @@
using System;
using System.Runtime.Serialization;
using Kyoo.Controllers;
namespace Kyoo.Models.Exceptions
{
/// <summary>
/// An exception raised when an <see cref="ITask"/> failed.
/// </summary>
[Serializable]
public class TaskFailedException : AggregateException
{
/// <summary>
/// Create a new <see cref="TaskFailedException"/> with a default message.
/// </summary>
public TaskFailedException()
: base("A task failed.")
{}
/// <summary>
/// Create a new <see cref="TaskFailedException"/> with a custom message.
/// </summary>
/// <param name="message">The message to use.</param>
public TaskFailedException(string message)
: base(message)
{}
/// <summary>
/// Create a new <see cref="TaskFailedException"/> wrapping another exception.
/// </summary>
/// <param name="exception">The exception to wrap.</param>
public TaskFailedException(Exception exception)
: base(exception)
{}
/// <summary>
/// The serialization constructor
/// </summary>
/// <param name="info">Serialization infos</param>
/// <param name="context">The serialization context</param>
protected TaskFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
}

View File

@ -20,9 +20,11 @@ namespace Kyoo.Models
{
get
{
if (ShowSlug == null && Show == null)
return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
if (ShowSlug != null || Show != null)
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
return ShowID != 0
? GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber)
: null;
}
[UsedImplicitly] [NotNull] private set
{

View File

@ -156,30 +156,17 @@ namespace Kyoo.Models
}
/// <summary>
/// Utility method to edit a track slug (this only return a slug with the modification, nothing is stored)
/// Utility method to create a track slug from a incomplete slug (only add the type of the track).
/// </summary>
/// <param name="baseSlug">The slug to edit</param>
/// <param name="type">The new type of this </param>
/// <param name="language"></param>
/// <param name="index"></param>
/// <param name="forced"></param>
/// <returns></returns>
public static string EditSlug(string baseSlug,
StreamType type = StreamType.Unknown,
string language = null,
int? index = null,
bool? forced = null)
public static string BuildSlug(string baseSlug,
StreamType type)
{
Track track = new() {Slug = baseSlug};
if (type != StreamType.Unknown)
track.Type = type;
if (language != null)
track.Language = language;
if (index != null)
track.TrackIndex = index.Value;
if (forced != null)
track.IsForced = forced.Value;
return track.Slug;
return baseSlug.EndsWith($".{type}", StringComparison.InvariantCultureIgnoreCase)
? baseSlug
: $"{baseSlug}.{type.ToString().ToLowerInvariant()}";
}
}
}

View File

@ -176,6 +176,7 @@ namespace Kyoo.Models
return new WatchItem
{
EpisodeID = ep.ID,
Slug = ep.Slug,
ShowSlug = ep.Show.Slug,
SeasonNumber = ep.SeasonNumber,
EpisodeNumber = ep.EpisodeNumber,
@ -183,6 +184,7 @@ namespace Kyoo.Models
Title = ep.Title,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
Container = PathIO.GetExtension(ep.Path)![1..],
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -5,7 +5,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -1,8 +1,11 @@
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{
@ -23,7 +26,7 @@ namespace Kyoo.Tests.Library
}
}
public abstract class ALibraryTests : RepositoryTests<Models.Library>
public abstract class ALibraryTests : RepositoryTests<Library>
{
private readonly ILibraryRepository _repository;
@ -32,5 +35,17 @@ namespace Kyoo.Tests.Library
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
[Fact]
public async Task CreateWithProvider()
{
Library library = TestSample.GetNew<Library>();
library.Providers = new[] { TestSample.Get<Provider>() };
await _repository.Create(library);
Library retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.Providers);
Assert.Equal(1, retrieved.Providers.Count);
Assert.Equal(TestSample.Get<Provider>().Slug, retrieved.Providers.First().Slug);
}
}
}

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -5,7 +5,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
public class GlobalTests : IDisposable, IAsyncDisposable
{

View File

@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -4,7 +4,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -3,7 +3,7 @@ using Kyoo.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Library
namespace Kyoo.Tests.Database
{
namespace SqLite
{

View File

@ -9,8 +9,14 @@ namespace Kyoo.Tests
private static readonly Dictionary<Type, Func<object>> NewSamples = new()
{
{
typeof(Show),
() => new Show()
typeof(Library),
() => new Library
{
ID = 2,
Slug = "new-library",
Name = "New Library",
Paths = new [] {"/a/random/path"}
}
}
};
@ -18,8 +24,8 @@ namespace Kyoo.Tests
private static readonly Dictionary<Type, Func<object>> Samples = new()
{
{
typeof(Models.Library),
() => new Models.Library
typeof(Library),
() => new Library
{
ID = 1,
Slug = "deck",

@ -1 +1 @@
Subproject commit 22a02671918201d6d9d4e80a76f01b59b216a82d
Subproject commit dcdebad14cbcdf1f9486cb9178e6518d10c0e97f

View File

@ -87,7 +87,7 @@ namespace Kyoo.Controllers
public Task<Track> IdentifyTrack(string path, string relativePath)
{
Regex regex = new(_configuration.Value.SubtitleRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match = regex.Match(relativePath);
Match match = regex.Match(path);
if (!match.Success)
throw new IdentificationFailed($"The subtitle at {path} does not match the subtitle's regex.");

View File

@ -30,7 +30,7 @@ namespace Kyoo.Controllers
/// Create a new <see cref="LibraryRepository"/> instance.
/// </summary>
/// <param name="database">The database handle</param>
/// <param name="providers">The providere repository</param>
/// <param name="providers">The provider repository</param>
public LibraryRepository(DatabaseContext database, IProviderRepository providers)
: base(database)
{
@ -53,8 +53,8 @@ namespace Kyoo.Controllers
public override async Task<Library> Create(Library obj)
{
await base.Create(obj);
obj.ProviderLinks = obj.Providers?.Select(x => Link.Create(obj, x)).ToList();
_database.Entry(obj).State = EntityState.Added;
obj.ProviderLinks.ForEach(x => _database.Entry(x).State = EntityState.Added);
await _database.SaveChangesAsync($"Trying to insert a duplicated library (slug {obj.Slug} already exists).");
return obj;
}
@ -63,6 +63,9 @@ namespace Kyoo.Controllers
protected override async Task Validate(Library resource)
{
await base.Validate(resource);
resource.ProviderLinks = resource.Providers?
.Select(x => Link.Create(resource, x))
.ToList();
await resource.ProviderLinks.ForEachAsync(async id =>
{
id.Second = await _providers.CreateIfNotExists(id.Second);

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions;
using Kyoo.Models.Options;
using Microsoft.Extensions.DependencyInjection;
@ -112,6 +110,10 @@ namespace Kyoo.Controllers
{
await RunTask(task, progress, args);
}
catch (TaskFailedException ex)
{
_logger.LogWarning("The task \"{Task}\" failed: {Message}", task.Name, ex.Message);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while running the task {Task}", task.Name);

View File

@ -150,6 +150,7 @@ namespace Kyoo.Tasks
string[] subtitles = files
.Where(FileExtensions.IsSubtitle)
.Where(x => x.Contains("/Extra/"))
.Where(x => tracks.All(y => y.Path != x))
.ToArray();
percent = 0;

View File

@ -79,6 +79,8 @@ namespace Kyoo.Tasks
if (library.Providers == null)
await LibraryManager.Load(library, x => x.Providers);
MetadataProvider.UseProviders(library.Providers);
try
{
(Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path,
relativePath);
progress.Report(15);
@ -96,12 +98,13 @@ namespace Kyoo.Tasks
}
else
{
throw new DuplicatedItemException($"Duplicated show found ({show.Slug}) " +
throw new TaskFailedException($"Duplicated show found ({show.Slug}) " +
$"at {registeredShow.Path} and {show.Path}");
}
}
else
show = registeredShow;
// If they are not already loaded, load external ids to allow metadata providers to use them.
if (show.ExternalIDs == null)
await LibraryManager.Load(show, x => x.ExternalIDs);
@ -128,6 +131,15 @@ namespace Kyoo.Tasks
await LibraryManager.AddShowLink(show, library, collection);
progress.Report(100);
}
catch (IdentificationFailed ex)
{
throw new TaskFailedException(ex);
}
catch (DuplicatedItemException ex)
{
throw new TaskFailedException(ex);
}
}
/// <summary>
/// Retrieve the equivalent item if it already exists in the database,

View File

@ -60,29 +60,36 @@ namespace Kyoo.Tasks
string path = arguments["path"].As<string>();
string relativePath = arguments["relativePath"].As<string>();
try
{
progress.Report(0);
Track track = await Identifier.IdentifyTrack(path, relativePath);
progress.Report(25);
if (track.Episode == null)
throw new IdentificationFailed($"No episode identified for the track at {path}");
throw new TaskFailedException($"No episode identified for the track at {path}");
if (track.Episode.ID == 0)
{
if (track.Episode.Slug != null)
track.Episode = await LibraryManager.Get<Episode>(track.Episode.Slug);
else if (track.Episode.Path != null)
{
track.Episode = await LibraryManager.GetOrDefault<Episode>(x => x.Path == track.Episode.Path);
track.Episode = await LibraryManager.GetOrDefault<Episode>(x => x.Path.StartsWith(track.Episode.Path));
if (track.Episode == null)
throw new ItemNotFoundException($"No episode found for subtitle at: ${path}.");
throw new TaskFailedException($"No episode found for the track at: {path}.");
}
else
throw new IdentificationFailed($"No episode identified for the track at {path}");
throw new TaskFailedException($"No episode identified for the track at {path}");
}
progress.Report(50);
await LibraryManager.Create(track);
progress.Report(100);
}
catch (IdentificationFailed ex)
{
throw new TaskFailedException(ex);
}
}
}
}

View File

@ -386,7 +386,7 @@ namespace Kyoo.Api
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)}");
x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}");
}
catch (ItemNotFoundException)
{

View File

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models.Permissions;
@ -21,12 +22,43 @@ namespace Kyoo.Api
_files = files;
}
[HttpGet("{slug}.{extension}")]
[HttpGet("{id:int}")]
[Permission(nameof(SubtitleApi), Kind.Read)]
public async Task<IActionResult> GetSubtitle(string slug, string extension)
public async Task<IActionResult> GetSubtitle(int id)
{
Track subtitle = await _libraryManager.GetOrDefault<Track>(Track.EditSlug(slug, StreamType.Subtitle));
Track subtitle = await _libraryManager.GetOrDefault<Track>(id);
return subtitle != null
? _files.FileResult(subtitle.Path)
: NotFound();
}
[HttpGet("{id:int}.{extension}")]
[Permission(nameof(SubtitleApi), Kind.Read)]
public async Task<IActionResult> GetSubtitle(int id, string extension)
{
Track subtitle = await _libraryManager.GetOrDefault<Track>(id);
if (subtitle == null)
return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path, _files);
return _files.FileResult(subtitle.Path);
}
[HttpGet("{slug}")]
[Permission(nameof(SubtitleApi), Kind.Read)]
public async Task<IActionResult> GetSubtitle(string slug)
{
string extension = null;
if (slug.Count(x => x == '.') == 2)
{
int idx = slug.LastIndexOf('.');
extension = slug[(idx + 1)..];
slug = slug[..idx];
}
Track subtitle = await _libraryManager.GetOrDefault<Track>(Track.BuildSlug(slug, StreamType.Subtitle));
if (subtitle == null)
return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt")

View File

@ -45,8 +45,6 @@ namespace Kyoo.Api
{
try
{
// TODO This won't work with the local repository implementation.
// TODO Implement something like this (a dotnet-ef's QueryCompilationContext): https://stackoverflow.com/questions/62687811/how-can-i-convert-a-custom-function-to-a-sql-expression-for-entity-framework-cor
return await _libraryManager.Get<Episode>(x => x.Tracks.Any(y => y.Slug == slug));
}
catch (ItemNotFoundException)