From 8aae1c9bd64ff3ae8bf51f92f7dffbfbd22a320e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 2 Feb 2021 00:41:38 +0100 Subject: [PATCH] Adding external subtitles support --- Kyoo.Common/Models/Resources/Track.cs | 2 + Kyoo.CommonAPI/ApiHelper.cs | 6 +- .../Repositories/TrackRepository.cs | 6 +- Kyoo/Controllers/Transcoder/Transcoder.cs | 5 +- Kyoo/Controllers/Transcoder/TranscoderAPI.cs | 2 +- Kyoo/Tasks/Crawler.cs | 90 +++++++++++++++---- Kyoo/appsettings.json | 3 +- 7 files changed, 88 insertions(+), 26 deletions(-) diff --git a/Kyoo.Common/Models/Resources/Track.cs b/Kyoo.Common/Models/Resources/Track.cs index 3c97e2fd..86e4bdbb 100644 --- a/Kyoo.Common/Models/Resources/Track.cs +++ b/Kyoo.Common/Models/Resources/Track.cs @@ -82,6 +82,8 @@ namespace Kyoo.Models string name = info?.EnglishName ?? language; if (IsForced) name += " Forced"; + if (IsExternal) + name += " (External)"; if (Title != null && Title.Length > 1) name += " - " + Title; return name; diff --git a/Kyoo.CommonAPI/ApiHelper.cs b/Kyoo.CommonAPI/ApiHelper.cs index 7aaef2f8..312ac908 100644 --- a/Kyoo.CommonAPI/ApiHelper.cs +++ b/Kyoo.CommonAPI/ApiHelper.cs @@ -116,9 +116,9 @@ namespace Kyoo.CommonApi valueConst = Expression.Constant(value); } - if (notEqual) - return Expression.NotEqual(field, valueConst); - return Expression.Equal(field, valueConst); + return notEqual + ? Expression.NotEqual(field, valueConst) + : Expression.Equal(field, valueConst); } private static Expression ContainsResourceExpression(MemberExpression xProperty, string value) diff --git a/Kyoo/Controllers/Repositories/TrackRepository.cs b/Kyoo/Controllers/Repositories/TrackRepository.cs index 25bce25b..ee5366aa 100644 --- a/Kyoo/Controllers/Repositories/TrackRepository.cs +++ b/Kyoo/Controllers/Repositories/TrackRepository.cs @@ -81,7 +81,11 @@ namespace Kyoo.Controllers public override async Task Create(Track obj) { if (obj.EpisodeID <= 0) - throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); + { + obj.EpisodeID = obj.Episode?.ID ?? -1; + if (obj.EpisodeID <= 0) + throw new InvalidOperationException($"Can't store a track not related to any episode (episodeID: {obj.EpisodeID})."); + } await base.Create(obj); _database.Entry(obj).State = EntityState.Added; diff --git a/Kyoo/Controllers/Transcoder/Transcoder.cs b/Kyoo/Controllers/Transcoder/Transcoder.cs index 3fc54028..479ac32b 100644 --- a/Kyoo/Controllers/Transcoder/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder/Transcoder.cs @@ -31,10 +31,7 @@ namespace Kyoo.Controllers if (dir == null) throw new ArgumentException("Invalid path."); - return Task.Factory.StartNew(() => - { - return TranscoderAPI.ExtractInfos(path, dir); - }, TaskCreationOptions.LongRunning); + return Task.Factory.StartNew(() => TranscoderAPI.ExtractInfos(path, dir), TaskCreationOptions.LongRunning); } public async Task Transmux(Episode episode) diff --git a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs index ff7e6905..6abb5a51 100644 --- a/Kyoo/Controllers/Transcoder/TranscoderAPI.cs +++ b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs @@ -50,7 +50,7 @@ namespace Kyoo.Controllers.TranscoderLink } } else - tracks = new Track[0]; + tracks = Array.Empty(); if (ptr != IntPtr.Zero) free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers. diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 48820e67..35eb2ae1 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -33,7 +33,7 @@ namespace Kyoo.Controllers { using IServiceScope serviceScope = _serviceProvider.CreateScope(); await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - return (await libraryManager.GetLibraries()).Select(x => x.Slug); + return (await libraryManager!.GetLibraries()).Select(x => x.Slug); } public int? Progress() @@ -58,31 +58,33 @@ namespace Kyoo.Controllers using IServiceScope serviceScope = _serviceProvider.CreateScope(); await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); - foreach (Show show in await libraryManager.GetShows()) + foreach (Show show in await libraryManager!.GetShows()) if (!Directory.Exists(show.Path)) await libraryManager.DeleteShow(show); ICollection episodes = await libraryManager.GetEpisodes(); + foreach (Episode episode in episodes) + if (!File.Exists(episode.Path)) + await libraryManager.DeleteEpisode(episode); + + ICollection tracks = await libraryManager.GetTracks(); + foreach (Track track in tracks) + if (!File.Exists(track.Path)) + await libraryManager.DeleteTrack(track); + ICollection libraries = argument == null ? await libraryManager.GetLibraries() : new [] { await libraryManager.GetLibrary(argument)}; - - foreach (Episode episode in episodes) - { - if (!File.Exists(episode.Path)) - await libraryManager.DeleteEpisode(episode); - } - // TODO replace this grotesque way to load the providers. foreach (Library library in libraries) library.Providers = library.Providers; foreach (Library library in libraries) - await Scan(library, episodes, cancellationToken); + await Scan(library, episodes, tracks, cancellationToken); Console.WriteLine("Scan finished!"); } - private async Task Scan(Library library, IEnumerable episodes, CancellationToken cancellationToken) + private async Task Scan(Library library, IEnumerable episodes, IEnumerable tracks, CancellationToken cancellationToken) { Console.WriteLine($"Scanning library {library.Name} at {string.Join(", ", library.Paths)}."); foreach (string path in library.Paths) @@ -130,9 +132,54 @@ namespace Kyoo.Controllers foreach (string[] episodeTasks in tasks.BatchBy(_parallelTasks * 2)) await Task.WhenAll(episodeTasks .Select(x => RegisterFile(x, x.Substring(path.Length), library, cancellationToken))); + + await Task.WhenAll(files.Where(x => IsSubtitle(x) && tracks.All(y => y.Path != x)) + .Select(x => RegisterExternalSubtitle(x, cancellationToken))); } } + private async Task RegisterExternalSubtitle(string path, CancellationToken token) + { + if (token.IsCancellationRequested || path.Split(Path.DirectorySeparatorChar).Contains("Subtitles")) + return; + using IServiceScope serviceScope = _serviceProvider.CreateScope(); + await using 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!.GetEpisode(x => x.Path.StartsWith(episodePath)); + + if (episode == null) + { + await Console.Error.WriteLineAsync($"No episode found for subtitle at: ${path}."); + return; + } + + Track track = new(StreamType.Subtitle, + null, + match.Groups["Language"].Value, + match.Groups["Default"].Value.Length > 0, + match.Groups["Forced"].Value.Length > 0, + SubtitleExtensions[Path.GetExtension(path)], + true, + path) + { + Episode = episode + }; + + await libraryManager.RegisterTrack(track); + Console.WriteLine($"Registering subtitle at: {path}."); + } + private async Task RegisterFile(string path, string relativePath, Library library, CancellationToken token) { if (token.IsCancellationRequested) @@ -144,7 +191,7 @@ namespace Kyoo.Controllers await using ILibraryManager libraryManager = serviceScope.ServiceProvider.GetService(); string patern = _config.GetValue("regex"); - Regex regex = new Regex(patern, RegexOptions.IgnoreCase); + Regex regex = new(patern, RegexOptions.IgnoreCase); Match match = regex.Match(relativePath); if (!match.Success) @@ -154,7 +201,7 @@ namespace Kyoo.Controllers } string showPath = Path.GetDirectoryName(path); - string collectionName = match.Groups["Collection"]?.Value; + string collectionName = match.Groups["Collection"].Value; string showName = match.Groups["Show"].Value; int seasonNumber = int.TryParse(match.Groups["Season"].Value, out int tmp) ? tmp : -1; int episodeNumber = int.TryParse(match.Groups["Episode"].Value, out tmp) ? tmp : -1; @@ -164,7 +211,7 @@ namespace Kyoo.Controllers bool isMovie = seasonNumber == -1 && episodeNumber == -1 && absoluteNumber == -1; Show show = await GetShow(libraryManager, showName, showPath, isMovie, library); if (isMovie) - await libraryManager.RegisterEpisode(await GetMovie(show, path)); + await libraryManager!.RegisterEpisode(await GetMovie(show, path)); else { Season season = await GetSeason(libraryManager, show, seasonNumber, library); @@ -175,7 +222,7 @@ namespace Kyoo.Controllers absoluteNumber, path, library); - await libraryManager.RegisterEpisode(episode); + await libraryManager!.RegisterEpisode(episode); } await libraryManager.AddShowLink(show, library, collection); @@ -295,7 +342,7 @@ namespace Kyoo.Controllers private async Task GetMovie(Show show, string episodePath) { - Episode episode = new Episode + Episode episode = new() { Title = show.Title, Path = episodePath, @@ -346,5 +393,16 @@ namespace Kyoo.Controllers { 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/appsettings.json b/Kyoo/appsettings.json index 6463ba2a..7845b158 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/appsettings.json @@ -33,5 +33,6 @@ "plugins": "plugins/", "defaultPermissions": "read,play,write,admin", "newUserPermissions": "read,play,write,admin", - "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$" + "regex": "(?:\\/(?.*?))?\\/(?.*?)(?: \\(\\d+\\))?\\/\\k(?: \\(\\d+\\))?(?:(?: S(?\\d+)E(?\\d+))| (?\\d+))?.*$", + "subtitleRegex": "^(?.*)\\.(?\\w{1,3})\\.(?default\\.)?(?forced\\.)?.*$" } \ No newline at end of file