Added new endpoints for matching and importing movies, allowing for automated processing of film files from the inbox. Introduced logic to parse file names into titles and years, and integrated metadata matching to streamline the import process. Updated the MediaOptions to include a configuration for automatic movie creation from recognized files. Enhanced the InboxScanner to attempt movie attachment upon asset registration, improving user experience and efficiency. Updated documentation to reflect these new features and their usage.
215 lines
7.7 KiB
C#
215 lines
7.7 KiB
C#
using NSubstitute;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Media.MovieImport;
|
|
using TeleWave.Application.Metadata;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Library;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Media;
|
|
|
|
/// <summary>
|
|
/// Подбор фильма по имени файла. Главное свойство то же, что у массового обогащения: уверенным
|
|
/// считается только дословное совпадение — чужой постер у фильма в эфире хуже, чем его отсутствие.
|
|
/// </summary>
|
|
public class MovieMatcherTests
|
|
{
|
|
private static IMetadataProvider ProviderWith(params MetadataCandidate[] candidates)
|
|
{
|
|
var provider = Substitute.For<IMetadataProvider>();
|
|
provider
|
|
.SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>())
|
|
.Returns(candidates);
|
|
return provider;
|
|
}
|
|
|
|
private static IMetadataProviderResolver Resolver(IMetadataProvider provider)
|
|
{
|
|
var resolver = Substitute.For<IMetadataProviderResolver>();
|
|
resolver.Resolve("tmdb").Returns(provider);
|
|
resolver.AvailableKeys.Returns(["tmdb"]);
|
|
return resolver;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Confident_WhenTitleAndYearMatch()
|
|
{
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("42", "Die Hard 2", 1990, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["Die Hard 2 [1990 HDRip].avi"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
var row = Assert.Single(rows);
|
|
Assert.Equal(MovieMatchStatus.Confident, row.Status);
|
|
Assert.Equal("42", row.PickedExternalId);
|
|
Assert.Equal("Die Hard 2", row.ParsedTitle);
|
|
Assert.Equal(1990, row.ParsedYear);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Confident_WhenYearIsOffByOne()
|
|
{
|
|
// Год проката и год производства у релизов расходятся на год — это норма, а не промах.
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["The Crow 1993 BDRip"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.Equal(MovieMatchStatus.Confident, Assert.Single(rows).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Uncertain_WhenYearIsFarOff()
|
|
{
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["The Crow 2024 BDRip"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
var row = Assert.Single(rows);
|
|
Assert.Equal(MovieMatchStatus.Uncertain, row.Status);
|
|
Assert.Null(row.PickedExternalId);
|
|
// Кандидаты всё равно показываются: выбрать из них — секунда, искать заново — минута.
|
|
Assert.NotEmpty(row.Candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Uncertain_WhenTwoCandidatesShareTitleAndYear()
|
|
{
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("1", "Gladiator", 2000, null, null, ShowKind.Single),
|
|
new MetadataCandidate("2", "Gladiator", 2000, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["Gladiator.2000.BDRip"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.Equal(MovieMatchStatus.Uncertain, Assert.Single(rows).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NotFound_WhenProviderReturnsNothing()
|
|
{
|
|
// Транслит: источник по «Shou Trumana» не найдёт ничего, и это отдельный статус — строку
|
|
// надо не выбрать, а переписать.
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(ProviderWith()));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["Shou.Trumana.1998.BDRip.AVC.mkv"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
var row = Assert.Single(rows);
|
|
Assert.Equal(MovieMatchStatus.NotFound, row.Status);
|
|
Assert.Empty(row.Candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AlreadyInLibrary_WhenExternalIdIsTaken()
|
|
{
|
|
var fixture = new TestDb();
|
|
var existing = Show.Create("Гладиатор", ShowKind.Single);
|
|
existing.ApplyMetadata("tmdb", "98", null, 2000, null);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Shows.Add(existing);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("98", "Gladiator", 2000, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var rows = await matcher.MatchAsync(
|
|
["Gladiator.2000.BDRip"],
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
var row = Assert.Single(rows);
|
|
Assert.Equal(MovieMatchStatus.AlreadyInLibrary, row.Status);
|
|
Assert.Equal(existing.Id, row.ExistingShowId);
|
|
Assert.Equal("Гладиатор", row.ExistingShowName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MatchQuery_UsesTitleTypedByHand()
|
|
{
|
|
// Транслит правится руками: имя файла остаётся ключом строки, а ищется вписанное название.
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("5", "Шоу Трумана", 1998, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var row = await matcher.MatchQueryAsync(
|
|
"Shou.Trumana.1998.BDRip.AVC.mkv",
|
|
"Шоу Трумана",
|
|
1998,
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.Equal("Shou.Trumana.1998.BDRip.AVC.mkv", row.Name);
|
|
Assert.Equal(MovieMatchStatus.Confident, row.Status);
|
|
Assert.Equal("5", row.PickedExternalId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PickConfident_ReturnsNothing_WhenMatchIsLoose()
|
|
{
|
|
// Автоматика inbox обязана молчать там, где таблица разбора показала бы жёлтым.
|
|
var fixture = new TestDb();
|
|
var provider = ProviderWith(
|
|
new MetadataCandidate("3", "Совсем другое кино", 1999, null, null, ShowKind.Single)
|
|
);
|
|
await using var db = fixture.New();
|
|
var matcher = new MovieMatcher(db, Resolver(provider));
|
|
|
|
var picked = await matcher.PickConfidentAsync(
|
|
"Green.Mile.1999.bdrip.mkv",
|
|
"tmdb",
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.Null(picked);
|
|
}
|
|
}
|