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\\.)?.*$"
},