From 503261477394e5afd1982e84d7bc731ebf217b2f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 30 Jul 2026 02:08:50 +0300 Subject: [PATCH] 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. --- ...freshShowEpisodesMetadataCommandHandler.cs | 19 ++++++++- .../Metadata/ShowMetadataApplier.cs | 4 +- .../Metadata/TmdbMetadataProvider.cs | 40 ++++++++++++++----- .../Metadata/RefreshEpisodesTests.cs | 4 +- .../Metadata/TmdbMetadataProviderTests.cs | 20 ++++++++++ 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/backend/src/TeleWave.Application/Metadata/RefreshEpisodes/RefreshShowEpisodesMetadataCommandHandler.cs b/backend/src/TeleWave.Application/Metadata/RefreshEpisodes/RefreshShowEpisodesMetadataCommandHandler.cs index b0dcd5b..1979507 100644 --- a/backend/src/TeleWave.Application/Metadata/RefreshEpisodes/RefreshShowEpisodesMetadataCommandHandler.cs +++ b/backend/src/TeleWave.Application/Metadata/RefreshEpisodes/RefreshShowEpisodesMetadataCommandHandler.cs @@ -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 logger ) : ICommandHandler> { public async Task> 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); diff --git a/backend/src/TeleWave.Application/Metadata/ShowMetadataApplier.cs b/backend/src/TeleWave.Application/Metadata/ShowMetadataApplier.cs index b960984..c3b1ede 100644 --- a/backend/src/TeleWave.Application/Metadata/ShowMetadataApplier.cs +++ b/backend/src/TeleWave.Application/Metadata/ShowMetadataApplier.cs @@ -117,7 +117,9 @@ public sealed class ShowMetadataApplier( downloaded.Content, cancellationToken ); - logger.LogInformation( + // Успех — отладочным уровнем: при массовом обогащении это строка на каждое шоу, а знать + // оператору интересно обратное — что постер не доехал. + logger.LogDebug( "Постер для «{Show}» сохранён как изображение {ImageId}", show.Name, image.Id diff --git a/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs b/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs index 3d8b4b8..987bb84 100644 --- a/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs +++ b/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs @@ -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( } /// - /// Постер без привязки к языку: тот же запрос, но без language. Второе обращение к API - /// делается только когда локализованного постера нет, — то есть редко. + /// Картинка без привязки к языку: тот же адрес, но без language. TMDb отдаёт постеры + /// и кадры под запрошенный язык, и у контента без локализованной картинки поле пустое — хотя + /// язык-нейтральная у него есть. Второе обращение к API делается только в этом случае. /// - private async Task NeutralPosterPathAsync( - bool movie, - string externalId, + private async Task 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); } /// @@ -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")) ); } diff --git a/backend/tests/TeleWave.Application.Tests/Metadata/RefreshEpisodesTests.cs b/backend/tests/TeleWave.Application.Tests/Metadata/RefreshEpisodesTests.cs index 2271ccd..5a23d66 100644 --- a/backend/tests/TeleWave.Application.Tests/Metadata/RefreshEpisodesTests.cs +++ b/backend/tests/TeleWave.Application.Tests/Metadata/RefreshEpisodesTests.cs @@ -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(), - store ?? Substitute.For() + store ?? Substitute.For(), + NullLogger.Instance ); /// Сериал с привязкой к источнику и одной серией на готовом ассете. diff --git a/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs b/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs index 1c66a11..be12920 100644 --- a/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs +++ b/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs @@ -146,6 +146,26 @@ public class TmdbMetadataProviderTests Assert.EndsWith("/neutral.jpg", show?.PosterUrl); } + /// Кадр серии — та же история, что и с постером шоу: под русский язык его часто нет. + [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() {