Starting to implement the register episode task

This commit is contained in:
Zoe Roux 2021-07-16 01:25:01 +02:00
parent 3ba0cffac2
commit 3acc69d602
8 changed files with 203 additions and 97 deletions

View File

@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Kyoo.Models;
namespace Kyoo.Controllers
{
/// <summary>
/// An interface to identify episodes, shows and metadata based on the episode file.
/// </summary>
public interface IIdentifier
{
/// <summary>
/// Identify a path and return the parsed metadata.
/// </summary>
/// <param name="path">The path of the episode file to parse.</param>
/// <returns>
/// A tuple of models representing parsed metadata.
/// If no metadata could be parsed for a type, null can be returned.
/// </returns>
Task<(Collection, Show, Season, Episode)> Identify(string path);
}
}

View File

@ -49,13 +49,28 @@ namespace Kyoo.Controllers
/// A special <see cref="IMetadataProvider"/> that merge results. /// A special <see cref="IMetadataProvider"/> that merge results.
/// This interface exists to specify witch provider to use but it can be used like any other metadata provider. /// This interface exists to specify witch provider to use but it can be used like any other metadata provider.
/// </summary> /// </summary>
public interface IProviderComposite : IMetadataProvider public abstract class AProviderComposite : IMetadataProvider
{ {
/// <inheritdoc />
[ItemNotNull]
public abstract Task<T> Get<T>(T item)
where T : class, IResource;
/// <inheritdoc />
public abstract Task<ICollection<T>> Search<T>(string query)
where T : class, IResource;
/// <summary>
/// Since this is a composite and not a real provider, no metadata is available.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// </summary>
public Provider Provider => null;
/// <summary> /// <summary>
/// Select witch providers to use. /// Select witch providers to use.
/// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used. /// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used.
/// </summary> /// </summary>
/// <param name="providers">The list of providers to use</param> /// <param name="providers">The list of providers to use</param>
void UseProviders(IEnumerable<Provider> providers); public abstract void UseProviders(IEnumerable<Provider> providers);
} }
} }

View File

