diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj index 58092f9..98fa8e3 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj @@ -3,8 +3,8 @@ net9.0 Jellyfin.Plugin.CinemaTrailers4Jellyfins - 1.0.0.11 - 1.0.0.11 + 1.0.0.12 + 1.0.0.12 enable false false diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/ScheduledTasks/DownloadTrailersTask.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/ScheduledTasks/DownloadTrailersTask.cs index c2b6ec9..897a1cf 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/ScheduledTasks/DownloadTrailersTask.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/ScheduledTasks/DownloadTrailersTask.cs @@ -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( diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs index 241e97a..46a551a 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TmdbService.cs @@ -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 Genres, string? Certification); + public record TmdbMediaMetadata(IReadOnlyList 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(), null); + return new TmdbMediaMetadata(Array.Empty(), 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 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) { diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs index f200d7f..dde290d 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs @@ -147,12 +147,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services } else { - // Animation is a style, not a topic: animated trailers (anime included) only play - // before animated content. Applied before the fallback below, so a live-action - // feature never gets one — it gets no trailer instead. - var eligible = IsAnimated(feature) + // 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 => !IsAnimated(p.Movie)).ToList(); + : 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); @@ -236,11 +236,19 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services return new IntroInfo { ItemId = movie.Id, Path = movie.Path }; } - // TMDB tags anime as "Animation"; anime metadata providers (e.g. AniDB) use "Anime". - private static readonly string[] AnimatedGenres = { "Animation", "Anime" }; + // 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 IsAnimated(BaseItem item) - => item.Genres.Any(g => AnimatedGenres.Contains(g, StringComparer.OrdinalIgnoreCase)); + 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, diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerTags.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerTags.cs index 097ecb7..a5d82a9 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerTags.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerTags.cs @@ -2,11 +2,14 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services { /// /// Tags written into each downloaded trailer's NFO so - /// 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. /// public static class TrailerTags { /// Marks a fake-movie folder as containing a TV show trailer. public const string TvShow = "ct4j-tvshow"; + + /// Marks a trailer as anime, so it only plays before anime content. + public const string Anime = "ct4j-anime"; } } diff --git a/build.yaml b/build.yaml index 07b8da2..1c3c8de 100644 --- a/build.yaml +++ b/build.yaml @@ -1,5 +1,5 @@ --- -version: 1.0.0.11 +version: 1.0.0.12 name: CinemaTrailers4Jellyfins guid: b581493e-1046-40ed-b6dc-cb8027624984 description: > @@ -12,11 +12,12 @@ category: General owner: 514mart targetAbi: 10.11.0.0 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 + - 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