Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174578e8d6 | ||
|
|
1de082b96e | ||
|
|
45405c815d | ||
|
|
d6280f3ce8 | ||
|
|
43e002fcdd | ||
|
|
fb75fa5a07 | ||
|
|
2664ab4d5a | ||
|
|
86d8e2a05b | ||
|
|
c013416473 | ||
|
|
e43e3f7245 | ||
|
|
4145e1f1dd | ||
|
|
6002ce7b46 | ||
|
|
d850e12964 | ||
|
|
205d64dc6f | ||
|
|
322d6cc65d | ||
|
|
c208a81b75 |
@@ -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 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@
|
|||||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||||
<legend><h3 class="sectionTitle">Trailer Languages</h3></legend>
|
<legend><h3 class="sectionTitle">Trailer Languages</h3></legend>
|
||||||
<p class="fieldDescription">
|
<p class="fieldDescription">
|
||||||
Only download trailers in the selected languages.
|
Only download trailers in the selected languages. Both the trailer and the
|
||||||
|
movie/show itself (its original language) must be in one of them, so a
|
||||||
|
subtitled foreign trailer is skipped.
|
||||||
Leave everything unchecked to allow all languages.
|
Leave everything unchecked to allow all languages.
|
||||||
</p>
|
</p>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 2em;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 2em;">
|
||||||
@@ -118,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 -->
|
||||||
@@ -167,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 -->
|
||||||
@@ -430,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;
|
||||||
@@ -466,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.10</AssemblyVersion>
|
<AssemblyVersion>1.0.0.17</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.10</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>
|
||||||
|
|||||||
@@ -177,6 +177,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A trailer's own language tag is often "en" just because it has English
|
||||||
|
// subtitles, so also require the title itself to be in an allowed language.
|
||||||
|
if (allowedLanguages != null && !allowedLanguages.Contains(movie.OriginalLanguage))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("|CinemaTrailers4Jellyfins| Skipping '{Title}': original language '{Lang}' is not allowed", movie.Title, movie.OriginalLanguage);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var trailers = await _tmdbService.GetTrailersAsync(
|
var trailers = await _tmdbService.GetTrailersAsync(
|
||||||
"movie", movie.Id.ToString(), config.TmdbApiKey, allowedLanguages, cancellationToken).ConfigureAwait(false);
|
"movie", movie.Id.ToString(), config.TmdbApiKey, allowedLanguages, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -217,7 +225,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
var metadata = await _tmdbService.GetMediaMetadataAsync(
|
var metadata = await _tmdbService.GetMediaMetadataAsync(
|
||||||
"movie", movie.Id.ToString(), config.TmdbApiKey, cancellationToken).ConfigureAwait(false);
|
"movie", movie.Id.ToString(), config.TmdbApiKey, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
_fakeMovieService.WriteNfo(paths.NfoPath, movie.Title, movie.Year, metadata.Genres, metadata.Certification);
|
_fakeMovieService.WriteNfo(paths.NfoPath, movie.Title, movie.Year, metadata.Genres, metadata.Certification,
|
||||||
|
metadata.IsAnime ? new[] { TrailerTags.Anime } : null);
|
||||||
|
|
||||||
downloaded++;
|
downloaded++;
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
@@ -283,6 +292,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A trailer's own language tag is often "en" just because it has English
|
||||||
|
// subtitles, so also require the title itself to be in an allowed language.
|
||||||
|
if (allowedLanguages != null && !allowedLanguages.Contains(show.OriginalLanguage))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("|CinemaTrailers4Jellyfins| Skipping '{Title}': original language '{Lang}' is not allowed", show.Title, show.OriginalLanguage);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var trailers = await _tmdbService.GetTrailersAsync(
|
var trailers = await _tmdbService.GetTrailersAsync(
|
||||||
"tv", show.Id.ToString(), config.TmdbApiKey, allowedLanguages, cancellationToken).ConfigureAwait(false);
|
"tv", show.Id.ToString(), config.TmdbApiKey, allowedLanguages, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -323,7 +340,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
var metadata = await _tmdbService.GetMediaMetadataAsync(
|
var metadata = await _tmdbService.GetMediaMetadataAsync(
|
||||||
"tv", show.Id.ToString(), config.TmdbApiKey, cancellationToken).ConfigureAwait(false);
|
"tv", show.Id.ToString(), config.TmdbApiKey, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
_fakeMovieService.WriteNfo(paths.NfoPath, show.Title, show.Year, metadata.Genres, metadata.Certification, new[] { TrailerTags.TvShow });
|
_fakeMovieService.WriteNfo(paths.NfoPath, show.Title, show.Year, metadata.Genres, metadata.Certification,
|
||||||
|
metadata.IsAnime ? new[] { TrailerTags.TvShow, TrailerTags.Anime } : new[] { TrailerTags.TvShow });
|
||||||
|
|
||||||
downloaded++;
|
downloaded++;
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
{
|
{
|
||||||
public record TmdbVideo(string Key, string Name, string Language, bool Official, int Size);
|
public record TmdbVideo(string Key, string Name, string Language, bool Official, int Size);
|
||||||
|
|
||||||
public record TmdbMediaResult(int Id, string Title, string ReleaseDate)
|
public record TmdbMediaResult(int Id, string Title, string ReleaseDate, string OriginalLanguage)
|
||||||
{
|
{
|
||||||
public int? Year => DateTime.TryParse(ReleaseDate, out var d) ? d.Year : (int?)null;
|
public int? Year => DateTime.TryParse(ReleaseDate, out var d) ? d.Year : (int?)null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TmdbMediaMetadata(IReadOnlyList<string> Genres, string? Certification);
|
public record TmdbMediaMetadata(IReadOnlyList<string> Genres, string? Certification, bool IsAnime);
|
||||||
|
|
||||||
public class TmdbService : IDisposable
|
public class TmdbService : IDisposable
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
@@ -170,6 +197,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
var releaseDate = entry.TryGetProperty(dateField, out var rd) ? rd.GetString() ?? string.Empty : string.Empty;
|
var releaseDate = entry.TryGetProperty(dateField, out var rd) ? rd.GetString() ?? string.Empty : string.Empty;
|
||||||
var title = entry.TryGetProperty(titleField, out var t) ? t.GetString() ?? string.Empty : string.Empty;
|
var title = entry.TryGetProperty(titleField, out var t) ? t.GetString() ?? string.Empty : string.Empty;
|
||||||
var id = entry.GetProperty("id").GetInt32();
|
var id = entry.GetProperty("id").GetInt32();
|
||||||
|
var originalLanguage = entry.TryGetProperty("original_language", out var ol) ? ol.GetString() ?? string.Empty : string.Empty;
|
||||||
|
|
||||||
if (releasedAfter.HasValue && DateTime.TryParse(releaseDate, out var parsed))
|
if (releasedAfter.HasValue && DateTime.TryParse(releaseDate, out var parsed))
|
||||||
{
|
{
|
||||||
@@ -177,7 +205,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
anyInRange = true;
|
anyInRange = true;
|
||||||
results.Add(new TmdbMediaResult(id, title, releaseDate));
|
results.Add(new TmdbMediaResult(id, title, releaseDate, originalLanguage));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page >= totalPages || (releasedAfter.HasValue && !anyInRange))
|
if (page >= totalPages || (releasedAfter.HasValue && !anyInRange))
|
||||||
@@ -189,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +264,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var appendField = mediaType == "movie" ? "release_dates" : "content_ratings";
|
var appendField = mediaType == "movie" ? "release_dates" : "content_ratings";
|
||||||
var url = $"{BaseUrl}/{mediaType}/{tmdbId}?append_to_response={appendField}&language=en-US";
|
var url = $"{BaseUrl}/{mediaType}/{tmdbId}?append_to_response={appendField},keywords&language=en-US";
|
||||||
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);
|
||||||
@@ -262,16 +290,40 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
? GetMovieCertification(doc)
|
? GetMovieCertification(doc)
|
||||||
: GetTvCertification(doc);
|
: GetTvCertification(doc);
|
||||||
|
|
||||||
return new TmdbMediaMetadata(genres.AsReadOnly(), certification);
|
return new TmdbMediaMetadata(genres.AsReadOnly(), certification, IsAnime(doc, mediaType, genres));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| GetMediaMetadata failed for {MediaType} TMDB ID {Id}", mediaType, tmdbId);
|
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| GetMediaMetadata failed for {MediaType} TMDB ID {Id}", mediaType, tmdbId);
|
||||||
return new TmdbMediaMetadata(Array.Empty<string>(), null);
|
return new TmdbMediaMetadata(Array.Empty<string>(), null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string[] AnimeKeywords = { "anime", "based on manga" };
|
||||||
|
private static readonly string[] AnimeLanguages = { "ja", "zh", "ko" };
|
||||||
|
|
||||||
|
// Anime vs. Western animation: both carry TMDB's "Animation" genre, so use the "anime" /
|
||||||
|
// "based on manga" keywords, or Animation made in Japanese, Chinese or Korean.
|
||||||
|
private static bool IsAnime(JsonDocument doc, string mediaType, List<string> genres)
|
||||||
|
{
|
||||||
|
// Movie keywords live under keywords.keywords, TV keywords under keywords.results.
|
||||||
|
var listName = mediaType == "movie" ? "keywords" : "results";
|
||||||
|
if (doc.RootElement.TryGetProperty("keywords", out var kw)
|
||||||
|
&& kw.TryGetProperty(listName, out var list)
|
||||||
|
&& list.ValueKind == JsonValueKind.Array
|
||||||
|
&& list.EnumerateArray().Any(k =>
|
||||||
|
k.TryGetProperty("name", out var n)
|
||||||
|
&& AnimeKeywords.Contains(n.GetString(), StringComparer.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return genres.Contains("Animation", StringComparer.OrdinalIgnoreCase)
|
||||||
|
&& doc.RootElement.TryGetProperty("original_language", out var lang)
|
||||||
|
&& AnimeLanguages.Contains(lang.GetString(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
// Finds the US theatrical certification (G, PG, PG-13, R, NC-17) from release_dates.
|
// Finds the US theatrical certification (G, PG, PG-13, R, NC-17) from release_dates.
|
||||||
private static string? GetMovieCertification(JsonDocument doc)
|
private static string? GetMovieCertification(JsonDocument doc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -63,8 +65,18 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(ytDlpPath) && File.Exists(ytDlpPath))
|
if (!string.IsNullOrWhiteSpace(ytDlpPath) && File.Exists(ytDlpPath))
|
||||||
{
|
{
|
||||||
return await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct)
|
if (await DownloadWithYtDlpAsync(youtubeKey, outputPath, preferredHeight, ytDlpPath, ct).ConfigureAwait(false))
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await DownloadWithYoutubeExplodeAsync(youtubeKey, outputPath, preferredHeight, ct)
|
return await DownloadWithYoutubeExplodeAsync(youtubeKey, outputPath, preferredHeight, ct)
|
||||||
@@ -88,31 +100,37 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
.GetManifestAsync($"https://www.youtube.com/watch?v={key}", ct)
|
.GetManifestAsync($"https://www.youtube.com/watch?v={key}", ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// Muxed streams include audio+video in one file. YouTube rarely serves them
|
// Best video-only stream at or below the height limit. MP4 (H.264) is preferred for
|
||||||
// anymore, but use one when it exists since it needs no merge.
|
// client compatibility; any container is the fallback.
|
||||||
|
var videoStreams = manifest.GetVideoOnlyStreams().ToList();
|
||||||
|
var audioStreams = manifest.GetAudioOnlyStreams().ToList();
|
||||||
|
var mp4Video = videoStreams.Where(s => s.Container == Container.Mp4).ToList();
|
||||||
|
var video = videoStreams.Count > 0
|
||||||
|
? PickByHeight(mp4Video.Count > 0 ? mp4Video : videoStreams, preferredHeight)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Muxed streams (audio+video in one file) need no merge, but YouTube only serves
|
||||||
|
// them at 360p. Use one only when the separate streams can't do better.
|
||||||
var muxedStreams = manifest.GetMuxedStreams().ToList();
|
var muxedStreams = manifest.GetMuxedStreams().ToList();
|
||||||
if (muxedStreams.Count > 0)
|
if (muxedStreams.Count > 0)
|
||||||
{
|
{
|
||||||
var muxed = PickByHeight(muxedStreams, preferredHeight);
|
var muxed = PickByHeight(muxedStreams, preferredHeight);
|
||||||
|
if (video == null || audioStreams.Count == 0
|
||||||
|
|| muxed.VideoQuality.MaxHeight >= video.VideoQuality.MaxHeight)
|
||||||
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"|CinemaTrailers4Jellyfins| Downloading {Key} at {Quality} (muxed) to {Path}",
|
"|CinemaTrailers4Jellyfins| Downloading {Key} at {Quality} (muxed) to {Path}",
|
||||||
key, muxed.VideoQuality.Label, outputPath);
|
key, muxed.VideoQuality.Label, outputPath);
|
||||||
await youtube.Videos.Streams.DownloadAsync(muxed, outputPath, cancellationToken: ct).ConfigureAwait(false);
|
await youtube.Videos.Streams.DownloadAsync(muxed, outputPath, cancellationToken: ct).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise download the best video-only and audio-only streams and merge them.
|
if (video == null || audioStreams.Count == 0)
|
||||||
// MP4 (H.264/AAC) is preferred for client compatibility; any container is the fallback.
|
|
||||||
var videoStreams = manifest.GetVideoOnlyStreams().ToList();
|
|
||||||
var audioStreams = manifest.GetAudioOnlyStreams().ToList();
|
|
||||||
if (videoStreams.Count == 0 || audioStreams.Count == 0)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("|CinemaTrailers4Jellyfins| No usable video/audio streams for {Key}.", key);
|
_logger.LogWarning("|CinemaTrailers4Jellyfins| No usable video/audio streams for {Key}.", key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mp4Video = videoStreams.Where(s => s.Container == Container.Mp4).ToList();
|
|
||||||
var video = PickByHeight(mp4Video.Count > 0 ? mp4Video : videoStreams, preferredHeight);
|
|
||||||
var mp4Audio = audioStreams.Where(s => s.Container == Container.Mp4).ToList();
|
var mp4Audio = audioStreams.Where(s => s.Container == Container.Mp4).ToList();
|
||||||
var audio = (mp4Audio.Count > 0 ? mp4Audio : audioStreams).GetWithHighestBitrate();
|
var audio = (mp4Audio.Count > 0 ? mp4Audio : audioStreams).GetWithHighestBitrate();
|
||||||
|
|
||||||
@@ -224,13 +242,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Format selects the best video at or below preferredHeight merged with the best audio.
|
// Format selects the best video at or below preferredHeight merged with the best audio,
|
||||||
// --merge-output-format mp4 ensures the output is always an mp4.
|
// preferring H.264 + AAC (plays or hardware-transcodes on every client; VP9/AV1 often
|
||||||
|
// need a CPU transcode). --merge-output-format mp4 ensures the output is always an mp4.
|
||||||
var args = string.Join(" ",
|
var args = string.Join(" ",
|
||||||
$"-f \"bestvideo[height<={preferredHeight}]+bestaudio/best[height<={preferredHeight}]\"",
|
$"-f \"bestvideo[height<={preferredHeight}][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[height<={preferredHeight}]+bestaudio/best[height<={preferredHeight}]\"",
|
||||||
"--merge-output-format mp4",
|
"--merge-output-format mp4",
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
"--no-warnings",
|
"--no-warnings",
|
||||||
|
"--no-progress",
|
||||||
|
$"--ffmpeg-location \"{FfmpegLocator.Path}\"",
|
||||||
$"-o \"{outputPath}\"",
|
$"-o \"{outputPath}\"",
|
||||||
$"\"https://www.youtube.com/watch?v={key}\"");
|
$"\"https://www.youtube.com/watch?v={key}\"");
|
||||||
|
|
||||||
@@ -252,11 +273,15 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
// Drain both pipes while yt-dlp runs so a full buffer can never block it.
|
||||||
|
var stdoutTask = process.StandardOutput.ReadToEndAsync(ct);
|
||||||
|
var stderrTask = process.StandardError.ReadToEndAsync(ct);
|
||||||
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
||||||
|
await stdoutTask.ConfigureAwait(false);
|
||||||
|
var stderr = await stderrTask.ConfigureAwait(false);
|
||||||
|
|
||||||
if (process.ExitCode != 0)
|
if (process.ExitCode != 0)
|
||||||
{
|
{
|
||||||
var stderr = await process.StandardError.ReadToEndAsync(ct).ConfigureAwait(false);
|
|
||||||
_logger.LogError("|CinemaTrailers4Jellyfins| yt-dlp exited with code {Code} for {Key}: {Error}",
|
_logger.LogError("|CinemaTrailers4Jellyfins| yt-dlp exited with code {Code} for {Key}: {Error}",
|
||||||
process.ExitCode, key, stderr);
|
process.ExitCode, key, stderr);
|
||||||
return false;
|
return false;
|
||||||
@@ -273,6 +298,32 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| yt-dlp download failed for {Key}", key);
|
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| yt-dlp download failed for {Key}", key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteYtDlpLeftovers(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// yt-dlp names its separate/partial downloads "<output name>.f137.mp4.part", ".f140.m4a",
|
||||||
|
// ".ytdl", ... It removes them after a successful merge, but not when it fails. Everything
|
||||||
|
// sharing the output's "<name>." prefix (other than the output itself) is one of those.
|
||||||
|
private void DeleteYtDlpLeftovers(string outputPath)
|
||||||
|
{
|
||||||
|
var folder = Path.GetDirectoryName(outputPath);
|
||||||
|
if (folder == null || !Directory.Exists(folder))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var outputName = Path.GetFileName(outputPath);
|
||||||
|
var prefix = Path.GetFileNameWithoutExtension(outputPath) + ".";
|
||||||
|
foreach (var file in Directory.EnumerateFiles(folder))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(file);
|
||||||
|
if (name.StartsWith(prefix, StringComparison.Ordinal)
|
||||||
|
&& !string.Equals(name, outputName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
TryDeleteFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,9 +147,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Apply enabled filters. If nothing survives, fall back to the full pool.
|
// Anime trailers only play before anime. Applied before the fallback below, so
|
||||||
var filtered = ApplyFilters(pairs, feature, config);
|
// non-anime content never gets one — it gets no trailer instead. Western animation
|
||||||
var pool = filtered.Count > 0 ? filtered : pairs;
|
// is not affected and follows the normal filters.
|
||||||
|
var eligible = IsAnimeFeature(feature)
|
||||||
|
? pairs
|
||||||
|
: pairs.Where(p => !IsAnimeTrailer(p.Movie)).ToList();
|
||||||
|
|
||||||
|
// Apply enabled filters. If nothing survives, fall back to the eligible pool.
|
||||||
|
var filtered = ApplyFilters(eligible, feature, config);
|
||||||
|
var pool = filtered.Count > 0 ? filtered : eligible;
|
||||||
|
|
||||||
// Prefer unseen trailers within the pool; reset the cycle when all have been shown.
|
// Prefer unseen trailers within the pool; reset the cycle when all have been shown.
|
||||||
pool = ApplyAvoidRepeats(pool, user.Id, config.AvoidRepeats);
|
pool = ApplyAvoidRepeats(pool, user.Id, config.AvoidRepeats);
|
||||||
@@ -229,6 +236,20 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Library items: anime providers (e.g. AniDB) set the "Anime" genre; TMDB-scraped items
|
||||||
|
// carry TMDB keywords as tags ("anime", "based on manga"). "Animation" alone is not enough —
|
||||||
|
// it also covers Western animation.
|
||||||
|
private static readonly string[] AnimeFeatureTags = { "anime", "based on manga" };
|
||||||
|
|
||||||
|
private static bool IsAnimeFeature(BaseItem item)
|
||||||
|
=> item.Genres.Contains("Anime", StringComparer.OrdinalIgnoreCase)
|
||||||
|
|| item.Tags.Any(t => AnimeFeatureTags.Contains(t, StringComparer.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// Trailers are tagged at download time from TMDB data (see TmdbService.IsAnime).
|
||||||
|
private static bool IsAnimeTrailer(BaseItem item)
|
||||||
|
=> item.Tags.Contains(TrailerTags.Anime, StringComparer.OrdinalIgnoreCase)
|
||||||
|
|| item.Genres.Contains("Anime", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
||||||
List<(Movie Movie, BaseItem Trailer)> pairs,
|
List<(Movie Movie, BaseItem Trailer)> pairs,
|
||||||
BaseItem feature,
|
BaseItem feature,
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tags written into each downloaded trailer's NFO so <see cref="TrailerIntroProvider"/>
|
/// Tags written into each downloaded trailer's NFO so <see cref="TrailerIntroProvider"/>
|
||||||
/// can tell movie trailers and TV show trailers apart within the shared output folder.
|
/// can tell movie trailers, TV show trailers and anime trailers apart within the shared output folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class TrailerTags
|
public static class TrailerTags
|
||||||
{
|
{
|
||||||
/// <summary>Marks a fake-movie folder as containing a TV show trailer.</summary>
|
/// <summary>Marks a fake-movie folder as containing a TV show trailer.</summary>
|
||||||
public const string TvShow = "ct4j-tvshow";
|
public const string TvShow = "ct4j-tvshow";
|
||||||
|
|
||||||
|
/// <summary>Marks a trailer as anime, so it only plays before anime content.</summary>
|
||||||
|
public const string Anime = "ct4j-anime";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
build.yaml
17
build.yaml
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: 1.0.0.10
|
version: 1.0.0.17
|
||||||
name: CinemaTrailers4Jellyfins
|
name: CinemaTrailers4Jellyfins
|
||||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||||
description: >
|
description: >
|
||||||
@@ -12,13 +12,14 @@ category: General
|
|||||||
owner: 514mart
|
owner: 514mart
|
||||||
targetAbi: 10.11.0.0
|
targetAbi: 10.11.0.0
|
||||||
changelog:
|
changelog:
|
||||||
- Fix trailer downloads, broken by a YouTube change. YouTube no longer
|
- New "Discover" source for TV shows (on by default) and movies (off by
|
||||||
serves combined video+audio files, so the built-in downloader now fetches
|
default). It asks TMDB for the newest titles in your selected
|
||||||
the video and audio separately, merges them with ffmpeg (no re-encode)
|
languages, most popular first. The TV lists (Popular, Top Rated, On The
|
||||||
and deletes the separate files afterwards
|
Air, Airing Today) are mostly long-running shows that the date range
|
||||||
- A failed download no longer leaves an empty trailer folder behind.
|
drops, so very few TV trailers were found before
|
||||||
Empty folders counted toward "Max trailers to keep" (pushing out real
|
- When yt-dlp fails (often a passing HTTP 403 from YouTube), it is tried
|
||||||
trailers) and were never retried
|
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,70 @@
|
|||||||
"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": "ac1b7e8c353e308a556ed76687c8d1e9",
|
||||||
|
"changelog": "- 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\n- 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\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.17/cinematrailers4jellyfins_1.0.0.17.zip",
|
||||||
|
"timestamp": "2026-10-06T04:00:50Z",
|
||||||
|
"version": "1.0.0.17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.15/cinematrailers4jellyfins_1.0.0.15.zip",
|
||||||
|
"timestamp": "2026-10-05T22:28:03Z",
|
||||||
|
"version": "1.0.0.15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "10ecbc35b84e98bade61576bbfc41864",
|
||||||
|
"changelog": "- When yt-dlp fails part-way, its partial download files (.part, .ytdl, separate video/audio) are now deleted instead of being left in the trailer folder\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.14/cinematrailers4jellyfins_1.0.0.14.zip",
|
||||||
|
"timestamp": "2026-10-05T22:19:38Z",
|
||||||
|
"version": "1.0.0.14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "f1fe201e7b0238fa432ae7fb5dbb1844",
|
||||||
|
"changelog": "- yt-dlp now prefers H.264 + AAC (plays or hardware-transcodes on every client; VP9/AV1 often needed a CPU transcode) and uses Jellyfin's own ffmpeg for the merge\n- If yt-dlp fails, the built-in downloader is tried instead\n- Fix a possible hang. yt-dlp's output is now read while it runs, so a full output buffer can no longer block it\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.13/cinematrailers4jellyfins_1.0.0.13.zip",
|
||||||
|
"timestamp": "2026-10-05T22:11:03Z",
|
||||||
|
"version": "1.0.0.13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "d7a565b0e4d47effb9db40fb44847920",
|
||||||
|
"changelog": "- Anime trailers now only play before anime. Western animation (Pixar, Disney, ...) is no longer blocked and follows the normal genre filter again. A trailer counts as anime when TMDB tags it \"anime\" or \"based on manga\", or it is animation in Japanese, Chinese or Korean; it is marked with a ct4j-anime tag in its NFO at download time. Your anime is recognised by the \"Anime\" genre or the same TMDB keywords\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.12/cinematrailers4jellyfins_1.0.0.12.zip",
|
||||||
|
"timestamp": "2026-10-05T21:57:24Z",
|
||||||
|
"version": "1.0.0.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "ccb45ef4bda23b245e47f6226e3c06fc",
|
||||||
|
"changelog": "- Animated trailers (anime included) now only play before animated content. Before, an anime trailer could play before a live-action movie or show when they shared a genre like Drama or Action, or when no trailer matched the genre filter. A live-action feature now gets no trailer rather than an animated one\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.11/cinematrailers4jellyfins_1.0.0.11.zip",
|
||||||
|
"timestamp": "2026-10-05T21:52:47Z",
|
||||||
|
"version": "1.0.0.11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "13e6467ce568b1abe987ef5a8a0ee95d",
|
||||||
|
"changelog": "- Fix trailer downloads, broken by a YouTube change. YouTube no longer serves combined video+audio files, so the built-in downloader now fetches the video and audio separately, merges them with ffmpeg (no re-encode) and deletes the separate files afterwards\n- A failed download no longer leaves an empty trailer folder behind. Empty folders counted toward \"Max trailers to keep\" (pushing out real trailers) and were never retried\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.10/cinematrailers4jellyfins_1.0.0.10.zip",
|
||||||
|
"timestamp": "2026-10-05T21:42:55Z",
|
||||||
|
"version": "1.0.0.10"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksum": "9594d14a5fed94ee5eea55d72c71728c",
|
"checksum": "9594d14a5fed94ee5eea55d72c71728c",
|
||||||
"changelog": "- Skip trailers and pre-roll/feature pre-roll bumpers the current user can't actually access (e.g. a library not in their library access list) \u2014 some Cinema Mode clients hang indefinitely trying to play an inaccessible item, so these are now filtered out per-user instead\n",
|
"changelog": "- Skip trailers and pre-roll/feature pre-roll bumpers the current user can't actually access (e.g. a library not in their library access list) \u2014 some Cinema Mode clients hang indefinitely trying to play an inaccessible item, so these are now filtered out per-user instead\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user