mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 13:44:33 -04:00
Cleaning up and adding tests for the regex identifier
This commit is contained in:
parent
48e81dfd92
commit
e55f60166e
@ -12,32 +12,26 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// Identify a path and return the parsed metadata.
|
||||
/// </summary>
|
||||
/// <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="IdentificationFailedException">The identifier could not work for the given path.</exception>
|
||||
/// <param name="path">The path of the episode file to parse.</param>
|
||||
/// <exception cref="IdentificationFailedException">
|
||||
/// The identifier could not work for the given path.
|
||||
/// </exception>
|
||||
/// <returns>
|
||||
/// A tuple of models representing parsed metadata.
|
||||
/// If no metadata could be parsed for a type, null can be returned.
|
||||
/// </returns>
|
||||
Task<(Collection, Show, Season, Episode)> Identify(string path, string relativePath);
|
||||
Task<(Collection, Show, Season, Episode)> Identify(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Identify an external subtitle or track file from it's path and return the parsed metadata.
|
||||
/// </summary>
|
||||
/// <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="IdentificationFailedException">The identifier could not work for the given path.</exception>
|
||||
/// <param name="path">The path of the external track file to parse.</param>
|
||||
/// <exception cref="IdentificationFailedException">
|
||||
/// The identifier could not work for the given path.
|
||||
/// </exception>
|
||||
/// <returns>
|
||||
/// The metadata of the track identified.
|
||||
/// </returns>
|
||||
Task<Track> IdentifyTrack(string path, string relativePath);
|
||||
Task<Track> IdentifyTrack(string path);
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Attributes;
|
||||
using Kyoo.Models.Exceptions;
|
||||
|
||||
@ -116,6 +117,15 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
if (typeof(T) == typeof(object))
|
||||
return (T)Value;
|
||||
|
||||
if (Value is IResource resource)
|
||||
{
|
||||
if (typeof(T) == typeof(string))
|
||||
return (T)(object)resource.Slug;
|
||||
if (typeof(T) == typeof(int))
|
||||
return (T)(object)resource.ID;
|
||||
}
|
||||
|
||||
return (T)Convert.ChangeType(Value, typeof(T));
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ namespace Kyoo.Tests
|
||||
public void Dispose()
|
||||
{
|
||||
Repositories.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
|
135
Kyoo.Tests/Identifier/IdentifierTests.cs
Normal file
135
Kyoo.Tests/Identifier/IdentifierTests.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests.Identifier
|
||||
{
|
||||
public class Identifier
|
||||
{
|
||||
private readonly Mock<ILibraryManager> _manager;
|
||||
private readonly IIdentifier _identifier;
|
||||
|
||||
public Identifier()
|
||||
{
|
||||
Mock<IOptionsMonitor<MediaOptions>> options = new();
|
||||
options.Setup(x => x.CurrentValue).Returns(new MediaOptions
|
||||
{
|
||||
Regex = new []
|
||||
{
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? S(?<Season>\\d+)E(?<Episode>\\d+)\\..*$",
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? (?<Absolute>\\d+)\\..*$",
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))?\\..*$"
|
||||
}
|
||||
});
|
||||
|
||||
_manager = new Mock<ILibraryManager>();
|
||||
_identifier = new RegexIdentifier(options.Object, _manager.Object);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeIdentification()
|
||||
{
|
||||
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), default)).ReturnsAsync(new[]
|
||||
{
|
||||
new Library {Paths = new [] {"/kyoo/Library/"}}
|
||||
});
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
|
||||
"/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
|
||||
Assert.Equal("Collection", collection.Name);
|
||||
Assert.Equal("collection", collection.Slug);
|
||||
Assert.Equal("Show", show.Title);
|
||||
Assert.Equal("show", show.Slug);
|
||||
Assert.Equal(2000, show.StartAir!.Value.Year);
|
||||
Assert.Equal(1, season.SeasonNumber);
|
||||
Assert.Equal(1, episode.SeasonNumber);
|
||||
Assert.Equal(1, episode.EpisodeNumber);
|
||||
Assert.Null(episode.AbsoluteNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeIdentificationWithoutLibraryTrailingSlash()
|
||||
{
|
||||
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), default)).ReturnsAsync(new[]
|
||||
{
|
||||
new Library {Paths = new [] {"/kyoo/Library"}}
|
||||
});
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
|
||||
"/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
|
||||
Assert.Equal("Collection", collection.Name);
|
||||
Assert.Equal("collection", collection.Slug);
|
||||
Assert.Equal("Show", show.Title);
|
||||
Assert.Equal("show", show.Slug);
|
||||
Assert.Equal(2000, show.StartAir!.Value.Year);
|
||||
Assert.Equal(1, season.SeasonNumber);
|
||||
Assert.Equal(1, episode.SeasonNumber);
|
||||
Assert.Equal(1, episode.EpisodeNumber);
|
||||
Assert.Null(episode.AbsoluteNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpisodeIdentificationMultiplePaths()
|
||||
{
|
||||
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), default)).ReturnsAsync(new[]
|
||||
{
|
||||
new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
|
||||
});
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
|
||||
"/kyoo/Library/Collection/Show (2000)/Show S01E01.extension");
|
||||
Assert.Equal("Collection", collection.Name);
|
||||
Assert.Equal("collection", collection.Slug);
|
||||
Assert.Equal("Show", show.Title);
|
||||
Assert.Equal("show", show.Slug);
|
||||
Assert.Equal(2000, show.StartAir!.Value.Year);
|
||||
Assert.Equal(1, season.SeasonNumber);
|
||||
Assert.Equal(1, episode.SeasonNumber);
|
||||
Assert.Equal(1, episode.EpisodeNumber);
|
||||
Assert.Null(episode.AbsoluteNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbsoluteEpisodeIdentification()
|
||||
{
|
||||
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), default)).ReturnsAsync(new[]
|
||||
{
|
||||
new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
|
||||
});
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
|
||||
"/kyoo/Library/Collection/Show (2000)/Show 100.extension");
|
||||
Assert.Equal("Collection", collection.Name);
|
||||
Assert.Equal("collection", collection.Slug);
|
||||
Assert.Equal("Show", show.Title);
|
||||
Assert.Equal("show", show.Slug);
|
||||
Assert.Equal(2000, show.StartAir!.Value.Year);
|
||||
Assert.Null(season);
|
||||
Assert.Null(episode.SeasonNumber);
|
||||
Assert.Null(episode.EpisodeNumber);
|
||||
Assert.Equal(100, episode.AbsoluteNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MovieEpisodeIdentification()
|
||||
{
|
||||
_manager.Setup(x => x.GetAll(null, new Sort<Library>(), default)).ReturnsAsync(new[]
|
||||
{
|
||||
new Library {Paths = new [] {"/kyoo", "/kyoo/Library/"}}
|
||||
});
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(
|
||||
"/kyoo/Library/Collection/Show (2000)/Show.extension");
|
||||
Assert.Equal("Collection", collection.Name);
|
||||
Assert.Equal("collection", collection.Slug);
|
||||
Assert.Equal("Show", show.Title);
|
||||
Assert.Equal("show", show.Slug);
|
||||
Assert.Equal(2000, show.StartAir!.Value.Year);
|
||||
Assert.Null(season);
|
||||
Assert.True(show.IsMovie);
|
||||
Assert.Null(episode.SeasonNumber);
|
||||
Assert.Null(episode.EpisodeNumber);
|
||||
Assert.Null(episode.AbsoluteNumber);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
<PackageReference Include="Divergic.Logging.Xunit" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Models;
|
||||
@ -18,24 +19,48 @@ namespace Kyoo.Controllers
|
||||
/// <summary>
|
||||
/// The configuration of kyoo to retrieve the identifier regex.
|
||||
/// </summary>
|
||||
private readonly IOptions<MediaOptions> _configuration;
|
||||
private readonly IOptionsMonitor<MediaOptions> _configuration;
|
||||
/// <summary>
|
||||
/// The library manager used to retrieve libraries paths.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RegexIdentifier"/>.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The regex patterns to use.</param>
|
||||
public RegexIdentifier(IOptions<MediaOptions> configuration)
|
||||
/// <param name="libraryManager">The library manager used to retrieve libraries paths.</param>
|
||||
public RegexIdentifier(IOptionsMonitor<MediaOptions> configuration, ILibraryManager libraryManager)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the relative path of an episode or subtitle.
|
||||
/// </summary>
|
||||
/// <param name="path">The full path of the episode</param>
|
||||
/// <returns>The path relative to the library root.</returns>
|
||||
private async Task<string> _GetRelativePath(string path)
|
||||
{
|
||||
string libraryPath = (await _libraryManager.GetAll<Library>())
|
||||
.SelectMany(x => x.Paths)
|
||||
.Where(path.StartsWith)
|
||||
.OrderByDescending(x => x.Length)
|
||||
.FirstOrDefault();
|
||||
return path[(libraryPath?.Length ?? 0)..];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(Collection, Show, Season, Episode)> Identify(string path, string relativePath)
|
||||
public async Task<(Collection, Show, Season, Episode)> Identify(string path)
|
||||
{
|
||||
Regex regex = new(_configuration.Value.Regex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
Match match = regex.Match(relativePath);
|
||||
string relativePath = await _GetRelativePath(path);
|
||||
Match match = _configuration.CurrentValue.Regex
|
||||
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
|
||||
.Select(x => x.Match(relativePath))
|
||||
.FirstOrDefault(x => x.Success);
|
||||
|
||||
if (!match.Success)
|
||||
if (match == null)
|
||||
throw new IdentificationFailedException($"The episode at {path} does not match the episode's regex.");
|
||||
|
||||
(Collection collection, Show show, Season season, Episode episode) ret = (
|
||||
@ -80,24 +105,27 @@ namespace Kyoo.Controllers
|
||||
ret.episode.Title = ret.show.Title;
|
||||
}
|
||||
|
||||
return Task.FromResult(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Track> IdentifyTrack(string path, string relativePath)
|
||||
public async Task<Track> IdentifyTrack(string path)
|
||||
{
|
||||
Regex regex = new(_configuration.Value.SubtitleRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
Match match = regex.Match(path);
|
||||
string relativePath = await _GetRelativePath(path);
|
||||
Match match = _configuration.CurrentValue.SubtitleRegex
|
||||
.Select(x => new Regex(x, RegexOptions.IgnoreCase | RegexOptions.Compiled))
|
||||
.Select(x => x.Match(relativePath))
|
||||
.FirstOrDefault(x => x.Success);
|
||||
|
||||
if (!match.Success)
|
||||
if (match == null)
|
||||
throw new IdentificationFailedException($"The subtitle at {path} does not match the subtitle's regex.");
|
||||
|
||||
string episodePath = match.Groups["Episode"].Value;
|
||||
return Task.FromResult(new Track
|
||||
return new Track
|
||||
{
|
||||
Type = StreamType.Subtitle,
|
||||
Language = match.Groups["Language"].Value,
|
||||
IsDefault = match.Groups["Default"].Value.Length > 0,
|
||||
IsDefault = match.Groups["Default"].Value.Length > 0,
|
||||
IsForced = match.Groups["Forced"].Value.Length > 0,
|
||||
Codec = FileExtensions.SubtitleExtensions[Path.GetExtension(path)],
|
||||
IsExternal = true,
|
||||
@ -106,7 +134,7 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
Path = episodePath
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -13,11 +13,11 @@ namespace Kyoo.Models.Options
|
||||
/// <summary>
|
||||
/// A regex for episodes
|
||||
/// </summary>
|
||||
public string Regex { get; set; }
|
||||
public string[] Regex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A regex for subtitles
|
||||
/// </summary>
|
||||
public string SubtitleRegex { get; set; }
|
||||
public string[] SubtitleRegex { get; set; }
|
||||
}
|
||||
}
|
@ -73,8 +73,6 @@ namespace Kyoo.Tasks
|
||||
return new()
|
||||
{
|
||||
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")
|
||||
};
|
||||
}
|
||||
@ -83,17 +81,19 @@ namespace Kyoo.Tasks
|
||||
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
string path = arguments["path"].As<string>();
|
||||
string relativePath = arguments["relativePath"].As<string>();
|
||||
Library library = arguments["library"].As<Library>();
|
||||
progress.Report(0);
|
||||
|
||||
if (library.Providers == null)
|
||||
await _libraryManager.Load(library, x => x.Providers);
|
||||
_metadataProvider.UseProviders(library.Providers);
|
||||
|
||||
if (library != null)
|
||||
{
|
||||
if (library.Providers == null)
|
||||
await _libraryManager.Load(library, x => x.Providers);
|
||||
_metadataProvider.UseProviders(library.Providers);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(path,
|
||||
relativePath);
|
||||
(Collection collection, Show show, Season season, Episode episode) = await _identifier.Identify(path);
|
||||
progress.Report(15);
|
||||
|
||||
collection = await _RegisterAndFill(collection);
|
||||
|
@ -39,9 +39,7 @@ namespace Kyoo.Tasks
|
||||
{
|
||||
return new()
|
||||
{
|
||||
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 /.")
|
||||
TaskParameter.CreateRequired<string>("path", "The path of the subtitle file")
|
||||
};
|
||||
}
|
||||
|
||||
@ -49,12 +47,11 @@ namespace Kyoo.Tasks
|
||||
public async Task Run(TaskParameters arguments, IProgress<float> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
string path = arguments["path"].As<string>();
|
||||
string relativePath = arguments["relativePath"].As<string>();
|
||||
|
||||
try
|
||||
{
|
||||
progress.Report(0);
|
||||
Track track = await _identifier.IdentifyTrack(path, relativePath);
|
||||
Track track = await _identifier.IdentifyTrack(path);
|
||||
progress.Report(25);
|
||||
|
||||
if (track.Episode == null)
|
||||
|
@ -44,8 +44,14 @@
|
||||
},
|
||||
|
||||
"media": {
|
||||
"regex": "(?:\\/(?<Collection>.*?))?\\/(?<Show>.*?)(?: \\(?<StartYear>\\d+\\))?\\/\\k<Show>(?: \\(\\d+\\))?(?:(?: S(?<Season>\\d+)E(?<Episode>\\d+))| (?<Absolute>\\d+))?.*$",
|
||||
"subtitleRegex": "^(?<Episode>.*)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
|
||||
"regex": [
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? S(?<Season>\\d+)E(?<Episode>\\d+)\\..*$",
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))? (?<Absolute>\\d+)\\..*$",
|
||||
"^\\/?(?<Collection>.+)?\\/(?<Show>.+?)(?: \\((?<StartYear>\\d+)\\))?\\/\\k<Show>(?: \\(\\d+\\))?\\..*$"
|
||||
],
|
||||
"subtitleRegex": [
|
||||
"^(?<Episode>.+)\\.(?<Language>\\w{1,3})\\.(?<Default>default\\.)?(?<Forced>forced\\.)?.*$"
|
||||
]
|
||||
},
|
||||
|
||||
"authentication": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user