feat: TMDB Discover source + retry yt-dlp once before fallback (v1.0.0.17)
All checks were successful
Publish Release / release (push) Successful in 14s
All checks were successful
Publish Release / release (push) Successful in 14s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration
|
|||||||
public bool SourceUpcoming { get; set; } = true;
|
public bool SourceUpcoming { get; set; } = true;
|
||||||
public bool SourcePopular { get; set; } = false;
|
public bool SourcePopular { get; set; } = false;
|
||||||
public bool SourceTopRated { 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) ───────────────────────────────────────────────
|
// ── Sources (TV Shows) ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -21,6 +23,9 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration
|
|||||||
public bool SourceTvOnTheAir { get; set; } = true;
|
public bool SourceTvOnTheAir { get; set; } = true;
|
||||||
public bool SourceTvPopular { get; set; } = false;
|
public bool SourceTvPopular { get; set; } = false;
|
||||||
public bool SourceTvTopRated { 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 ────────────────────────────────────────────────────────
|
// ── Date Range ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,16 @@
|
|||||||
Highest rated movies on TMDB, filtered by the date range below.
|
Highest rated movies on TMDB, filtered by the date range below.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="source-discover" />
|
||||||
|
<span>Discover</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">
|
||||||
|
Newest movies in your selected languages, most popular first.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- TV Show Sources -->
|
<!-- TV Show Sources -->
|
||||||
@@ -169,6 +179,16 @@
|
|||||||
Highest rated TV shows on TMDB, filtered by the date range below.
|
Highest rated TV shows on TMDB, filtered by the date range below.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="source-tv-discover" />
|
||||||
|
<span>Discover (recommended)</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">
|
||||||
|
Newest TV shows in your selected languages, most popular first. The lists above are mostly long-running shows that the date range drops.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Date Range -->
|
<!-- Date Range -->
|
||||||
@@ -432,10 +452,12 @@
|
|||||||
document.getElementById('source-upcoming').checked = config.SourceUpcoming !== false;
|
document.getElementById('source-upcoming').checked = config.SourceUpcoming !== false;
|
||||||
document.getElementById('source-popular').checked = !!config.SourcePopular;
|
document.getElementById('source-popular').checked = !!config.SourcePopular;
|
||||||
document.getElementById('source-top-rated').checked = !!config.SourceTopRated;
|
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-airing-today').checked = config.SourceTvAiringToday !== false;
|
||||||
document.getElementById('source-tv-on-the-air').checked = config.SourceTvOnTheAir !== false;
|
document.getElementById('source-tv-on-the-air').checked = config.SourceTvOnTheAir !== false;
|
||||||
document.getElementById('source-tv-popular').checked = !!config.SourceTvPopular;
|
document.getElementById('source-tv-popular').checked = !!config.SourceTvPopular;
|
||||||
document.getElementById('source-tv-top-rated').checked = !!config.SourceTvTopRated;
|
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('date-range').value = String(config.ReleaseDateRangeMonths ?? 6);
|
||||||
document.getElementById('download-folder').value = config.DownloadFolder || '';
|
document.getElementById('download-folder').value = config.DownloadFolder || '';
|
||||||
document.getElementById('max-trailers').value = config.MaxTrailersToDownload ?? 20;
|
document.getElementById('max-trailers').value = config.MaxTrailersToDownload ?? 20;
|
||||||
@@ -468,10 +490,12 @@
|
|||||||
config.SourceUpcoming = document.getElementById('source-upcoming').checked;
|
config.SourceUpcoming = document.getElementById('source-upcoming').checked;
|
||||||
config.SourcePopular = document.getElementById('source-popular').checked;
|
config.SourcePopular = document.getElementById('source-popular').checked;
|
||||||
config.SourceTopRated = document.getElementById('source-top-rated').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.SourceTvAiringToday = document.getElementById('source-tv-airing-today').checked;
|
||||||
config.SourceTvOnTheAir = document.getElementById('source-tv-on-the-air').checked;
|
config.SourceTvOnTheAir = document.getElementById('source-tv-on-the-air').checked;
|
||||||
config.SourceTvPopular = document.getElementById('source-tv-popular').checked;
|
config.SourceTvPopular = document.getElementById('source-tv-popular').checked;
|
||||||
config.SourceTvTopRated = document.getElementById('source-tv-top-rated').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.ReleaseDateRangeMonths = parseInt(document.getElementById('date-range').value, 10);
|
||||||
config.DownloadFolder = document.getElementById('download-folder').value;
|
config.DownloadFolder = document.getElementById('download-folder').value;
|
||||||
config.MaxTrailersToDownload = parseIntOrDefault(document.getElementById('max-trailers').value, 20);
|
config.MaxTrailersToDownload = parseIntOrDefault(document.getElementById('max-trailers').value, 20);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
|
||||||
<AssemblyVersion>1.0.0.16</AssemblyVersion>
|
<AssemblyVersion>1.0.0.17</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.16</FileVersion>
|
<FileVersion>1.0.0.17</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
if (config.SourceUpcoming) sources.Add("upcoming");
|
if (config.SourceUpcoming) sources.Add("upcoming");
|
||||||
if (config.SourcePopular) sources.Add("popular");
|
if (config.SourcePopular) sources.Add("popular");
|
||||||
if (config.SourceTopRated) sources.Add("top_rated");
|
if (config.SourceTopRated) sources.Add("top_rated");
|
||||||
|
if (config.SourceDiscover) sources.Add(DiscoverSource);
|
||||||
|
|
||||||
return GetCandidatesAsync("movie", sources, config, ct);
|
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.SourceTvOnTheAir) sources.Add("on_the_air");
|
||||||
if (config.SourceTvPopular) sources.Add("popular");
|
if (config.SourceTvPopular) sources.Add("popular");
|
||||||
if (config.SourceTvTopRated) sources.Add("top_rated");
|
if (config.SourceTvTopRated) sources.Add("top_rated");
|
||||||
|
if (config.SourceTvDiscover) sources.Add(DiscoverSource);
|
||||||
|
|
||||||
return GetCandidatesAsync("tv", sources, config, ct);
|
return GetCandidatesAsync("tv", sources, config, ct);
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,10 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
|
|
||||||
foreach (var endpoint in sourceEndpoints)
|
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);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
@@ -133,9 +138,31 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
return results;
|
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<string> { "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<List<TmdbMediaResult>> FetchSourcePagesAsync(
|
private async Task<List<TmdbMediaResult>> FetchSourcePagesAsync(
|
||||||
string mediaType,
|
string mediaType,
|
||||||
string endpoint,
|
string path,
|
||||||
string apiKey,
|
string apiKey,
|
||||||
DateTime? releasedAfter,
|
DateTime? releasedAfter,
|
||||||
int maxPages,
|
int maxPages,
|
||||||
@@ -153,7 +180,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
|
|
||||||
try
|
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);
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
ApplyAuth(request, apiKey);
|
ApplyAuth(request, apiKey);
|
||||||
using var response = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
|
using var response = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
|
||||||
@@ -190,7 +217,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
_httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(10) };
|
_httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(10) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly TimeSpan YtDlpRetryDelay = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
public void Dispose() => _httpClient.Dispose();
|
public void Dispose() => _httpClient.Dispose();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -66,6 +68,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
if (await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct).ConfigureAwait(false))
|
if (await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct).ConfigureAwait(false))
|
||||||
return true;
|
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);
|
_logger.LogWarning("|CinemaTrailers4Jellyfins| yt-dlp failed for {Key}; falling back to the built-in downloader.", youtubeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
build.yaml
13
build.yaml
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: 1.0.0.16
|
version: 1.0.0.17
|
||||||
name: CinemaTrailers4Jellyfins
|
name: CinemaTrailers4Jellyfins
|
||||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||||
description: >
|
description: >
|
||||||
@@ -12,9 +12,14 @@ category: General
|
|||||||
owner: 514mart
|
owner: 514mart
|
||||||
targetAbi: 10.11.0.0
|
targetAbi: 10.11.0.0
|
||||||
changelog:
|
changelog:
|
||||||
- The built-in downloader no longer picks YouTube's 360p combined file
|
- New "Discover" source for TV shows (on by default) and movies (off by
|
||||||
when separate video and audio streams give a better picture. This
|
default). It asks TMDB for the newest titles in your selected
|
||||||
mostly showed up when yt-dlp failed and the built-in downloader took over
|
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:
|
dotnetProjects:
|
||||||
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||||
|
|||||||
Reference in New Issue
Block a user