From e66e59cf3206ce0cb37fa04d1e19db39a1921f41 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 17 Jul 2021 01:34:24 +0200 Subject: [PATCH] Creating a registerr subtitle task --- Kyoo.Common/Controllers/IIdentifier.cs | 12 ++ .../Models/Exceptions/IdentificationFailed.cs | 37 +++++ Kyoo/Controllers/RegexIdentifier.cs | 45 ++++-- Kyoo/Models/FileExtensions.cs | 53 ++++++ Kyoo/Models/LazyDi.cs | 12 -- Kyoo/Tasks/Crawler.cs | 81 ++++------ Kyoo/Tasks/RegisterEpisode.cs | 153 +++++++----------- Kyoo/Tasks/RegisterSubtitle.cs | 85 ++++++++++ 8 files changed, 300 insertions(+), 178 deletions(-) create mode 100644 Kyoo.Common/Models/Exceptions/IdentificationFailed.cs create mode 100644 Kyoo/Models/FileExtensions.cs delete mode 100644 Kyoo/Models/LazyDi.cs create mode 100644 Kyoo/Tasks/RegisterSubtitle.cs diff --git a/Kyoo.Common/Controllers/IIdentifier.cs b/Kyoo.Common/Controllers/IIdentifier.cs index c3b65bf9..b841a63b 100644 --- a/Kyoo.Common/Controllers/IIdentifier.cs +++ b/Kyoo.Common/Controllers/IIdentifier.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Kyoo.Models; +using Kyoo.Models.Exceptions; namespace Kyoo.Controllers { @@ -12,10 +13,21 @@ namespace Kyoo.Controllers /// Identify a path and return the parsed metadata. /// /// The path of the episode file to parse. + /// The identifier could not work for the given path. /// /// 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); + + /// + /// Identify an external subtitle or track file from it's path and return the parsed metadata. + /// + /// The path of the external track file to parse. + /// The identifier could not work for the given path. + /// + /// The metadata of the track identified. + /// + Task IdentifyTrack(string path); } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Exceptions/IdentificationFailed.cs b/Kyoo.Common/Models/Exceptions/IdentificationFailed.cs new file mode 100644 index 00000000..a8838fea --- /dev/null +++ b/Kyoo.Common/Models/Exceptions/IdentificationFailed.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.Serialization; +using Kyoo.Controllers; + +namespace Kyoo.Models.Exceptions +{ + /// + /// An exception raised when an failed. + /// + [Serializable] + public class IdentificationFailed : Exception + { + /// + /// Create a new with a default message. + /// + public IdentificationFailed() + : base("An identification failed.") + {} + + /// + /// Create a new with a custom message. + /// + /// The message to use. + public IdentificationFailed(string message) + : base(message) + {} + + /// + /// The serialization constructor + /// + /// Serialization infos + /// The serialization context + protected IdentificationFailed(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/RegexIdentifier.cs b/Kyoo/Controllers/RegexIdentifier.cs index e104d70a..d99f8255 100644 --- a/Kyoo/Controllers/RegexIdentifier.cs +++ b/Kyoo/Controllers/RegexIdentifier.cs @@ -3,8 +3,9 @@ using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; using Kyoo.Models; +using Kyoo.Models.Exceptions; using Kyoo.Models.Options; -using Microsoft.Extensions.Logging; +using Kyoo.Models.Watch; using Microsoft.Extensions.Options; namespace Kyoo.Controllers @@ -18,23 +19,16 @@ namespace Kyoo.Controllers /// 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) + public RegexIdentifier(IOptions configuration) { _configuration = configuration; - _logger = logger; } - - + /// public Task<(Collection, Show, Season, Episode)> Identify(string path) { @@ -42,10 +36,7 @@ namespace Kyoo.Controllers 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); - } + throw new IdentificationFailed($"The episode at {path} does not match the episode's regex."); (Collection collection, Show show, Season season, Episode episode) ret = new(); @@ -81,5 +72,31 @@ namespace Kyoo.Controllers return Task.FromResult(ret); } + + /// + public Task IdentifyTrack(string path) + { + Regex regex = new(_configuration.Value.SubtitleRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled); + Match match = regex.Match(path); + + if (!match.Success) + throw new IdentificationFailed($"The subtitle at {path} does not match the subtitle's regex."); + + string episodePath = match.Groups["Episode"].Value; + return Task.FromResult(new Track + { + Type = StreamType.Subtitle, + Language = match.Groups["Language"].Value, + IsDefault = match.Groups["Default"].Value.Length > 0, + IsForced = match.Groups["Forced"].Value.Length > 0, + Codec = FileExtensions.SubtitleExtensions[Path.GetExtension(path)], + IsExternal = true, + Path = path, + Episode = new Episode + { + Path = episodePath + } + }); + } } } \ No newline at end of file diff --git a/Kyoo/Models/FileExtensions.cs b/Kyoo/Models/FileExtensions.cs new file mode 100644 index 00000000..f8058a67 --- /dev/null +++ b/Kyoo/Models/FileExtensions.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Kyoo.Models.Watch +{ + public static class FileExtensions + { + public static readonly string[] VideoExtensions = + { + ".webm", + ".mkv", + ".flv", + ".vob", + ".ogg", + ".ogv", + ".avi", + ".mts", + ".m2ts", + ".ts", + ".mov", + ".qt", + ".asf", + ".mp4", + ".m4p", + ".m4v", + ".mpg", + ".mp2", + ".mpeg", + ".mpe", + ".mpv", + ".m2v", + ".3gp", + ".3g2" + }; + + public static bool IsVideo(string filePath) + { + return VideoExtensions.Contains(Path.GetExtension(filePath)); + } + + public static readonly Dictionary SubtitleExtensions = new() + { + {".ass", "ass"}, + {".str", "subrip"} + }; + + public static bool IsSubtitle(string filePath) + { + return SubtitleExtensions.ContainsKey(Path.GetExtension(filePath)); + } + } +} \ No newline at end of file diff --git a/Kyoo/Models/LazyDi.cs b/Kyoo/Models/LazyDi.cs deleted file mode 100644 index 477e1ec4..00000000 --- a/Kyoo/Models/LazyDi.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; - -namespace Kyoo.Models -{ - public class LazyDi : Lazy - { - public LazyDi(IServiceProvider provider) - : base(provider.GetRequiredService) - { } - } -} \ No newline at end of file diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 59dd1e35..6f4cf9eb 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -3,13 +3,11 @@ using Kyoo.Models; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models.Attributes; -using Kyoo.Models.Exceptions; -using Microsoft.Extensions.DependencyInjection; +using Kyoo.Models.Watch; using Microsoft.Extensions.Logging; namespace Kyoo.Tasks @@ -86,6 +84,7 @@ namespace Kyoo.Tasks float percent = 0; ICollection episodes = await LibraryManager.GetAll(); + ICollection tracks = await LibraryManager.GetAll(); foreach (Library library in libraries) { IProgress reporter = new Progress(x => @@ -93,7 +92,7 @@ namespace Kyoo.Tasks // ReSharper disable once AccessToModifiedClosure progress.Report(percent + x / libraries.Count); }); - await Scan(library, episodes, reporter, cancellationToken); + await Scan(library, episodes, tracks, reporter, cancellationToken); percent += 100f / libraries.Count; if (cancellationToken.IsCancellationRequested) @@ -105,6 +104,7 @@ namespace Kyoo.Tasks private async Task Scan(Library library, IEnumerable episodes, + IEnumerable tracks, IProgress progress, CancellationToken cancellationToken) { @@ -120,19 +120,20 @@ namespace Kyoo.Tasks // This speeds up the scan process because further episodes of a show are registered when all metadata // of the show has already been fetched. List> shows = files - .Where(IsVideo) + .Where(FileExtensions.IsVideo) .Where(x => episodes.All(y => y.Path != x)) .GroupBy(Path.GetDirectoryName) .ToList(); + + string[] paths = shows.Select(x => x.First()) .Concat(shows.SelectMany(x => x.Skip(1))) .ToArray(); - float percent = 0; IProgress reporter = new Progress(x => { // ReSharper disable once AccessToModifiedClosure - progress.Report((percent + x / paths.Length) / library.Paths.Length); + progress.Report((percent + x / paths.Length - 10) / library.Paths.Length); }); foreach (string episodePath in paths) @@ -145,53 +146,27 @@ namespace Kyoo.Tasks percent += 100f / paths.Length; } - // await Task.WhenAll(files.Where(x => IsSubtitle(x) && tracks.All(y => y.Path != x)) - // .Select(x => RegisterExternalSubtitle(x, cancellationToken))); + + string[] subtitles = files + .Where(FileExtensions.IsSubtitle) + .Where(x => tracks.All(y => y.Path != x)) + .ToArray(); + percent = 0; + reporter = new Progress(x => + { + // ReSharper disable once AccessToModifiedClosure + progress.Report((90 + (percent + x / subtitles.Length)) / library.Paths.Length); + }); + + foreach (string trackPath in subtitles) + { + TaskManager.StartTask(reporter, new Dictionary + { + ["path"] = trackPath + }, cancellationToken); + percent += 100f / subtitles.Length; + } } } - - private static readonly string[] VideoExtensions = - { - ".webm", - ".mkv", - ".flv", - ".vob", - ".ogg", - ".ogv", - ".avi", - ".mts", - ".m2ts", - ".ts", - ".mov", - ".qt", - ".asf", - ".mp4", - ".m4p", - ".m4v", - ".mpg", - ".mp2", - ".mpeg", - ".mpe", - ".mpv", - ".m2v", - ".3gp", - ".3g2" - }; - - private static bool IsVideo(string filePath) - { - return VideoExtensions.Contains(Path.GetExtension(filePath)); - } - - private static readonly Dictionary SubtitleExtensions = new() - { - {".ass", "ass"}, - {".str", "subrip"} - }; - - private static bool IsSubtitle(string filePath) - { - return SubtitleExtensions.ContainsKey(Path.GetExtension(filePath)); - } } } diff --git a/Kyoo/Tasks/RegisterEpisode.cs b/Kyoo/Tasks/RegisterEpisode.cs index 799ac8aa..6ef512e6 100644 --- a/Kyoo/Tasks/RegisterEpisode.cs +++ b/Kyoo/Tasks/RegisterEpisode.cs @@ -6,7 +6,6 @@ using Kyoo.Controllers; using Kyoo.Models; using Kyoo.Models.Attributes; using Kyoo.Models.Exceptions; -using Microsoft.Extensions.Logging; namespace Kyoo.Tasks { @@ -56,11 +55,7 @@ namespace Kyoo.Tasks /// The transcoder used to extract subtitles and metadata. /// [Injected] public ITranscoder Transcoder { private get; set; } - /// - /// The logger used to inform the current status to the console. - /// - [Injected] public ILogger Logger { private get; set; } - + /// public TaskParameters GetParameters() { @@ -76,58 +71,67 @@ namespace Kyoo.Tasks { string path = arguments["path"].As(); Library library = arguments["library"].As(); + progress.Report(0); - try + if (library.Providers == null) + await LibraryManager.Load(library, x => x.Providers); + MetadataProvider.UseProviders(library.Providers); + (Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path); + progress.Report(15); + + collection = await _RegisterAndFill(collection); + progress.Report(20); + + Show registeredShow = await _RegisterAndFill(show); + if (registeredShow.Path != show.Path) { - if (library != null) + if (show.StartAir.HasValue) { - if (library.Providers == null) - await LibraryManager.Load(library, x => x.Providers); - MetadataProvider.UseProviders(library.Providers); - } - - (Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path); - - collection = await _RegisterAndFill(collection); - - Show registeredShow = await _RegisterAndFill(show); - if (registeredShow.Path != show.Path) - { - if (show.StartAir.HasValue) - { - show.Slug += $"-{show.StartAir.Value.Year}"; - show = await LibraryManager.Create(show); - } - else - { - Logger.LogError("Duplicated show found ({Slug}) at {Path1} and {Path2}", - show.Slug, registeredShow.Path, show.Path); - return; - } + show.Slug += $"-{show.StartAir.Value.Year}"; + show = await LibraryManager.Create(show); } else - show = registeredShow; - - if (season != null) - season.Show = show; - season = await _RegisterAndFill(season); - - episode = await MetadataProvider.Get(episode); - episode.Season = season; - episode.Tracks = (await Transcoder.ExtractInfos(episode, false)) - .Where(x => x.Type != StreamType.Attachment) - .ToArray(); - await ThumbnailsManager.DownloadImages(episode); - - await LibraryManager.Create(episode); - await LibraryManager.AddShowLink(show, library, collection); - } - catch (DuplicatedItemException ex) - { - Logger.LogWarning(ex, "Duplicated found at {Path}", path); + { + throw new DuplicatedItemException($"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); + progress.Report(50); + + if (season != null) + season.Show = show; + season = await _RegisterAndFill(season); + progress.Report(60); + + episode = await MetadataProvider.Get(episode); + progress.Report(70); + episode.Show = show; + episode.Season = season; + episode.Tracks = (await Transcoder.ExtractInfos(episode, false)) + .Where(x => x.Type != StreamType.Attachment) + .ToArray(); + await ThumbnailsManager.DownloadImages(episode); + progress.Report(90); + + await LibraryManager.Create(episode); + progress.Report(95); + await LibraryManager.AddShowLink(show, library, collection); + progress.Report(100); } + /// + /// Retrieve the equivalent item if it already exists in the database, + /// if it does not, fill metadata using the metadata provider, download images and register the item to the + /// database. + /// + /// The item to retrieve or fill and register + /// The type of the item + /// The existing or filled item. private async Task _RegisterAndFill(T item) where T : class, IResource { @@ -141,54 +145,5 @@ namespace Kyoo.Tasks await ThumbnailsManager.DownloadImages(item); return await LibraryManager.CreateIfNotExists(item); } - - /* - * - private async Task RegisterExternalSubtitle(string path, CancellationToken token) - { - try - { - if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) - return; - using IServiceScope serviceScope = ServiceProvider.CreateScope(); - ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - - string patern = Config.GetValue("subtitleRegex"); - Regex regex = new(patern, RegexOptions.IgnoreCase); - Match match = regex.Match(path); - - if (!match.Success) - { - await Console.Error.WriteLineAsync($"The subtitle at {path} does not match the subtitle's regex."); - return; - } - - string episodePath = match.Groups["Episode"].Value; - Episode episode = await libraryManager!.Get(x => x.Path.StartsWith(episodePath)); - Track track = new() - { - Type = StreamType.Subtitle, - Language = match.Groups["Language"].Value, - IsDefault = match.Groups["Default"].Value.Length > 0, - IsForced = match.Groups["Forced"].Value.Length > 0, - Codec = SubtitleExtensions[Path.GetExtension(path)], - IsExternal = true, - Path = path, - Episode = episode - }; - - await libraryManager.Create(track); - Console.WriteLine($"Registering subtitle at: {path}."); - } - catch (ItemNotFoundException) - { - await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}."); - } - catch (Exception ex) - { - await Console.Error.WriteLineAsync($"Unknown error while registering subtitle: {ex.Message}"); - } - } - */ } } \ No newline at end of file diff --git a/Kyoo/Tasks/RegisterSubtitle.cs b/Kyoo/Tasks/RegisterSubtitle.cs new file mode 100644 index 00000000..524977ed --- /dev/null +++ b/Kyoo/Tasks/RegisterSubtitle.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.Models.Attributes; +using Kyoo.Models.Exceptions; + +namespace Kyoo.Tasks +{ + /// + /// A task to register a new episode + /// + public class RegisterSubtitle : ITask + { + /// + public string Slug => "register-sub"; + + /// + public string Name => "Register subtitle"; + + /// + public string Description => "Register a new subtitle"; + + /// + public string HelpMessage => null; + + /// + public bool RunOnStartup => false; + + /// + public int Priority => 0; + + /// + 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; } + + /// + public TaskParameters GetParameters() + { + return new() + { + TaskParameter.CreateRequired("path", "The path of the episode file"), + }; + } + + /// + public async Task Run(TaskParameters arguments, IProgress progress, CancellationToken cancellationToken) + { + string path = arguments["path"].As(); + + progress.Report(0); + Track track = await Identifier.IdentifyTrack(path); + progress.Report(25); + + if (track.Episode == null) + throw new IdentificationFailed($"No episode identified for the track at {path}"); + if (track.Episode.ID == 0) + { + if (track.Episode.Slug != null) + track.Episode = await LibraryManager.Get(track.Episode.Slug); + else if (track.Episode.Path != null) + { + track.Episode = await LibraryManager.GetOrDefault(x => x.Path == track.Episode.Path); + if (track.Episode == null) + throw new ItemNotFoundException($"No episode found for subtitle at: ${path}."); + } + else + throw new IdentificationFailed($"No episode identified for the track at {path}"); + } + + progress.Report(50); + await LibraryManager.Create(track); + progress.Report(100); + } + } +} \ No newline at end of file