Remove obsolete rule formats, batch SQLite work, preserve artwork source URLs, and coordinate refresh workers with maintenance. Add regression coverage and document backup format changes.
233 lines
11 KiB
C#
233 lines
11 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using Jellyfin.Plugin.Multilang.Configuration;
|
|
using Jellyfin.Plugin.Multilang.Data;
|
|
using Jellyfin.Plugin.Multilang.Services.Assets;
|
|
using Jellyfin.Plugin.Multilang.Services.Providers;
|
|
using Jellyfin.Plugin.Multilang.Services.Refresh;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Jellyfin.Plugin.Multilang.Tests;
|
|
|
|
public sealed class ItemFetcherTests : IDisposable
|
|
{
|
|
private readonly string _root = TestPaths.CreateRoot();
|
|
private readonly TranslationStore _store;
|
|
private readonly TestHttpClientFactory _http;
|
|
private readonly ItemFetcher _fetcher;
|
|
private readonly RefreshItemInfo _item = new("11111111111111111111111111111111", "1", "movie", RefreshWorkClass.Movies, 0, 0, null, "Movie");
|
|
private readonly PluginConfiguration _config = new()
|
|
{
|
|
Languages = ["en", "fi"],
|
|
TmdbApiKey = "test",
|
|
Providers = [new() { Id = "tmdb", MetadataOrder = 0 }]
|
|
};
|
|
private string _original = "en";
|
|
private string _overview = "Overview";
|
|
private bool _missingFinnishTagline;
|
|
private bool _notFound;
|
|
private bool _missingTmdbPoster;
|
|
private bool _fanartUnauthorized;
|
|
|
|
public ItemFetcherTests()
|
|
{
|
|
_store = new(new TestApplicationPaths(_root));
|
|
_http = new(request =>
|
|
{
|
|
if (_notFound) return new(HttpStatusCode.NotFound);
|
|
var url = request.RequestUri!;
|
|
if (url.Host == "webservice.fanart.tv")
|
|
return _fanartUnauthorized ? new(HttpStatusCode.Unauthorized)
|
|
: Json(new { movieposter = new[] { new { lang = "en", url = "https://assets.fanart.tv/poster.jpg", likes = "5" } } });
|
|
if (url.Host == "image.tmdb.org")
|
|
return new(HttpStatusCode.OK) { Content = new ByteArrayContent([1, 2, 3]) };
|
|
if (url.AbsolutePath.EndsWith("/images"))
|
|
return Json(new { posters = _missingTmdbPoster ? [] : new[] { new { iso_639_1 = "en", file_path = "/poster.jpg" } }, logos = Array.Empty<object>(), backdrops = Array.Empty<object>() });
|
|
var language = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(url.Query)["language"].ToString();
|
|
return Json(new
|
|
{
|
|
title = language + " title",
|
|
overview = _overview,
|
|
tagline = language == "fi" && _missingFinnishTagline ? "" : "Tagline",
|
|
original_language = _original,
|
|
original_title = "Original title",
|
|
genres = Array.Empty<object>()
|
|
});
|
|
});
|
|
_fetcher = new(_store, new(_http), new(_http), new(_http, _store), NullLogger<ItemFetcher>.Instance);
|
|
}
|
|
|
|
private static HttpResponseMessage Json(object value)
|
|
=> new(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(value)) };
|
|
private Task<bool> Fetch(RefreshJobType job = RefreshJobType.Missing) => _fetcher.RefreshAsync(_item, _config, job, CancellationToken.None);
|
|
private string Text(string language, string field) => _store.GetTranslations([_item.ItemId], [language])[_item.ItemId][language][field];
|
|
|
|
[Fact]
|
|
public async Task CompleteMetadataIsNotRefetchedAndOriginalReusesConfiguredLanguage()
|
|
{
|
|
Assert.True(await Fetch());
|
|
Assert.Equal(2, _http.Requests.Count);
|
|
Assert.Equal(Text("en", "title"), Text("Original", "title"));
|
|
Assert.False(_fetcher.NeedsRefresh(_item, _config));
|
|
Assert.False(await Fetch());
|
|
Assert.Equal(2, _http.Requests.Count);
|
|
await Fetch(RefreshJobType.Full);
|
|
Assert.Equal(4, _http.Requests.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingRetryFetchesOnlyIncompleteLanguageAndUpdatesWholeResponse()
|
|
{
|
|
_missingFinnishTagline = true;
|
|
await Fetch();
|
|
Assert.False(_fetcher.NeedsRefresh(_item, _config));
|
|
await Fetch();
|
|
Assert.Equal(2, _http.Requests.Count);
|
|
_config.WaitDaysForMissingData = 0;
|
|
Assert.True(_fetcher.NeedsRefresh(_item, _config));
|
|
_missingFinnishTagline = false;
|
|
_overview = "Updated overview";
|
|
await Fetch();
|
|
Assert.Equal(3, _http.Requests.Count);
|
|
Assert.Contains("language=fi", _http.Requests.Last());
|
|
Assert.Equal("Updated overview", Text("fi", "overview"));
|
|
Assert.Equal("Overview", Text("en", "overview"));
|
|
Assert.False(_fetcher.NeedsRefresh(_item, _config));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AdditionalOriginalLanguageIsFetchedOnceAndNewConfiguredLanguageDoesNotRefetchOthers()
|
|
{
|
|
_original = "sv";
|
|
await Fetch();
|
|
Assert.Equal(3, _http.Requests.Count);
|
|
Assert.Equal("sv title", Text("Original", "title"));
|
|
await Fetch();
|
|
Assert.Equal(3, _http.Requests.Count);
|
|
_config.Languages = ["en", "fi", "fr"];
|
|
await Fetch();
|
|
Assert.Equal(4, _http.Requests.Count);
|
|
Assert.Contains("language=fr", _http.Requests.Last());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NotFoundIsRememberedUntilRetryCutoff()
|
|
{
|
|
_notFound = true;
|
|
Assert.False(await Fetch());
|
|
Assert.False(_fetcher.NeedsRefresh(_item, _config));
|
|
await Fetch();
|
|
Assert.Equal(2, _http.Requests.Count);
|
|
_config.WaitDaysForMissingData = 0;
|
|
Assert.True(_fetcher.NeedsRefresh(_item, _config));
|
|
await Fetch();
|
|
Assert.Equal(4, _http.Requests.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LocalRepairDoesNotFetchMetadataAndUrlCleanupOnlyHappensInScheduledHousekeeping()
|
|
{
|
|
_config.Providers[0].ArtworkOrder = 0;
|
|
await Fetch();
|
|
Assert.Equal(3, _http.Requests.Count);
|
|
_config.AssetStorageMode = "local";
|
|
Assert.True(_fetcher.NeedsLocalWork(_item.ItemId, _config));
|
|
await Fetch(RefreshJobType.Aggregate);
|
|
Assert.Equal(4, _http.Requests.Count);
|
|
Assert.Contains("image.tmdb.org", _http.Requests.Last());
|
|
var path = _store.GetAssetPath(_item.ItemId, "en", "poster")!;
|
|
Assert.Equal(path, _store.GetAssetPath(_item.ItemId, "Original", "poster"));
|
|
Assert.True(_store.TryNormalizeLocalAssetPath(path, out var file));
|
|
File.Delete(file);
|
|
await Fetch();
|
|
Assert.Equal(5, _http.Requests.Count);
|
|
Assert.True(File.Exists(file));
|
|
_config.AssetStorageMode = "url";
|
|
await Fetch();
|
|
Assert.Equal(path, _store.GetAssetPath(_item.ItemId, "en", "poster"));
|
|
Assert.True(File.Exists(file));
|
|
_store.CleanupForConfiguration(new HashSet<string> { _item.ItemId }, _config.Languages, false);
|
|
Assert.StartsWith("https://", _store.GetAssetPath(_item.ItemId, "en", "poster"));
|
|
Assert.False(File.Exists(file));
|
|
Assert.Equal(5, _http.Requests.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ArtworkUsesProviderPriorityAndFallsBackWhenHigherPriorityImageDisappears()
|
|
{
|
|
_config.Providers = [new() { Id = "tmdb", MetadataOrder = 0, ArtworkOrder = 0 }, new() { Id = "fanart", ArtworkOrder = 1 }];
|
|
_config.FanartApiKey = "test";
|
|
await Fetch();
|
|
Assert.Contains("image.tmdb.org", _store.GetAssetPath(_item.ItemId, "en", "poster"));
|
|
_missingTmdbPoster = true;
|
|
await Fetch(RefreshJobType.Full);
|
|
Assert.Contains("assets.fanart.tv", _store.GetAssetPath(_item.ItemId, "en", "poster"));
|
|
_fanartUnauthorized = true;
|
|
await Assert.ThrowsAsync<HttpRequestException>(() => Fetch(RefreshJobType.Full));
|
|
Assert.Contains("assets.fanart.tv", _store.GetAssetPath(_item.ItemId, "en", "poster"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BackupPreservesSourceUrlsAndFetchStateAndFiltersUnwantedLanguages()
|
|
{
|
|
_config.Providers[0].ArtworkOrder = 0;
|
|
await Fetch();
|
|
var backup = Path.Combine(_root, "export.sqlite");
|
|
_store.ExportTranslationDatabase(backup);
|
|
_store.ResetAll();
|
|
_store.ImportTranslationDatabase(backup, new HashSet<string> { _item.ItemId }, new HashSet<string> { "en" });
|
|
Assert.Equal(2, _store.GetFetchStates(_item.ItemId).Count);
|
|
Assert.DoesNotContain("metadata:fi", _store.GetFetchStates(_item.ItemId).Keys);
|
|
Assert.Equal("https://image.tmdb.org/t/p/original/poster.jpg", _store.GetStoredAssets(_item.ItemId).First().SourceUrl);
|
|
Assert.Equal("tmdb", _store.GetStoredAssets(_item.ItemId).First().Provider);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true, false, false)]
|
|
[InlineData(true, true, true)]
|
|
[InlineData(false, false, false)]
|
|
[InlineData(false, true, false)]
|
|
public void YoungFetchStateHonorsKnownMissingVersusDeletedData(bool complete, bool missing, bool expected)
|
|
=> Assert.Equal(expected, ItemFetcher.NeedsFetch(new("scope", 100, complete), "scope", missing, false, 50));
|
|
|
|
[Fact]
|
|
public void FailedArtworkPacketRollsBackDeletionAndNewRowsTogether()
|
|
{
|
|
var original = new StoredAsset("en", "poster", "https://example.test/old.jpg", "https://example.test/old.jpg", "tmdb");
|
|
_store.SaveArtwork(_item.ItemId, [original], new Dictionary<string, FetchState>());
|
|
Assert.Throws<Microsoft.Data.Sqlite.SqliteException>(() => _store.SaveArtwork(_item.ItemId,
|
|
[original with { Path = "new" }, original], new Dictionary<string, FetchState>()));
|
|
Assert.Equal(original, Assert.Single(_store.GetStoredAssets(_item.ItemId)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ParallelMetadataPacketsDoNotLoseRows()
|
|
{
|
|
await Task.WhenAll(Enumerable.Range(0, 12).Select(index => Task.Run(() =>
|
|
_store.SaveMetadata(_item with { ItemId = index.ToString() }, "en",
|
|
new("Title", "Overview", "Tagline", "Title", "en", [], [], [], []),
|
|
new("scope", 1, true), true))));
|
|
foreach (var index in Enumerable.Range(0, 12))
|
|
{
|
|
Assert.NotNull(_store.GetFacts(index.ToString()));
|
|
Assert.Equal(3, _store.GetTranslations([index.ToString()], ["en"])[index.ToString()]["en"].Count);
|
|
Assert.True(_store.GetFetchStates(index.ToString())["metadata:en"].Complete);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectionAggregationKeepsOnlySharedSingleLanguageAndUnionsCountrySets()
|
|
{
|
|
var collection = new FactsData("collection", "1", "collection", "Collection", "en", "en", "[]", "[]", "[]", "", "[]", 1, 1);
|
|
var finnish = collection with { Kind = "movie", OriginalLanguage = "fi", ProductionCountriesJson = "[\"FI\"]" };
|
|
var swedish = finnish with { OriginalLanguage = "sv", ProductionCountriesJson = "[\"SE\",\"FI\"]" };
|
|
var mixed = Services.ItemsProxyTransformer.AggregateCollectionFacts(collection, [finnish, swedish]);
|
|
Assert.Equal("", mixed.OriginalLanguage);
|
|
Assert.Equal("fi,sv", mixed.OriginalLanguageAll);
|
|
Assert.Equal(new HashSet<string> { "FI", "SE" }, JsonSerializer.Deserialize<HashSet<string>>(mixed.ProductionCountriesJson));
|
|
Assert.Equal("fi", Services.ItemsProxyTransformer.AggregateCollectionFacts(collection, [finnish, finnish]).OriginalLanguage);
|
|
}
|
|
|
|
public void Dispose() => TestPaths.DeleteRoot(_root);
|
|
}
|