4 Commits

Author SHA1 Message Date
Martin
d850e12964 feat: restrict anime trailers, not all animation (v1.0.0.12)
All checks were successful
Publish Release / release (push) Successful in 9s
v1.0.0.11 kept every animated trailer away from non-animated content, which
also blocked Western animation (Pixar, Disney...). Only anime should be
restricted.

At download, a trailer is tagged ct4j-anime when TMDB has the "anime" or
"based on manga" keyword, or it is Animation in ja/zh/ko (keywords are now
appended to the details request). At playback, anime trailers are dropped
unless the feature is anime (genre "Anime", or those keywords as tags).
Western animation follows the normal genre/rating filters again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:57:12 -04:00
gitea-actions
205d64dc6f chore: update manifest.json for v1.0.0.11 2026-10-05 21:52:47 +00:00
Martin
322d6cc65d feat: only play animated trailers before animated content (v1.0.0.11)
All checks were successful
Publish Release / release (push) Successful in 10s
An anime trailer could play before live-action content: the genre filter
accepts any single shared genre (anime is tagged Animation + Drama/Action/...),
and when nothing matched it fell back to the whole pool. Trailers tagged
Animation or Anime are now dropped for non-animated features before the
filters and the fallback run, so a live-action feature gets no trailer
rather than an animated one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:52:33 -04:00
gitea-actions
c208a81b75 chore: update manifest.json for v1.0.0.10 2026-10-05 21:42:55 +00:00
7 changed files with 85 additions and 20 deletions

View File

@@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
<AssemblyVersion>1.0.0.10</AssemblyVersion>
<FileVersion>1.0.0.10</FileVersion>
<AssemblyVersion>1.0.0.12</AssemblyVersion>
<FileVersion>1.0.0.12</FileVersion>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>

View File

@@ -217,7 +217,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
var metadata = await _tmdbService.GetMediaMetadataAsync(
"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++;
_logger.LogInformation(
@@ -323,7 +324,8 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
var metadata = await _tmdbService.GetMediaMetadataAsync(
"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++;
_logger.LogInformation(

View File

@@ -19,7 +19,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
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
{
@@ -236,7 +236,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
try
{
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);
ApplyAuth(request, apiKey);
using var response = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
@@ -262,16 +262,40 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
? GetMovieCertification(doc)
: GetTvCertification(doc);
return new TmdbMediaMetadata(genres.AsReadOnly(), certification);
return new TmdbMediaMetadata(genres.AsReadOnly(), certification, IsAnime(doc, mediaType, genres));
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
_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.
private static string? GetMovieCertification(JsonDocument doc)
{

View File

@@ -147,9 +147,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
}
else
{
// Apply enabled filters. If nothing survives, fall back to the full pool.
var filtered = ApplyFilters(pairs, feature, config);
var pool = filtered.Count > 0 ? filtered : pairs;
// Anime trailers only play before anime. Applied before the fallback below, so
// non-anime content never gets one — it gets no trailer instead. Western animation
// 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.
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 };
}
// 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(
List<(Movie Movie, BaseItem Trailer)> pairs,
BaseItem feature,

View File

@@ -2,11 +2,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
{
/// <summary>
/// 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>
public static class TrailerTags
{
/// <summary>Marks a fake-movie folder as containing a TV show trailer.</summary>
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";
}
}

View File

@@ -1,5 +1,5 @@
---
version: 1.0.0.10
version: 1.0.0.12
name: CinemaTrailers4Jellyfins
guid: b581493e-1046-40ed-b6dc-cb8027624984
description: >
@@ -12,13 +12,12 @@ category: General
owner: 514mart
targetAbi: 10.11.0.0
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
- 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
- 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
dotnetProjects:
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins

View File

@@ -8,6 +8,22 @@
"owner": "514mart",
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
"versions": [
{
"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",
"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",