Fixing track register and directory exist

This commit is contained in:
Zoe Roux 2021-07-18 18:43:14 +02:00
parent f1887d1fab
commit 5a480402e1
15 changed files with 101 additions and 31 deletions

View File

@ -12,22 +12,32 @@ namespace Kyoo.Controllers
/// <summary> /// <summary>
/// Identify a path and return the parsed metadata. /// Identify a path and return the parsed metadata.
/// </summary> /// </summary>
/// <param name="path">The path of the episode file to parse.</param> /// <param name="path">
/// The path of the episode file to parse.
/// </param>
/// <param name="relativePath">
/// The path of the episode file relative to the library root. It starts with a <c>/</c>.
/// </param>
/// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception> /// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception>
/// <returns> /// <returns>
/// A tuple of models representing parsed metadata. /// A tuple of models representing parsed metadata.
/// If no metadata could be parsed for a type, null can be returned. /// If no metadata could be parsed for a type, null can be returned.
/// </returns> /// </returns>
Task<(Collection, Show, Season, Episode)> Identify(string path); Task<(Collection, Show, Season, Episode)> Identify(string path, string relativePath);
/// <summary> /// <summary>
/// Identify an external subtitle or track file from it's path and return the parsed metadata. /// Identify an external subtitle or track file from it's path and return the parsed metadata.
/// </summary> /// </summary>
/// <param name="path">The path of the external track file to parse.</param> /// <param name="path">
/// The path of the external track file to parse.
/// </param>
/// <param name="relativePath">
/// The path of the episode file relative to the library root. It starts with a <c>/</c>.
/// </param>
/// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception> /// <exception cref="IdentificationFailed">The identifier could not work for the given path.</exception>
/// <returns> /// <returns>
/// The metadata of the track identified. /// The metadata of the track identified.
/// </returns> /// </returns>
Task<Track> IdentifyTrack(string path); Task<Track> IdentifyTrack(string path, string relativePath);
} }
} }

View File

@ -58,7 +58,7 @@ namespace Kyoo.Models
/// By default, the http path for this poster is returned from the public API. /// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag. /// This can be disabled using the internal query flag.
/// </summary> /// </summary>
[SerializeAs("{HOST}/api/{Type}/{Slug}/poster")] public string Poster { get; set; } [SerializeAs("{HOST}/api/{Type:l}/{Slug}/poster")] public string Poster { get; set; }
/// <summary> /// <summary>
/// The type of this item (ether a collection, a show or a movie). /// The type of this item (ether a collection, a show or a movie).

View File

@ -35,8 +35,8 @@ namespace Kyoo.Models
{ {
string type = Type.ToString().ToLower(); string type = Type.ToString().ToLower();
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;
string episode = EpisodeSlug ?? Episode.Slug ?? EpisodeID.ToString(); string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString();
return $"{episode}.{Language}{index}{(IsForced ? ".forced" : "")}.{type}"; return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : "")}.{type}";
} }
[UsedImplicitly] private set [UsedImplicitly] private set
{ {
@ -47,11 +47,13 @@ namespace Kyoo.Models
if (!match.Success) if (!match.Success)
throw new ArgumentException("Invalid track slug. " + throw new ArgumentException("Invalid track slug. " +
"Format: {episodeSlug}.{language}[-{index}][-forced].{type}[.{extension}]"); "Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]");
EpisodeSlug = match.Groups["ep"].Value; EpisodeSlug = match.Groups["ep"].Value;
Language = match.Groups["lang"].Value; Language = match.Groups["lang"].Value;
TrackIndex = int.Parse(match.Groups["index"].Value); if (Language == "und")
Language = null;
TrackIndex = match.Groups["index"].Success ? int.Parse(match.Groups["index"].Value) : 0;
IsForced = match.Groups["forced"].Success; IsForced = match.Groups["forced"].Success;
Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true); Type = Enum.Parse<StreamType>(match.Groups["type"].Value, true);
} }

View File

