Explorar el Código

Rework parental ratings (#12615)

Tim Eisele hace 2 meses
padre
commit
3fc3b04daf
Se han modificado 80 ficheros con 3992 adiciones y 802 borrados
  1. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  2. 6 7
      Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
  3. 78 90
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  4. 0 11
      Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
  5. 34 0
      Emby.Server.Implementations/Localization/Ratings/0-prefer.json
  6. 0 17
      Emby.Server.Implementations/Localization/Ratings/au.csv
  7. 69 0
      Emby.Server.Implementations/Localization/Ratings/au.json
  8. 0 11
      Emby.Server.Implementations/Localization/Ratings/be.csv
  9. 55 0
      Emby.Server.Implementations/Localization/Ratings/be.json
  10. 0 14
      Emby.Server.Implementations/Localization/Ratings/br.csv
  11. 55 0
      Emby.Server.Implementations/Localization/Ratings/br.json
  12. 0 18
      Emby.Server.Implementations/Localization/Ratings/ca.csv
  13. 90 0
      Emby.Server.Implementations/Localization/Ratings/ca.json
  14. 41 0
      Emby.Server.Implementations/Localization/Ratings/cl.json
  15. 0 7
      Emby.Server.Implementations/Localization/Ratings/co.csv
  16. 55 0
      Emby.Server.Implementations/Localization/Ratings/co.json
  17. 0 17
      Emby.Server.Implementations/Localization/Ratings/de.csv
  18. 41 0
      Emby.Server.Implementations/Localization/Ratings/de.json
  19. 0 7
      Emby.Server.Implementations/Localization/Ratings/dk.csv
  20. 48 0
      Emby.Server.Implementations/Localization/Ratings/dk.json
  21. 0 25
      Emby.Server.Implementations/Localization/Ratings/es.csv
  22. 90 0
      Emby.Server.Implementations/Localization/Ratings/es.json
  23. 0 10
      Emby.Server.Implementations/Localization/Ratings/fi.csv
  24. 41 0
      Emby.Server.Implementations/Localization/Ratings/fi.json
  25. 0 13
      Emby.Server.Implementations/Localization/Ratings/fr.csv
  26. 69 0
      Emby.Server.Implementations/Localization/Ratings/fr.json
  27. 0 23
      Emby.Server.Implementations/Localization/Ratings/gb.csv
  28. 97 0
      Emby.Server.Implementations/Localization/Ratings/gb.json
  29. 0 10
      Emby.Server.Implementations/Localization/Ratings/ie.csv
  30. 55 0
      Emby.Server.Implementations/Localization/Ratings/ie.json
  31. 0 11
      Emby.Server.Implementations/Localization/Ratings/jp.csv
  32. 62 0
      Emby.Server.Implementations/Localization/Ratings/jp.json
  33. 0 6
      Emby.Server.Implementations/Localization/Ratings/kz.csv
  34. 41 0
      Emby.Server.Implementations/Localization/Ratings/kz.json
  35. 0 6
      Emby.Server.Implementations/Localization/Ratings/mx.csv
  36. 41 0
      Emby.Server.Implementations/Localization/Ratings/mx.json
  37. 0 8
      Emby.Server.Implementations/Localization/Ratings/nl.csv
  38. 55 0
      Emby.Server.Implementations/Localization/Ratings/nl.json
  39. 0 10
      Emby.Server.Implementations/Localization/Ratings/no.csv
  40. 69 0
      Emby.Server.Implementations/Localization/Ratings/no.json
  41. 0 16
      Emby.Server.Implementations/Localization/Ratings/nz.csv
  42. 69 0
      Emby.Server.Implementations/Localization/Ratings/nz.json
  43. 0 6
      Emby.Server.Implementations/Localization/Ratings/ro.csv
  44. 48 0
      Emby.Server.Implementations/Localization/Ratings/ro.json
  45. 0 6
      Emby.Server.Implementations/Localization/Ratings/ru.csv
  46. 48 0
      Emby.Server.Implementations/Localization/Ratings/ru.json
  47. 0 10
      Emby.Server.Implementations/Localization/Ratings/se.csv
  48. 55 0
      Emby.Server.Implementations/Localization/Ratings/se.json
  49. 0 6
      Emby.Server.Implementations/Localization/Ratings/sk.csv
  50. 41 0
      Emby.Server.Implementations/Localization/Ratings/sk.json
  51. 0 22
      Emby.Server.Implementations/Localization/Ratings/uk.csv
  52. 97 0
      Emby.Server.Implementations/Localization/Ratings/uk.json
  53. 0 52
      Emby.Server.Implementations/Localization/Ratings/us.csv
  54. 83 0
      Emby.Server.Implementations/Localization/Ratings/us.json
  55. 40 31
      Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
  56. 2 2
      Jellyfin.Api/Controllers/ItemsController.cs
  57. 2 3
      Jellyfin.Api/Controllers/LocalizationController.cs
  58. 70 0
      Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs
  59. 49 34
      Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
  60. 4 2
      Jellyfin.Server.Implementations/Users/UserManager.cs
  61. 1 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  62. 34 54
      Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
  63. 2 1
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  64. 40 12
      MediaBrowser.Controller/Entities/BaseItem.cs
  65. 10 9
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  66. 45 25
      MediaBrowser.Model/Dto/MetadataEditorInfo.cs
  67. 31 24
      MediaBrowser.Model/Entities/ParentalRating.cs
  68. 22 0
      MediaBrowser.Model/Entities/ParentalRatingEntry.cs
  69. 32 0
      MediaBrowser.Model/Entities/ParentalRatingScore.cs
  70. 28 0
      MediaBrowser.Model/Entities/ParentalRatingSystem.cs
  71. 50 51
      MediaBrowser.Model/Globalization/ILocalizationManager.cs
  72. 1 0
      MediaBrowser.Model/Querying/ItemFields.cs
  73. 2 0
      MediaBrowser.Model/Users/UserPolicy.cs
  74. 1 0
      MediaBrowser.Providers/Manager/MetadataService.cs
  75. 2 0
      src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
  76. 7 2
      src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
  77. 1658 0
      src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs
  78. 48 0
      src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
  79. 143 79
      src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
  80. 34 32
      tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs

+ 1 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -68,6 +68,6 @@
     <EmbeddedResource Include="Localization\iso6392.txt" />
     <EmbeddedResource Include="Localization\countries.json" />
     <EmbeddedResource Include="Localization\Core\*.json" />
-    <EmbeddedResource Include="Localization\Ratings\*.csv" />
+    <EmbeddedResource Include="Localization\Ratings\*.json" />
   </ItemGroup>
 </Project>

+ 6 - 7
Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Library;
@@ -78,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
             CollapseBoxSetItems = false,
             Recursive = true,
             DtoOptions = new DtoOptions(false),
-            ImageTypes = new[] { imageType },
+            ImageTypes = [imageType],
             Limit = 30,
             // TODO max parental rating configurable
-            MaxParentalRating = 10,
-            OrderBy = new[]
-            {
+            MaxParentalRating = new(10, null),
+            OrderBy =
+            [
                 (ItemSortBy.Random, SortOrder.Ascending)
-            },
-            IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
+            ],
+            IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
         });
     }
 }

+ 78 - 90
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -26,20 +25,18 @@ namespace Emby.Server.Implementations.Localization
         private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
         private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
         private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
-        private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" };
+        private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"];
 
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger<LocalizationManager> _logger;
 
-        private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
-            new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
+        private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase);
 
-        private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
-            new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.OrdinalIgnoreCase);
 
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
-        private List<CultureDto> _cultures = new List<CultureDto>();
+        private List<CultureDto> _cultures = [];
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.
@@ -68,35 +65,26 @@ namespace Emby.Server.Implementations.Localization
                     continue;
                 }
 
-                string countryCode = resource.Substring(RatingsPath.Length, 2);
-                var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
-
-                var stream = _assembly.GetManifestResourceStream(resource);
-                await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
+                using var stream = _assembly.GetManifestResourceStream(resource);
+                if (stream is not null)
                 {
-                    using var reader = new StreamReader(stream!);
-                    await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+                    var ratingSystem = await JsonSerializer.DeserializeAsync<ParentalRatingSystem>(stream, _jsonOptions).ConfigureAwait(false)
+                                ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+                    var dict = new Dictionary<string, ParentalRatingScore?>();
+                    if (ratingSystem.Ratings is not null)
                     {
-                        if (string.IsNullOrWhiteSpace(line))
+                        foreach (var ratingEntry in ratingSystem.Ratings)
                         {
-                            continue;
+                            foreach (var ratingString in ratingEntry.RatingStrings)
+                            {
+                                dict[ratingString] = ratingEntry.RatingScore;
+                            }
                         }
 
-                        string[] parts = line.Split(',');
-                        if (parts.Length == 2
-                            && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
-                        {
-                            var name = parts[0];
-                            dict.Add(name, new ParentalRating(name, value));
-                        }
-                        else
-                        {
-                            _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
-                        }
+                        _allParentalRatings[ratingSystem.CountryCode] = dict;
                     }
                 }
-
-                _allParentalRatings[countryCode] = dict;
             }
 
             await LoadCultures().ConfigureAwait(false);
