mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Starting to implement the register episode task
This commit is contained in:
parent
3ba0cffac2
commit
3acc69d602
21
Kyoo.Common/Controllers/IIdentifier.cs
Normal file
21
Kyoo.Common/Controllers/IIdentifier.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -49,13 +49,28 @@ namespace Kyoo.Controllers
|
||||
/// 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.
|
||||
/// </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>
|
||||
/// Select witch providers to use.
|
||||
/// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used.
|
||||
/// </summary>
|
||||
/// <param name="providers">The list of providers to use</param>
|
||||
void UseProviders(IEnumerable<Provider> providers);
|
||||
public abstract void UseProviders(IEnumerable<Provider> providers);
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ namespace Kyoo
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <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)
|
||||
return null;
|
||||
|
@ -10,7 +10,7 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// A metadata provider composite that merge results from all available providers.
|
||||
/// </summary>
|
||||
public class ProviderComposite : IProviderComposite
|
||||
public class ProviderComposite : AProviderComposite
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of metadata providers
|
||||
@ -26,12 +26,6 @@ namespace Kyoo.Controllers
|
||||
/// The logger used to print errors.
|
||||
/// </summary>
|
||||
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>
|
||||
@ -47,7 +41,7 @@ namespace Kyoo.Controllers
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UseProviders(IEnumerable<Provider> providers)
|
||||
public override void UseProviders(IEnumerable<Provider> providers)
|
||||
{
|
||||
_selectedProviders = providers.ToArray();
|
||||
}
|
||||
@ -65,8 +59,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> Get<T>(T item)
|
||||
where T : class, IResource
|
||||
public override async Task<T> Get<T>(T item)
|
||||
{
|
||||
T ret = item;
|
||||
|
||||
@ -87,8 +80,7 @@ namespace Kyoo.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<T>> Search<T>(string query)
|
||||
where T : class, IResource
|
||||
public override async Task<ICollection<T>> Search<T>(string query)
|
||||
{
|
||||
List<T> ret = new();
|
||||
|
||||
|
79
Kyoo/Controllers/RegexIdentifier.cs
Normal file
79
Kyoo/Controllers/RegexIdentifier.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string> files = await FileManager.ListFiles(path, SearchOption.AllDirectories);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
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.
|
||||
// This speeds up the scan process because further episodes of a show are registered when all metadata
|
||||
// of the show has already been fetched.
|
||||
|
@ -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
|
||||
/// <inheritdoc />
|
||||
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 />
|
||||
public TaskParameters GetParameters()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
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>();
|
||||
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
|
||||
{
|
||||
@ -103,83 +175,7 @@ namespace Kyoo.Tasks
|
||||
if (token.IsCancellationRequested)
|
||||
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,
|
||||
|
@ -44,7 +44,7 @@
|
||||
},
|
||||
|
||||
"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\\.)?.*$"
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user