feat: restrict anime trailers, not all animation (v1.0.0.12)
All checks were successful
Publish Release / release (push) Successful in 9s
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>
This commit is contained in:
@@ -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.11</AssemblyVersion>
|
<AssemblyVersion>1.0.0.12</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.11</FileVersion>
|
<FileVersion>1.0.0.12</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
|||||||
@@ -217,7 +217,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(
|
||||||
@@ -323,7 +324,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(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
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
|
||||||
{
|
{
|
||||||
@@ -236,7 +236,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 +262,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -147,12 +147,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Animation is a style, not a topic: animated trailers (anime included) only play
|
// Anime trailers only play before anime. Applied before the fallback below, so
|
||||||
// before animated content. Applied before the fallback below, so a live-action
|
// non-anime content never gets one — it gets no trailer instead. Western animation
|
||||||
// feature never gets one — it gets no trailer instead.
|
// is not affected and follows the normal filters.
|
||||||
var eligible = IsAnimated(feature)
|
var eligible = IsAnimeFeature(feature)
|
||||||
? pairs
|
? pairs
|
||||||
: pairs.Where(p => !IsAnimated(p.Movie)).ToList();
|
: pairs.Where(p => !IsAnimeTrailer(p.Movie)).ToList();
|
||||||
|
|
||||||
// Apply enabled filters. If nothing survives, fall back to the eligible pool.
|
// Apply enabled filters. If nothing survives, fall back to the eligible pool.
|
||||||
var filtered = ApplyFilters(eligible, feature, config);
|
var filtered = ApplyFilters(eligible, feature, config);
|
||||||
@@ -236,11 +236,19 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
||||||
}
|
}
|
||||||
|
|
||||||
// TMDB tags anime as "Animation"; anime metadata providers (e.g. AniDB) use "Anime".
|
// Library items: anime providers (e.g. AniDB) set the "Anime" genre; TMDB-scraped items
|
||||||
private static readonly string[] AnimatedGenres = { "Animation", "Anime" };
|
// 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 IsAnimated(BaseItem item)
|
private static bool IsAnimeFeature(BaseItem item)
|
||||||
=> item.Genres.Any(g => AnimatedGenres.Contains(g, StringComparer.OrdinalIgnoreCase));
|
=> 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,
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
build.yaml
13
build.yaml
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: 1.0.0.11
|
version: 1.0.0.12
|
||||||
name: CinemaTrailers4Jellyfins
|
name: CinemaTrailers4Jellyfins
|
||||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||||
description: >
|
description: >
|
||||||
@@ -12,11 +12,12 @@ category: General
|
|||||||
owner: 514mart
|
owner: 514mart
|
||||||
targetAbi: 10.11.0.0
|
targetAbi: 10.11.0.0
|
||||||
changelog:
|
changelog:
|
||||||
- Animated trailers (anime included) now only play before animated
|
- Anime trailers now only play before anime. Western animation (Pixar,
|
||||||
content. Before, an anime trailer could play before a live-action
|
Disney, ...) is no longer blocked and follows the normal genre filter
|
||||||
movie or show when they shared a genre like Drama or Action, or when
|
again. A trailer counts as anime when TMDB tags it "anime" or "based on
|
||||||
no trailer matched the genre filter. A live-action feature now gets no
|
manga", or it is animation in Japanese, Chinese or Korean; it is marked
|
||||||
trailer rather than an animated one
|
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:
|
dotnetProjects:
|
||||||
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||||
|
|||||||
Reference in New Issue
Block a user