diff --git a/Kyoo.Common/Controllers/IIdentifier.cs b/Kyoo.Common/Controllers/IIdentifier.cs new file mode 100644 index 00000000..c3b65bf9 --- /dev/null +++ b/Kyoo.Common/Controllers/IIdentifier.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Kyoo.Models; + +namespace Kyoo.Controllers +{ + /// + /// An interface to identify episodes, shows and metadata based on the episode file. + /// + public interface IIdentifier + { + /// + /// Identify a path and return the parsed metadata. + /// + /// The path of the episode file to parse. + /// + /// A tuple of models representing parsed metadata. + /// If no metadata could be parsed for a type, null can be returned. + /// + Task<(Collection, Show, Season, Episode)> Identify(string path); + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs index 33e4cd2b..ed6ab40d 100644 --- a/Kyoo.Common/Controllers/IMetadataProvider.cs +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -49,13 +49,28 @@ namespace Kyoo.Controllers /// A special that merge results. /// This interface exists to specify witch provider to use but it can be used like any other metadata provider. /// - public interface IProviderComposite : IMetadataProvider + public abstract class AProviderComposite : IMetadataProvider { + /// + [ItemNotNull] + public abstract Task Get(T item) + where T : class, IResource; + + /// + public abstract Task> Search(string query) + where T : class, IResource; + + /// + /// 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. + /// + public Provider Provider => null; + /// /// Select witch providers to use. /// The associated with the given will be used. /// /// The list of providers to use - void UseProviders(IEnumerable providers); + public abstract void UseProviders(IEnumerable providers); } } diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index f7c1a94b..6b6dc472 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -72,7 +72,7 @@ namespace Kyoo /// /// The string to slugify /// The slug version of the given string - public static string ToSlug(string str) + public static string ToSlug([CanBeNull] string str) { if (str == null) return null; diff --git a/Kyoo/Controllers/ProviderComposite.cs b/Kyoo/Controllers/ProviderComposite.cs index 88ad7529..2474016c 100644 --- a/Kyoo/Controllers/ProviderComposite.cs +++ b/Kyoo/Controllers/ProviderComposite.cs @@ -10,7 +10,7 @@ namespace Kyoo.Controllers /// /// A metadata provider composite that merge results from all available providers. /// - public class ProviderComposite : IProviderComposite + public class ProviderComposite : AProviderComposite { /// /// The list of metadata providers @@ -26,12 +26,6 @@ namespace Kyoo.Controllers /// The logger used to print errors. /// private readonly ILogger _logger; - - /// - /// 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. - /// - public Provider Provider => null; /// @@ -47,7 +41,7 @@ namespace Kyoo.Controllers /// - public void UseProviders(IEnumerable providers) + public override void UseProviders(IEnumerable providers) { _selectedProviders = providers.ToArray(); } @@ -65,8 +59,7 @@ namespace Kyoo.Controllers } /// - public async Task Get(T item) - where T : class, IResource + public override async Task Get(T item) { T ret = item; @@ -87,8 +80,7 @@ namespace Kyoo.Controllers } /// - public async Task> Search(string query) - where T : class, IResource + public override async Task> Search(string query) { List ret = new(); diff --git a/Kyoo/Controllers/RegexIdentifier.cs b/Kyoo/Controllers/RegexIdentifier.cs new file mode 100644 index 00000000..f46778eb --- /dev/null +++ b/Kyoo/Controllers/RegexIdentifier.cs @@ -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 +{ + /// + /// An identifier that use a regex to extract basics metadata. + /// + public class RegexIdentifier : IIdentifier + { + /// + /// The configuration of kyoo to retrieve the identifier regex. + /// + private readonly IOptions _configuration; + /// + /// A logger to print errors. + /// + private readonly ILogger _logger; + + /// + /// Create a new . + /// + /// The regex patterns to use. + /// The logger to use. + public RegexIdentifier(IOptions configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + + /// + 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); + } + } +} \ No newline at end of file diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index a96f470e..59dd1e35 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -95,6 +95,9 @@ namespace Kyoo.Tasks }); await Scan(library, episodes, reporter, cancellationToken); percent += 100f / libraries.Count; + + if (cancellationToken.IsCancellationRequested) + return; } progress.Report(100); @@ -108,11 +111,11 @@ namespace Kyoo.Tasks Logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths); foreach (string path in library.Paths) { + ICollection files = await FileManager.ListFiles(path, SearchOption.AllDirectories); + if (cancellationToken.IsCancellationRequested) return; - ICollection files = await FileManager.ListFiles(path, SearchOption.AllDirectories); - // 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 // of the show has already been fetched. diff --git a/Kyoo/Tasks/RegisterEpisode.cs b/Kyoo/Tasks/RegisterEpisode.cs index 7b0a4eba..74001792 100644 --- a/Kyoo/Tasks/RegisterEpisode.cs +++ b/Kyoo/Tasks/RegisterEpisode.cs @@ -3,6 +3,9 @@ using System.Threading; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; +using Kyoo.Models.Attributes; +using Kyoo.Models.Exceptions; +using Microsoft.Extensions.Logging; namespace Kyoo.Tasks { @@ -32,13 +35,30 @@ namespace Kyoo.Tasks /// public bool IsHidden => false; + /// + /// An identifier to extract metadata from paths. + /// + [Injected] public IIdentifier Identifier { private get; set; } + /// + /// The library manager used to register the episode + /// + [Injected] public ILibraryManager LibraryManager { private get; set; } + /// + /// A metadata provider to retrieve the metadata of the new episode (and related items if they do not exist). + /// + [Injected] public AProviderComposite MetadataProvider { private get; set; } + /// + /// The logger used to inform the current status to the console. + /// + [Injected] public ILogger Logger { private get; set; } + /// public TaskParameters GetParameters() { return new() { TaskParameter.CreateRequired("path", "The path of the episode file"), - TaskParameter.CreateRequired("library", "The library in witch the episode is") + TaskParameter.Create("library", "The library in witch the episode is") }; } @@ -48,11 +68,63 @@ namespace Kyoo.Tasks string path = arguments["path"].As(); Library library = arguments["library"].As(); + 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 _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.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 { @@ -103,83 +175,7 @@ namespace Kyoo.Tasks if (token.IsCancellationRequested) return; - try - { - using IServiceScope serviceScope = ServiceProvider.CreateScope(); - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - - string patern = Config.GetValue("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 GetCollection(ILibraryManager libraryManager, - string collectionName, - Library library) - { - if (string.IsNullOrEmpty(collectionName)) - return null; - Collection collection = await libraryManager.GetOrDefault(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.Slug); - } + } private async Task GetShow(ILibraryManager libraryManager, diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 2ff0e24b..3233e7f5 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -44,7 +44,7 @@ }, "media": { - "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", + "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(?\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", "subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" },