@ -72,7 +72,7 @@ namespace Kyoo
/// </summary> /// </summary>
/// <param name="str">The string to slugify</param> /// <param name="str">The string to slugify</param>
/// <returns>The slug version of the given string</returns> /// <returns>The slug version of the given string</returns>
public static string ToSlug(string str) public static string ToSlug([CanBeNull] string str)
{ {
if (str == null) if (str == null)
return null; return null;

View File

@ -10,7 +10,7 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// A metadata provider composite that merge results from all available providers. /// A metadata provider composite that merge results from all available providers.
/// </summary> /// </summary>
public class ProviderComposite : IProviderComposite public class ProviderComposite : AProviderComposite
{ {
/// <summary> /// <summary>
/// The list of metadata providers /// The list of metadata providers
@ -26,12 +26,6 @@ namespace Kyoo.Controllers
/// The logger used to print errors. /// The logger used to print errors.
/// </summary> /// </summary>
private readonly ILogger<ProviderComposite> _logger; private readonly ILogger<ProviderComposite> _logger;
/// <summary>
/// Since this is a composite and not a real provider, no metadata is available.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// </summary>
public Provider Provider => null;
/// <summary> /// <summary>
@ -47,7 +41,7 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public void UseProviders(IEnumerable<Provider> providers) public override void UseProviders(IEnumerable<Provider> providers)
{ {
_selectedProviders = providers.ToArray(); _selectedProviders = providers.ToArray();
} }
@ -65,8 +59,7 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<T> Get<T>(T item) public override async Task<T> Get<T>(T item)
where T : class, IResource
{ {
T ret = item; T ret = item;
@ -87,8 +80,7 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<ICollection<T>> Search<T>(string query) public override async Task<ICollection<T>> Search<T>(string query)
where T : class, IResource
{ {
List<T> ret = new(); List<T> ret = new();

View File

@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Kyoo.Models;
using Kyoo.Models.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers
{
/// <summary>
/// An identifier that use a regex to extract basics metadata.
/// </summary>
public class RegexIdentifier : IIdentifier
{
/// <summary>
/// The configuration of kyoo to retrieve the identifier regex.
/// </summary>
private readonly IOptions<MediaOptions> _configuration;
/// <summary>
/// A logger to print errors.
/// </summary>
private readonly ILogger<RegexIdentifier> _logger;
/// <summary>
/// Create a new <see cref="RegexIdentifier"/>.
/// </summary>
/// <param name="configuration">The regex patterns to use.</param>
/// <param name="logger">The logger to use.</param>
public RegexIdentifier(IOptions<MediaOptions> configuration, ILogger<RegexIdentifier> logger)
{
_configuration = configuration;
_logger = logger;
}
/// <inheritdoc />
public Task<(Collection, Show, Season, Episode)> Identify(string path)
{
string pattern = _configuration.Value.Regex;
Regex regex = new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match = regex.Match(path);
if (!match.Success)
{
_logger.LogError("The episode at {Path} does not match the episode's regex", path);
return Task.FromResult<(Collection, Show, Season, Episode)>(default);
}
(Collection collection, Show show, Season season, Episode episode) ret = new();
ret.collection.Name = match.Groups["Collection"].Value;
ret.show.Title = match.Groups["Show"].Value;
ret.show.Path = Path.GetDirectoryName(path);
if (match.Groups["StartYear"].Success && int.TryParse(match.Groups["StartYear"].Value, out int tmp))
ret.show.StartAir = new DateTime(tmp, 1, 1);
if (match.Groups["Season"].Success && int.TryParse(match.Groups["Season"].Value, out tmp))
{
ret.season.SeasonNumber = tmp;
ret.episode.SeasonNumber = tmp;
}
if (match.Groups["Episode"].Success && int.TryParse(match.Groups["Episode"].Value, out tmp))
ret.episode.EpisodeNumber = tmp;
if (match.Groups["Absolute"].Success && int.TryParse(match.Groups["Absolute"].Value, out tmp))
ret.episode.AbsoluteNumber = tmp;
ret.show.IsMovie = ret.episode.SeasonNumber == null && ret.episode.EpisodeNumber == null
&& ret.episode.AbsoluteNumber == null;
return Task.FromResult(ret);
}
}
}

View File

@ -95,6 +95,9 @@ namespace Kyoo.Tasks
}); });
await Scan(library, episodes, reporter, cancellationToken); await Scan(library, episodes, reporter, cancellationToken);
percent += 100f / libraries.Count; percent += 100f / libraries.Count;
if (cancellationToken.IsCancellationRequested)
return;
} }
progress.Report(100); progress.Report(100);
@ -108,11 +111,11 @@ namespace Kyoo.Tasks
Logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths); Logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths);
foreach (string path in library.Paths) foreach (string path in library.Paths)
{ {
ICollection<string> files = await FileManager.ListFiles(path, SearchOption.AllDirectories);
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
ICollection<string> files = await FileManager.ListFiles(path, SearchOption.AllDirectories);
// We try to group episodes by shows to register one episode of each show first. // We try to group episodes by shows to register one episode of each show first.
// This speeds up the scan process because further episodes of a show are registered when all metadata // This speeds up the scan process because further episodes of a show are registered when all metadata
// of the show has already been fetched. // of the show has already been fetched.

View File

@ -3,6 +3,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Attributes;
using Kyoo.Models.Exceptions;
using Microsoft.Extensions.Logging;
namespace Kyoo.Tasks namespace Kyoo.Tasks
{ {
@ -32,13 +35,30 @@ namespace Kyoo.Tasks
/// <inheritdoc /> /// <inheritdoc />
public bool IsHidden => false; public bool IsHidden => false;
/// <summary>
/// An identifier to extract metadata from paths.
/// </summary>
[Injected] public IIdentifier Identifier { private get; set; }
/// <summary>
/// The library manager used to register the episode
/// </summary>
[Injected] public ILibraryManager LibraryManager { private get; set; }
/// <summary>
/// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist).
/// </summary>
[Injected] public AProviderComposite MetadataProvider { private get; set; }
/// <summary>
/// The logger used to inform the current status to the console.
/// </summary>
[Injected] public ILogger<RegisterEpisode> Logger { private get; set; }
/// <inheritdoc /> /// <inheritdoc />
public TaskParameters GetParameters() public TaskParameters GetParameters()
{ {
return new() return new()
{ {
TaskParameter.CreateRequired<string>("path", "The path of the episode file"), TaskParameter.CreateRequired<string>("path", "The path of the episode file"),
TaskParameter.CreateRequired<Library>("library", "The library in witch the episode is") TaskParameter.Create<Library>("library", "The library in witch the episode is")
}; };
} }
@ -48,11 +68,63 @@ namespace Kyoo.Tasks
string path = arguments["path"].As<string>(); string path = arguments["path"].As<string>();
Library library = arguments["library"].As<Library>(); Library library = arguments["library"].As<Library>();
try
{
(Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path);
if (library != null)
MetadataProvider.UseProviders(library.Providers);
collection = await _RegisterAndFillCollection(collection);
// show = await _RegisterAndFillShow(show);
// if (isMovie)
// await libraryManager!.Create(await GetMovie(show, path));
// else
// {
// Season season = seasonNumber != null
// ? await GetSeason(libraryManager, show, seasonNumber.Value, library)
// : null;
// Episode episode = await GetEpisode(libraryManager,
// show,
// season,
// episodeNumber,
// absoluteNumber,
// path,
// library);
// await libraryManager!.Create(episode);
// }
//
// await libraryManager.AddShowLink(show, library, collection);
// Console.WriteLine($"Episode at {path} registered.");
}
catch (DuplicatedItemException ex)
{
Logger.LogWarning(ex, "Duplicated found at {Path}", path);
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Unknown exception thrown while registering episode at {Path}", path);
}
}
private async Task<Collection> _RegisterAndFillCollection(Collection collection)
{
if (collection == null)
return null;
collection.Slug ??= Utility.ToSlug(collection.Name);
if (string.IsNullOrEmpty(collection.Slug))
return null;
Collection existing = await LibraryManager.GetOrDefault<Collection>(collection.Slug);
if (existing != null)
return existing;
collection = await MetadataProvider.Get(collection);
return await LibraryManager.CreateIfNotExists(collection);
} }
/* /*
* private async Task RegisterExternalSubtitle(string path, CancellationToken token) *
private async Task RegisterExternalSubtitle(string path, CancellationToken token)
{ {
try try
{ {
@ -103,83 +175,7 @@ namespace Kyoo.Tasks
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
return; return;
try
{
using IServiceScope serviceScope = ServiceProvider.CreateScope();
ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService<ILibraryManager>();
string patern = Config.GetValue<string>("regex");
Regex regex = new(patern, RegexOptions.IgnoreCase);
Match match = regex.Match(relativePath);
if (!match.Success)
{
await Console.Error.WriteLineAsync($"The episode at {path} does not match the episode's regex.");
return;
}
string showPath = Path.GetDirectoryName(path);
string collectionName = match.Groups["Collection"].Value;
string showName = match.Groups["Show"].Value;
int? seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : null;
int? episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : null;
int? absoluteNumber = int.TryParse(match.Groups["Absolute"].Value, out tmp) ? tmp : null;
Collection collection = await GetCollection(libraryManager, collectionName, library);
bool isMovie = seasonNumber == null && episodeNumber == null && absoluteNumber == null;
Show show = await GetShow(libraryManager, showName, showPath, isMovie, library);
if (isMovie)
await libraryManager!.Create(await GetMovie(show, path));
else
{
Season season = seasonNumber != null
? await GetSeason(libraryManager, show, seasonNumber.Value, library)
: null;
Episode episode = await GetEpisode(libraryManager,
show,
season,
episodeNumber,
absoluteNumber,
path,
library);
await libraryManager!.Create(episode);
}
await libraryManager.AddShowLink(show, library, collection);
Console.WriteLine($"Episode at {path} registered.");
}
catch (DuplicatedItemException ex)
{
await Console.Error.WriteLineAsync($"{path}: {ex.Message}");
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Unknown exception thrown while registering episode at {path}." +
$"\nException: {ex.Message}" +
$"\n{ex.StackTrace}");
}
}
private async Task<Collection> GetCollection(ILibraryManager libraryManager,
string collectionName,
Library library)
{
if (string.IsNullOrEmpty(collectionName))
return null;
Collection collection = await libraryManager.GetOrDefault<Collection>(Utility.ToSlug(collectionName));
if (collection != null)
return collection;
// collection = await MetadataProvider.GetCollectionFromName(collectionName, library);
try
{
await libraryManager.Create(collection);
return collection;
}
catch (DuplicatedItemException)
{
return await libraryManager.GetOrDefault<Collection>(collection.Slug);
}
} }
private async Task<Show> GetShow(ILibraryManager libraryManager, private async Task<Show> GetShow(ILibraryManager libraryManager,

View File

@ -44,7 +44,7 @@
}, },
"media": { "media": {
"regex": "(?:\\/(?<Collection>.*?))?\\/(?<Show>.*?)(?: \\(\\d+\\))?\\/\\k<Show>(?: \\(\\d+\\))?(?:(?: S(?<Season>\\d+)E(?<Episode>\\d+))| (?<Absolute>\\d+))?.*$", "regex": "(?:\\/(?<Collection>.*?))?\\/(?<Show>.*?)(?: \\(?<StartYear>\\d+\\))?\\/\\k<Show>(?: \\(\\d+\\))?(?:(?: S(?<Season>\\d+)E(?<Episode>\\d+))| (?<Absolute>\\d+))?.*$",
"subtitleRegex": "^(?<Episode>.*)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$" "subtitleRegex": "^(?<Episode>.*)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
}, },