@ -115,9 +115,10 @@ namespace Kyoo.Controllers
public object GetValue(object target) public object GetValue(object target)
{ {
return Regex.Replace(_format, @"(?<!{){(\w+)}", x => return Regex.Replace(_format, @"(?<!{){(\w+)(:(\w+))?}", x =>
{ {
string value = x.Groups[1].Value; string value = x.Groups[1].Value;
string modifier = x.Groups[3].Value;
if (value == "HOST") if (value == "HOST")
return _host; return _host;
@ -127,9 +128,22 @@ namespace Kyoo.Controllers
.FirstOrDefault(y => y.Name == value); .FirstOrDefault(y => y.Name == value);
if (properties == null) if (properties == null)
return null; return null;
if (properties.GetValue(target) is string ret) object objValue = properties.GetValue(target);
return ret; if (objValue is not string ret)
throw new ArgumentException($"Invalid serializer replacement {value}"); ret = objValue?.ToString();
if (ret == null)
throw new ArgumentException($"Invalid serializer replacement {value}");
foreach (char modification in modifier)
{
ret = modification switch
{
'l' => ret.ToLowerInvariant(),
'u' => ret.ToUpperInvariant(),
_ => throw new ArgumentException($"Invalid serializer modificator {modification}.")
};
}
return ret;
}); });
} }

View File

@ -91,7 +91,7 @@ namespace Kyoo.Postgresql.Migrations
END, END,
CASE (is_forced) CASE (is_forced)
WHEN false THEN '' WHEN false THEN ''
ELSE '-forced' ELSE '.forced'
END, END,
'.', type '.', type
) WHERE episode_id = NEW.id; ) WHERE episode_id = NEW.id;
@ -117,14 +117,14 @@ namespace Kyoo.Postgresql.Migrations
END IF; END IF;
NEW.slug := CONCAT( NEW.slug := CONCAT(
(SELECT slug FROM episodes WHERE id = NEW.episode_id), (SELECT slug FROM episodes WHERE id = NEW.episode_id),
'.', NEW.language, '.', COALESCE(NEW.language, 'und'),
CASE (NEW.track_index) CASE (NEW.track_index)
WHEN 0 THEN '' WHEN 0 THEN ''
ELSE CONCAT('-', NEW.track_index) ELSE CONCAT('-', NEW.track_index)
END, END,
CASE (NEW.is_forced) CASE (NEW.is_forced)
WHEN false THEN '' WHEN false THEN ''
ELSE '-forced' ELSE '.forced'
END, END,
'.', NEW.type '.', NEW.type
); );

View File

@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Npgsql;
namespace Kyoo.Postgresql namespace Kyoo.Postgresql
{ {
@ -73,6 +74,10 @@ namespace Kyoo.Postgresql
{ {
DatabaseContext context = provider.GetRequiredService<DatabaseContext>(); DatabaseContext context = provider.GetRequiredService<DatabaseContext>();
context.Database.Migrate(); context.Database.Migrate();
using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection();
conn.Open();
conn.ReloadTypes();
} }
} }
} }

View File

@ -61,14 +61,14 @@ namespace Kyoo.SqLite.Migrations
AND Language = new.Language AND IsForced = new.IsForced AND Language = new.Language AND IsForced = new.IsForced
) WHERE ID = new.ID AND TrackIndex = 0; ) WHERE ID = new.ID AND TrackIndex = 0;
UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) || UPDATE Tracks SET Slug = (SELECT Slug FROM Episodes WHERE ID = EpisodeID) ||
'.' || Language || '.' || COALESCE(Language, 'und') ||
CASE (TrackIndex) CASE (TrackIndex)
WHEN 0 THEN '' WHEN 0 THEN ''
ELSE '-' || (TrackIndex) ELSE '-' || (TrackIndex)
END || END ||
CASE (IsForced) CASE (IsForced)
WHEN false THEN '' WHEN false THEN ''
ELSE '-forced' ELSE '.forced'
END || END ||
CASE (Type) CASE (Type)
WHEN 1 THEN '.video' WHEN 1 THEN '.video'
@ -98,7 +98,7 @@ namespace Kyoo.SqLite.Migrations
END || END ||
CASE (IsForced) CASE (IsForced)
WHEN false THEN '' WHEN false THEN ''
ELSE '-forced' ELSE '.forced'
END || END ||
CASE (Type) CASE (Type)
WHEN 1 THEN '.video' WHEN 1 THEN '.video'
@ -123,7 +123,7 @@ namespace Kyoo.SqLite.Migrations
END || END ||
CASE (IsForced) CASE (IsForced)
WHEN false THEN '' WHEN false THEN ''
ELSE '-forced' ELSE '.forced'
END || END ||
CASE (Type) CASE (Type)
WHEN 1 THEN '.video' WHEN 1 THEN '.video'

