aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-05-06 20:33:58 +0200
committerGitHub <noreply@github.com>2026-05-06 20:33:58 +0200
commit1bbbc1c8239494b1825343feced908f49e66a8f3 (patch)
treeed136d3f1bad686e0bec2f23133c5408b5715224
parenta8f361f8c049cc8ef83138cb8f5e8f8f1043386b (diff)
parenta12736a0ce7f1664d33bbf24fd8223ea9873dc69 (diff)
Merge pull request #16328 from Shadowghost/rating-fix
Fix Canadian rating and fallback to unrated if we have a CountryCode but no matching rating
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs77
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.json15
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs3
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs34
4 files changed, 106 insertions, 23 deletions
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 6fca5bc1ba..d8797e612b 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -320,6 +320,14 @@ namespace Emby.Server.Implementations.Localization
{
return value;
}
+
+ if (ratingsDictionary is not null && rating.Length > countryCode.Length
+ && rating.StartsWith(countryCode, StringComparison.OrdinalIgnoreCase)
+ && (rating[countryCode.Length] == '-' || rating[countryCode.Length] == ':')
+ && ratingsDictionary.TryGetValue(rating[(countryCode.Length + 1)..].Trim(), out var normalizedValue))
+ {
+ return normalizedValue;
+ }
}
else
{
@@ -345,33 +353,68 @@ namespace Emby.Server.Implementations.Localization
}
}
- // Try splitting by : to handle "Germany: FSK-18"
- if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
+ // Try splitting by country prefix separator to handle "US:PG-13", "Germany: FSK-18", "DE-FSK-18"
+ if (TryGetRatingScoreBySeparator(rating, ':', out var result)
+ || TryGetRatingScoreBySeparator(rating, '-', out result))
{
- var ratingLevelRightPart = rating.AsSpan().RightPart(':');
- if (ratingLevelRightPart.Length != 0)
- {
- return GetRatingScore(ratingLevelRightPart.ToString());
- }
+ return result;
}
- // Handle prefix country code to handle "DE-18"
- if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
+ return null;
+ }
+
+ private bool TryGetRatingScoreBySeparator(string rating, char separator, out ParentalRatingScore? result)
+ {
+ result = null;
+
+ if (rating.IndexOf(separator, StringComparison.Ordinal) < 0)
{
- var ratingSpan = rating.AsSpan();
+ return false;
+ }
- // Extract culture from country prefix
- var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
+ var ratingSpan = rating.AsSpan();
+ var countryPart = ratingSpan.LeftPart(separator).Trim().ToString();
+ var ratingPart = ratingSpan.RightPart(separator).Trim().ToString();
+ if (ratingPart.Length == 0)
+ {
+ return false;
+ }
- var ratingLevelRightPart = ratingSpan.RightPart('-');
- if (ratingLevelRightPart.Length != 0)
+ string? resolvedCountryCode = null;
+
+ if (_allParentalRatings.ContainsKey(countryPart))
+ {
+ resolvedCountryCode = countryPart;
+ }
+ else
+ {
+ var culture = FindLanguageInfo(countryPart);
+ if (culture is not null)
{
- // Check rating system of culture
- return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+ resolvedCountryCode = culture.TwoLetterISOLanguageName;
}
}
- return null;
+ if (resolvedCountryCode is not null
+ && _allParentalRatings.TryGetValue(resolvedCountryCode, out var countryRatings))
+ {
+ if (countryRatings.TryGetValue(ratingPart, out result))
+ {
+ return true;
+ }
+
+ _logger.LogWarning(
+ "Rating '{Rating}' not found in the '{CountryCode}' rating system, treating as unrated",
+ rating,
+ resolvedCountryCode);
+
+ return true;
+ }
+
+ // Country not identified or no rating data available, try recursive lookup
+ result = GetRatingScore(ratingPart, resolvedCountryCode);
+
+ return true;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json
index fa43a8f2b7..76550b64c3 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ca.json
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.json
@@ -3,7 +3,7 @@
"supportsSubScores": true,
"ratings": [
{
- "ratingStrings": ["E", "G", "TV-Y", "TV-G"],
+ "ratingStrings": ["C", "E", "G", "TV-Y", "TV-G"],
"ratingScore": {
"score": 0,
"subScore": 0
@@ -24,13 +24,20 @@
}
},
{
- "ratingStrings": ["PG", "TV-PG"],
+ "ratingStrings": ["C8"],
"ratingScore": {
- "score": 9,
+ "score": 8,
"subScore": 0
}
},
{
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 1
+ }
+ },
+ {
"ratingStrings": ["14A"],
"ratingScore": {
"score": 14,
@@ -38,7 +45,7 @@
}
},
{
- "ratingStrings": ["TV-14"],
+ "ratingStrings": ["14+", "TV-14"],
"ratingScore": {
"score": 14,
"subScore": 1
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 2a6db01cf3..ed92c34aa3 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -1,4 +1,3 @@
-using System;
using System.Linq;
using Jellyfin.Database.Implementations;
using Jellyfin.Server.ServerSetupApp;
@@ -12,7 +11,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// Migrate rating levels.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
-[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
+[JellyfinMigration("2026-03-02T09:00:00", nameof(MigrateRatingLevels))]
[JellyfinMigrationBackup(JellyfinDb = true)]
#pragma warning restore CS0618 // Type or member is obsolete
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 5bcfc580ff..acabaf3acb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -242,6 +242,40 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
}
[Theory]
+ [InlineData("US:INVALID", "US")] // Colon separator, known country code, unknown rating
+ [InlineData("us:INVALID", "US")] // Colon separator, lowercase country code
+ [InlineData("DE-INVALID", "US")] // Hyphen separator, known language prefix, unknown rating
+ [InlineData("ca:INVALID", "US")] // Colon separator, known country code (Canada)
+ public async Task GetRatingScore_UnknownRatingWithKnownCountry_ReturnsNull(string rating, string countryCode)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ MetadataCountryCode = countryCode
+ });
+ await localizationManager.LoadAll();
+
+ Assert.Null(localizationManager.GetRatingScore(rating));
+ }
+
+ [Theory]
+ [InlineData("us:R", "DE", 17, 0)] // Colon separator, explicit US country, valid US rating
+ [InlineData("US:PG-13", "DE", 13, 0)] // Colon separator, explicit US country, valid US rating
+ [InlineData("ca:R", "US", 18, 1)] // Colon separator, Canada country code, valid CA rating
+ public async Task GetRatingScore_ValidRatingWithCountrySeparator_ReturnsScore(string rating, string countryCode, int expectedScore, int? expectedSubScore)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ MetadataCountryCode = countryCode
+ });
+ await localizationManager.LoadAll();
+
+ var score = localizationManager.GetRatingScore(rating);
+ Assert.NotNull(score);
+ Assert.Equal(expectedScore, score.Score);
+ Assert.Equal(expectedSubScore, score.SubScore);
+ }
+
+ [Theory]
[InlineData("Default", "Default")]
[InlineData("HeaderLiveTV", "Live TV")]
public void GetLocalizedString_Valid_Success(string key, string expected)