@@ -111,22 +99,29 @@ namespace Emby.Server.Implementations.Localization
 
         private async Task LoadCultures()
         {
-            List<CultureDto> list = new List<CultureDto>();
+            List<CultureDto> list = [];
 
-            await using var stream = _assembly.GetManifestResourceStream(CulturesPath)
-                ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
-            using var reader = new StreamReader(stream);
-            await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+            using var stream = _assembly.GetManifestResourceStream(CulturesPath);
+            if (stream is null)
+            {
+                throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
+            }
+            else
             {
-                if (string.IsNullOrWhiteSpace(line))
+                using var reader = new StreamReader(stream);
+                await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
                 {
-                    continue;
-                }
+                    if (string.IsNullOrWhiteSpace(line))
+                    {
+                        continue;
+                    }
 
-                var parts = line.Split('|');
+                    var parts = line.Split('|');
+                    if (parts.Length != 5)
+                    {
+                        throw new InvalidDataException($"Invalid culture data found at: '{line}'");
+                    }
 
-                if (parts.Length == 5)
-                {
                     string name = parts[3];
                     if (string.IsNullOrWhiteSpace(name))
                     {
@@ -139,21 +134,21 @@ namespace Emby.Server.Implementations.Localization
                         continue;
                     }
 
-                    string[] threeletterNames;
+                    string[] threeLetterNames;
                     if (string.IsNullOrWhiteSpace(parts[1]))
                     {
-                        threeletterNames = new[] { parts[0] };
+                        threeLetterNames = [parts[0]];
                     }
                     else
                     {
-                        threeletterNames = new[] { parts[0], parts[1] };
+                        threeLetterNames = [parts[0], parts[1]];
                     }
 
-                    list.Add(new CultureDto(name, name, twoCharName, threeletterNames));
+                    list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
                 }
-            }
 
-            _cultures = list;
+                _cultures = list;
+            }
         }
 
         /// <inheritdoc />
@@ -176,82 +171,80 @@ namespace Emby.Server.Implementations.Localization
         }
 
         /// <inheritdoc />
-        public IEnumerable<CountryInfo> GetCountries()
+        public IReadOnlyList<CountryInfo> GetCountries()
         {
-            using StreamReader reader = new StreamReader(
-                _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"));
-            return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions)
-                ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'");
+            using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+            return JsonSerializer.Deserialize<IReadOnlyList<CountryInfo>>(stream, _jsonOptions) ?? [];
         }
 
         /// <inheritdoc />
-        public IEnumerable<ParentalRating> GetParentalRatings()
+        public IReadOnlyList<ParentalRating> GetParentalRatings()
         {
             // Use server default language for ratings
             // Fall back to empty list if there are no parental ratings for that language
-            var ratings = GetParentalRatingsDictionary()?.Values.ToList()
-                ?? new List<ParentalRating>();
+            var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? [];
 
             // Add common ratings to ensure them being available for selection
             // Based on the US rating system due to it being the main source of rating in the metadata providers
             // Unrated
-            if (!ratings.Any(x => x.Value is null))
+            if (!ratings.Any(x => x is null))
             {
-                ratings.Add(new ParentalRating("Unrated", null));
+                ratings.Add(new("Unrated", null));
             }
 
             // Minimum rating possible
-            if (ratings.All(x => x.Value != 0))
+            if (ratings.All(x => x.RatingScore?.Score != 0))
             {
-                ratings.Add(new ParentalRating("Approved", 0));
+                ratings.Add(new("Approved", new(0, null)));
             }
 
             // Matches PG (this has different age restrictions depending on country)
-            if (ratings.All(x => x.Value != 10))
+            if (ratings.All(x => x.RatingScore?.Score != 10))
             {
-                ratings.Add(new ParentalRating("10", 10));
+                ratings.Add(new("10", new(10, null)));
             }
 
             // Matches PG-13
-            if (ratings.All(x => x.Value != 13))
+            if (ratings.All(x => x.RatingScore?.Score != 13))
             {
-                ratings.Add(new ParentalRating("13", 13));
+                ratings.Add(new("13", new(13, null)));
             }
 
             // Matches TV-14
-            if (ratings.All(x => x.Value != 14))
+            if (ratings.All(x => x.RatingScore?.Score != 14))
             {
-                ratings.Add(new ParentalRating("14", 14));
+                ratings.Add(new("14", new(14, null)));
             }
 
             // Catchall if max rating of country is less than 21
             // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned
-            if (!ratings.Any(x => x.Value >= 21))
+            if (!ratings.Any(x => x.RatingScore?.Score >= 21))
             {
-                ratings.Add(new ParentalRating("21", 21));
+                ratings.Add(new ParentalRating("21", new(21, null)));
             }
 
             // A lot of countries don't explicitly have a separate rating for adult content
-            if (ratings.All(x => x.Value != 1000))
+            if (ratings.All(x => x.RatingScore?.Score != 1000))
             {
-                ratings.Add(new ParentalRating("XXX", 1000));
+                ratings.Add(new ParentalRating("XXX",  new(1000, null)));
             }
 
             // A lot of countries don't explicitly have a separate rating for banned content
-            if (ratings.All(x => x.Value != 1001))
+            if (ratings.All(x => x.RatingScore?.Score != 1001))
             {
-                ratings.Add(new ParentalRating("Banned", 1001));
+                ratings.Add(new ParentalRating("Banned",  new(1001, null)));
             }
 
-            return ratings.OrderBy(r => r.Value);
+            return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)];
         }
 
         /// <summary>
         /// Gets the parental ratings dictionary.
         /// </summary>
         /// <param name="countryCode">The optional two letter ISO language string.</param>
-        /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
-        private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
+        /// <returns><see cref="Dictionary{String, ParentalRatingScore}" />.</returns>
+        private Dictionary<string, ParentalRatingScore?>? GetParentalRatingsDictionary(string? countryCode = null)
         {
             // Fallback to server default if no country code is specified.
             if (string.IsNullOrEmpty(countryCode))
@@ -268,7 +261,7 @@ namespace Emby.Server.Implementations.Localization
         }
 
         /// <inheritdoc />
-        public int? GetRatingLevel(string rating, string? countryCode = null)
+        public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null)
         {
             ArgumentException.ThrowIfNullOrEmpty(rating);
 
@@ -278,11 +271,11 @@ namespace Emby.Server.Implementations.Localization
                 return null;
             }
 
-            // Convert integers directly
+            // Convert ints directly
             // This may override some of the locale specific age ratings (but those always map to the same age)
             if (int.TryParse(rating, out var ratingAge))
             {
-                return ratingAge;
+                return new(ratingAge, null);
             }
 
             // Fairly common for some users to have "Rated R" in their rating field
@@ -295,9 +288,9 @@ namespace Emby.Server.Implementations.Localization
             if (!string.IsNullOrEmpty(countryCode))
             {
                 var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
-                if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+                if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
                 {
-                    return value.Value;
+                    return value;
                 }
             }
             else
@@ -305,9 +298,9 @@ namespace Emby.Server.Implementations.Localization
                 // Fall back to server default language for ratings check
                 // If it has no ratings, use the US ratings
                 var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
-                if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+                if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
                 {
-                    return value.Value;
+                    return value;
                 }
             }
 
@@ -316,7 +309,7 @@ namespace Emby.Server.Implementations.Localization
             {
                 if (dictionary.TryGetValue(rating, out var value))
                 {
-                    return value.Value;
+                    return value;
                 }
             }
 
@@ -326,7 +319,7 @@ namespace Emby.Server.Implementations.Localization
                 var ratingLevelRightPart = rating.AsSpan().RightPart(':');
                 if (ratingLevelRightPart.Length != 0)
                 {
-                    return GetRatingLevel(ratingLevelRightPart.ToString());
+                    return GetRatingScore(ratingLevelRightPart.ToString());
                 }
             }
 
@@ -342,7 +335,7 @@ namespace Emby.Server.Implementations.Localization
                 if (ratingLevelRightPart.Length != 0)
                 {
                     // Check rating system of culture
-                    return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+                    return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
                 }
             }
 