View File

@ -47,5 +47,20 @@ namespace Kyoo.Tests.Library
Track track = await _repository.Get(1); Track track = await _repository.Get(1);
Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug); Assert.Equal("new-slug-s1e1.eng-1.subtitle", track.Slug);
} }
[Fact]
public async Task UndefinedLanguageSlugTest()
{
await _repository.Create(new Track
{
ID = 5,
TrackIndex = 0,
Type = StreamType.Video,
Language = null,
EpisodeID = TestSample.Get<Episode>().ID
});
Track track = await _repository.Get(5);
Assert.Equal("anohana-s1e1.und.video", track.Slug);
}
} }
} }

View File

@ -98,7 +98,7 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
public Task<bool> Exists(string path) public Task<bool> Exists(string path)
{ {
return Task.FromResult(File.Exists(path)); return Task.FromResult(File.Exists(path) || Directory.Exists(path));
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -30,10 +30,10 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<(Collection, Show, Season, Episode)> Identify(string path) public Task<(Collection, Show, Season, Episode)> Identify(string path, string relativePath)
{ {
Regex regex = new(_configuration.Value.Regex, RegexOptions.IgnoreCase | RegexOptions.Compiled); Regex regex = new(_configuration.Value.Regex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match = regex.Match(path); Match match = regex.Match(relativePath);
if (!match.Success) if (!match.Success)
throw new IdentificationFailed($"The episode at {path} does not match the episode's regex."); throw new IdentificationFailed($"The episode at {path} does not match the episode's regex.");
@ -84,10 +84,10 @@ namespace Kyoo.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<Track> IdentifyTrack(string path) public Task<Track> IdentifyTrack(string path, string relativePath)
{ {
Regex regex = new(_configuration.Value.SubtitleRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled); Regex regex = new(_configuration.Value.SubtitleRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
Match match = regex.Match(path); Match match = regex.Match(relativePath);
if (!match.Success) if (!match.Success)
throw new IdentificationFailed($"The subtitle at {path} does not match the subtitle's regex."); throw new IdentificationFailed($"The subtitle at {path} does not match the subtitle's regex.");

View File

@ -96,7 +96,12 @@ namespace Kyoo.Controllers
protected override async Task Validate(Season resource) protected override async Task Validate(Season resource)
{ {
if (resource.ShowID <= 0) if (resource.ShowID <= 0)
throw new InvalidOperationException($"Can't store a season not related to any show (showID: {resource.ShowID})."); {
if (resource.Show == null)
throw new InvalidOperationException(
$"Can't store a season not related to any show (showID: {resource.ShowID}).");
resource.ShowID = resource.Show.ID;
}
await base.Validate(resource); await base.Validate(resource);
await resource.ExternalIDs.ForEachAsync(async id => await resource.ExternalIDs.ForEachAsync(async id =>

View File

@ -140,7 +140,8 @@ namespace Kyoo.Tasks
{ {
TaskManager.StartTask<RegisterEpisode>(reporter, new Dictionary<string, object> TaskManager.StartTask<RegisterEpisode>(reporter, new Dictionary<string, object>
{ {
["path"] = episodePath[path.Length..], ["path"] = episodePath,
["relativePath"] = episodePath[path.Length..],
["library"] = library ["library"] = library
}, cancellationToken); }, cancellationToken);
percent += 100f / paths.Length; percent += 100f / paths.Length;
@ -162,7 +163,8 @@ namespace Kyoo.Tasks
{ {
TaskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object> TaskManager.StartTask<RegisterSubtitle>(reporter, new Dictionary<string, object>
{ {
["path"] = trackPath ["path"] = trackPath,
["relativePath"] = trackPath[path.Length..]
}, cancellationToken); }, cancellationToken);
percent += 100f / subtitles.Length; percent += 100f / subtitles.Length;
} }

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Microsoft.Extensions.Logging;
namespace Kyoo.Tasks namespace Kyoo.Tasks
{ {
@ -39,6 +40,10 @@ namespace Kyoo.Tasks
/// The file manager used walk inside directories and check they existences. /// The file manager used walk inside directories and check they existences.
/// </summary> /// </summary>
[Injected] public IFileManager FileManager { private get; set; } [Injected] public IFileManager FileManager { private get; set; }
/// <summary>
/// The logger used to inform the user that episodes has been removed.
/// </summary>
[Injected] public ILogger<Housekeeping> Logger { private get; set; }
/// <inheritdoc /> /// <inheritdoc />
@ -55,6 +60,8 @@ namespace Kyoo.Tasks
if (await FileManager.Exists(show.Path)) if (await FileManager.Exists(show.Path))
continue; continue;
Logger.LogWarning("Show {Name}'s folder has been deleted (was {Path}), removing it from kyoo",
show.Title, show.Path);
await LibraryManager.Delete(show); await LibraryManager.Delete(show);
} }
@ -65,6 +72,8 @@ namespace Kyoo.Tasks
if (await FileManager.Exists(episode.Path)) if (await FileManager.Exists(episode.Path))
continue; continue;
Logger.LogWarning("Episode {Slug}'s file has been deleted (was {Path}), removing it from kyoo",
episode.Slug, episode.Path);
await LibraryManager.Delete(episode); await LibraryManager.Delete(episode);
} }

View File

@ -62,6 +62,8 @@ namespace Kyoo.Tasks
return new() return new()
{ {
TaskParameter.CreateRequired<string>("path", "The path of the episode file"), TaskParameter.CreateRequired<string>("path", "The path of the episode file"),
TaskParameter.CreateRequired<string>("relativePath",
"The path of the episode file relative to the library root. It starts with a /."),
TaskParameter.CreateRequired<Library>("library", "The library in witch the episode is") TaskParameter.CreateRequired<Library>("library", "The library in witch the episode is")
}; };
} }
@ -70,13 +72,15 @@ namespace Kyoo.Tasks
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken) public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{ {
string path = arguments["path"].As<string>(); string path = arguments["path"].As<string>();
string relativePath = arguments["relativePath"].As<string>();
Library library = arguments["library"].As<Library>(); Library library = arguments["library"].As<Library>();
progress.Report(0); progress.Report(0);
if (library.Providers == null) if (library.Providers == null)
await LibraryManager.Load(library, x => x.Providers); await LibraryManager.Load(library, x => x.Providers);
MetadataProvider.UseProviders(library.Providers); MetadataProvider.UseProviders(library.Providers);
(Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path); (Collection collection, Show show, Season season, Episode episode) = await Identifier.Identify(path,
relativePath);
progress.Report(15); progress.Report(15);
collection = await _RegisterAndFill(collection); collection = await _RegisterAndFill(collection);
@ -105,6 +109,7 @@ namespace Kyoo.Tasks
if (season != null) if (season != null)
season.Show = show; season.Show = show;
season = await _RegisterAndFill(season); season = await _RegisterAndFill(season);
progress.Report(60); progress.Report(60);

View File

@ -48,7 +48,9 @@ namespace Kyoo.Tasks
{ {
return new() return new()
{ {
TaskParameter.CreateRequired<string>("path", "The path of the episode file"), TaskParameter.CreateRequired<string>("path", "The path of the subtitle file"),
TaskParameter.CreateRequired<string>("relativePath",
"The path of the subtitle file relative to the library root. It starts with a /.")
}; };
} }
@ -56,9 +58,10 @@ namespace Kyoo.Tasks
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken) public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
{ {
string path = arguments["path"].As<string>(); string path = arguments["path"].As<string>();
string relativePath = arguments["relativePath"].As<string>();
progress.Report(0); progress.Report(0);
Track track = await Identifier.IdentifyTrack(path); Track track = await Identifier.IdentifyTrack(path, relativePath);
progress.Report(25); progress.Report(25);
if (track.Episode == null) if (track.Episode == null)