From 1de082b96ef7c4fbf56a0fc44d327b1c74eba517 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 6 Oct 2026 00:00:32 -0400 Subject: [PATCH] feat: TMDB Discover source + retry yt-dlp once before fallback (v1.0.0.17) The fixed TV lists are mostly long-running shows whose first_air_date is outside ReleaseDateRangeMonths, and paging stops at the first page with no in-range title, so only ~7 TV candidates were found (5 then dropped by language). Add a "discover" source per media type that asks /discover for recent titles in AllowedLanguages, sorted by popularity (1600+ TV results for en|fr today). TV Discover defaults on, movie Discover off. yt-dlp failures were mostly passing HTTP 403s from back-to-back requests; the built-in fallback is sometimes only offered 360p. Retry yt-dlp once after 15s before falling back. Co-Authored-By: Claude Opus 5.5 --- .../Configuration/PluginConfiguration.cs | 5 +++ .../Configuration/config.html | 24 +++++++++++++ ...fin.Plugin.CinemaTrailers4Jellyfins.csproj | 4 +-- .../Services/TmdbService.cs | 35 ++++++++++++++++--- .../Services/TrailerDownloadService.cs | 10 ++++++ build.yaml | 13 ++++--- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs index 1883608..0f640d6 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs @@ -14,6 +14,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration public bool SourceUpcoming { get; set; } = true; public bool SourcePopular { get; set; } = false; public bool SourceTopRated { get; set; } = false; + // TMDB Discover: newest titles in the allowed languages, most popular first. + public bool SourceDiscover { get; set; } = false; // ── Sources (TV Shows) ─────────────────────────────────────────────── @@ -21,6 +23,9 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration public bool SourceTvOnTheAir { get; set; } = true; public bool SourceTvPopular { get; set; } = false; public bool SourceTvTopRated { get; set; } = false; + // TMDB Discover: newest shows in the allowed languages, most popular first. On by default: + // the TV lists above are mostly long-running shows the date range filters out. + public bool SourceTvDiscover { get; set; } = true; // ── Date Range ──────────────────────────────────────────────────────── diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html index ce83daf..706f208 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html @@ -120,6 +120,16 @@ Highest rated movies on TMDB, filtered by the date range below. + +
+ +
+ Newest movies in your selected languages, most popular first. +
+
@@ -169,6 +179,16 @@ Highest rated TV shows on TMDB, filtered by the date range below. + +
+ +
+ Newest TV shows in your selected languages, most popular first. The lists above are mostly long-running shows that the date range drops. +
+
@@ -432,10 +452,12 @@ document.getElementById('source-upcoming').checked = config.SourceUpcoming !== false; document.getElementById('source-popular').checked = !!config.SourcePopular; document.getElementById('source-top-rated').checked = !!config.SourceTopRated; + document.getElementById('source-discover').checked = !!config.SourceDiscover; document.getElementById('source-tv-airing-today').checked = config.SourceTvAiringToday !== false; document.getElementById('source-tv-on-the-air').checked = config.SourceTvOnTheAir !== false; document.getElementById('source-tv-popular').checked = !!config.SourceTvPopular; document.getElementById('source-tv-top-rated').checked = !!config.SourceTvTopRated; + document.getElementById('source-tv-discover').checked = config.SourceTvDiscover !== false; document.getElementById('date-range').value = String(config.ReleaseDateRangeMonths ?? 6); document.getElementById('download-folder').value = config.DownloadFolder || ''; document.getElementById('max-trailers').value = config.MaxTrailersToDownload ?? 20; @@ -468,10 +490,12 @@ config.SourceUpcoming = document.getElementById('source-upcoming').checked; config.SourcePopular = document.getElementById('source-popular').checked; config.SourceTopRated = document.getElementById('source-top-rated').checked; + config.SourceDiscover = document.getElementById('source-discover').checked; config.SourceTvAiringToday = document.getElementById('source-tv-airing-today').checked; config.SourceTvOnTheAir = document.getElementById('source-tv-on-the-air').checked; config.SourceTvPopular = document.getElementById('source-tv-popular').checked; config.SourceTvTopRated = document.getElementById('source-tv-top-rated').checked; + config.SourceTvDiscover = document.getElementById('source-tv-discover').checked; config.ReleaseDateRangeMonths = parseInt(document.getElementById('date-range').value, 10); config.DownloadFolder = document.getElementById('download-folder').value; config.MaxTrailersToDownload = parseIntOrDefault(document.getElementById('max-trailers').value, 20); diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj index 55000da..4606f43 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj @@ -3,8 +3,8 @@ net9.0 Jellyfin.Plugin.CinemaTrailers4Jellyfins - 1.0.0.16 - 1.0.0.16 + 1.0.0.17 + 1.0.0.17 enable false false diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs index 036eb0d..3e97635 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs @@ -84,6 +84,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services if (config.SourceUpcoming) sources.Add("upcoming"); if (config.SourcePopular) sources.Add("popular"); if (config.SourceTopRated) sources.Add("top_rated"); + if (config.SourceDiscover) sources.Add(DiscoverSource); return GetCandidatesAsync("movie", sources, config, ct); } @@ -101,6 +102,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services if (config.SourceTvOnTheAir) sources.Add("on_the_air"); if (config.SourceTvPopular) sources.Add("popular"); if (config.SourceTvTopRated) sources.Add("top_rated"); + if (config.SourceTvDiscover) sources.Add(DiscoverSource); return GetCandidatesAsync("tv", sources, config, ct); } @@ -120,7 +122,10 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services foreach (var endpoint in sourceEndpoints) { - var items = await FetchSourcePagesAsync(mediaType, endpoint, config.TmdbApiKey, releasedAfter, config.MaxPagesPerSource, ct) + var path = endpoint == DiscoverSource + ? DiscoverPath(mediaType, releasedAfter, config.AllowedLanguages) + : $"{mediaType}/{endpoint}"; + var items = await FetchSourcePagesAsync(mediaType, path, config.TmdbApiKey, releasedAfter, config.MaxPagesPerSource, ct) .ConfigureAwait(false); foreach (var item in items) @@ -133,9 +138,31 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services return results; } + private const string DiscoverSource = "discover"; + + // /discover asks TMDB directly for recent titles in the allowed languages, sorted by + // popularity, unlike the fixed lists, which are mostly older titles the date range drops. + private static string DiscoverPath(string mediaType, DateTime? releasedAfter, string allowedLanguages) + { + var query = new List { "sort_by=popularity.desc" }; + if (releasedAfter.HasValue) + { + var dateParam = mediaType == "movie" ? "primary_release_date.gte" : "first_air_date.gte"; + query.Add($"{dateParam}={releasedAfter.Value:yyyy-MM-dd}"); + } + + var languages = (allowedLanguages ?? string.Empty) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (languages.Length > 0) + query.Add("with_original_language=" + Uri.EscapeDataString(string.Join("|", languages))); + + return $"discover/{mediaType}?" + string.Join("&", query); + } + + // path is relative to the API root, e.g. "tv/popular" or "discover/tv?sort_by=...". private async Task> FetchSourcePagesAsync( string mediaType, - string endpoint, + string path, string apiKey, DateTime? releasedAfter, int maxPages, @@ -153,7 +180,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services try { - var url = $"{BaseUrl}/{mediaType}/{endpoint}?language=en-US&page={page}"; + var url = $"{BaseUrl}/{path}{(path.Contains('?') ? '&' : '?')}language=en-US&page={page}"; using var request = new HttpRequestMessage(HttpMethod.Get, url); ApplyAuth(request, apiKey); using var response = await _httpClient.SendAsync(request, ct).ConfigureAwait(false); @@ -190,7 +217,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services } catch (Exception ex) { - _logger.LogError(ex, "|CinemaTrailers4Jellyfins| Failed to fetch TMDB source '{MediaType}/{Endpoint}' page {Page}", mediaType, endpoint, page); + _logger.LogError(ex, "|CinemaTrailers4Jellyfins| Failed to fetch TMDB source '{Path}' page {Page}", path, page); break; } } diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerDownloadService.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerDownloadService.cs index b793b07..98623dc 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerDownloadService.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerDownloadService.cs @@ -45,6 +45,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services _httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(10) }; } + private static readonly TimeSpan YtDlpRetryDelay = TimeSpan.FromSeconds(15); + public void Dispose() => _httpClient.Dispose(); /// @@ -66,6 +68,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services if (await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct).ConfigureAwait(false)) return true; + // YouTube answers back-to-back downloads with occasional HTTP 403s that clear on + // their own. One more try after a pause usually gets full quality, where the + // built-in fallback may only be offered a 360p stream. + _logger.LogWarning("|CinemaTrailers4Jellyfins| yt-dlp failed for {Key}; retrying in {Seconds}s.", youtubeKey, YtDlpRetryDelay.TotalSeconds); + await Task.Delay(YtDlpRetryDelay, ct).ConfigureAwait(false); + if (await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct).ConfigureAwait(false)) + return true; + _logger.LogWarning("|CinemaTrailers4Jellyfins| yt-dlp failed for {Key}; falling back to the built-in downloader.", youtubeKey); } diff --git a/build.yaml b/build.yaml index 8d643ea..f7b2dba 100644 --- a/build.yaml +++ b/build.yaml @@ -1,5 +1,5 @@ --- -version: 1.0.0.16 +version: 1.0.0.17 name: CinemaTrailers4Jellyfins guid: b581493e-1046-40ed-b6dc-cb8027624984 description: > @@ -12,9 +12,14 @@ category: General owner: 514mart targetAbi: 10.11.0.0 changelog: - - The built-in downloader no longer picks YouTube's 360p combined file - when separate video and audio streams give a better picture. This - mostly showed up when yt-dlp failed and the built-in downloader took over + - New "Discover" source for TV shows (on by default) and movies (off by + default). It asks TMDB for the newest titles in your selected + languages, most popular first. The TV lists (Popular, Top Rated, On The + Air, Airing Today) are mostly long-running shows that the date range + drops, so very few TV trailers were found before + - When yt-dlp fails (often a passing HTTP 403 from YouTube), it is tried + once more after 15 seconds before the built-in downloader takes over, + which YouTube sometimes only offers 360p dotnetProjects: - name: Jellyfin.Plugin.CinemaTrailers4Jellyfins