@@ -406,7 +399,7 @@ namespace Emby.Server.Implementations.Localization
 
         private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
         {
-            await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+            using var stream = _assembly.GetManifestResourceStream(resourcePath);
             // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
             if (stream is null)
             {
@@ -414,12 +407,7 @@ namespace Emby.Server.Implementations.Localization
                 return;
             }
 
-            var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
-            if (dict is null)
-            {
-                throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
-            }
-
+            var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
             foreach (var key in dict.Keys)
             {
                 dictionary[key] = dict[key];

+ 0 - 11
Emby.Server.Implementations/Localization/Ratings/0-prefer.csv

@@ -1,11 +0,0 @@
-E,0
-EC,0
-T,7
-M,18
-AO,18
-UR,18
-RP,18
-X,1000
-XX,1000
-XXX,1000
-XXXX,1000

+ 34 - 0
Emby.Server.Implementations/Localization/Ratings/0-prefer.json

@@ -0,0 +1,34 @@
+{
+    "countryCode": "0-prefer",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["E", "EC"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["T"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["M", "AO", "UR", "RP"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["X", "XX", "XXX", "XXXX"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 17
Emby.Server.Implementations/Localization/Ratings/au.csv

@@ -1,17 +0,0 @@
-Exempt,0
-G,0
-7+,7
-PG,15
-M,15
-MA,15
-MA15+,15
-MA 15+,15
-16+,16
-R,18
-R18+,18
-R 18+,18
-18+,18
-X18+,1000
-X 18+,1000
-X,1000
-RC,1001

+ 69 - 0
Emby.Server.Implementations/Localization/Ratings/au.json

@@ -0,0 +1,69 @@
+{
+    "countryCode": "au",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["Exempt", "G"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["7+"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["PG"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["M"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 2
+            }
+        },
+        {
+            "ratingStrings": ["MA", "MA 15+", "MA15+"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 3
+            }
+        },
+        {
+            "ratingStrings": ["16+"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18+", "R", "R18+", "R 18+"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["X", "X18", "X 18"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["RC"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 0 - 11
Emby.Server.Implementations/Localization/Ratings/be.csv

@@ -1,11 +0,0 @@
-AL,0
-KT,0
-TOUS,0
-MG6,6
-6,6
-9,9
-KNT,12
-12,12
-14,14
-16,16
-18,18

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/be.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "be",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["AL", "KT", "TOUS"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6", "MG6"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12", "KNT"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 14
Emby.Server.Implementations/Localization/Ratings/br.csv

@@ -1,14 +0,0 @@
-Livre,0
-L,0
-AL,0
-ER,10
-10,10
-A10,10
-12,12
-A12,12
-14,14
-A14,14
-16,16
-A16,16
-18,18
-A18,18

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/br.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "br",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["L", "AL", "Livre"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["10", "A10", "ER"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12", "A12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14", "A14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16", "A16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18", "A18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 18
Emby.Server.Implementations/Localization/Ratings/ca.csv

@@ -1,18 +0,0 @@
-E,0
-G,0
-TV-Y,0
-TV-G,0
-TV-Y7,7
-TV-Y7-FV,7
-PG,9
-TV-PG,9
-TV-14,14
-14A,14
-16+,16
-NC-17,17
-R,18
-TV-MA,18
-18A,18
-18+,18
-A,1000
-Prohibited,1001

+ 90 - 0
Emby.Server.Implementations/Localization/Ratings/ca.json

@@ -0,0 +1,90 @@
+{
+    "countryCode": "ca",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["E", "G", "TV-Y", "TV-G"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-Y7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-Y7-FV"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["PG", "TV-PG"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["14A"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["16+"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["NC-17"],
+            "ratingScore": {
+                "score": 17,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18A"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18+", "TV-MA", "R"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["A"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["Prohibited"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/cl.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "cl",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["TE"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["TE+7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18", "18V", "18S"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 7
Emby.Server.Implementations/Localization/Ratings/co.csv

@@ -1,7 +0,0 @@
-T,0
-7,7
-12,12
-15,15
-18,18
-X,1000
-Prohibited,1001

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/co.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "co",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["T"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["X"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["Prohibited"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 17
Emby.Server.Implementations/Localization/Ratings/de.csv

@@ -1,17 +0,0 @@
-Educational,0
-Infoprogramm,0
-FSK-0,0
-FSK 0,0
-0,0
-FSK-6,6
-FSK 6,6
-6,6
-FSK-12,12
-FSK 12,12
-12,12
-FSK-16,16
-FSK 16,16
-16,16
-FSK-18,18
-FSK 18,18
-18,18

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/de.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "de",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6", "FSK 6", "FSK-6"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12", "FSK 12", "FSK-12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16", "FSK 16", "FSK-16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18", "FSK 18", "FSK-18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 7
Emby.Server.Implementations/Localization/Ratings/dk.csv

@@ -1,7 +0,0 @@
-F,0
-A,0
-7,7
-11,11
-12,12
-15,15
-16,16

+ 48 - 0
Emby.Server.Implementations/Localization/Ratings/dk.json

@@ -0,0 +1,48 @@
+{
+    "countryCode": "dk",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["F", "A"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["11"],
+            "ratingScore": {
+                "score": 11,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 25
Emby.Server.Implementations/Localization/Ratings/es.csv

@@ -1,25 +0,0 @@
-A,0
-A/fig,0
-A/i,0
-A/i/fig,0
-APTA,0
-ERI,0
-TP,0
-0+,0
-6+,6
-7/fig,7
-7/i,7
-7/i/fig,7
-7,7
-9+,9
-10,10
-12,12
-12/fig,12
-13,13
-14,14
-16,16
-16/fig,16
-18,18
-18/fig,18
-X,1000
-Banned,1001

+ 90 - 0
Emby.Server.Implementations/Localization/Ratings/es.json

@@ -0,0 +1,90 @@
+{
+    "countryCode": "es",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["0+", "A", "A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6+"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7", "7/i", "7/fig", "7/i/fig"],
+            "ratingScore": {
+                "score": 11,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9+"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["10"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12", "12/fig"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["13"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16", "16/fig"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18", "18/fig"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["X"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["Banned"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 10
Emby.Server.Implementations/Localization/Ratings/fi.csv

@@ -1,10 +0,0 @@
-S,0
-T,0
-K7,7
-7,7
-K12,12
-12,12
-K16,16
-16,16
-K18,18
-18,18

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/fi.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "fi",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["S", "T"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7", "K7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12", "K12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16", "K16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18", "K18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 13
Emby.Server.Implementations/Localization/Ratings/fr.csv

@@ -1,13 +0,0 @@
-Public Averti,0
-Tous Publics,0
-TP,0
-U,0
-0+,0
-6+,6
-9+,9
-10,10
-12,12
-14+,14
-16,16
-18,18
-X,1000

+ 69 - 0
Emby.Server.Implementations/Localization/Ratings/fr.json

@@ -0,0 +1,69 @@
+{
+    "countryCode": "fr",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6+"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9+"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["10"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14+"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["X"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 23
Emby.Server.Implementations/Localization/Ratings/gb.csv

@@ -1,23 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9,9
-12,12
-12+,12
-12A,12
-12PG,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000

+ 97 - 0
Emby.Server.Implementations/Localization/Ratings/gb.json

@@ -0,0 +1,97 @@
+{
+    "countryCode": "gb",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["0+", "All", "E", "G", "U"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["6+"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["7+"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["PG"],
+            "ratingScore": {
+                "score": 8,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12A", "12PG"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12", "12+"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["13+", "Teen"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["14+"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 3
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18", "Caution"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["Mature", "Adult", "R18"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 0 - 10
Emby.Server.Implementations/Localization/Ratings/ie.csv

@@ -1,10 +0,0 @@
-G,4
-PG,12
-12,12
-12A,12
-12PG,12
-15,15
-15PG,15
-15A,15
-16,16
-18,18

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/ie.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "ie",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["G"],
+            "ratingScore": {
+                "score": 4,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12A", "12PG", "PG"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["15A", "15PG"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 3
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        }
+    ]
+}

+ 0 - 11
Emby.Server.Implementations/Localization/Ratings/jp.csv

@@ -1,11 +0,0 @@
-A,0
-G,0
-B,12
-PG12,12
-C,15
-15+,15
-R15+,15
-16+,16
-D,17
-Z,18
-18+,18

+ 62 - 0
Emby.Server.Implementations/Localization/Ratings/jp.json

@@ -0,0 +1,62 @@
+{
+    "countryCode": "jp",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["A", "G"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["PG12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["B"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["15A", "15PG"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["C", "15+", "R15+"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["16+"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["D"],
+            "ratingScore": {
+                "score": 17,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18+", "Z"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 6
Emby.Server.Implementations/Localization/Ratings/kz.csv

@@ -1,6 +0,0 @@
-K,0
-БА,12
-Б14,14
-E16,16
-E18,18
-HA,18

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/kz.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "kz",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["K"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["БА"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["Б14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["E16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["E18", "HA"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 6
Emby.Server.Implementations/Localization/Ratings/mx.csv

@@ -1,6 +0,0 @@
-A,0
-AA,0
-B,12
-B-15,15
-C,18
-D,1000

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/mx.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "mx",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["A", "AA"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["B"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["B-15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["C"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["D"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 8
Emby.Server.Implementations/Localization/Ratings/nl.csv

@@ -1,8 +0,0 @@
-AL,0
-MG6,6
-6,6
-9,9
-12,12
-14,14
-16,16
-18,18

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/nl.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "nl",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["AL"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6", "MG6"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 10
Emby.Server.Implementations/Localization/Ratings/no.csv

@@ -1,10 +0,0 @@
-A,0
-6,6
-7,7
-9,9
-11,11
-12,12
-15,15
-18,18
-C,18
-Not approved,1001

+ 69 - 0
Emby.Server.Implementations/Localization/Ratings/no.json

@@ -0,0 +1,69 @@
+{
+    "countryCode": "no",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["A"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["11"],
+            "ratingScore": {
+                "score": 11,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["Not approved"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 16
Emby.Server.Implementations/Localization/Ratings/nz.csv

@@ -1,16 +0,0 @@
-Exempt,0
-G,0
-GY,13
-PG,13
-R13,13
-RP13,13
-R15,15
-M,16
-R16,16
-RP16,16
-GA,18
-R18,18
-RP18,18
-MA,1000
-R,1001
-Objectionable,1001

+ 69 - 0
Emby.Server.Implementations/Localization/Ratings/nz.json

@@ -0,0 +1,69 @@
+{
+    "countryCode": "nz",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["Exempt", "G"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["RP13", "PG"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["GY", "R13"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["RP16", "M"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["R16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["RP18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["R18", "GA"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["MA"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["Objectionable", "R"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 0 - 6
Emby.Server.Implementations/Localization/Ratings/ro.csv

@@ -1,6 +0,0 @@
-AG,0
-AP-12,12
-N-15,15
-IM-18,18
-IM-18-XXX,1000
-IC,1001

+ 48 - 0
Emby.Server.Implementations/Localization/Ratings/ro.json

@@ -0,0 +1,48 @@
+{
+    "countryCode": "ro",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["AG"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["AP-12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["N-15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["IM-18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["IM-18-XXX"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["IC"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 6
Emby.Server.Implementations/Localization/Ratings/ru.csv

@@ -1,6 +0,0 @@
-0+,0
-6+,6
-12+,12
-16+,16
-18+,18
-Refused classification,1001

+ 48 - 0
Emby.Server.Implementations/Localization/Ratings/ru.json

@@ -0,0 +1,48 @@
+{
+    "countryCode": "ru",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["0+"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["6+"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12+"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["16+"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18+"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["Refused classification"],
+            "ratingScore": {
+                "score": 1001,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 10
Emby.Server.Implementations/Localization/Ratings/se.csv

@@ -1,10 +0,0 @@
-Alla,0
-Barntillåten,0
-Btl,0
-0+,0
-7,7
-9+,9
-10+,10
-11,11
-14,14
-15,15

+ 55 - 0
Emby.Server.Implementations/Localization/Ratings/se.json

@@ -0,0 +1,55 @@
+{
+    "countryCode": "se",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["9+"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["10+"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["11"],
+            "ratingScore": {
+                "score": 11,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 6
Emby.Server.Implementations/Localization/Ratings/sk.csv

@@ -1,6 +0,0 @@
-NR,0
-U,0
-7,7
-12,12
-15,15
-18,18

+ 41 - 0
Emby.Server.Implementations/Localization/Ratings/sk.json

@@ -0,0 +1,41 @@
+{
+    "countryCode": "sk",
+    "supportsSubScores": false,
+    "ratings": [
+        {
+            "ratingStrings": ["U", "NR"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["12"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": null
+            }
+        },
+        {
+            "ratingStrings": ["18"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": null
+            }
+        }
+    ]
+}

+ 0 - 22
Emby.Server.Implementations/Localization/Ratings/uk.csv

@@ -1,22 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9+,9
-12,12
-12+,12
-12A,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000

+ 97 - 0
Emby.Server.Implementations/Localization/Ratings/uk.json

@@ -0,0 +1,97 @@
+{
+    "countryCode": "gb",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["0+", "All", "E", "G", "U"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["6+"],
+            "ratingScore": {
+                "score": 6,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["7+"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["PG"],
+            "ratingScore": {
+                "score": 8,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["9"],
+            "ratingScore": {
+                "score": 9,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12A", "12PG"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["12", "12+"],
+            "ratingScore": {
+                "score": 12,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["13+", "Teen"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["14+"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["15"],
+            "ratingScore": {
+                "score": 15,
+                "subScore": 3
+            }
+        },
+        {
+            "ratingStrings": ["16"],
+            "ratingScore": {
+                "score": 16,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["18", "Caution"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["Mature", "Adult", "R18"],
+            "ratingScore": {
+                "score": 1000,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 0 - 52
Emby.Server.Implementations/Localization/Ratings/us.csv

@@ -1,52 +0,0 @@
-Approved,0
-G,0
-TV-G,0
-TV-Y,0
-TV-Y7,7
-TV-Y7-FV,7
-PG,10
-TV-PG,10
-TV-PG-D,10
-TV-PG-L,10
-TV-PG-S,10
-TV-PG-V,10
-TV-PG-DL,10
-TV-PG-DS,10
-TV-PG-DV,10
-TV-PG-LS,10
-TV-PG-LV,10
-TV-PG-SV,10
-TV-PG-DLS,10
-TV-PG-DLV,10
-TV-PG-DSV,10
-TV-PG-LSV,10
-TV-PG-DLSV,10
-PG-13,13
-TV-14,14
-TV-14-D,14
-TV-14-L,14
-TV-14-S,14
-TV-14-V,14
-TV-14-DL,14
-TV-14-DS,14
-TV-14-DV,14
-TV-14-LS,14
-TV-14-LV,14
-TV-14-SV,14
-TV-14-DLS,14
-TV-14-DLV,14
-TV-14-DSV,14
-TV-14-LSV,14
-TV-14-DLSV,14
-NC-17,17
-R,17
-TV-MA,17
-TV-MA-L,17
-TV-MA-S,17
-TV-MA-V,17
-TV-MA-LS,17
-TV-MA-LV,17
-TV-MA-SV,17
-TV-MA-LSV,17
-TV-X,18
-TV-AO,18

+ 83 - 0
Emby.Server.Implementations/Localization/Ratings/us.json

@@ -0,0 +1,83 @@
+{
+    "countryCode": "us",
+    "supportsSubScores": true,
+    "ratings": [
+        {
+            "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"],
+            "ratingScore": {
+                "score": 0,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-Y7"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-Y7-FV"],
+            "ratingScore": {
+                "score": 7,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["PG", "TV-PG"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"],
+            "ratingScore": {
+                "score": 10,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["PG-13"],
+            "ratingScore": {
+                "score": 13,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-14"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"],
+            "ratingScore": {
+                "score": 14,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["R"],
+            "ratingScore": {
+                "score": 17,
+                "subScore": 0
+            }
+        },
+        {
+            "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"],
+            "ratingScore": {
+                "score": 17,
+                "subScore": 1
+            }
+        },
+        {
+            "ratingStrings": ["TV-X", "TV-AO"],
+            "ratingScore": {
+                "score": 18,
+                "subScore": 0
+            }
+        }
+    ]
+}

+ 40 - 31
Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs

@@ -1,45 +1,54 @@
-#pragma warning disable CS1591
-
 using System;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
 
-namespace Emby.Server.Implementations.Sorting
+namespace Emby.Server.Implementations.Sorting;
+
+/// <summary>
+/// Class providing comparison for official ratings.
+/// </summary>
+public class OfficialRatingComparer : IBaseItemComparer
 {
-    public class OfficialRatingComparer : IBaseItemComparer
+    private readonly ILocalizationManager _localizationManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="OfficialRatingComparer"/> class.
+    /// </summary>
+    /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+    public OfficialRatingComparer(ILocalizationManager localizationManager)
     {
-        private readonly ILocalizationManager _localization;
+        _localizationManager = localizationManager;
+    }
 
-        public OfficialRatingComparer(ILocalizationManager localization)
+    /// <summary>
+    /// Gets the name.
+    /// </summary>
+    /// <value>The name.</value>
+    public ItemSortBy Type => ItemSortBy.OfficialRating;
+
+    /// <summary>
+    /// Compares the specified x.
+    /// </summary>
+    /// <param name="x">The x.</param>
+    /// <param name="y">The y.</param>
+    /// <returns>System.Int32.</returns>
+    public int Compare(BaseItem? x, BaseItem? y)
+    {
+        ArgumentNullException.ThrowIfNull(x);
+        ArgumentNullException.ThrowIfNull(y);
+        var zeroRating = new ParentalRatingScore(0, 0);
+
+        var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating;
+        var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating;
+        var scoreCompare = ratingX.Score.CompareTo(ratingY.Score);
+        if (scoreCompare is 0)
         {
-            _localization = localization;
+            return (ratingX.SubScore ?? 0).CompareTo(ratingY.SubScore ?? 0);
         }
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public ItemSortBy Type => ItemSortBy.OfficialRating;
-
-        /// <summary>
-        /// Compares the specified x.
-        /// </summary>
-        /// <param name="x">The x.</param>
-        /// <param name="y">The y.</param>
-        /// <returns>System.Int32.</returns>
-        public int Compare(BaseItem? x, BaseItem? y)
-        {
-            ArgumentNullException.ThrowIfNull(x);
-
-            ArgumentNullException.ThrowIfNull(y);
-
-            var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0;
-            var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0;
-
-            return levelX.CompareTo(levelY);
-        }
+        return scoreCompare;
     }
 }

+ 2 - 2
Jellyfin.Api/Controllers/ItemsController.cs

@@ -448,13 +448,13 @@ public class ItemsController : BaseJellyfinApiController
             // Min official rating
             if (!string.IsNullOrWhiteSpace(minOfficialRating))
             {
-                query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
+                query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
             }
 
             // Max official rating
             if (!string.IsNullOrWhiteSpace(maxOfficialRating))
             {
-                query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
+                query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
             }
 
             // Artists

+ 2 - 3
Jellyfin.Api/Controllers/LocalizationController.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
@@ -45,7 +44,7 @@ public class LocalizationController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
     [HttpGet("Countries")]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<CountryInfo>> GetCountries()
+    public ActionResult<IReadOnlyList<CountryInfo>> GetCountries()
     {
         return Ok(_localization.GetCountries());
     }
@@ -57,7 +56,7 @@ public class LocalizationController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
     [HttpGet("ParentalRatings")]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
+    public ActionResult<IReadOnlyList<ParentalRating>> GetParentalRatings()
     {
         return Ok(_localization.GetParentalRatings());
     }

+ 70 - 0
Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+/// <summary>
+/// Provides <see cref="Expression"/> extension methods.
+/// </summary>
+public static class ExpressionExtensions
+{
+    /// <summary>
+    /// Combines two predicates into a single predicate using a logical OR operation.
+    /// </summary>
+    /// <typeparam name="T">The predicate parameter type.</typeparam>
+    /// <param name="firstPredicate">The first predicate expression to combine.</param>
+    /// <param name="secondPredicate">The second predicate expression to combine.</param>
+    /// <returns>A new expression representing the OR combination of the input predicates.</returns>
+    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+    {
+        ArgumentNullException.ThrowIfNull(firstPredicate);
+        ArgumentNullException.ThrowIfNull(secondPredicate);
+
+        var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+    }
+
+    /// <summary>
+    /// Combines multiple predicates into a single predicate using a logical OR operation.
+    /// </summary>
+    /// <typeparam name="T">The predicate parameter type.</typeparam>
+    /// <param name="predicates">A collection of predicate expressions to combine.</param>
+    /// <returns>A new expression representing the OR combination of all input predicates.</returns>
+    public static Expression<Func<T, bool>> Or<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+    {
+        ArgumentNullException.ThrowIfNull(predicates);
+
+        return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.Or(nextPredicate));
+    }
+
+    /// <summary>
+    /// Combines two predicates into a single predicate using a logical AND operation.
+    /// </summary>
+    /// <typeparam name="T">The predicate parameter type.</typeparam>
+    /// <param name="firstPredicate">The first predicate expression to combine.</param>
+    /// <param name="secondPredicate">The second predicate expression to combine.</param>
+    /// <returns>A new expression representing the AND combination of the input predicates.</returns>
+    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+    {
+        ArgumentNullException.ThrowIfNull(firstPredicate);
+        ArgumentNullException.ThrowIfNull(secondPredicate);
+
+        var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+    }
+
+    /// <summary>
+    /// Combines multiple predicates into a single predicate using a logical AND operation.
+    /// </summary>
+    /// <typeparam name="T">The predicate parameter type.</typeparam>
+    /// <param name="predicates">A collection of predicate expressions to combine.</param>
+    /// <returns>A new expression representing the AND combination of all input predicates.</returns>
+    public static Expression<Func<T, bool>> And<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+    {
+        ArgumentNullException.ThrowIfNull(predicates);
+
+        return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.And(nextPredicate));
+    }
+}

+ 49 - 34
Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

@@ -9,6 +9,7 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Linq.Expressions;
 using System.Reflection;
 using System.Text;
 using System.Text.Json;
@@ -19,6 +20,7 @@ using Jellyfin.Database.Implementations.Entities;
 using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Server.Implementations.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Channels;
@@ -781,6 +783,7 @@ public sealed class BaseItemRepository
         entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
         entity.IsInMixedFolder = dto.IsInMixedFolder;
         entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
+        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
         entity.CriticRating = dto.CriticRating;
         entity.PresentationUniqueKey = dto.PresentationUniqueKey;
         entity.OriginalTitle = dto.OriginalTitle;
@@ -1796,61 +1799,73 @@ public sealed class BaseItemRepository
                    .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
         }
 
-        if (filter.HasParentalRating ?? false)
+        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
+        if (filter.MinParentalRating != null)
         {
-            if (filter.MinParentalRating.HasValue)
+            var min = filter.MinParentalRating;
+            minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
+            if (min.SubScore != null)
             {
-                baseQuery = baseQuery
-                   .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+                minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
             }
+        }
 
-            if (filter.MaxParentalRating.HasValue)
+        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
+        if (filter.MaxParentalRating != null)
+        {
+            var max = filter.MaxParentalRating;
+            maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
+            if (max.SubScore != null)
             {
-                baseQuery = baseQuery
-                   .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
+                maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
             }
         }
-        else if (filter.BlockUnratedItems.Length > 0)
+
+        if (filter.HasParentalRating ?? false)
         {
-            var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
-            if (filter.MinParentalRating.HasValue)
+            if (minParentalRatingFilter != null)
             {
-                if (filter.MaxParentalRating.HasValue)
-                {
-                    baseQuery = baseQuery
-                        .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
-                        || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
-                }
-                else
-                {
-                    baseQuery = baseQuery
-                        .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
-                        || e.InheritedParentalRatingValue >= filter.MinParentalRating);
-                }
+                baseQuery = baseQuery.Where(minParentalRatingFilter);
             }
-            else
+
+            if (maxParentalRatingFilter != null)
             {
-                baseQuery = baseQuery
-                    .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
+                baseQuery = baseQuery.Where(maxParentalRatingFilter);
             }
         }
-        else if (filter.MinParentalRating.HasValue)
+        else if (filter.BlockUnratedItems.Length > 0)
         {
-            if (filter.MaxParentalRating.HasValue)
+            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType);
+
+            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
             {
-                baseQuery = baseQuery
-                    .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
+                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)));
+            }
+            else if (minParentalRatingFilter != null)
+            {
+                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
+            }
+            else if (maxParentalRatingFilter != null)
+            {
+                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
             }
             else
             {
-                baseQuery = baseQuery
-                    .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+                baseQuery = baseQuery.Where(unratedItemFilter);
             }
         }
-        else if (filter.MaxParentalRating.HasValue)
+        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
         {
-            baseQuery = baseQuery
-                .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
+            if (minParentalRatingFilter != null)
+            {
+                baseQuery = baseQuery.Where(minParentalRatingFilter);
+            }
+
+            if (maxParentalRatingFilter != null)
+            {
+                baseQuery = baseQuery.Where(maxParentalRatingFilter);
+            }
         }
         else if (!filter.HasParentalRating ?? false)
         {

+ 4 - 2
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -342,7 +342,8 @@ namespace Jellyfin.Server.Implementations.Users
                 },
                 Policy = new UserPolicy
                 {
-                    MaxParentalRating = user.MaxParentalAgeRating,
+                    MaxParentalRating = user.MaxParentalRatingScore,
+                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
                     EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
                     RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
                     AuthenticationProviderId = user.AuthenticationProviderId,
@@ -668,7 +669,8 @@ namespace Jellyfin.Server.Implementations.Users
                     _ => policy.LoginAttemptsBeforeLockout
                 };
 
-                user.MaxParentalAgeRating = policy.MaxParentalRating;
+                user.MaxParentalRatingScore = policy.MaxParentalRating;
+                user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
                 user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
                 user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
                 user.AuthenticationProviderId = policy.AuthenticationProviderId;

+ 1 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -49,12 +49,12 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDownloadImagesInAdvance),
             typeof(Routines.MigrateAuthenticationDb),
             typeof(Routines.FixPlaylistOwner),
-            typeof(Routines.MigrateRatingLevels),
             typeof(Routines.AddDefaultCastReceivers),
             typeof(Routines.UpdateDefaultPluginRepository),
             typeof(Routines.FixAudioData),
             typeof(Routines.RemoveDuplicatePlaylistChildren),
             typeof(Routines.MigrateLibraryDb),
+            typeof(Routines.MigrateRatingLevels),
             typeof(Routines.MoveTrickplayFiles),
         };
 

+ 34 - 54
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs

@@ -1,36 +1,33 @@
 using System;
-using System.Globalization;
-using System.IO;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
+using System.Linq;
+using Jellyfin.Database.Implementations;
 using MediaBrowser.Model.Globalization;
-using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server.Migrations.Routines
 {
     /// <summary>
-    /// Migrate rating levels to new rating level system.
+    /// Migrate rating levels.
     /// </summary>
-    internal class MigrateRatingLevels : IMigrationRoutine
+    internal class MigrateRatingLevels : IDatabaseMigrationRoutine
     {
-        private const string DbFilename = "library.db";
         private readonly ILogger<MigrateRatingLevels> _logger;
-        private readonly IServerApplicationPaths _applicationPaths;
+        private readonly IDbContextFactory<JellyfinDbContext> _provider;
         private readonly ILocalizationManager _localizationManager;
 
         public MigrateRatingLevels(
-            IServerApplicationPaths applicationPaths,
+            IDbContextFactory<JellyfinDbContext> provider,
             ILoggerFactory loggerFactory,
             ILocalizationManager localizationManager)
         {
-            _applicationPaths = applicationPaths;
+            _provider = provider;
             _localizationManager = localizationManager;
             _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
         }
 
         /// <inheritdoc/>
-        public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}");
+        public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}");
 
         /// <inheritdoc/>
         public string Name => "MigrateRatingLevels";
@@ -41,54 +38,37 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public void Perform()
         {
-            var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
-
-            // Back up the database before modifying any entries
-            for (int i = 1; ; i++)
+            _logger.LogInformation("Recalculating parental rating levels based on rating string.");
+            using var context = _provider.CreateDbContext();
+            using var transaction = context.Database.BeginTransaction();
+            var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
+            foreach (var rating in ratings)
             {
-                var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
-                if (!File.Exists(bakPath))
+                if (string.IsNullOrEmpty(rating))
                 {
-                    try
-                    {
-                        File.Copy(dbPath, bakPath);
-                        _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
-                        break;
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
-                        throw;
-                    }
+                    int? value = null;
+                    context.BaseItems
+                        .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+                        .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
+                    context.BaseItems
+                        .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+                        .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
                 }
-            }
-
-            // Migrate parental rating strings to new levels
-            _logger.LogInformation("Recalculating parental rating levels based on rating string.");
-            using var connection = new SqliteConnection($"Filename={dbPath}");
-            connection.Open();
-            using (var transaction = connection.BeginTransaction())
-            {
-                var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
-                foreach (var entry in queryResult)
+                else
                 {
-                    if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
-                    {
-                        connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
-                    }
-                    else
-                    {
-                        var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL";
-
-                        using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
-                        statement.TryBind("@Value", ratingValue);
-                        statement.TryBind("@Rating", ratingString);
-                        statement.ExecuteNonQuery();
-                    }
+                    var ratingValue = _localizationManager.GetRatingScore(rating);
+                    var score = ratingValue?.Score;
+                    var subScore = ratingValue?.SubScore;
+                    context.BaseItems
+                        .Where(e => e.OfficialRating == rating)
+                        .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
+                    context.BaseItems
+                        .Where(e => e.OfficialRating == rating)
+                        .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
                 }
-
-                transaction.Commit();
             }
+
+            transaction.Commit();
         }
     }
 }

+ 2 - 1
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -112,7 +112,8 @@ namespace Jellyfin.Server.Migrations.Routines
                     {
                         Id = entry.GetGuid(1),
                         InternalId = entry.GetInt64(0),
-                        MaxParentalAgeRating = policy.MaxParentalRating,
+                        MaxParentalRatingScore = policy.MaxParentalRating,
+                        MaxParentalRatingSubScore = null,
                         EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
                         RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
                         InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,

+ 40 - 12
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -581,6 +581,9 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public int? InheritedParentalRatingValue { get; set; }
 
+        [JsonIgnore]
+        public int? InheritedParentalRatingSubValue { get; set; }
+
         /// <summary>
         /// Gets or sets the critic rating.
         /// </summary>
@@ -1540,7 +1543,8 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
 
-            var maxAllowedRating = user.MaxParentalAgeRating;
+            var maxAllowedRating = user.MaxParentalRatingScore;
+            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
             var rating = CustomRatingForComparison;
 
             if (string.IsNullOrEmpty(rating))
@@ -1554,10 +1558,10 @@ namespace MediaBrowser.Controller.Entities
                 return !GetBlockUnratedValue(user);
             }
 
-            var value = LocalizationManager.GetRatingLevel(rating);
+            var ratingScore = LocalizationManager.GetRatingScore(rating);
 
             // Could not determine rating level
-            if (!value.HasValue)
+            if (ratingScore is null)
             {
                 var isAllowed = !GetBlockUnratedValue(user);
 
@@ -1569,10 +1573,15 @@ namespace MediaBrowser.Controller.Entities
                 return isAllowed;
             }
 
-            return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value;
+            if (maxAllowedSubRating is not null)
+            {
+                return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
+            }
+
+            return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
         }
 
-        public int? GetInheritedParentalRatingValue()
+        public ParentalRatingScore GetParentalRatingScore()
         {
             var rating = CustomRatingForComparison;
 
@@ -1586,7 +1595,7 @@ namespace MediaBrowser.Controller.Entities
                 return null;
             }
 
-            return LocalizationManager.GetRatingLevel(rating);
+            return LocalizationManager.GetRatingScore(rating);
         }
 
         public List<string> GetInheritedTags()
@@ -2518,11 +2527,29 @@ namespace MediaBrowser.Controller.Entities
 
             var item = this;
 
-            var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null;
-            if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
+            var rating = item.GetParentalRatingScore();
+            if (rating is not null)
             {
-                item.InheritedParentalRatingValue = inheritedParentalRatingValue;
-                updateType |= ItemUpdateType.MetadataImport;
+                if (rating.Score != item.InheritedParentalRatingValue)
+                {
+                    item.InheritedParentalRatingValue = rating.Score;
+                    updateType |= ItemUpdateType.MetadataImport;
+                }
+
+                if (rating.SubScore != item.InheritedParentalRatingSubValue)
+                {
+                    item.InheritedParentalRatingSubValue = rating.SubScore;
+                    updateType |= ItemUpdateType.MetadataImport;
+                }
+            }
+            else
+            {
+                if (item.InheritedParentalRatingValue is not null)
+                {
+                    item.InheritedParentalRatingValue = null;
+                    item.InheritedParentalRatingSubValue = null;
+                    updateType |= ItemUpdateType.MetadataImport;
+                }
             }
 
             return updateType;
@@ -2542,8 +2569,9 @@ namespace MediaBrowser.Controller.Entities
                 .Select(i => i.OfficialRating)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(rating => (rating, LocalizationManager.GetRatingLevel(rating)))
-                .OrderBy(i => i.Item2 ?? 1000)
+                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
+                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
+                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
                 .Select(i => i.rating);
 
             OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;

+ 10 - 9
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -232,9 +232,9 @@ namespace MediaBrowser.Controller.Entities
 
         public int? IndexNumber { get; set; }
 
-        public int? MinParentalRating { get; set; }
+        public ParentalRatingScore? MinParentalRating { get; set; }
 
-        public int? MaxParentalRating { get; set; }
+        public ParentalRatingScore? MaxParentalRating { get; set; }
 
         public bool? HasDeadParentId { get; set; }
 
@@ -360,16 +360,17 @@ namespace MediaBrowser.Controller.Entities
 
         public void SetUser(User user)
         {
-            MaxParentalRating = user.MaxParentalAgeRating;
-
-            if (MaxParentalRating.HasValue)
+            var maxRating = user.MaxParentalRatingScore;
+            if (maxRating.HasValue)
             {
-                string other = UnratedItem.Other.ToString();
-                BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
-                    .Where(i => i != other)
-                    .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+                MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore);
             }
 
+            var other = UnratedItem.Other.ToString();
+            BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+                .Where(i => i != other)
+                .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+
             ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
             IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
 

+ 45 - 25
MediaBrowser.Model/Dto/MetadataEditorInfo.cs

@@ -1,35 +1,55 @@
-#pragma warning disable CS1591
-
-using System;
 using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Providers;
 
-namespace MediaBrowser.Model.Dto
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// A class representing metadata editor information.
+/// </summary>
+public class MetadataEditorInfo
 {
-    public class MetadataEditorInfo
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MetadataEditorInfo"/> class.
+    /// </summary>
+    public MetadataEditorInfo()
     {
-        public MetadataEditorInfo()
-        {
-            ParentalRatingOptions = Array.Empty<ParentalRating>();
-            Countries = Array.Empty<CountryInfo>();
-            Cultures = Array.Empty<CultureDto>();
-            ExternalIdInfos = Array.Empty<ExternalIdInfo>();
-            ContentTypeOptions = Array.Empty<NameValuePair>();
-        }
-
-        public IReadOnlyList<ParentalRating> ParentalRatingOptions { get; set; }
-
-        public IReadOnlyList<CountryInfo> Countries { get; set; }
-
-        public IReadOnlyList<CultureDto> Cultures { get; set; }
-
-        public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
-
-        public CollectionType? ContentType { get; set; }
-
-        public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
+        ParentalRatingOptions = [];
+        Countries = [];
+        Cultures = [];
+        ExternalIdInfos = [];
+        ContentTypeOptions = [];
     }
+
+    /// <summary>
+    /// Gets or sets the parental rating options.
+    /// </summary>
+    public IReadOnlyList<ParentalRating> ParentalRatingOptions { get; set; }
+
+    /// <summary>
+    /// Gets or sets the countries.
+    /// </summary>
+    public IReadOnlyList<CountryInfo> Countries { get; set; }
+
+    /// <summary>
+    /// Gets or sets the cultures.
+    /// </summary>
+    public IReadOnlyList<CultureDto> Cultures { get; set; }
+
+    /// <summary>
+    /// Gets or sets the external id infos.
+    /// </summary>
+    public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
+
+    /// <summary>
+    /// Gets or sets the content type.
+    /// </summary>
+    public CollectionType? ContentType { get; set; }
+
+    /// <summary>
+    /// Gets or sets the content type options.
+    /// </summary>
+    public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
 }

+ 31 - 24
MediaBrowser.Model/Entities/ParentalRating.cs

@@ -1,33 +1,40 @@
-#nullable disable
-#pragma warning disable CS1591
+namespace MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Model.Entities
+/// <summary>
+/// Class ParentalRating.
+/// </summary>
+public class ParentalRating
 {
     /// <summary>
-    /// Class ParentalRating.
+    /// Initializes a new instance of the <see cref="ParentalRating"/> class.
     /// </summary>
-    public class ParentalRating
+    /// <param name="name">The name.</param>
+    /// <param name="score">The score.</param>
+    public ParentalRating(string name, ParentalRatingScore? score)
     {
-        public ParentalRating()
-        {
-        }
+        Name = name;
+        Value = score?.Score;
+        RatingScore = score;
+    }
 
-        public ParentalRating(string name, int? value)
-        {
-            Name = name;
-            Value = value;
-        }
+    /// <summary>
+    /// Gets or sets the name.
+    /// </summary>
+    /// <value>The name.</value>
+    public string Name { get; set; }
 
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
+    /// <summary>
+    /// Gets or sets the value.
+    /// </summary>
+    /// <value>The value.</value>
+    /// <remarks>
+    /// Deprecated.
+    /// </remarks>
+    public int? Value { get; set; }
 
-        /// <summary>
-        /// Gets or sets the value.
-        /// </summary>
-        /// <value>The value.</value>
-        public int? Value { get; set; }
-    }
+    /// <summary>
+    /// Gets or sets the rating score.
+    /// </summary>
+    /// <value>The rating score.</value>
+    public ParentalRatingScore? RatingScore { get; set; }
 }

+ 22 - 0
MediaBrowser.Model/Entities/ParentalRatingEntry.cs

@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing an parental rating entry.
+/// </summary>
+public class ParentalRatingEntry
+{
+    /// <summary>
+    /// Gets or sets the rating strings.
+    /// </summary>
+    [JsonPropertyName("ratingStrings")]
+    public required IReadOnlyList<string> RatingStrings { get; set; }
+
+    /// <summary>
+    /// Gets or sets the score.
+    /// </summary>
+    [JsonPropertyName("ratingScore")]
+    public required ParentalRatingScore RatingScore { get; set; }
+}

+ 32 - 0
MediaBrowser.Model/Entities/ParentalRatingScore.cs

@@ -0,0 +1,32 @@
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing an parental rating score.
+/// </summary>
+public class ParentalRatingScore
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ParentalRatingScore"/> class.
+    /// </summary>
+    /// <param name="score">The score.</param>
+    /// <param name="subScore">The sub score.</param>
+    public ParentalRatingScore(int score, int? subScore)
+    {
+        Score = score;
+        SubScore = subScore;
+    }
+
+    /// <summary>
+    /// Gets or sets the score.
+    /// </summary>
+    [JsonPropertyName("score")]
+    public int Score { get; set; }
+
+    /// <summary>
+    /// Gets or sets the sub score.
+    /// </summary>
+    [JsonPropertyName("subScore")]
+    public int? SubScore { get; set; }
+}

+ 28 - 0
MediaBrowser.Model/Entities/ParentalRatingSystem.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing a parental rating system.
+/// </summary>
+public class ParentalRatingSystem
+{
+    /// <summary>
+    /// Gets or sets the country code.
+    /// </summary>
+    [JsonPropertyName("countryCode")]
+    public required string CountryCode { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether sub scores are supported.
+    /// </summary>
+    [JsonPropertyName("supportsSubScores")]
+    public bool SupportsSubScores { get; set; }
+
+    /// <summary>
+    /// Gets or sets the ratings.
+    /// </summary>
+    [JsonPropertyName("ratings")]
+    public IReadOnlyList<ParentalRatingEntry>? Ratings { get; set; }
+}

+ 50 - 51
MediaBrowser.Model/Globalization/ILocalizationManager.cs

@@ -1,65 +1,64 @@
 using System.Collections.Generic;
 using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Model.Globalization
+namespace MediaBrowser.Model.Globalization;
+
+/// <summary>
+/// Interface ILocalizationManager.
+/// </summary>
+public interface ILocalizationManager
 {
     /// <summary>
-    /// Interface ILocalizationManager.
+    /// Gets the cultures.
     /// </summary>
-    public interface ILocalizationManager
-    {
-        /// <summary>
-        /// Gets the cultures.
-        /// </summary>
-        /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
-        IEnumerable<CultureDto> GetCultures();
+    /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
+    IEnumerable<CultureDto> GetCultures();
 
-        /// <summary>
-        /// Gets the countries.
-        /// </summary>
-        /// <returns><see cref="IEnumerable{CountryInfo}" />.</returns>
-        IEnumerable<CountryInfo> GetCountries();
+    /// <summary>
+    /// Gets the countries.
+    /// </summary>
+    /// <returns><see cref="IReadOnlyList{CountryInfo}" />.</returns>
+    IReadOnlyList<CountryInfo> GetCountries();
 
-        /// <summary>
-        /// Gets the parental ratings.
-        /// </summary>
-        /// <returns><see cref="IEnumerable{ParentalRating}" />.</returns>
-        IEnumerable<ParentalRating> GetParentalRatings();
+    /// <summary>
+    /// Gets the parental ratings.
+    /// </summary>
+    /// <returns><see cref="IReadOnlyList{ParentalRating}" />.</returns>
+    IReadOnlyList<ParentalRating> GetParentalRatings();
 
-        /// <summary>
-        /// Gets the rating level.
-        /// </summary>
-        /// <param name="rating">The rating.</param>
-        /// <param name="countryCode">The optional two letter ISO language string.</param>
-        /// <returns><see cref="int" /> or <c>null</c>.</returns>
-        int? GetRatingLevel(string rating, string? countryCode = null);
+    /// <summary>
+    /// Gets the rating level.
+    /// </summary>
+    /// <param name="rating">The rating.</param>
+    /// <param name="countryCode">The optional two letter ISO language string.</param>
+    /// <returns><see cref="ParentalRatingScore" /> or <c>null</c>.</returns>
+    ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null);
 
-        /// <summary>
-        /// Gets the localized string.
-        /// </summary>
-        /// <param name="phrase">The phrase.</param>
-        /// <param name="culture">The culture.</param>
-        /// <returns><see cref="string" />.</returns>
-        string GetLocalizedString(string phrase, string culture);
+    /// <summary>
+    /// Gets the localized string.
+    /// </summary>
+    /// <param name="phrase">The phrase.</param>
+    /// <param name="culture">The culture.</param>
+    /// <returns><see cref="string" />.</returns>
+    string GetLocalizedString(string phrase, string culture);
 
-        /// <summary>
-        /// Gets the localized string.
-        /// </summary>
-        /// <param name="phrase">The phrase.</param>
-        /// <returns>System.String.</returns>
-        string GetLocalizedString(string phrase);
+    /// <summary>
+    /// Gets the localized string.
+    /// </summary>
+    /// <param name="phrase">The phrase.</param>
+    /// <returns>System.String.</returns>
+    string GetLocalizedString(string phrase);
 
-        /// <summary>
-        /// Gets the localization options.
-        /// </summary>
-        /// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
-        IEnumerable<LocalizationOption> GetLocalizationOptions();
+    /// <summary>
+    /// Gets the localization options.
+    /// </summary>
+    /// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
+    IEnumerable<LocalizationOption> GetLocalizationOptions();
 
-        /// <summary>
-        /// Returns the correct <see cref="CultureDto" /> for the given language.
-        /// </summary>
-        /// <param name="language">The language.</param>
-        /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
-        CultureDto? FindLanguageInfo(string language);
-    }
+    /// <summary>
+    /// Returns the correct <see cref="CultureDto" /> for the given language.
+    /// </summary>
+    /// <param name="language">The language.</param>
+    /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
+    CultureDto? FindLanguageInfo(string language);
 }

+ 1 - 0
MediaBrowser.Model/Querying/ItemFields.cs

@@ -209,6 +209,7 @@ namespace MediaBrowser.Model.Querying
         ExternalEtag,
         PresentationUniqueKey,
         InheritedParentalRatingValue,
+        InheritedParentalRatingSubValue,
         ExternalSeriesId,
         SeriesPresentationUniqueKey,
         DateLastRefreshed,

+ 2 - 0
MediaBrowser.Model/Users/UserPolicy.cs

@@ -111,6 +111,8 @@ namespace MediaBrowser.Model.Users
         /// <value>The max parental rating.</value>
         public int? MaxParentalRating { get; set; }
 
+        public int? MaxParentalSubRating { get; set; }
+
         public string[] BlockedTags { get; set; }
 
         public string[] AllowedTags { get; set; }

+ 1 - 0
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -193,6 +193,7 @@ namespace MediaBrowser.Providers.Manager
             if (hasRefreshedMetadata && hasRefreshedImages)
             {
                 item.DateLastRefreshed = DateTime.UtcNow;
+                updateType |= item.OnMetadataChanged();
             }
 
             updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);

+ 2 - 0
src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs

@@ -84,6 +84,8 @@ public class BaseItemEntity
 
     public int? InheritedParentalRatingValue { get; set; }
 
+    public int? InheritedParentalRatingSubValue { get; set; }
+
     public string? UnratedType { get; set; }
 
     public float? CriticRating { get; set; }

+ 7 - 2
src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs

@@ -249,9 +249,14 @@ namespace Jellyfin.Database.Implementations.Entities
         public bool EnableUserPreferenceAccess { get; set; }
 
         /// <summary>
-        /// Gets or sets the maximum parental age rating.
+        /// Gets or sets the maximum parental rating score.
         /// </summary>
-        public int? MaxParentalAgeRating { get; set; }
+        public int? MaxParentalRatingScore { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum parental rating sub score.
+        /// </summary>
+        public int? MaxParentalRatingSubScore { get; set; }
 
         /// <summary>
         /// Gets or sets the remote client bitrate limit.

+ 1658 - 0
src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs

@@ -0,0 +1,1658 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDbContext))]
+    [Migration("20250326065026_AddInheritedParentalRatingSubValue")]
+    partial class AddInheritedParentalRatingSubValue
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingSubValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "TopParentId", "StartDate");
+
+                    b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+                    b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.ToTable("BaseItems");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ProviderId");
+
+                    b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+                    b.ToTable("BaseItemProviders");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client", "Key")
+                        .IsUnique();
+
+                    b.ToTable("CustomItemDisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue")
+                        .IsUnique();
+
+                    b.ToTable("ItemValues");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "StreamIndex");
+
+                    b.HasIndex("StreamIndex");
+
+                    b.HasIndex("StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType", "Language");
+
+                    b.ToTable("MediaStreamInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Permissions");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Preferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("AccessToken")
+                        .IsUnique();
+
+                    b.ToTable("ApiKeys");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId");
+
+                    b.HasIndex("AccessToken", "DateLastActivity");
+
+                    b.HasIndex("DeviceId", "DateLastActivity");
+
+                    b.HasIndex("UserId", "DeviceId");
+
+                    b.ToTable("Devices");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalRatingScore")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalRatingSubScore")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Username")
+                        .IsUnique();
+
+                    b.ToTable("Users");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "UserId", "Played");
+
+                    b.ToTable("UserData");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .HasForeignKey("ParentItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ParentItem");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("Images")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("LockedFields")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("Provider")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("TrailerTypes")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("ItemValues")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+                        .WithMany("BaseItemsMap")
+                        .HasForeignKey("ItemValueId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ItemValue");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("MediaStreams")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+                {
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    b.Navigation("Peoples");
+
+                    b.Navigation("Provider");
+
+                    b.Navigation("TrailerTypes");
+
+                    b.Navigation("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+                {
+                    b.Navigation("BaseItemsMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+                {
+                    b.Navigation("AccessSchedules");
+
+                    b.Navigation("DisplayPreferences");
+
+                    b.Navigation("ItemDisplayPreferences");
+
+                    b.Navigation("Permissions");
+
+                    b.Navigation("Preferences");
+
+                    b.Navigation("ProfileImage");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 48 - 0
src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs

@@ -0,0 +1,48 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddInheritedParentalRatingSubValue : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.RenameColumn(
+                name: "MaxParentalAgeRating",
+                table: "Users",
+                newName: "MaxParentalRatingScore");
+
+            migrationBuilder.AddColumn<int>(
+                name: "MaxParentalRatingSubScore",
+                table: "Users",
+                type: "INTEGER",
+                nullable: true);
+
+            migrationBuilder.AddColumn<int>(
+                name: "InheritedParentalRatingSubValue",
+                table: "BaseItems",
+                type: "INTEGER",
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "MaxParentalRatingSubScore",
+                table: "Users");
+
+            migrationBuilder.DropColumn(
+                name: "InheritedParentalRatingValue",
+                table: "BaseItems");
+
+            migrationBuilder.RenameColumn(
+                name: "MaxParentalRatingScore",
+                table: "Users",
+                newName: "MaxParentalAgeRating");
+        }
+    }
+}

+ 143 - 79
src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs

@@ -15,9 +15,9 @@ namespace Jellyfin.Server.Implementations.Migrations
         protected override void BuildModel(ModelBuilder modelBuilder)
         {
 #pragma warning disable 612, 618
-            modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -40,9 +40,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId");
 
                     b.ToTable("AccessSchedules");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -88,9 +90,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("DateCreated");
 
                     b.ToTable("ActivityLogs");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -103,9 +107,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ParentItemId");
 
                     b.ToTable("AncestorIds");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -132,9 +138,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasKey("ItemId", "Index");
 
                     b.ToTable("AttachmentStreamInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
                 {
                     b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
@@ -218,6 +226,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int?>("IndexNumber")
                         .HasColumnType("INTEGER");
 
+                    b.Property<int?>("InheritedParentalRatingSubValue")
+                        .HasColumnType("INTEGER");
+
                     b.Property<int?>("InheritedParentalRatingValue")
                         .HasColumnType("INTEGER");
 
@@ -380,9 +391,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
 
                     b.ToTable("BaseItems");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
                 {
                     b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
@@ -415,9 +428,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId");
 
                     b.ToTable("BaseItemImageInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
                 {
                     b.Property<int>("Id")
                         .HasColumnType("INTEGER");
@@ -430,9 +445,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId");
 
                     b.ToTable("BaseItemMetadataFields");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -449,9 +466,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ProviderId", "ProviderValue", "ItemId");
 
                     b.ToTable("BaseItemProviders");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
                 {
                     b.Property<int>("Id")
                         .HasColumnType("INTEGER");
@@ -464,9 +483,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId");
 
                     b.ToTable("BaseItemTrailerTypes");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -489,9 +510,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasKey("ItemId", "ChapterIndex");
 
                     b.ToTable("Chapters");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -521,9 +544,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("CustomItemDisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -578,9 +603,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("DisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -600,9 +627,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("DisplayPreferencesId");
 
                     b.ToTable("HomeSection");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -625,9 +654,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("ImageInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -669,9 +700,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId");
 
                     b.ToTable("ItemDisplayPreferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
                 {
                     b.Property<Guid>("ItemValueId")
                         .ValueGeneratedOnAdd()
@@ -694,9 +727,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("ItemValues");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
                 {
                     b.Property<Guid>("ItemValueId")
                         .HasColumnType("TEXT");
@@ -709,9 +744,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId");
 
                     b.ToTable("ItemValuesMap");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
                 {
                     b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
@@ -736,9 +773,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasKey("Id");
 
                     b.ToTable("MediaSegments");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -889,9 +928,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("StreamIndex", "StreamType", "Language");
 
                     b.ToTable("MediaStreamInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
                 {
                     b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
@@ -909,9 +950,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("Name");
 
                     b.ToTable("Peoples");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -937,9 +980,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId", "SortOrder");
 
                     b.ToTable("PeopleBaseItemMap");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -968,9 +1013,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasFilter("[UserId] IS NOT NULL");
 
                     b.ToTable("Permissions");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -1001,9 +1048,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasFilter("[UserId] IS NOT NULL");
 
                     b.ToTable("Preferences");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -1030,9 +1079,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("ApiKeys");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -1088,9 +1139,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId", "DeviceId");
 
                     b.ToTable("Devices");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
                 {
                     b.Property<int>("Id")
                         .ValueGeneratedOnAdd()
@@ -1109,9 +1162,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("DeviceOptions");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -1140,9 +1195,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasKey("ItemId", "Width");
 
                     b.ToTable("TrickplayInfos");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
                 {
                     b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
@@ -1200,7 +1257,10 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("MaxActiveSessions")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int?>("MaxParentalAgeRating")
+                    b.Property<int?>("MaxParentalRatingScore")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalRatingSubScore")
                         .HasColumnType("INTEGER");
 
                     b.Property<bool>("MustUpdatePassword")
@@ -1252,9 +1312,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique();
 
                     b.ToTable("Users");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
                 {
                     b.Property<Guid>("ItemId")
                         .HasColumnType("TEXT");
@@ -1305,26 +1367,28 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("ItemId", "UserId", "Played");
 
                     b.ToTable("UserData");
+
+                    b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithMany("AccessSchedules")
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("Children")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
                         .WithMany("ParentAncestors")
                         .HasForeignKey("ParentItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1335,9 +1399,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("ParentItem");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany()
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1346,9 +1410,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("Images")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1357,9 +1421,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("LockedFields")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1368,9 +1432,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("Provider")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1379,9 +1443,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("TrailerTypes")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1390,9 +1454,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("Chapters")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1401,50 +1465,50 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithMany("DisplayPreferences")
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
                         .WithMany("HomeSections")
                         .HasForeignKey("DisplayPreferencesId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithOne("ProfileImage")
-                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
                         .OnDelete(DeleteBehavior.Cascade);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithMany("ItemDisplayPreferences")
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("ItemValues")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
-                    b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
                         .WithMany("BaseItemsMap")
                         .HasForeignKey("ItemValueId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1455,9 +1519,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("ItemValue");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("MediaStreams")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1466,15 +1530,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("Item");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("Peoples")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
-                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
                         .WithMany("BaseItems")
                         .HasForeignKey("PeopleId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1485,25 +1549,25 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("People");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithMany("Permissions")
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
                         .WithMany("Preferences")
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade);
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
                         .WithMany()
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1512,15 +1576,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("User");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
                         .WithMany("UserData")
                         .HasForeignKey("ItemId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
-                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                    b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
                         .WithMany()
                         .HasForeignKey("UserId")
                         .OnDelete(DeleteBehavior.Cascade)
@@ -1531,7 +1595,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("User");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
                 {
                     b.Navigation("Chapters");
 
@@ -1556,22 +1620,22 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("UserData");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
                 {
                     b.Navigation("HomeSections");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
                 {
                     b.Navigation("BaseItemsMap");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
                 {
                     b.Navigation("BaseItems");
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+            modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
                 {
                     b.Navigation("AccessSchedules");
 

+ 34 - 32
tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs

@@ -88,7 +88,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
 
             var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
             Assert.NotNull(tvma);
-            Assert.Equal(17, tvma!.Value);
+            Assert.Equal(17, tvma!.RatingScore!.Score);
         }
 
         [Fact]
@@ -105,47 +105,49 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
 
             var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
             Assert.NotNull(fsk);
-            Assert.Equal(12, fsk!.Value);
+            Assert.Equal(12, fsk!.RatingScore!.Score);
         }
 
         [Theory]
-        [InlineData("CA-R", "CA", 18)]
-        [InlineData("FSK-16", "DE", 16)]
-        [InlineData("FSK-18", "DE", 18)]
-        [InlineData("FSK-18", "US", 18)]
-        [InlineData("TV-MA", "US", 17)]
-        [InlineData("XXX", "asdf", 1000)]
-        [InlineData("Germany: FSK-18", "DE", 18)]
-        [InlineData("Rated : R", "US", 17)]
-        [InlineData("Rated: R", "US", 17)]
-        [InlineData("Rated R", "US", 17)]
-        [InlineData(" PG-13 ", "US", 13)]
-        public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
+        [InlineData("CA-R", "CA", 18, 1)]
+        [InlineData("FSK-16", "DE", 16, null)]
+        [InlineData("FSK-18", "DE", 18, null)]
+        [InlineData("FSK-18", "US", 18, null)]
+        [InlineData("TV-MA", "US", 17, 1)]
+        [InlineData("XXX", "asdf", 1000, null)]
+        [InlineData("Germany: FSK-18", "DE", 18, null)]
+        [InlineData("Rated : R", "US", 17, 0)]
+        [InlineData("Rated: R", "US", 17, 0)]
+        [InlineData("Rated R", "US", 17, 0)]
+        [InlineData(" PG-13 ", "US", 13, 0)]
+        public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int? expectedScore, int? expectedSubScore)
         {
             var localizationManager = Setup(new ServerConfiguration()
             {
                 MetadataCountryCode = countryCode
             });
             await localizationManager.LoadAll();
-            var level = localizationManager.GetRatingLevel(value);
-            Assert.NotNull(level);
-            Assert.Equal(expectedLevel, level!);
+            var score = localizationManager.GetRatingScore(value);
+            Assert.NotNull(score);
+            Assert.Equal(expectedScore, score.Score);
+            Assert.Equal(expectedSubScore, score.SubScore);
         }
 
         [Theory]
-        [InlineData("0", 0)]
-        [InlineData("1", 1)]
-        [InlineData("6", 6)]
-        [InlineData("12", 12)]
-        [InlineData("42", 42)]
-        [InlineData("9999", 9999)]
-        public async Task GetRatingLevel_GivenValidAge_Success(string value, int expectedLevel)
+        [InlineData("0", 0, null)]
+        [InlineData("1", 1, null)]
+        [InlineData("6", 6, null)]
+        [InlineData("12", 12, null)]
+        [InlineData("42", 42, null)]
+        [InlineData("9999", 9999, null)]
+        public async Task GetRatingLevel_GivenValidAge_Success(string value, int? expectedScore, int? expectedSubScore)
         {
             var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" });
             await localizationManager.LoadAll();
-            var level = localizationManager.GetRatingLevel(value);
-            Assert.NotNull(level);
-            Assert.Equal(expectedLevel, level);
+            var score = localizationManager.GetRatingScore(value);
+            Assert.NotNull(score);
+            Assert.Equal(expectedScore, score.Score);
+            Assert.Equal(expectedSubScore, score.SubScore);
         }
 
         [Fact]
@@ -156,10 +158,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
                 UICulture = "de-DE"
             });
             await localizationManager.LoadAll();
-            Assert.Null(localizationManager.GetRatingLevel("NR"));
-            Assert.Null(localizationManager.GetRatingLevel("unrated"));
-            Assert.Null(localizationManager.GetRatingLevel("Not Rated"));
-            Assert.Null(localizationManager.GetRatingLevel("n/a"));
+            Assert.Null(localizationManager.GetRatingScore("NR"));
+            Assert.Null(localizationManager.GetRatingScore("unrated"));
+            Assert.Null(localizationManager.GetRatingScore("Not Rated"));
+            Assert.Null(localizationManager.GetRatingScore("n/a"));
         }
 
         [Theory]
@@ -173,7 +175,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
             });
             await localizationManager.LoadAll();
 
-            Assert.Null(localizationManager.GetRatingLevel(value));
+            Assert.Null(localizationManager.GetRatingScore(value));
         }
 
         [Theory]