Enhance logging in RefreshShowEpisodesMetadataCommandHandler and TmdbMetadataProvider for image retrieval failures
Updated the RefreshShowEpisodesMetadataCommandHandler to include logging for scenarios where episode still images are not retrieved successfully. Introduced warning logs for missing still URLs and download failures, improving traceability. Additionally, modified the TmdbMetadataProvider to implement a fallback mechanism for episode still images, ensuring that appropriate imagery is displayed even when localized images are unavailable. Updated related tests to verify the correct behavior of these logging enhancements and fallback logic.
This commit is contained in:
+18
-1
@@ -1,5 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Application.Library;
|
||||
@@ -12,7 +13,8 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
IMetadataProviderResolver resolver,
|
||||
IImageDownloader downloader,
|
||||
IImageStore imageStore
|
||||
IImageStore imageStore,
|
||||
ILogger<RefreshShowEpisodesMetadataCommandHandler> logger
|
||||
) : ICommandHandler<RefreshShowEpisodesMetadataCommand, Result<int>>
|
||||
{
|
||||
public async Task<Result<int>> Handle(
|
||||
@@ -99,12 +101,27 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Как и с постером шоу: снаружи «кадра нет» неотличимо от «кадр не доехал», а чинится это
|
||||
// по-разному. Подробности отказа скачивания пишет сам загрузчик.
|
||||
if (string.IsNullOrEmpty(meta.StillUrl))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Кадр серии «{Episode}» не проставлен: источник не вернул ссылку",
|
||||
meta.Title
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var downloaded = await downloader.DownloadAsync(meta.StillUrl, cancellationToken);
|
||||
if (downloaded is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Кадр серии «{Episode}» не проставлен: {Url} не скачался",
|
||||
meta.Title,
|
||||
meta.StillUrl
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var image = Image.Create(ImageCategory.EpisodeStill, downloaded.Extension, meta.Title);
|
||||
dbContext.Images.Add(image);
|
||||
|
||||
@@ -117,7 +117,9 @@ public sealed class ShowMetadataApplier(
|
||||
downloaded.Content,
|
||||
cancellationToken
|
||||
);
|
||||
logger.LogInformation(
|
||||
// Успех — отладочным уровнем: при массовом обогащении это строка на каждое шоу, а знать
|
||||
// оператору интересно обратное — что постер не доехал.
|
||||
logger.LogDebug(
|
||||
"Постер для «{Show}» сохранён как изображение {ImageId}",
|
||||
show.Name,
|
||||
image.Id
|
||||
|
||||
@@ -110,7 +110,11 @@ public sealed class TmdbMetadataProvider(
|
||||
// оставляло бы шоу без постера, и это неотличимо от «постера нет вовсе».
|
||||
var posterPath =
|
||||
GetString(root, "poster_path")
|
||||
?? await NeutralPosterPathAsync(movie, externalId, cancellationToken);
|
||||
?? await NeutralImagePathAsync(
|
||||
$"{Segment(movie)}/{externalId}",
|
||||
"poster_path",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
return new ShowMetadata(
|
||||
externalId,
|
||||
@@ -126,19 +130,20 @@ public sealed class TmdbMetadataProvider(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Постер без привязки к языку: тот же запрос, но без <c>language</c>. Второе обращение к API
|
||||
/// делается только когда локализованного постера нет, — то есть редко.
|
||||
/// Картинка без привязки к языку: тот же адрес, но без <c>language</c>. TMDb отдаёт постеры
|
||||
/// и кадры под запрошенный язык, и у контента без локализованной картинки поле пустое — хотя
|
||||
/// язык-нейтральная у него есть. Второе обращение к API делается только в этом случае.
|
||||
/// </summary>
|
||||
private async Task<string?> NeutralPosterPathAsync(
|
||||
bool movie,
|
||||
string externalId,
|
||||
private async Task<string?> NeutralImagePathAsync(
|
||||
string path,
|
||||
string field,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var query = UsesBearer ? string.Empty : $"?api_key={Tmdb.ApiKey}";
|
||||
var url = $"{Tmdb.BaseUrl}/{Segment(movie)}/{externalId}{query}";
|
||||
var url = $"{Tmdb.BaseUrl}/{path}{query}";
|
||||
using var doc = await TryGetAsync(httpFactory, url, cancellationToken, BearerToken);
|
||||
return doc is null ? null : GetString(doc.RootElement, "poster_path");
|
||||
return doc is null ? null : GetString(doc.RootElement, field);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,15 +279,28 @@ public sealed class TmdbMetadataProvider(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var url = $"{Tmdb.BaseUrl}/tv/{externalId}/season/{season}/episode/{episode}{BaseQuery}";
|
||||
using var doc = await TryGetAsync(httpFactory, url, cancellationToken, BearerToken);
|
||||
var path = $"tv/{externalId}/season/{season}/episode/{episode}";
|
||||
using var doc = await TryGetAsync(
|
||||
httpFactory,
|
||||
$"{Tmdb.BaseUrl}/{path}{BaseQuery}",
|
||||
cancellationToken,
|
||||
BearerToken
|
||||
);
|
||||
if (doc is null)
|
||||
return null;
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Кадр серии — та же история, что и с постером шоу: под русский язык его обычно нет,
|
||||
// а язык-нейтральный есть. Добор идёт по серии, поэтому у сезона без локализованных
|
||||
// кадров запросов вдвое больше — «Обновить серии» жмут руками, это терпимо.
|
||||
var stillPath =
|
||||
GetString(root, "still_path")
|
||||
?? await NeutralImagePathAsync(path, "still_path", cancellationToken);
|
||||
|
||||
return new EpisodeMetadata(
|
||||
GetString(root, "name") ?? "—",
|
||||
GetString(root, "overview"),
|
||||
StillUrl(GetString(root, "still_path")),
|
||||
StillUrl(stillPath),
|
||||
DateFrom(GetString(root, "air_date"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Metadata;
|
||||
@@ -39,7 +40,8 @@ public class RefreshEpisodesTests
|
||||
db,
|
||||
resolver,
|
||||
downloader ?? Substitute.For<IImageDownloader>(),
|
||||
store ?? Substitute.For<IImageStore>()
|
||||
store ?? Substitute.For<IImageStore>(),
|
||||
NullLogger<RefreshShowEpisodesMetadataCommandHandler>.Instance
|
||||
);
|
||||
|
||||
/// <summary>Сериал с привязкой к источнику и одной серией на готовом ассете.</summary>
|
||||
|
||||
@@ -146,6 +146,26 @@ public class TmdbMetadataProviderTests
|
||||
Assert.EndsWith("/neutral.jpg", show?.PosterUrl);
|
||||
}
|
||||
|
||||
/// <summary>Кадр серии — та же история, что и с постером шоу: под русский язык его часто нет.</summary>
|
||||
[Fact]
|
||||
public async Task GetEpisode_WithoutLocalizedStill_FallsBackToNeutralOne()
|
||||
{
|
||||
var http = new FakeHttpClientFactory(url =>
|
||||
FakeHttpClientFactory.Ok(
|
||||
url.Contains("language=", StringComparison.Ordinal)
|
||||
? """{"name":"Серия"}"""
|
||||
: """{"name":"Серия","still_path":"/neutral.jpg"}"""
|
||||
)
|
||||
);
|
||||
|
||||
var episode = await Provider(http).GetEpisodeAsync("9", 2, 5, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, http.Urls.Count);
|
||||
Assert.DoesNotContain("language=", http.LastUrl);
|
||||
Assert.Contains("/tv/9/season/2/episode/5", http.LastUrl);
|
||||
Assert.EndsWith("/neutral.jpg", episode?.StillUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetShow_Movie_FallsBackToAnyReleaseCertification()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user