diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index ee141bde..36489881 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -61,7 +61,9 @@ namespace Kyoo.Controllers long EditShow(Show show); long RegisterMovie(Episode movie); long RegisterSeason(Season season); + long EditSeason(Season season); long RegisterEpisode(Episode episode); + long EditEpisode(Episode episode); long RegisterTrack(Track track); void RegisterShowLinks(Library library, Collection collection, Show show); IEnumerable ValidateExternalIDs(IEnumerable ids); diff --git a/Kyoo.Common/Controllers/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs index d1423f77..9dad54ed 100644 --- a/Kyoo.Common/Controllers/IThumbnailsManager.cs +++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs @@ -7,7 +7,8 @@ namespace Kyoo.Controllers public interface IThumbnailsManager { Task Validate(Show show, bool alwaysDownload = false); - Task> Validate(IEnumerable actors, bool alwaysDownload = false); + Task Validate(Season season, bool alwaysDownload = false); Task Validate(Episode episode, bool alwaysDownload = false); + Task> Validate(IEnumerable actors, bool alwaysDownload = false); } } diff --git a/Kyoo.Common/Models/Season.cs b/Kyoo.Common/Models/Season.cs index 11b72849..4db7f166 100644 --- a/Kyoo.Common/Models/Season.cs +++ b/Kyoo.Common/Models/Season.cs @@ -9,6 +9,8 @@ namespace Kyoo.Models [JsonIgnore] public long ShowID { get; set; } public long SeasonNumber { get; set; } = -1; + + public string Slug => $"{Show.Title}-s{SeasonNumber}"; public string Title { get; set; } public string Overview { get; set; } public long? Year { get; set; } diff --git a/Kyoo.Common/Models/Track.cs b/Kyoo.Common/Models/Track.cs index ea7b5ce7..045bedd4 100644 --- a/Kyoo.Common/Models/Track.cs +++ b/Kyoo.Common/Models/Track.cs @@ -1,6 +1,5 @@ using Kyoo.Models.Watch; using Newtonsoft.Json; -using System; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; @@ -68,8 +67,47 @@ namespace Kyoo.Models get => isForced; set => isForced = value; } - public string DisplayName; - public string Link; + + public string DisplayName + { + get + { + string language = GetLanguage(Language); + + if (language == null) + return $"Unknown Language (id: {ID.ToString()})"; + CultureInfo info = CultureInfo.GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(x => x.ThreeLetterISOLanguageName == language); + string name = info?.EnglishName ?? language; + if (IsForced) + name += " Forced"; + if (Title != null && Title.Length > 1) + name += " - " + Title; + return name; + } + } + + public string Link + { + get + { + if (Type != StreamType.Subtitle) + return null; + string link = "/subtitle/" + Episode.Link + "." + Language; + if (IsForced) + link += "-forced"; + switch (Codec) + { + case "ass": + link += ".ass"; + break; + case "subrip": + link += ".srt"; + break; + } + return link; + } + } [JsonIgnore] public bool IsExternal { get; set; } [JsonIgnore] public virtual Episode Episode { get; set; } @@ -88,46 +126,12 @@ namespace Kyoo.Models IsExternal = false; } - public Track SetLink(string episodeSlug) + //Converting mkv track language to c# system language tag. + public static string GetLanguage(string mkvLanguage) { - if (Type == StreamType.Subtitle) - { - string language = Language; - //Converting mkv track language to c# system language tag. - if (language == "fre") - language = "fra"; - - if (language == null) - { - Language = ID.ToString(); - DisplayName = $"Unknown Language (id: {ID.ToString()})"; - } - else - DisplayName = CultureInfo.GetCultures(CultureTypes.NeutralCultures).FirstOrDefault(x => x.ThreeLetterISOLanguageName == language)?.EnglishName ?? language; - Link = "/subtitle/" + episodeSlug + "." + Language; - - if (IsForced) - { - DisplayName += " Forced"; - Link += "-forced"; - } - - if (Title != null && Title.Length > 1) - DisplayName += " - " + Title; - - switch (Codec) - { - case "ass": - Link += ".ass"; - break; - case "subrip": - Link += ".srt"; - break; - } - } - else - Link = null; - return this; + if (mkvLanguage == "fre") + return "fra"; + return mkvLanguage; } } } \ No newline at end of file diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs index bb21aef7..5bc23647 100644 --- a/Kyoo.Common/Utility.cs +++ b/Kyoo.Common/Utility.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using Kyoo.Models; @@ -14,31 +16,28 @@ namespace Kyoo public static class Utility { - public static string ToSlug(string name) + public static string ToSlug(string str) { - if (name == null) + if (str == null) return null; - //First to lower case - name = name.ToLowerInvariant(); + str = str.ToLowerInvariant(); + + string normalizedString = str.Normalize(NormalizationForm.FormD); + StringBuilder stringBuilder = new StringBuilder(); + foreach (char c in normalizedString) + { + UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + stringBuilder.Append(c); + } + str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); - //Remove all accents - //var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(showTitle); - //showTitle = Encoding.ASCII.GetString(bytes); - - //Replace spaces - name = Regex.Replace(name, @"\s", "-", RegexOptions.Compiled); - - //Remove invalid chars - name = Regex.Replace(name, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); - - //Trim dashes from end - name = name.Trim('-', '_'); - - //Replace double occurences of - or \_ - name = Regex.Replace(name, @"([-_]){2,}", "$1", RegexOptions.Compiled); - - return name; + str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); + str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); + str = str.Trim('-', '_'); + str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); + return str; } diff --git a/Kyoo/Controllers/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs index 162aa2f4..20bdc451 100644 --- a/Kyoo/Controllers/LibraryManager.cs +++ b/Kyoo/Controllers/LibraryManager.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; namespace Kyoo.Controllers { @@ -39,9 +38,9 @@ namespace Kyoo.Controllers public (Track video, IEnumerable audios, IEnumerable subtitles) GetStreams(long episodeID, string episodeSlug) { IEnumerable tracks = _database.Tracks.Where(track => track.EpisodeID == episodeID); - return ((from track in tracks where track.Type == StreamType.Video select track.SetLink(episodeSlug)).FirstOrDefault(), - from track in tracks where track.Type == StreamType.Audio select track.SetLink(episodeSlug), - from track in tracks where track.Type == StreamType.Subtitle select track.SetLink(episodeSlug)); + return (tracks.FirstOrDefault(x => x.Type == StreamType.Video), + tracks.Where(x => x.Type == StreamType.Audio), + tracks.Where(track => track.Type == StreamType.Subtitle)); } public Track GetSubtitle(string showSlug, long seasonNumber, long episodeNumber, string languageTag, bool forced) @@ -478,6 +477,48 @@ namespace Kyoo.Controllers _database.SaveChanges(); return season.ID; } + + public long EditSeason(Season edited) + { + if (edited == null) + throw new ArgumentNullException(nameof(edited)); + + _database.ChangeTracker.LazyLoadingEnabled = false; + _database.ChangeTracker.AutoDetectChangesEnabled = false; + + try + { + var query = _database.Seasons + .Include(x => x.ExternalIDs) + .Include(x => x.Episodes); + Season season = _database.Entry(edited).IsKeySet + ? query.FirstOrDefault(x => x.ID == edited.ID) + : query.FirstOrDefault(x => x.Slug == edited.Slug); + + if (season == null) + throw new ItemNotFound($"No season could be found with the id {edited.ID} or the slug {edited.Slug}"); + + Utility.Complete(season, edited); + + season.Episodes = edited.Episodes?.Select(x => + { + return _database.Episodes.FirstOrDefault(y => y.ShowID == x.ShowID + && y.SeasonNumber == x.SeasonNumber + && y.EpisodeNumber == x.EpisodeNumber) ?? x; + }).ToList(); + season.ExternalIDs = ValidateExternalIDs(season.ExternalIDs); + + _database.ChangeTracker.DetectChanges(); + _database.SaveChanges(); + } + finally + { + _database.ChangeTracker.LazyLoadingEnabled = true; + _database.ChangeTracker.AutoDetectChangesEnabled = true; + } + + return edited.ID; + } public long RegisterEpisode(Episode episode) { @@ -488,6 +529,45 @@ namespace Kyoo.Controllers _database.SaveChanges(); return episode.ID; } + + public long EditEpisode(Episode edited) + { + if (edited == null) + throw new ArgumentNullException(nameof(edited)); + + _database.ChangeTracker.LazyLoadingEnabled = false; + _database.ChangeTracker.AutoDetectChangesEnabled = false; + + try + { + var query = _database.Episodes + .Include(x => x.Tracks) + .Include(x => x.Season) + .Include(x => x.ExternalIDs); + Episode episode = query.FirstOrDefault(x => x.ID == edited.ID); + + if (episode == null) + throw new ItemNotFound($"No episode could be found with the id {edited.ID}"); + + Utility.Complete(episode, edited); + + episode.Season = _database.Seasons + .FirstOrDefault(x => x.ShowID == episode.ShowID + && x.SeasonNumber == edited.SeasonNumber) ?? episode.Season; + episode.Season.ExternalIDs = ValidateExternalIDs(episode.Season.ExternalIDs); + episode.ExternalIDs = ValidateExternalIDs(episode.ExternalIDs); + + _database.ChangeTracker.DetectChanges(); + _database.SaveChanges(); + } + finally + { + _database.ChangeTracker.LazyLoadingEnabled = true; + _database.ChangeTracker.AutoDetectChangesEnabled = true; + } + + return edited.ID; + } public long RegisterTrack(Track track) { diff --git a/Kyoo/Controllers/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs index 681dd00d..c43fef04 100644 --- a/Kyoo/Controllers/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -77,6 +77,20 @@ namespace Kyoo.Controllers return people; } + public async Task Validate(Season season, bool alwaysDownload) + { + if (season?.Show?.Path == null) + return default; + + if (season.ImgPrimary == null) + { + string localPath = Path.Combine(season.Show.Path, $"season-{season.SeasonNumber}.jpg"); + if (alwaysDownload || !File.Exists(localPath)) + await DownloadImage(season.ImgPrimary, localPath, $"The poster of {season.Show.Title}'s season {season.SeasonNumber}"); + } + return season; + } + public async Task Validate(Episode episode, bool alwaysDownload) { if (episode?.Path == null) diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index c30f19b1..dde61eaa 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -162,6 +162,7 @@ namespace Kyoo.Controllers { season = await _metadataProvider.GetSeason(show, seasonNumber, library); season.ExternalIDs = _libraryManager.ValidateExternalIDs(season.ExternalIDs); + await _thumbnailsManager.Validate(season); } season.Show = show; return season; diff --git a/Kyoo/Tasks/ReScan.cs b/Kyoo/Tasks/ReScan.cs new file mode 100644 index 00000000..8445d78c --- /dev/null +++ b/Kyoo/Tasks/ReScan.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Tasks +{ + public class ReScan: ITask + { + public string Slug => "re-scan"; + public string Name => "ReScan"; + public string Description => "Re download metadata of an item using it's external ids."; + public string HelpMessage => null; + public bool RunOnStartup => false; + public int Priority => 0; + + + private ILibraryManager _libraryManager; + private IThumbnailsManager _thumbnailsManager; + private IProviderManager _providerManager; + private DatabaseContext _database; + + public Task Run(IServiceProvider serviceProvider, CancellationToken cancellationToken, string arguments = null) + { + using IServiceScope serviceScope = serviceProvider.CreateScope(); + _libraryManager = serviceScope.ServiceProvider.GetService(); + _thumbnailsManager = serviceScope.ServiceProvider.GetService(); + _providerManager = serviceScope.ServiceProvider.GetService(); + _database = serviceScope.ServiceProvider.GetService(); + + if (arguments == null || !arguments.Contains('/')) + return Task.CompletedTask; + + string slug = arguments.Substring(arguments.IndexOf('/') + 1); + return arguments.Substring(0, arguments.IndexOf('/')) switch + { + "show" => ReScanShow(slug), + "season" => ReScanSeason(slug), + _ => Task.CompletedTask + }; + } + + private async Task ReScanShow(string slug) + { + Show old = _database.Shows.AsNoTracking().FirstOrDefault(x => x.Slug == slug); + if (old == null) + return; + Library library = _libraryManager.GetLibraryForShow(slug); + Show edited = await _providerManager.CompleteShow(old, library); + edited.ID = old.ID; + edited.Slug = old.Slug; + edited.Path = old.Path; + _libraryManager.EditShow(edited); + await _thumbnailsManager.Validate(edited, true); + await Task.WhenAll(edited.Seasons.Select(x => ReScanSeason(edited, x))); + } + + private async Task ReScanSeason(Show show, Season old) + { + Library library = _libraryManager.GetLibraryForShow(show.Slug); + Season edited = await _providerManager.GetSeason(show, old.SeasonNumber, library); + edited.ID = old.ID; + _libraryManager.EditSeason(edited); + await _thumbnailsManager.Validate(edited, true); + await Task.WhenAll(edited.Episodes.Select(x => ReScanEpisode(show, x))); + } + + private async Task ReScanSeason(string slug) + { + + } + + private async Task ReScanEpisode(Show show, Episode old) + { + Library library = _libraryManager.GetLibraryForShow(show.Slug); + Episode edited = await _providerManager.GetEpisode(show, old.Path, old.SeasonNumber, old.EpisodeNumber, old.AbsoluteNumber, library); + edited.ID = old.ID; + _libraryManager.EditEpisode(edited); + await _thumbnailsManager.Validate(edited, true); + } + + public IEnumerable GetPossibleParameters() + { + return default; + } + + public int? Progress() + { + return null; + } + } +} \ No newline at end of file diff --git a/Kyoo/Views/API/ShowsAPI.cs b/Kyoo/Views/API/ShowsAPI.cs index a1da7543..13c48198 100644 --- a/Kyoo/Views/API/ShowsAPI.cs +++ b/Kyoo/Views/API/ShowsAPI.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; -using Kyoo.Models.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; @@ -19,13 +18,19 @@ namespace Kyoo.Api private readonly IProviderManager _providerManager; private readonly DatabaseContext _database; private readonly IThumbnailsManager _thumbnailsManager; + private readonly ITaskManager _taskManager; - public ShowsAPI(ILibraryManager libraryManager, IProviderManager providerManager, DatabaseContext database, IThumbnailsManager thumbnailsManager) + public ShowsAPI(ILibraryManager libraryManager, + IProviderManager providerManager, + DatabaseContext database, + IThumbnailsManager thumbnailsManager, + ITaskManager taskManager) { _libraryManager = libraryManager; _providerManager = providerManager; _database = database; _thumbnailsManager = thumbnailsManager; + _taskManager = taskManager; } [HttpGet] @@ -67,19 +72,16 @@ namespace Kyoo.Api [HttpPost("re-identify/{slug}")] [Authorize(Policy = "Write")] - public async Task ReIdentityShow(string slug, [FromBody] Show show) + public IActionResult ReIdentityShow(string slug, [FromBody] IEnumerable externalIDs) { if (!ModelState.IsValid) - return BadRequest(show); - Show old = _database.Shows.FirstOrDefault(x => x.Slug == slug); - if (old == null) + return BadRequest(externalIDs); + Show show = _database.Shows.FirstOrDefault(x => x.Slug == slug); + if (show == null) return NotFound(); - Show edited = await _providerManager.CompleteShow(show, _libraryManager.GetLibraryForShow(slug)); - edited.ID = old.ID; - edited.Slug = old.Slug; - edited.Path = old.Path; - _libraryManager.EditShow(edited); - await _thumbnailsManager.Validate(edited, true); + show.ExternalIDs = externalIDs; + _libraryManager.EditShow(show); + _taskManager.StartTask("re-scan-show", $"show/{slug}"); return Ok(); } diff --git a/Kyoo/Views/API/TaskAPI.cs b/Kyoo/Views/API/TaskAPI.cs index 887cd567..24ab8845 100644 --- a/Kyoo/Views/API/TaskAPI.cs +++ b/Kyoo/Views/API/TaskAPI.cs @@ -16,7 +16,7 @@ namespace Kyoo.Api } - [HttpGet("{taskSlug}/{args?}")] + [HttpGet("{taskSlug}/{*args}")] [Authorize(Policy="Admin")] public IActionResult RunTask(string taskSlug, string args = null) { diff --git a/Kyoo/Views/WebClient b/Kyoo/Views/WebClient index e4cd29a4..4135c957 160000 --- a/Kyoo/Views/WebClient +++ b/Kyoo/Views/WebClient @@ -1 +1 @@ -Subproject commit e4cd29a489d5f20de42bb89f7db2c8299b7c5171 +Subproject commit 4135c957573c13b4655fa858746ac0e7de97b3ea