Adding a task to rescan a show and it's related properties

This commit is contained in:
Zoe Roux 2020-05-05 03:23:39 +02:00
parent 562c39b970
commit 40ea4a37af
12 changed files with 285 additions and 82 deletions

View File

@ -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<MetadataID> ValidateExternalIDs(IEnumerable<MetadataID> ids);

View File

@ -7,7 +7,8 @@ namespace Kyoo.Controllers
public interface IThumbnailsManager
{
Task<Show> Validate(Show show, bool alwaysDownload = false);
Task<IEnumerable<PeopleLink>> Validate(IEnumerable<PeopleLink> actors, bool alwaysDownload = false);
Task<Season> Validate(Season season, bool alwaysDownload = false);
Task<Episode> Validate(Episode episode, bool alwaysDownload = false);
Task<IEnumerable<PeopleLink>> Validate(IEnumerable<PeopleLink> actors, bool alwaysDownload = false);
}
}

View File

@ -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; }

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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<Track> audios, IEnumerable<Track> subtitles) GetStreams(long episodeID, string episodeSlug)
{
IEnumerable<Track> 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)
{

View File

@ -77,6 +77,20 @@ namespace Kyoo.Controllers
return people;
}
public async Task<Season> 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<Episode> Validate(Episode episode, bool alwaysDownload)
{
if (episode?.Path == null)

View File

@ -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;

98
Kyoo/Tasks/ReScan.cs Normal file
View File

@ -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<ILibraryManager>();
_thumbnailsManager = serviceScope.ServiceProvider.GetService<IThumbnailsManager>();
_providerManager = serviceScope.ServiceProvider.GetService<IProviderManager>();
_database = serviceScope.ServiceProvider.GetService<DatabaseContext>();
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<string> GetPossibleParameters()
{
return default;
}
public int? Progress()
{
return null;
}
}
}

View File

@ -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<IActionResult> ReIdentityShow(string slug, [FromBody] Show show)
public IActionResult ReIdentityShow(string slug, [FromBody] IEnumerable<MetadataID> 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();
}

View File

@ -16,7 +16,7 @@ namespace Kyoo.Api
}
[HttpGet("{taskSlug}/{args?}")]
[HttpGet("{taskSlug}/{*args}")]
[Authorize(Policy="Admin")]
public IActionResult RunTask(string taskSlug, string args = null)
{

@ -1 +1 @@
Subproject commit e4cd29a489d5f20de42bb89f7db2c8299b7c5171
Subproject commit 4135c957573c13b4655fa858746ac0e7de97b3ea