diff options
80 files changed, 3992 insertions, 802 deletions
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 6722c20da..d99923b4f 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/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> diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 0c9edd839..71ce3b601 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/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] }); } } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 754a01329..9598f9e6c 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/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]; diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv deleted file mode 100644 index 36886ba76..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json new file mode 100644 index 000000000..b39015161 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv deleted file mode 100644 index 6e12759a4..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json new file mode 100644 index 000000000..a563df899 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv deleted file mode 100644 index d171a7132..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json new file mode 100644 index 000000000..18ea2c260 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv deleted file mode 100644 index f6053c88c..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json new file mode 100644 index 000000000..f455b6643 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv deleted file mode 100644 index 41dbda134..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json new file mode 100644 index 000000000..fa43a8f2b --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json new file mode 100644 index 000000000..086619471 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv deleted file mode 100644 index e1e96c590..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ /dev/null @@ -1,7 +0,0 @@ -T,0 -7,7 -12,12 -15,15 -18,18 -X,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json new file mode 100644 index 000000000..4eff6dcc5 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv deleted file mode 100644 index f6181575e..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json new file mode 100644 index 000000000..30c34b230 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv deleted file mode 100644 index 4ef63b2ea..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ /dev/null @@ -1,7 +0,0 @@ -F,0 -A,0 -7,7 -11,11 -12,12 -15,15 -16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json new file mode 100644 index 000000000..9fcd6d44f --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv deleted file mode 100644 index ee5866090..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json new file mode 100644 index 000000000..c19629939 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv deleted file mode 100644 index 7ff92f259..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ /dev/null @@ -1,10 +0,0 @@ -S,0 -T,0 -K7,7 -7,7 -K12,12 -12,12 -K16,16 -16,16 -K18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json new file mode 100644 index 000000000..3152317b5 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv deleted file mode 100644 index 139ea376b..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json new file mode 100644 index 000000000..e8bafd6b8 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv deleted file mode 100644 index 858b9a32d..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json new file mode 100644 index 000000000..7fc88272c --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv deleted file mode 100644 index d3c634fc9..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ /dev/null @@ -1,10 +0,0 @@ -G,4 -PG,12 -12,12 -12A,12 -12PG,12 -15,15 -15PG,15 -15A,15 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json new file mode 100644 index 000000000..f6cc56ed6 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv deleted file mode 100644 index bfb5fdaae..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json new file mode 100644 index 000000000..efff9e92c --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv deleted file mode 100644 index e26b32b67..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ /dev/null @@ -1,6 +0,0 @@ -K,0 -БА,12 -Б14,14 -E16,16 -E18,18 -HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json new file mode 100644 index 000000000..0f8f0c68e --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv deleted file mode 100644 index 305912f23..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ /dev/null @@ -1,6 +0,0 @@ -A,0 -AA,0 -B,12 -B-15,15 -C,18 -D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json new file mode 100644 index 000000000..9dc3b89bd --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv deleted file mode 100644 index 44f372b2d..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ /dev/null @@ -1,8 +0,0 @@ -AL,0 -MG6,6 -6,6 -9,9 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json new file mode 100644 index 000000000..2e43eb83a --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv deleted file mode 100644 index 6856a2dbb..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json new file mode 100644 index 000000000..a5e952316 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv deleted file mode 100644 index 633da78fe..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json new file mode 100644 index 000000000..3c1332271 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv deleted file mode 100644 index 44c23e248..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ /dev/null @@ -1,6 +0,0 @@ -AG,0 -AP-12,12 -N-15,15 -IM-18,18 -IM-18-XXX,1000 -IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json new file mode 100644 index 000000000..9cf735a54 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv deleted file mode 100644 index 8b264070b..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ /dev/null @@ -1,6 +0,0 @@ -0+,0 -6+,6 -12+,12 -16+,16 -18+,18 -Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json new file mode 100644 index 000000000..d1b8b13aa --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv deleted file mode 100644 index e129c3561..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json new file mode 100644 index 000000000..70084995d --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv deleted file mode 100644 index dbafd8efa..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/sk.csv +++ /dev/null @@ -1,6 +0,0 @@ -NR,0
-U,0
-7,7
-12,12
-15,15
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json new file mode 100644 index 000000000..5ec6111ec --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv deleted file mode 100644 index 75b1c2058..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json new file mode 100644 index 000000000..7fc88272c --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv deleted file mode 100644 index 9aa5c00eb..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ /dev/null @@ -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 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json new file mode 100644 index 000000000..08a637312 --- /dev/null +++ b/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 + } + } + ] +} diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index b4ee2c723..789af01cc 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/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; } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 803c2f1f7..a49128336 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/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 diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index f65d95c41..bbce5a9e1 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/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()); } diff --git a/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs new file mode 100644 index 000000000..d70ac672f --- /dev/null +++ b/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)); + } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b0a36b3ae..08c024f43 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/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) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 3c39e5503..3dfb14d71 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/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; diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 49568b087..68a3491b5 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/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), }; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9c2184029..c38beb723 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/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(); } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index c40560660..1b5fab7a8 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/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, diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 53c832ff3..d48426672 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/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; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 5ce5fd4fa..9a83dba45 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/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); diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index a3035bf61..2f3a5d117 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/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; } } diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs index c92640818..4f1198902 100644 --- a/MediaBrowser.Model/Entities/ParentalRating.cs +++ b/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; } } diff --git a/MediaBrowser.Model/Entities/ParentalRatingEntry.cs b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs new file mode 100644 index 000000000..69be74ac0 --- /dev/null +++ b/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; } +} diff --git a/MediaBrowser.Model/Entities/ParentalRatingScore.cs b/MediaBrowser.Model/Entities/ParentalRatingScore.cs new file mode 100644 index 000000000..b9bb99685 --- /dev/null +++ b/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; } +} diff --git a/MediaBrowser.Model/Entities/ParentalRatingSystem.cs b/MediaBrowser.Model/Entities/ParentalRatingSystem.cs new file mode 100644 index 000000000..b452f2901 --- /dev/null +++ b/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; } +} diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 20deaa505..d9df95325 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/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); } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 49d7c0bcb..6605064ad 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -209,6 +209,7 @@ namespace MediaBrowser.Model.Querying ExternalEtag, PresentationUniqueKey, InheritedParentalRatingValue, + InheritedParentalRatingSubValue, ExternalSeriesId, SeriesPresentationUniqueKey, DateLastRefreshed, diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 3d430e101..2c393ca86 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/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; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e8994693d..45f66f85f 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/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); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index fc9695a09..332dec2e6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/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; } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs index 31538b5bf..4da7074ec 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs +++ b/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. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs new file mode 100644 index 000000000..d6befbe5e --- /dev/null +++ b/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 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs new file mode 100644 index 000000000..71f56a149 --- /dev/null +++ b/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"); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 5d8ddde08..8b2b26934 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs +++ b/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"); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 5babc9117..026da4992 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/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] |
