using System.Collections.Frozen;
using Jellyfin.Plugin.Audience.Configuration;
using MediaBrowser.Controller.Entities;
namespace Jellyfin.Plugin.Audience;
///
/// The selection and rating rules, kept free of Jellyfin service dependencies so they
/// can be reasoned about and tested on their own.
///
public static class AudienceRules
{
///
/// Ratings Jellyfin cannot parse, mapped to the US rating that means the same thing.
///
///
/// Every entry is a documented equivalence between national classification boards,
/// NOT a judgement about content. Guessing a rating from genre or synopsis is refused
/// on purpose: a wrong guess fails OPEN, which is the exact failure mode parental
/// controls exist to prevent.
///
/// The mapping cuts both ways. It hides adult Singapore-rated titles that were
/// previously visible to every account, and it rescues genuinely all-ages titles
/// (Spain's TP, Brazil's L) that would otherwise be blocked as unrated.
///
public static readonly FrozenDictionary RatingRemap =
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["GP"] = "PG", // US MPAA 1970-72, the direct predecessor of PG
["PG13"] = "PG-13", // plain typo for PG-13
["M"] = "TV-14", // Australia, Mature, recommended 15+
["TP"] = "TV-G", // Spain, Todos los Publicos, all ages
["L"] = "TV-G", // Brazil, Livre, all ages
["B"] = "TV-PG", // Mexico, 12+
["B-15"] = "TV-14", // Mexico, 15+
["VM14"] = "TV-14", // Italy, Vietato ai Minori di 14
["NC16"] = "TV-MA", // Singapore, 16+
["M18"] = "TV-MA", // Singapore, 18+
["R21"] = "TV-MA", // Singapore, 21+
["PASSED"] = "Approved" // pre-1968 US Production Code seal
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
///
/// Genres that disqualify an item outright.
///
public static readonly FrozenSet HarshGenres =
new[]
{
"Horror", "Thriller", "War", "Science Fiction", "Sci-Fi & Fantasy",
"Animation", "Anime", "Action", "Action & Adventure", "Crime",
"Reality", "Talk", "News"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
///
/// Genres an item must have at least one of to qualify.
///
public static readonly FrozenSet GentleGenres =
new[]
{
"Comedy", "Drama", "Romance", "Family", "Music", "Western",
"Mystery", "Documentary", "History", "Soap", "Kids"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
///
/// Values that Jellyfin itself treats as "no rating".
///
private static readonly FrozenSet UnratedValues =
new[] { "n/a", "unrated", "not rated", "nr" }
.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
///
/// Returns the replacement rating for an unparseable value, or null to leave it alone.
///
public static string? RemapRating(string? officialRating)
{
if (string.IsNullOrWhiteSpace(officialRating))
{
return null;
}
return RatingRemap.TryGetValue(officialRating.Trim(), out var mapped)
? mapped
: null;
}
///
/// Returns true when the value is one Jellyfin resolves to "unrated".
///
public static bool IsUnratedValue(string? officialRating)
=> string.IsNullOrWhiteSpace(officialRating)
|| UnratedValues.Contains(officialRating.Trim());
///
/// Decides whether an item belongs in the reduced library.
///
/// The item under test.
///
/// The score from Jellyfin's localisation manager, or null when the rating is absent
/// or unrecognised. Passed in rather than resolved here so this stays dependency-free.
///
/// Plugin settings.
public static bool WantsTag(BaseItem item, int? ratingScore, PluginConfiguration config)
{
var genres = item.Genres;
if (genres is null || genres.Length == 0)
{
return false;
}
foreach (var genre in genres)
{
if (HarshGenres.Contains(genre))
{
return false;
}
}
var hasGentle = false;
foreach (var genre in genres)
{
if (GentleGenres.Contains(genre))
{
hasGentle = true;
break;
}
}
if (!hasGentle)
{
return false;
}
// A usable rating is judged on the rating. Anything at or below the ceiling is in.
if (ratingScore.HasValue)
{
return ratingScore.Value <= config.MaxRatingScore;
}
// No usable rating: fall back to reputation. This is what keeps the older
// classics, which are exactly what this audience wants and which TMDB
// frequently has no US certification for.
return item.CommunityRating.HasValue
&& item.CommunityRating.Value >= config.MinCommunityRatingWhenUnrated;
}
}