Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1de082b96e | ||
|
|
45405c815d |
@@ -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
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
"owner": "514mart",
|
"owner": "514mart",
|
||||||
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
|
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"checksum": "5329a3a0daf9f82849b441ca2784f622",
|
||||||
|
"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\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.16/cinematrailers4jellyfins_1.0.0.16.zip",
|
||||||
|
"timestamp": "2026-10-06T01:40:16Z",
|
||||||
|
"version": "1.0.0.16"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksum": "211f58d992857f9e91263293306fbd0d",
|
"checksum": "211f58d992857f9e91263293306fbd0d",
|
||||||
"changelog": "- Trailer Languages now also checks the original language of the movie or show. Before, only the trailer's own language tag was checked, and TMDB often tags subtitled foreign trailers as English, so Korean, Chinese or Japanese trailers got through\n",
|
"changelog": "- Trailer Languages now also checks the original language of the movie or show. Before, only the trailer's own language tag was checked, and TMDB often tags subtitled foreign trailers as English, so Korean, Chinese or Japanese trailers got through\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user