From aad7506e854e5118bf70a5f103e2128f765ea7f2 Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Sun, 2 Mar 2025 11:23:01 -0500 Subject: Backport pull request #13618 from jellyfin/release-10.10.z Include Role and SortOrder in MergePeople to fix "Search for missing metadata" Original-merge: fcdef875a2b0e49bc0ebeec12797c91ddb8f9bdc Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Providers/Manager/MetadataService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 778fbc7125..1d3ddc4e24 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1162,6 +1162,16 @@ namespace MediaBrowser.Providers.Manager { person.ImageUrl = personInSource.ImageUrl; } + + if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role)) + { + person.Role = personInSource.Role; + } + + if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue) + { + person.SortOrder = personInSource.SortOrder; + } } } } -- cgit v1.2.3 From aabaf1a656e732e5208e251271bd1553c2b576de Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Thu, 20 Mar 2025 05:55:51 -0400 Subject: Backport pull request #13720 from jellyfin/release-10.10.z Fix regression where "Search for missing metadata" not handling cast having multiple roles Original-merge: 91ca81eca7d2c984a096a396cbd83d0111f41c9d Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Providers/Manager/MetadataService.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1d3ddc4e24..e8994693de 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager private static void MergePeople(IReadOnlyList source, IReadOnlyList target) { - foreach (var person in target) + var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); + var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); + + foreach (var name in targetByName.Select(g => g.Key)) { - var normalizedName = person.Name.RemoveDiacritics(); - var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase)); + var targetPeople = targetByName[name].ToArray(); + var sourcePeople = sourceByName[name].ToArray(); - if (personInSource is not null) + if (sourcePeople.Length == 0) { + continue; + } + + for (int i = 0; i < targetPeople.Length; i++) + { + var person = targetPeople[i]; + var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0]; + foreach (var providerId in personInSource.ProviderIds) { person.ProviderIds.TryAdd(providerId.Key, providerId.Value); -- cgit v1.2.3 From c906c1ca0d4ded3430f4328240f487ed962e9b32 Mon Sep 17 00:00:00 2001 From: LJQ Date: Sat, 29 Mar 2025 02:33:58 +0800 Subject: rebase --- MediaBrowser.Providers/Manager/MetadataService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e8994693de..2cb3814095 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -780,7 +780,9 @@ namespace MediaBrowser.Providers.Manager } else { - var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata; + var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options.ReplaceAllMetadata) + // Case for Scan for new and updated files + || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadata); MergeData(temp, metadata, item.LockedFields, shouldReplace, true); } } -- cgit v1.2.3 From 3fc3b04daf929d1d3a9533fc410cb77885eb2e8a Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 31 Mar 2025 05:51:54 +0200 Subject: Rework parental ratings (#12615) --- .../Emby.Server.Implementations.csproj | 2 +- .../Library/SplashscreenPostScanTask.cs | 13 +- .../Localization/LocalizationManager.cs | 168 +- .../Localization/Ratings/0-prefer.csv | 11 - .../Localization/Ratings/0-prefer.json | 34 + .../Localization/Ratings/au.csv | 17 - .../Localization/Ratings/au.json | 69 + .../Localization/Ratings/be.csv | 11 - .../Localization/Ratings/be.json | 55 + .../Localization/Ratings/br.csv | 14 - .../Localization/Ratings/br.json | 55 + .../Localization/Ratings/ca.csv | 18 - .../Localization/Ratings/ca.json | 90 ++ .../Localization/Ratings/cl.json | 41 + .../Localization/Ratings/co.csv | 7 - .../Localization/Ratings/co.json | 55 + .../Localization/Ratings/de.csv | 17 - .../Localization/Ratings/de.json | 41 + .../Localization/Ratings/dk.csv | 7 - .../Localization/Ratings/dk.json | 48 + .../Localization/Ratings/es.csv | 25 - .../Localization/Ratings/es.json | 90 ++ .../Localization/Ratings/fi.csv | 10 - .../Localization/Ratings/fi.json | 41 + .../Localization/Ratings/fr.csv | 13 - .../Localization/Ratings/fr.json | 69 + .../Localization/Ratings/gb.csv | 23 - .../Localization/Ratings/gb.json | 97 ++ .../Localization/Ratings/ie.csv | 10 - .../Localization/Ratings/ie.json | 55 + .../Localization/Ratings/jp.csv | 11 - .../Localization/Ratings/jp.json | 62 + .../Localization/Ratings/kz.csv | 6 - .../Localization/Ratings/kz.json | 41 + .../Localization/Ratings/mx.csv | 6 - .../Localization/Ratings/mx.json | 41 + .../Localization/Ratings/nl.csv | 8 - .../Localization/Ratings/nl.json | 55 + .../Localization/Ratings/no.csv | 10 - .../Localization/Ratings/no.json | 69 + .../Localization/Ratings/nz.csv | 16 - .../Localization/Ratings/nz.json | 69 + .../Localization/Ratings/ro.csv | 6 - .../Localization/Ratings/ro.json | 48 + .../Localization/Ratings/ru.csv | 6 - .../Localization/Ratings/ru.json | 48 + .../Localization/Ratings/se.csv | 10 - .../Localization/Ratings/se.json | 55 + .../Localization/Ratings/sk.csv | 6 - .../Localization/Ratings/sk.json | 41 + .../Localization/Ratings/uk.csv | 22 - .../Localization/Ratings/uk.json | 97 ++ .../Localization/Ratings/us.csv | 52 - .../Localization/Ratings/us.json | 83 + .../Sorting/OfficialRatingComparer.cs | 71 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LocalizationController.cs | 5 +- .../Extensions/ExpressionExtensions.cs | 70 + .../Item/BaseItemRepository.cs | 83 +- .../Users/UserManager.cs | 6 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 2 +- .../Migrations/Routines/MigrateRatingLevels.cs | 88 +- .../Migrations/Routines/MigrateUserDb.cs | 3 +- MediaBrowser.Controller/Entities/BaseItem.cs | 52 +- .../Entities/InternalItemsQuery.cs | 19 +- MediaBrowser.Model/Dto/MetadataEditorInfo.cs | 70 +- MediaBrowser.Model/Entities/ParentalRating.cs | 55 +- MediaBrowser.Model/Entities/ParentalRatingEntry.cs | 22 + MediaBrowser.Model/Entities/ParentalRatingScore.cs | 32 + .../Entities/ParentalRatingSystem.cs | 28 + .../Globalization/ILocalizationManager.cs | 101 +- MediaBrowser.Model/Querying/ItemFields.cs | 1 + MediaBrowser.Model/Users/UserPolicy.cs | 2 + MediaBrowser.Providers/Manager/MetadataService.cs | 1 + .../Entities/BaseItemEntity.cs | 2 + .../Entities/User.cs | 9 +- ..._AddInheritedParentalRatingSubValue.Designer.cs | 1658 ++++++++++++++++++++ ...326065026_AddInheritedParentalRatingSubValue.cs | 48 + .../Migrations/JellyfinDbModelSnapshot.cs | 222 ++- .../Localization/LocalizationManagerTests.cs | 66 +- 80 files changed, 3992 insertions(+), 802 deletions(-) delete mode 100644 Emby.Server.Implementations/Localization/Ratings/0-prefer.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/0-prefer.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/au.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/au.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/be.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/be.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/br.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/br.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ca.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ca.json create mode 100644 Emby.Server.Implementations/Localization/Ratings/cl.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/co.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/co.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/de.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/de.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/dk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/dk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/es.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/es.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/fi.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/fi.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/fr.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/fr.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/gb.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/gb.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ie.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ie.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/jp.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/jp.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/kz.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/kz.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/mx.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/mx.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/nl.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/nl.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/no.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/no.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/nz.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/nz.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ro.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ro.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/se.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/se.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/sk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/sk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/uk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/uk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/us.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/us.json create mode 100644 Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingEntry.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingScore.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingSystem.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 6722c20da6..d99923b4fc 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -68,6 +68,6 @@ - + diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 0c9edd8398..71ce3b6012 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 754a01329b..9598f9e6c7 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 _logger; - private readonly Dictionary> _allParentalRatings = - new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary> _dictionaries = - new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _dictionaries = new(StringComparer.OrdinalIgnoreCase); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private List _cultures = new List(); + private List _cultures = []; /// /// Initializes a new instance of the class. @@ -68,35 +65,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string countryCode = resource.Substring(RatingsPath.Length, 2); - var dict = new Dictionary(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(stream, _jsonOptions).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + var dict = new Dictionary(); + 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 list = new List(); + List 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; + } } /// @@ -176,82 +171,80 @@ namespace Emby.Server.Implementations.Localization } /// - public IEnumerable GetCountries() + public IReadOnlyList GetCountries() { - using StreamReader reader = new StreamReader( - _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'")); - return JsonSerializer.Deserialize>(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>(stream, _jsonOptions) ?? []; } /// - public IEnumerable GetParentalRatings() + public IReadOnlyList 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(); + 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)]; } /// /// Gets the parental ratings dictionary. /// /// The optional two letter ISO language string. - /// . - private Dictionary? GetParentalRatingsDictionary(string? countryCode = null) + /// . + private Dictionary? 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 } /// - 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 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>(stream, _jsonOptions).ConfigureAwait(false); - if (dict is null) - { - throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); - } - + var dict = await JsonSerializer.DeserializeAsync>(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 36886ba760..0000000000 --- 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 0000000000..b390151611 --- /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 6e12759a46..0000000000 --- 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 0000000000..a563df899d --- /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 d171a71328..0000000000 --- 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 0000000000..18ea2c2605 --- /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 f6053c88c7..0000000000 --- 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 0000000000..f455b6643f --- /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 41dbda1349..0000000000 --- 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 0000000000..fa43a8f2b7 --- /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 0000000000..0866194715 --- /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 e1e96c5909..0000000000 --- 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 0000000000..4eff6dcc53 --- /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 f6181575e2..0000000000 --- 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 0000000000..30c34b230c --- /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 4ef63b2eac..0000000000 --- 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 0000000000..9fcd6d44fd --- /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 ee58660900..0000000000 --- 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 0000000000..c19629939d --- /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 7ff92f259b..0000000000 --- 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 0000000000..3152317b59 --- /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 139ea376b7..0000000000 --- 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 0000000000..e8bafd6b87 --- /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 858b9a32dd..0000000000 --- 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 0000000000..7fc88272cf --- /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 d3c634fc93..0000000000 --- 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 0000000000..f6cc56ed6d --- /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 bfb5fdaae9..0000000000 --- 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 0000000000..efff9e92ce --- /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 e26b32b67e..0000000000 --- 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 0000000000..0f8f0c68e5 --- /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 305912f239..0000000000 --- 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 0000000000..9dc3b89bd6 --- /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 44f372b2d6..0000000000 --- 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 0000000000..2e43eb83ab --- /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 6856a2dbbb..0000000000 --- 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 0000000000..a5e9523163 --- /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 633da78fe1..0000000000 --- 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 0000000000..3c1332271e --- /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 44c23e2486..0000000000 --- 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 0000000000..9cf735a54c --- /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 8b264070ba..0000000000 --- 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 0000000000..d1b8b13aa0 --- /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 e129c35617..0000000000 --- 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 0000000000..70084995d1 --- /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 dbafd8efa3..0000000000 --- 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 0000000000..5ec6111ecd --- /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 75b1c20589..0000000000 --- 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 0000000000..7fc88272cf --- /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 9aa5c00eb3..0000000000 --- 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 0000000000..08a6373129 --- /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 b4ee2c7234..789af01cc3 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; + +/// +/// Class providing comparison for official ratings. +/// +public class OfficialRatingComparer : IBaseItemComparer { - public class OfficialRatingComparer : IBaseItemComparer + private readonly ILocalizationManager _localizationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public OfficialRatingComparer(ILocalizationManager localizationManager) { - private readonly ILocalizationManager _localization; + _localizationManager = localizationManager; + } - public OfficialRatingComparer(ILocalizationManager localization) + /// + /// Gets the name. + /// + /// The name. + public ItemSortBy Type => ItemSortBy.OfficialRating; + + /// + /// Compares the specified x. + /// + /// The x. + /// The y. + /// System.Int32. + 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); } - /// - /// Gets the name. - /// - /// The name. - public ItemSortBy Type => ItemSortBy.OfficialRating; - - /// - /// Compares the specified x. - /// - /// The x. - /// The y. - /// System.Int32. - 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 803c2f1f78..a491283363 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 f65d95c411..bbce5a9e13 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 /// An containing the list of countries. [HttpGet("Countries")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetCountries() + public ActionResult> GetCountries() { return Ok(_localization.GetCountries()); } @@ -57,7 +56,7 @@ public class LocalizationController : BaseJellyfinApiController /// An containing the list of parental ratings. [HttpGet("ParentalRatings")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetParentalRatings() + public ActionResult> 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 0000000000..d70ac672f2 --- /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; + +/// +/// Provides extension methods. +/// +public static class ExpressionExtensions +{ + /// + /// Combines two predicates into a single predicate using a logical OR operation. + /// + /// The predicate parameter type. + /// The first predicate expression to combine. + /// The second predicate expression to combine. + /// A new expression representing the OR combination of the input predicates. + public static Expression> Or(this Expression> firstPredicate, Expression> secondPredicate) + { + ArgumentNullException.ThrowIfNull(firstPredicate); + ArgumentNullException.ThrowIfNull(secondPredicate); + + var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters); + return Expression.Lambda>(Expression.OrElse(firstPredicate.Body, invokedExpression), firstPredicate.Parameters); + } + + /// + /// Combines multiple predicates into a single predicate using a logical OR operation. + /// + /// The predicate parameter type. + /// A collection of predicate expressions to combine. + /// A new expression representing the OR combination of all input predicates. + public static Expression> Or(this IEnumerable>> predicates) + { + ArgumentNullException.ThrowIfNull(predicates); + + return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.Or(nextPredicate)); + } + + /// + /// Combines two predicates into a single predicate using a logical AND operation. + /// + /// The predicate parameter type. + /// The first predicate expression to combine. + /// The second predicate expression to combine. + /// A new expression representing the AND combination of the input predicates. + public static Expression> And(this Expression> firstPredicate, Expression> secondPredicate) + { + ArgumentNullException.ThrowIfNull(firstPredicate); + ArgumentNullException.ThrowIfNull(secondPredicate); + + var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters); + return Expression.Lambda>(Expression.AndAlso(firstPredicate.Body, invokedExpression), firstPredicate.Parameters); + } + + /// + /// Combines multiple predicates into a single predicate using a logical AND operation. + /// + /// The predicate parameter type. + /// A collection of predicate expressions to combine. + /// A new expression representing the AND combination of all input predicates. + public static Expression> And(this IEnumerable>> 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 b0a36b3ae6..08c024f437 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>? 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>? 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> 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 3c39e5503b..3dfb14d716 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 49568b087a..68a3491b57 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 9c2184029c..c38beb7232 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 { /// - /// Migrate rating levels to new rating level system. + /// Migrate rating levels. /// - internal class MigrateRatingLevels : IMigrationRoutine + internal class MigrateRatingLevels : IDatabaseMigrationRoutine { - private const string DbFilename = "library.db"; private readonly ILogger _logger; - private readonly IServerApplicationPaths _applicationPaths; + private readonly IDbContextFactory _provider; private readonly ILocalizationManager _localizationManager; public MigrateRatingLevels( - IServerApplicationPaths applicationPaths, + IDbContextFactory provider, ILoggerFactory loggerFactory, ILocalizationManager localizationManager) { - _applicationPaths = applicationPaths; + _provider = provider; _localizationManager = localizationManager; _logger = loggerFactory.CreateLogger(); } /// - public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}"); + public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}"); /// public string Name => "MigrateRatingLevels"; @@ -41,54 +38,37 @@ namespace Jellyfin.Server.Migrations.Routines /// 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 c40560660d..1b5fab7a89 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 53c832ff37..d484266726 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; } + /// /// Gets or sets the critic rating. /// @@ -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 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 5ce5fd4fa9..9a83dba458 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(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(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 a3035bf612..2f3a5d1179 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; + +/// +/// A class representing metadata editor information. +/// +public class MetadataEditorInfo { - public class MetadataEditorInfo + /// + /// Initializes a new instance of the class. + /// + public MetadataEditorInfo() { - public MetadataEditorInfo() - { - ParentalRatingOptions = Array.Empty(); - Countries = Array.Empty(); - Cultures = Array.Empty(); - ExternalIdInfos = Array.Empty(); - ContentTypeOptions = Array.Empty(); - } - - public IReadOnlyList ParentalRatingOptions { get; set; } - - public IReadOnlyList Countries { get; set; } - - public IReadOnlyList Cultures { get; set; } - - public IReadOnlyList ExternalIdInfos { get; set; } - - public CollectionType? ContentType { get; set; } - - public IReadOnlyList ContentTypeOptions { get; set; } + ParentalRatingOptions = []; + Countries = []; + Cultures = []; + ExternalIdInfos = []; + ContentTypeOptions = []; } + + /// + /// Gets or sets the parental rating options. + /// + public IReadOnlyList ParentalRatingOptions { get; set; } + + /// + /// Gets or sets the countries. + /// + public IReadOnlyList Countries { get; set; } + + /// + /// Gets or sets the cultures. + /// + public IReadOnlyList Cultures { get; set; } + + /// + /// Gets or sets the external id infos. + /// + public IReadOnlyList ExternalIdInfos { get; set; } + + /// + /// Gets or sets the content type. + /// + public CollectionType? ContentType { get; set; } + + /// + /// Gets or sets the content type options. + /// + public IReadOnlyList ContentTypeOptions { get; set; } } diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs index c92640818c..4f11989021 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 +/// +/// Class ParentalRating. +/// +public class ParentalRating { /// - /// Class ParentalRating. + /// Initializes a new instance of the class. /// - public class ParentalRating + /// The name. + /// The score. + 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; - } + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } + /// + /// Gets or sets the value. + /// + /// The value. + /// + /// Deprecated. + /// + public int? Value { get; set; } - /// - /// Gets or sets the value. - /// - /// The value. - public int? Value { get; set; } - } + /// + /// Gets or sets the rating score. + /// + /// The rating score. + public ParentalRatingScore? RatingScore { get; set; } } diff --git a/MediaBrowser.Model/Entities/ParentalRatingEntry.cs b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs new file mode 100644 index 0000000000..69be74ac0a --- /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; + +/// +/// A class representing an parental rating entry. +/// +public class ParentalRatingEntry +{ + /// + /// Gets or sets the rating strings. + /// + [JsonPropertyName("ratingStrings")] + public required IReadOnlyList RatingStrings { get; set; } + + /// + /// Gets or sets the score. + /// + [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 0000000000..b9bb99685f --- /dev/null +++ b/MediaBrowser.Model/Entities/ParentalRatingScore.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace MediaBrowser.Model.Entities; + +/// +/// A class representing an parental rating score. +/// +public class ParentalRatingScore +{ + /// + /// Initializes a new instance of the class. + /// + /// The score. + /// The sub score. + public ParentalRatingScore(int score, int? subScore) + { + Score = score; + SubScore = subScore; + } + + /// + /// Gets or sets the score. + /// + [JsonPropertyName("score")] + public int Score { get; set; } + + /// + /// Gets or sets the sub score. + /// + [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 0000000000..b452f29018 --- /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; + +/// +/// A class representing a parental rating system. +/// +public class ParentalRatingSystem +{ + /// + /// Gets or sets the country code. + /// + [JsonPropertyName("countryCode")] + public required string CountryCode { get; set; } + + /// + /// Gets or sets a value indicating whether sub scores are supported. + /// + [JsonPropertyName("supportsSubScores")] + public bool SupportsSubScores { get; set; } + + /// + /// Gets or sets the ratings. + /// + [JsonPropertyName("ratings")] + public IReadOnlyList? Ratings { get; set; } +} diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 20deaa5057..d9df95325c 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; + +/// +/// Interface ILocalizationManager. +/// +public interface ILocalizationManager { /// - /// Interface ILocalizationManager. + /// Gets the cultures. /// - public interface ILocalizationManager - { - /// - /// Gets the cultures. - /// - /// . - IEnumerable GetCultures(); + /// . + IEnumerable GetCultures(); - /// - /// Gets the countries. - /// - /// . - IEnumerable GetCountries(); + /// + /// Gets the countries. + /// + /// . + IReadOnlyList GetCountries(); - /// - /// Gets the parental ratings. - /// - /// . - IEnumerable GetParentalRatings(); + /// + /// Gets the parental ratings. + /// + /// . + IReadOnlyList GetParentalRatings(); - /// - /// Gets the rating level. - /// - /// The rating. - /// The optional two letter ISO language string. - /// or null. - int? GetRatingLevel(string rating, string? countryCode = null); + /// + /// Gets the rating level. + /// + /// The rating. + /// The optional two letter ISO language string. + /// or null. + ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null); - /// - /// Gets the localized string. - /// - /// The phrase. - /// The culture. - /// . - string GetLocalizedString(string phrase, string culture); + /// + /// Gets the localized string. + /// + /// The phrase. + /// The culture. + /// . + string GetLocalizedString(string phrase, string culture); - /// - /// Gets the localized string. - /// - /// The phrase. - /// System.String. - string GetLocalizedString(string phrase); + /// + /// Gets the localized string. + /// + /// The phrase. + /// System.String. + string GetLocalizedString(string phrase); - /// - /// Gets the localization options. - /// - /// . - IEnumerable GetLocalizationOptions(); + /// + /// Gets the localization options. + /// + /// . + IEnumerable GetLocalizationOptions(); - /// - /// Returns the correct for the given language. - /// - /// The language. - /// The correct for the given language. - CultureDto? FindLanguageInfo(string language); - } + /// + /// Returns the correct for the given language. + /// + /// The language. + /// The correct for the given language. + CultureDto? FindLanguageInfo(string language); } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 49d7c0bcb0..6605064ad6 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 3d430e1015..2c393ca862 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -111,6 +111,8 @@ namespace MediaBrowser.Model.Users /// The max parental rating. 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 e8994693de..45f66f85fc 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 fc9695a091..332dec2e67 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 31538b5bf3..4da7074ec7 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; } /// - /// Gets or sets the maximum parental age rating. + /// Gets or sets the maximum parental rating score. /// - public int? MaxParentalAgeRating { get; set; } + public int? MaxParentalRatingScore { get; set; } + + /// + /// Gets or sets the maximum parental rating sub score. + /// + public int? MaxParentalRatingSubScore { get; set; } /// /// 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 0000000000..d6befbe5eb --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs @@ -0,0 +1,1658 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("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("Id") + .HasColumnType("INTEGER"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .HasColumnType("INTEGER"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("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("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("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("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("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 0000000000..71f56a1492 --- /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 +{ + /// + public partial class AddInheritedParentalRatingSubValue : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "MaxParentalAgeRating", + table: "Users", + newName: "MaxParentalRatingScore"); + + migrationBuilder.AddColumn( + name: "MaxParentalRatingSubScore", + table: "Users", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "InheritedParentalRatingSubValue", + table: "BaseItems", + type: "INTEGER", + nullable: true); + } + + /// + 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 5d8ddde082..8b2b269345 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("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("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("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("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("Id") .ValueGeneratedOnAdd() @@ -218,6 +226,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("IndexNumber") .HasColumnType("INTEGER"); + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + b.Property("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("Id") .ValueGeneratedOnAdd() @@ -1200,7 +1257,10 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("MaxActiveSessions") .HasColumnType("INTEGER"); - b.Property("MaxParentalAgeRating") + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") .HasColumnType("INTEGER"); b.Property("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("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 5babc91174..026da4992a 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] -- cgit v1.2.3 From 5d65cfcd994d44665cc7804b3a8e9049d4915788 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Apr 2025 18:32:12 +0300 Subject: improved performance of save operations (#13889) --- .../Item/BaseItemRepository.cs | 143 +++++++++++++-------- .../Item/PeopleRepository.cs | 51 ++++---- MediaBrowser.Providers/Manager/MetadataService.cs | 5 +- 3 files changed, 122 insertions(+), 77 deletions(-) (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7f4364cf61..e7f82389db 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -453,11 +453,9 @@ public sealed class BaseItemRepository var images = item.ImageInfos.Select(e => Map(item.Id, e)); using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); context.BaseItemImageInfos.AddRange(images); context.SaveChanges(); - transaction.Commit(); } /// @@ -487,17 +485,19 @@ public sealed class BaseItemRepository tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); } - var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>(); - using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); + + var ids = tuples.Select(f => f.Item.Id).ToArray(); + var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); + foreach (var item in tuples) { var entity = Map(item.Item); // TODO: refactor this "inconsistency" entity.TopParentId = item.TopParent?.Id; - if (!context.BaseItems.Any(e => e.Id == entity.Id)) + if (!existingItems.Any(e => e == entity.Id)) { context.BaseItems.Add(entity); } @@ -506,59 +506,98 @@ public sealed class BaseItemRepository context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItems.Attach(entity).State = EntityState.Modified; } + } - context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - if (item.Item.SupportsAncestors && item.AncestorIds != null) + context.SaveChanges(); + + var itemValueMaps = tuples + .Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) + .ToArray(); + var allListedItemValues = itemValueMaps + .SelectMany(f => f.Values) + .Distinct() + .ToArray(); + var existingValues = context.ItemValues + .Select(e => new { - foreach (var ancestorId in item.AncestorIds) - { - if (!context.BaseItems.Any(f => f.Id == ancestorId)) - { - continue; - } + item = e, + Key = e.Type + "+" + e.Value + }) + .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key)) + .Select(e => e.item) + .ToArray(); + var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue() + { + CleanValue = GetCleanValue(f.Value), + ItemValueId = Guid.NewGuid(), + Type = f.MagicNumber, + Value = f.Value + }).ToArray(); + context.ItemValues.AddRange(missingItemValues); + context.SaveChanges(); + + var itemValuesStore = existingValues.Concat(missingItemValues).ToArray(); + var valueMap = itemValueMaps + .Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray())) + .ToArray(); + + var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList(); - context.AncestorIds.Add(new AncestorId() + foreach (var item in valueMap) + { + var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList(); + foreach (var itemValue in item.Values) + { + var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId); + if (existingItem is null) + { + context.ItemValuesMap.Add(new ItemValueMap() { - ParentItemId = ancestorId, - ItemId = entity.Id, Item = null!, - ParentItem = null! + ItemId = item.Item.Id, + ItemValue = null!, + ItemValueId = itemValue.ItemValueId }); } - } - - // Never save duplicate itemValues as they are now mapped anyway. - var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber)); - context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - foreach (var itemValue in itemValuesToSave) - { - if (!localItemValueCache.TryGetValue(itemValue, out var refValue)) + else { - refValue = context.ItemValues - .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber) - .Select(e => e.ItemValueId) - .FirstOrDefault(); + // map exists, remove from list so its been handled. + itemMappedValues.Remove(existingItem); } + } - if (refValue.IsEmpty()) + // all still listed values are not in the new list so remove them. + context.ItemValuesMap.RemoveRange(itemMappedValues); + } + + context.SaveChanges(); + + foreach (var item in tuples) + { + if (item.Item.SupportsAncestors && item.AncestorIds != null) + { + var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList(); + var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).ToArray(); + foreach (var ancestorId in validAncestorIds) { - context.ItemValues.Add(new ItemValue() + var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId); + if (existingAncestorId is null) { - CleanValue = GetCleanValue(itemValue.Value), - Type = (ItemValueType)itemValue.MagicNumber, - ItemValueId = refValue = Guid.NewGuid(), - Value = itemValue.Value - }); - localItemValueCache[itemValue] = refValue; + context.AncestorIds.Add(new AncestorId() + { + ParentItemId = ancestorId, + ItemId = item.Item.Id, + Item = null!, + ParentItem = null! + }); + } + else + { + existingAncestorIds.Remove(existingAncestorId); + } } - context.ItemValuesMap.Add(new ItemValueMap() - { - Item = null!, - ItemId = entity.Id, - ItemValue = null!, - ItemValueId = refValue - }); + context.AncestorIds.RemoveRange(existingAncestorIds); } } @@ -1102,27 +1141,27 @@ public sealed class BaseItemRepository return value.RemoveDiacritics().ToLowerInvariant(); } - private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List inheritedTags) + private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List inheritedTags) { - var list = new List<(int, string)>(); + var list = new List<(ItemValueType, string)>(); if (item is IHasArtist hasArtist) { - list.AddRange(hasArtist.Artists.Select(i => (0, i))); + list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i))); } if (item is IHasAlbumArtist hasAlbumArtist) { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i))); } - list.AddRange(item.Genres.Select(i => (2, i))); - list.AddRange(item.Studios.Select(i => (3, i))); - list.AddRange(item.Tags.Select(i => (4, i))); + list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i))); + list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i))); + list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i))); // keywords was 5 - list.AddRange(inheritedTags.Select(i => (6, i))); + list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i))); // Remove all invalid values. list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 77877835e0..4e898119b7 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Libraries; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; @@ -68,34 +69,41 @@ public class PeopleRepository(IDbContextFactory dbProvider, I public void UpdatePeople(Guid itemId, IReadOnlyList people) { using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete(); // TODO: yes for __SOME__ reason there can be duplicates. - foreach (var item in people.DistinctBy(e => e.Id)) + people = people.DistinctBy(e => e.Id).ToArray(); + var personids = people.Select(f => f.Id); + var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray(); + context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map)); + context.SaveChanges(); + + var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + foreach (var person in people) { - var personEntity = Map(item); - var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id); - if (existingEntity is null) + var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id); + if (existingMap is null) { - context.Peoples.Add(personEntity); - existingEntity = personEntity; + context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = null!, + PeopleId = person.Id, + ListOrder = person.SortOrder, + SortOrder = person.SortOrder, + Role = person.Role + }); } - - context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + else { - Item = null!, - ItemId = itemId, - People = existingEntity, - PeopleId = existingEntity.Id, - ListOrder = item.SortOrder, - SortOrder = item.SortOrder, - Role = item.Role - }); + // person mapping already exists so remove from list + maps.Remove(existingMap); + } } + context.PeopleBaseItemMap.RemoveRange(maps); + context.SaveChanges(); - transaction.Commit(); } private PersonInfo Map(People people) @@ -133,9 +141,8 @@ public class PeopleRepository(IDbContextFactory dbProvider, I if (filter.User is not null && filter.IsFavorite.HasValue) { var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; - query = query.Where(e => e.PersonType == personType) - .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id))) - .Select(f => f.Name).Contains(e.Name)); + query = query + .Where(e => context.BaseItems.Any(b => b.Type == personType && b.Name == e.Name && b.UserData!.Any(u => u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.User.Id)))); } if (!filter.ItemId.IsEmpty()) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 45f66f85fc..413bf2d30b 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -128,8 +128,7 @@ namespace MediaBrowser.Providers.Manager var metadataResult = new MetadataResult { - Item = itemOfType, - People = LibraryManager.GetPeople(item) + Item = itemOfType }; var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType); @@ -253,7 +252,7 @@ namespace MediaBrowser.Providers.Manager protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) { - if (result.Item.SupportsPeople) + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; -- cgit v1.2.3 From f576783ae11d66ab1c4437dd975eefeea638bb3f Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 27 Apr 2025 02:49:30 +0200 Subject: Fix distinction queries (#14007) --- Emby.Server.Implementations/IO/FileRefresher.cs | 2 +- Emby.Server.Implementations/IO/LibraryMonitor.cs | 2 +- .../Library/LibraryManager.cs | 6 ++-- Jellyfin.Api/Controllers/ItemUpdateController.cs | 8 ++--- Jellyfin.Api/Controllers/YearsController.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 2 +- .../Encoder/EncoderValidator.cs | 30 +++++++++---------- MediaBrowser.Providers/Manager/MetadataService.cs | 8 ++--- .../Music/AlbumMetadataService.cs | 2 +- .../Music/AudioMetadataService.cs | 2 +- .../Music/MusicVideoMetadataService.cs | 3 +- .../Playlists/PlaylistItemsProvider.cs | 2 +- src/Jellyfin.LiveTv/LiveTvManager.cs | 34 +++++++++++----------- .../Recordings/RecordingsManager.cs | 2 +- 15 files changed, 54 insertions(+), 53 deletions(-) (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7378cf8851..f634084034 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List paths) { IEnumerable itemsToRefresh = paths - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(GetAffectedBaseItem) .Where(item => item is not null) .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 6af2a553d6..d87ad729ee 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO .Where(IsLibraryMonitorEnabled) .OfType() .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Order(); foreach (var path in paths) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 21c953fb2d..64a96c4e5a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -668,10 +668,10 @@ namespace Emby.Server.Implementations.Library var list = originalList.Where(i => i.IsDirectory) .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); - var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) + var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) .ToList(); foreach (var dupe in dupes) @@ -679,7 +679,7 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Found duplicate path: {0}", dupe); } - var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList(); + var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList(); newList.AddRange(originalList.Where(i => !i.IsDirectory)); return newList; } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index d49e0753ee..50eeaeac67 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -299,7 +299,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!season.LockedFields.Contains(MetadataField.Tags)) { - season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } season.OnMetadataChanged(); @@ -316,7 +316,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -337,7 +337,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -357,7 +357,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!track.LockedFields.Contains(MetadataField.Tags)) { - track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } track.OnMetadataChanged(); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index ebf98da456..b602585863 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -223,6 +223,6 @@ public class YearsController : BaseJellyfinApiController .Select(i => i.ProductionYear ?? 0) .Where(i => i > 0) .Distinct() - .Select(year => _libraryManager.GetYear(year)); + .Select(_libraryManager.GetYear); } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a7ff75bb1c..b90ec8222d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1804,7 +1804,7 @@ namespace MediaBrowser.Controller.Entities public void SetStudios(IEnumerable names) { - Studios = names.Trimmed().Distinct().ToArray(); + Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } /// diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index d656fccb4f..dd5852823e 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -197,7 +197,7 @@ namespace MediaBrowser.Controller.Entities.Movies var expandedFolders = new List(); return FlattenItems(this, expandedFolders) - .SelectMany(i => LibraryManager.GetCollectionFolders(i)) + .SelectMany(LibraryManager.GetCollectionFolders) .Select(i => i.Id) .Distinct() .ToArray(); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d28cd70ef5..5683de169f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -14,8 +14,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { public partial class EncoderValidator { - private static readonly string[] _requiredDecoders = new[] - { + private static readonly string[] _requiredDecoders = + [ "h264", "hevc", "vp8", @@ -57,10 +57,10 @@ namespace MediaBrowser.MediaEncoding.Encoder "vp8_rkmpp", "vp9_rkmpp", "av1_rkmpp" - }; + ]; - private static readonly string[] _requiredEncoders = new[] - { + private static readonly string[] _requiredEncoders = + [ "libx264", "libx265", "libsvtav1", @@ -97,10 +97,10 @@ namespace MediaBrowser.MediaEncoding.Encoder "h264_rkmpp", "hevc_rkmpp", "mjpeg_rkmpp" - }; + ]; - private static readonly string[] _requiredFilters = new[] - { + private static readonly string[] _requiredFilters = + [ // sw "alphasrc", "zscale", @@ -148,7 +148,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "scale_rkrga", "vpp_rkrga", "overlay_rkrga" - }; + ]; private static readonly Dictionary _filterOptionsDict = new Dictionary { @@ -471,10 +471,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } - var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList(); + var found = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList(); _logger.LogInformation("Available hwaccel types: {Types}", found); return found; @@ -580,12 +580,12 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available {Codec}", codecstr); - return Enumerable.Empty(); + return []; } if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders; @@ -610,12 +610,12 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available filters"); - return Enumerable.Empty(); + return []; } if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } var found = FilterRegex() diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 413bf2d30b..50bbf0974a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1041,7 +1041,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray(); + target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1053,7 +1053,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray(); + target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1065,7 +1065,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray(); + target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1214,7 +1214,7 @@ namespace MediaBrowser.Providers.Manager } else if (sourceHasAlbumArtist.AlbumArtists.Count > 0) { - targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray(); + targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 0bcc301cb9..64b6273670 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -227,7 +227,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist))) diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 7b25bc0e49..71962d952b 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } if (replaceData || string.IsNullOrEmpty(targetItem.Album)) diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index 24c4b55015..4022bedc1a 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -48,7 +49,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index 51a3ba0c7f..a986b0b699 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -99,7 +99,7 @@ public class PlaylistItemsProvider : ILocalMetadataProvider, .OfType() .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value)) .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); using (var stream = File.OpenRead(path)) diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 40adb51a58..53bc6751fc 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -125,8 +125,8 @@ namespace Jellyfin.LiveTv IsKids = query.IsKids, IsSports = query.IsSports, IsSeries = query.IsSeries, - IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, - TopParentIds = new[] { topFolder.Id }, + IncludeItemTypes = [BaseItemKind.LiveTvChannel], + TopParentIds = [topFolder.Id], IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, StartIndex = query.StartIndex, @@ -199,17 +199,17 @@ namespace Jellyfin.LiveTv if (query.OrderBy.Count == 0) { // Unless something else was specified, order by start date to take advantage of a specialized index - query.OrderBy = new[] - { + query.OrderBy = + [ (ItemSortBy.StartDate, SortOrder.Ascending) - }; + ]; } RemoveFields(options); var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], MinEndDate = query.MinEndDate, MinStartDate = query.MinStartDate, MaxEndDate = query.MaxEndDate, @@ -226,7 +226,7 @@ namespace Jellyfin.LiveTv Limit = query.Limit, OrderBy = query.OrderBy, EnableTotalRecordCount = query.EnableTotalRecordCount, - TopParentIds = new[] { topFolder.Id }, + TopParentIds = [topFolder.Id], Name = query.Name, DtoOptions = options, HasAired = query.HasAired, @@ -272,7 +272,7 @@ namespace Jellyfin.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], IsAiring = query.IsAiring, HasAired = query.HasAired, IsNews = query.IsNews, @@ -281,8 +281,8 @@ namespace Jellyfin.LiveTv IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { topFolder.Id }, + OrderBy = [(ItemSortBy.StartDate, SortOrder.Ascending)], + TopParentIds = [topFolder.Id], DtoOptions = options, GenreIds = query.GenreIds }; @@ -497,19 +497,19 @@ namespace Jellyfin.LiveTv // TotalRecordCount = items.Length // }; - dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); + dtoOptions.Fields = dtoOptions.Fields.Concat([ItemFields.Tags]).Distinct().ToArray(); } var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - MediaTypes = new[] { MediaType.Video }, + MediaTypes = [MediaType.Video], Recursive = true, AncestorIds = folderIds, IsFolder = false, IsVirtualItem = false, Limit = limit, StartIndex = query.StartIndex, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)], EnableTotalRecordCount = query.EnableTotalRecordCount, IncludeItemTypes = includeItemTypes.ToArray(), ExcludeItemTypes = excludeItemTypes.ToArray(), @@ -959,13 +959,13 @@ namespace Jellyfin.LiveTv var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], ChannelIds = channelIds, MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id }, + OrderBy = [(ItemSortBy.StartDate, SortOrder.Ascending)], + TopParentIds = [GetInternalLiveTvFolder(CancellationToken.None).Id], DtoOptions = options }) : new List(); @@ -1269,7 +1269,7 @@ namespace Jellyfin.LiveTv { var folders = _recordingsManager.GetRecordingFolders() .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(i => _libraryManager.FindByPath(i, true)) .Where(i => i is not null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 9ca5d7420b..846f9baf71 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -230,7 +230,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) { pathsAdded.InsertRange(0, config.MediaLocationsCreated); - config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct().ToArray(); _config.SaveConfiguration("livetv", config); } -- cgit v1.2.3 From d976f13970e034a24c1d0f69384501e31475a127 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 5 May 2025 05:21:44 +0200 Subject: Recognize file changes and remove data on change (#13839) --- Emby.Server.Implementations/ApplicationHost.cs | 4 +- .../Collections/CollectionManager.cs | 8 +- .../Library/KeyframeManager.cs | 44 +++ .../Library/LibraryManager.cs | 122 +++--- Emby.Server.Implementations/Library/PathManager.cs | 14 + .../Library/ResolverHelper.cs | 20 +- .../Playlists/PlaylistManager.cs | 6 +- .../Tasks/MediaSegmentExtractionTask.cs | 2 +- .../Controllers/MediaSegmentsController.cs | 2 +- .../Item/KeyframeRepository.cs | 8 + .../MediaSegments/MediaSegmentManager.cs | 9 +- .../Migrations/Routines/MigrateKeyframeData.cs | 5 + .../Migrations/Routines/MoveExtractedFiles.cs | 5 + .../Routines/RefreshInternalDateModified.cs | 131 +++++++ MediaBrowser.Controller/Entities/BaseItem.cs | 40 +- MediaBrowser.Controller/IO/IPathManager.cs | 10 +- .../Library/IKeyframeManager.cs | 37 ++ MediaBrowser.Controller/Library/ILibraryManager.cs | 8 +- .../MediaSegments/IMediaSegmentManager.cs | 9 +- .../MediaSegments/IMediaSegmentProvider.cs | 6 +- .../Persistence/IKeyframeRepository.cs | 8 + MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 +- .../Books/AudioBookMetadataService.cs | 80 ++-- .../Books/BookMetadataService.cs | 58 +-- .../BoxSets/BoxSetMetadataService.cs | 124 +++--- .../Channels/ChannelMetadataService.cs | 42 ++- .../Folders/CollectionFolderMetadataService.cs | 42 ++- .../Folders/FolderMetadataService.cs | 50 ++- .../Folders/UserViewMetadataService.cs | 42 ++- .../Genres/GenreMetadataService.cs | 42 ++- .../LiveTv/LiveTvMetadataService.cs | 42 ++- MediaBrowser.Providers/Manager/MetadataService.cs | 76 +++- MediaBrowser.Providers/Manager/ProviderManager.cs | 3 +- .../MediaInfo/AudioFileProber.cs | 1 - .../MediaInfo/FFProbeVideoInfo.cs | 16 +- MediaBrowser.Providers/MediaInfo/ProbeProvider.cs | 4 +- .../MediaInfo/SubtitleScheduledTask.cs | 1 - .../Movies/MovieMetadataService.cs | 62 +-- .../Movies/TrailerMetadataService.cs | 64 ++-- .../Music/AlbumMetadataService.cs | 359 +++++++++--------- .../Music/ArtistMetadataService.cs | 69 ++-- .../Music/AudioMetadataService.cs | 111 +++--- .../Music/MusicVideoMetadataService.cs | 88 +++-- .../MusicGenres/MusicGenreMetadataService.cs | 42 ++- .../People/PersonMetadataService.cs | 42 ++- .../Photos/PhotoAlbumMetadataService.cs | 42 ++- .../Photos/PhotoMetadataService.cs | 42 ++- .../Playlists/PlaylistMetadataService.cs | 110 +++--- .../Studios/StudioMetadataService.cs | 42 ++- .../TV/EpisodeMetadataService.cs | 164 ++++---- MediaBrowser.Providers/TV/SeasonMetadataService.cs | 155 ++++---- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 419 +++++++++++---------- .../Videos/VideoMetadataService.cs | 50 ++- .../Years/YearMetadataService.cs | 42 ++- src/Jellyfin.Drawing/ImageProcessor.cs | 4 +- src/Jellyfin.LiveTv/Channels/ChannelManager.cs | 5 +- .../Manager/ProviderManagerTests.cs | 1 + 57 files changed, 1914 insertions(+), 1124 deletions(-) create mode 100644 Emby.Server.Implementations/Library/KeyframeManager.cs create mode 100644 Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs create mode 100644 MediaBrowser.Controller/Library/IKeyframeManager.cs (limited to 'MediaBrowser.Providers/Manager/MetadataService.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index fa6e9ff977..987ce8b843 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -36,7 +36,6 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; -using Jellyfin.Database.Implementations; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; @@ -63,6 +62,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -93,7 +93,6 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -560,6 +559,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 60f515f24d..0eb387ffdd 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections var libraryOptions = new LibraryOptions { - PathInfos = new[] { new MediaPathInfo(path) }, + PathInfos = [new MediaPathInfo(path)], EnableRealtimeMonitor = false, SaveLocalMetadata = true }; @@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections try { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); var collection = new BoxSet { Name = name, Path = path, IsLocked = options.IsLocked, ProviderIds = options.ProviderIds, - DateCreated = DateTime.UtcNow + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; parentFolder.AddChild(collection); diff --git a/Emby.Server.Implementations/Library/KeyframeManager.cs b/Emby.Server.Implementations/Library/KeyframeManager.cs new file mode 100644 index 0000000000..18f4ce0479 --- /dev/null +++ b/Emby.Server.Implementations/Library/KeyframeManager.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library; + +/// +/// Manager for Keyframe data. +/// +public class KeyframeManager : IKeyframeManager +{ + private readonly IKeyframeRepository _repository; + + /// + /// Initializes a new instance of the class. + /// + /// The keyframe repository. + public KeyframeManager(IKeyframeRepository repository) + { + _repository = repository; + } + + /// + public IReadOnlyList GetKeyframeData(Guid itemId) + { + return _repository.GetKeyframeData(itemId); + } + + /// + public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken) + { + await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken) + { + await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 51f3307465..1fdd80bd8a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -208,7 +208,7 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } = []; + private ILibraryPostScanTask[] PostScanTasks { get; set; } = []; /// /// Gets or sets the intro providers. @@ -245,20 +245,20 @@ namespace Emby.Server.Implementations.Library /// The resolvers. /// The intro providers. /// The item comparers. - /// The post scan tasks. + /// The post scan tasks. public void AddParts( IEnumerable rules, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, - IEnumerable postscanTasks) + IEnumerable postScanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); - PostscanTasks = postscanTasks.ToArray(); + PostScanTasks = postScanTasks.ToArray(); } /// @@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.Library } } - if (options.DeleteFileLocation && item.IsFileProtocol) + if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item)) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future @@ -472,6 +472,36 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } + private bool IsInternalItem(BaseItem item) + { + if (!item.IsFileProtocol) + { + return false; + } + + var pathToCheck = item switch + { + Genre => _configurationManager.ApplicationPaths.GenrePath, + MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, + MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + Person => _configurationManager.ApplicationPaths.PeoplePath, + Studio => _configurationManager.ApplicationPaths.StudioPath, + Year => _configurationManager.ApplicationPaths.YearPath, + _ => null + }; + + var itemPath = item.Path; + if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath)) + { + var cleanPath = _fileSystem.GetValidFilename(itemPath); + var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck); + + return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal); + } + + return false; + } + private List GetMetadataPaths(BaseItem item, IEnumerable children) { var list = GetInternalMetadataPaths(item); @@ -639,7 +669,7 @@ namespace Emby.Server.Implementations.Library } } - // Need to remove subpaths that may have been resolved from shortcuts + // Need to remove sub-paths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { @@ -772,11 +802,12 @@ namespace Emby.Server.Implementations.Library // Add in the plug-in folders var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists"); - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); Folder folder = new PlaylistsFolder { - Path = path + Path = path, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, }; if (folder.Id.IsEmpty()) @@ -862,7 +893,7 @@ namespace Emby.Server.Implementations.Library { Path = path, IsFolder = isFolder, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)], Limit = 1, DtoOptions = new DtoOptions(true) }; @@ -968,7 +999,7 @@ namespace Emby.Server.Implementations.Library { var existing = GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, + IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, DtoOptions = options }).Cast() @@ -987,12 +1018,13 @@ namespace Emby.Server.Implementations.Library var item = GetItemById(id) as T; if (item is null) { + var info = Directory.CreateDirectory(path); item = new T { Name = name, Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Path = path }; @@ -1118,7 +1150,7 @@ namespace Emby.Server.Implementations.Library /// Task. private async Task RunPostScanTasks(IProgress progress, CancellationToken cancellationToken) { - var tasks = PostscanTasks.ToList(); + var tasks = PostScanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; @@ -1241,7 +1273,7 @@ namespace Emby.Server.Implementations.Library private CollectionTypeOptions? GetCollectionType(string path) { - var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); + var files = _fileSystem.GetFilePaths(path, [".collection"], true, false); foreach (ReadOnlySpan file in files) { if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res)) @@ -1312,7 +1344,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1343,7 +1375,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1531,7 +1563,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1561,7 +1593,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } else @@ -1572,7 +1604,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { - query.AncestorIds = new[] { Guid.NewGuid() }; + query.AncestorIds = [Guid.NewGuid()]; } } @@ -1601,7 +1633,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } } @@ -1612,7 +1644,7 @@ namespace Emby.Server.Implementations.Library { if (view.ViewType == CollectionType.livetv) { - return new[] { view.Id }; + return [view.Id]; } // Translate view into folders @@ -1661,7 +1693,7 @@ namespace Emby.Server.Implementations.Library var topParent = item.GetTopParent(); if (topParent is not null) { - return new[] { topParent.Id }; + return [topParent.Id]; } return []; @@ -1868,7 +1900,7 @@ namespace Emby.Server.Implementations.Library /// public void CreateItem(BaseItem item, BaseItem? parent) { - CreateItems(new[] { item }, parent, CancellationToken.None); + CreateItems([item], parent, CancellationToken.None); } /// @@ -2054,7 +2086,7 @@ namespace Emby.Server.Implementations.Library /// public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) - => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + => UpdateItemsAsync([item], parent, updateReason, cancellationToken); public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { @@ -2283,13 +2315,13 @@ namespace Emby.Server.Implementations.Library if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName @@ -2331,13 +2363,13 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName, @@ -2395,20 +2427,19 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; @@ -2465,20 +2496,19 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; @@ -2989,12 +3019,14 @@ namespace Emby.Server.Implementations.Library if (personEntity is null) { var path = Person.GetPath(person.Name); + var info = Directory.CreateDirectory(path); + var lastWriteTime = info.LastWriteTimeUtc; personEntity = new Person() { Name = person.Name, Id = GetItemByNameId(path), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = lastWriteTime, Path = path }; diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs index 853d85e5e5..a9b7a1274b 100644 --- a/Emby.Server.Implementations/Library/PathManager.cs +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using MediaBrowser.Common.Configuration; @@ -84,4 +85,17 @@ public class PathManager : IPathManager return Path.Combine(GetChapterImageFolderPath(item), filename); } + + /// + public IReadOnlyList GetExtractedDataPaths(BaseItem item) + { + var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture); + return [ + GetAttachmentFolderPath(mediaSourceId), + GetSubtitleFolderPath(mediaSourceId), + GetTrickplayDirectory(item, false), + GetTrickplayDirectory(item, true), + GetChapterImageFolderPath(item) + ]; + } } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index c9e3a4daf8..ab6bc49073 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library if (config.UseFileCreationTimeForDateAdded) { - // directoryService.getFile may return null - if (info is not null) + var fileCreationDate = info?.CreationTimeUtc; + if (fileCreationDate is not null) { - var dateCreated = info.CreationTimeUtc; - + var dateCreated = fileCreationDate; if (dateCreated.Equals(DateTime.MinValue)) { dateCreated = DateTime.UtcNow; } - item.DateCreated = dateCreated; + item.DateCreated = dateCreated.Value; } } else { item.DateCreated = DateTime.UtcNow; } + + if (info is not null && !info.IsDirectory) + { + item.Size = info.Length; + } + + var fileModificationDate = info?.LastWriteTimeUtc; + if (fileModificationDate.HasValue) + { + item.DateModified = fileModificationDate.Value; + } } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 98a43b6c98..1ce363de5c 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists try { - Directory.CreateDirectory(path); + var info = Directory.CreateDirectory(path); var playlist = new Playlist { Name = name, Path = path, OwnerUserId = request.UserId, Shares = request.Users ?? [], - OpenAccess = request.Public ?? false + OpenAccess = request.Public ?? false, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; playlist.SetMediaType(request.MediaType); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs index b8c944d3d9..c3f17c2aeb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -4,10 +4,10 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Controller; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index e30e2b54e4..2a91a84557 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Database.Implementations.Enums; -using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Model.MediaSegments; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs index a2267700fb..93c6f472e2 100644 --- a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs +++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs @@ -61,4 +61,12 @@ public class KeyframeRepository : IKeyframeRepository await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } + + /// + public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken) + { + using var context = _dbProvider.CreateDbContext(); + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index d6eeafacc3..5a2032c1f7 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -10,10 +10,10 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; @@ -139,6 +139,13 @@ public class MediaSegmentManager : IMediaSegmentManager await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false); } + /// + public async Task DeleteSegmentsAsync(Guid itemId) + { + using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync().ConfigureAwait(false); + } + /// public async Task> GetSegmentsAsync(Guid itemId, IEnumerable? typeFilter, bool filterByProvider = true) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index c5bc702789..1ee4c41c37 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -73,6 +73,11 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine } offset += Limit; + if (offset > records) + { + offset = records; + } + _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed); } while (offset < records); diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index 9031f2fdcf..c6471b24c8 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -95,6 +95,11 @@ public class MoveExtractedFiles : IMigrationRoutine } offset += Limit; + if (offset > records) + { + offset = records; + } + _logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed); } while (offset < records); diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs new file mode 100644 index 0000000000..9a95757a8c --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to re-read creation dates for library items with internal metadata paths. +/// +[JellyfinMigration("2025-04-20T23:00:00", nameof(RefreshInternalDateModified), "32E762EB-4918-45CE-A44C-C801F66B877D", RunMigrationOnSetup = false)] +public class RefreshInternalDateModified : IDatabaseMigrationRoutine +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _applicationHost; + private readonly bool _useFileCreationTimeForDateAdded; + + private IReadOnlyList _internalTypes = [ + typeof(Genre).FullName!, + typeof(MusicGenre).FullName!, + typeof(MusicArtist).FullName!, + typeof(People).FullName!, + typeof(Studio).FullName! + ]; + + private IReadOnlyList _internalPaths; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// The logger. + /// Instance of the interface. + public RefreshInternalDateModified( + IServerApplicationHost applicationHost, + IServerApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager, + IDbContextFactory dbProvider, + ILogger logger, + IFileSystem fileSystem) + { + _dbProvider = dbProvider; + _logger = logger; + _fileSystem = fileSystem; + _applicationHost = applicationHost; + _internalPaths = [ + applicationPaths.ArtistsPath, + applicationPaths.GenrePath, + applicationPaths.MusicGenrePath, + applicationPaths.StudioPath, + applicationPaths.PeoplePath + ]; + _useFileCreationTimeForDateAdded = configurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded; + } + + /// + public void Perform() + { + const int Limit = 5000; + int itemCount = 0, offset = 0; + + var sw = Stopwatch.StartNew(); + + using var context = _dbProvider.CreateDbContext(); + var records = context.BaseItems.Count(b => _internalTypes.Contains(b.Type)); + _logger.LogInformation("Checking if {Count} potentially internal items require refreshed DateModified", records); + + do + { + var results = context.BaseItems + .Where(b => _internalTypes.Contains(b.Type)) + .OrderBy(e => e.Id) + .Skip(offset) + .Take(Limit) + .ToList(); + + foreach (var item in results) + { + var itemPath = item.Path; + if (itemPath is not null) + { + var realPath = _applicationHost.ExpandVirtualPath(item.Path); + if (_internalPaths.Any(path => realPath.StartsWith(path, StringComparison.Ordinal))) + { + var writeTime = _fileSystem.GetLastWriteTimeUtc(realPath); + var itemModificationTime = item.DateModified; + if (writeTime != itemModificationTime) + { + _logger.LogDebug("Reset file modification date: Old: {Old} - New: {New} - Path: {Path}", itemModificationTime, writeTime, realPath); + item.DateModified = writeTime; + if (_useFileCreationTimeForDateAdded) + { + item.DateCreated = _fileSystem.GetCreationTimeUtc(realPath); + } + + itemCount++; + } + } + } + } + + offset += Limit; + if (offset > records) + { + offset = records; + } + + _logger.LogInformation("Checked: {Count} - Refreshed: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed); + } while (offset < records); + + context.SaveChanges(); + + _logger.LogInformation("Refreshed DateModified for {Count} items in {Time}", itemCount, sw.Elapsed); + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b90ec8222d..16fde9440a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -25,6 +25,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; @@ -1265,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Overrides the base implementation to refresh metadata for local trailers. + /// The base implementation to refresh metadata. /// /// The options. /// The cancellation token. @@ -1362,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { - var path = ContainingFolderPath; - - return directoryService.GetFileSystemEntries(path); + return directoryService.GetFileSystemEntries(ContainingFolderPath); } private async Task RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) @@ -1393,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); + // Cleanup removed extras + var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray(); + if (removedExtraIds.Length > 0) + { + var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery() + { + ItemIds = removedExtraIds + }); + foreach (var removedExtra in removedExtras) + { + LibraryManager.DeleteItem(removedExtra, new DeleteOptions() + { + DeleteFileLocation = false + }); + } + } + await Task.WhenAll(tasks).ConfigureAwait(false); item.ExtraIds = newExtraIds; @@ -1407,6 +1423,22 @@ namespace MediaBrowser.Controller.Entities public virtual bool RequiresRefresh() { + if (string.IsNullOrEmpty(Path) || DateModified == default) + { + return false; + } + + var info = FileSystem.GetFileSystemInfo(Path); + if (info.Exists) + { + if (info.IsDirectory) + { + return info.LastWriteTimeUtc != DateModified; + } + + return info.LastWriteTimeUtc != DateModified && info.Length != (Size ?? 0); + } + return false; } diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs index 4e4eb514e6..eb67437545 100644 --- a/MediaBrowser.Controller/IO/IPathManager.cs +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -1,9 +1,10 @@ +using System.Collections.Generic; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.IO; /// -/// Interface ITrickplayManager. +/// Interface IPathManager. /// public interface IPathManager { @@ -60,4 +61,11 @@ public interface IPathManager /// The chapter position. /// The chapter images data path. public string GetChapterImagePath(BaseItem item, long chapterPositionTicks); + + /// + /// Gets the paths of extracted data folders. + /// + /// The base item. + /// The absolute paths. + public IReadOnlyList GetExtractedDataPaths(BaseItem item); } diff --git a/MediaBrowser.Controller/Library/IKeyframeManager.cs b/MediaBrowser.Controller/Library/IKeyframeManager.cs new file mode 100644 index 0000000000..b0155efdd3 --- /dev/null +++ b/MediaBrowser.Controller/Library/IKeyframeManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; + +namespace MediaBrowser.Controller.IO; + +/// +/// Interface IKeyframeManager. +/// +public interface IKeyframeManager +{ + /// + /// Gets the keyframe data. + /// + /// The item id. + /// The keyframe data. + IReadOnlyList GetKeyframeData(Guid itemId); + + /// + /// Saves the keyframe data. + /// + /// The item id. + /// The keyframe data. + /// The cancellation token. + /// The task object representing the asynchronous operation. + Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// + /// Deletes the keyframe data. + /// + /// The item id. + /// The cancellation token. + /// The task object representing the asynchronous operation. + Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index df90f546cd..98ed15eb6c 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -220,13 +220,13 @@ namespace MediaBrowser.Controller.Library /// The resolvers. /// The intro providers. /// The item comparers. - /// The postscan tasks. + /// The post scan tasks. void AddParts( IEnumerable rules, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, - IEnumerable postscanTasks); + IEnumerable postScanTasks); /// /// Sorts the specified items. @@ -593,11 +593,11 @@ namespace MediaBrowser.Controller.Library QueryResult GetItemsResult(InternalItemsQuery query); /// - /// Ignores the file. + /// Checks if the file is ignored. /// /// The file. /// The parent. - /// true if XXXX, false otherwise. + /// true if ignored, false otherwise. bool IgnoreFile(FileSystemMetadata file, BaseItem parent); Guid GetStudioId(string name); diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 456977b88e..6cd6474f7b 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -7,7 +7,7 @@ using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// /// Defines methods for interacting with media segments. @@ -45,6 +45,13 @@ public interface IMediaSegmentManager /// a task. Task DeleteSegmentAsync(Guid segmentId); + /// + /// Deletes all media segments of an item. + /// + /// The to delete all segments for. + /// a task. + Task DeleteSegmentsAsync(Guid itemId); + /// /// Obtains all segments associated with the itemId. /// diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 39bb58bef2..5a6d15d781 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// /// Provides methods for Obtaining the Media Segments from an Item. diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs index 4930434a7f..2596784ba4 100644 --- a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs +++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs @@ -26,4 +26,12 @@ public interface IKeyframeRepository /// The cancellation token. /// The task object representing the asynchronous operation. Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// + /// Deletes the keyframe data. + /// + /// The item id. + /// The cancellation token. + /// The task object representing the asynchronous operation. + Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 897652fcd6..2eb647e264 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -537,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder EnableRaisingEvents = true }; - _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args); + _logger.LogDebug("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args); var memoryStream = new MemoryStream(); await using (memoryStream.ConfigureAwait(false)) @@ -637,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch (Exception ex) { - _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument); + _logger.LogWarning(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument); } } diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs index 96e1165b69..79cd33aa06 100644 --- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs +++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs @@ -1,50 +1,66 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Books +namespace MediaBrowser.Providers.Books; + +/// +/// Service to manage audiobook metadata. +/// +public class AudioBookMetadataService : MetadataService { - public class AudioBookMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public AudioBookMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public AudioBookMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } - /// - protected override void MergeData( - MetadataResult source, - MetadataResult target, - MetadataField[] lockedFields, - bool replaceData, - bool mergeMetadataSettings) - { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + /// + protected override void MergeData( + MetadataResult source, + MetadataResult target, + MetadataField[] lockedFields, + bool replaceData, + bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - var sourceItem = source.Item; - var targetItem = target.Item; + var sourceItem = source.Item; + var targetItem = target.Item; - if (replaceData || targetItem.Artists.Count == 0) - { - targetItem.Artists = sourceItem.Artists; - } + if (replaceData || targetItem.Artists.Count == 0) + { + targetItem.Artists = sourceItem.Artists; + } - if (replaceData || string.IsNullOrEmpty(targetItem.Album)) - { - targetItem.Album = sourceItem.Album; - } + if (replaceData || string.IsNullOrEmpty(targetItem.Album)) + { + targetItem.Album = sourceItem.Album; } } } diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs index 50b9922c69..6df8feab80 100644 --- a/MediaBrowser.Providers/Books/BookMetadataService.cs +++ b/MediaBrowser.Providers/Books/BookMetadataService.cs @@ -1,37 +1,53 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Books +namespace MediaBrowser.Providers.Books; + +/// +/// Service to manage book metadata. +/// +public class BookMetadataService : MetadataService { - public class BookMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public BookMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public BookMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } - /// - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName)) - { - target.Item.SeriesName = source.Item.SeriesName; - } + if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName)) + { + target.Item.SeriesName = source.Item.SeriesName; } } } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index b51ab4c08e..83f0f2485b 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -1,85 +1,101 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.BoxSets +namespace MediaBrowser.Providers.BoxSets; + +/// +/// Service to manage boxset metadata. +/// +public class BoxSetMetadataService : MetadataService { - public class BoxSetMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public BoxSetMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public BoxSetMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } - /// - protected override bool EnableUpdatingGenresFromChildren => true; + /// + protected override bool EnableUpdatingGenresFromChildren => true; - /// - protected override bool EnableUpdatingOfficialRatingFromChildren => true; + /// + protected override bool EnableUpdatingOfficialRatingFromChildren => true; - /// - protected override bool EnableUpdatingStudiosFromChildren => true; + /// + protected override bool EnableUpdatingStudiosFromChildren => true; - /// - protected override bool EnableUpdatingPremiereDateFromChildren => true; + /// + protected override bool EnableUpdatingPremiereDateFromChildren => true; - /// - protected override IReadOnlyList GetChildrenForMetadataUpdates(BoxSet item) - { - return item.GetLinkedChildren(); - } + /// + protected override IReadOnlyList GetChildrenForMetadataUpdates(BoxSet item) + { + return item.GetLinkedChildren(); + } - /// - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - var sourceItem = source.Item; - var targetItem = target.Item; + var sourceItem = source.Item; + var targetItem = target.Item; - if (mergeMetadataSettings) + if (mergeMetadataSettings) + { + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else { - if (replaceData || targetItem.LinkedChildren.Length == 0) - { - targetItem.LinkedChildren = sourceItem.LinkedChildren; - } - else - { - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); - } + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); } } + } - /// - protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType) - { - var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); - - var libraryFolderIds = item.GetLibraryFolderIds(); + /// + protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType) + { + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); - var itemLibraryFolderIds = item.LibraryFolderIds; - if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds)) - { - item.LibraryFolderIds = libraryFolderIds; - updatedType |= ItemUpdateType.MetadataImport; - } + var libraryFolderIds = item.GetLibraryFolderIds(); - return updatedType; + var itemLibraryFolderIds = item.LibraryFolderIds; + if (itemLibraryFolderIds is null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds)) + { + item.LibraryFolderIds = libraryFolderIds; + updatedType |= ItemUpdateType.MetadataImport; } + + return updatedType; } } diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs index 0267fa13f7..a1f77e0a8d 100644 --- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs +++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs @@ -1,25 +1,41 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Channels +namespace MediaBrowser.Providers.Channels; + +/// +/// Service to manage channel metadata. +/// +public class ChannelMetadataService : MetadataService { - public class ChannelMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ChannelMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public ChannelMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index 0629824d30..6407b1a61e 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -1,25 +1,41 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Folders +namespace MediaBrowser.Providers.Folders; + +/// +/// Service to manage collection folder metadata. +/// +public class CollectionFolderMetadataService : MetadataService { - public class CollectionFolderMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public CollectionFolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public CollectionFolderMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs index 79d52991a1..7843f729d1 100644 --- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs @@ -1,29 +1,45 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Folders +namespace MediaBrowser.Providers.Folders; + +/// +/// Service to manage folder metadata. +/// +public class FolderMetadataService : MetadataService { - public class FolderMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public FolderMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } - - /// - // Make sure the type-specific services get picked first - public override int Order => 10; } + + /// + // Make sure the type-specific services get picked first + public override int Order => 10; } diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs index 79c5597e54..834fba4587 100644 --- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs +++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs @@ -1,25 +1,41 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Folders +namespace MediaBrowser.Providers.Folders; + +/// +/// Service to manage user view metadata. +/// +public class UserViewMetadataService : MetadataService { - public class UserViewMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserViewMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public UserViewMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs index 4d10d89874..2a2a0bf509 100644 --- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -1,25 +1,41 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Genres +namespace MediaBrowser.Providers.Genres; + +/// +/// Service to manage genre metadata. +/// +public class GenreMetadataService : MetadataService { - public class GenreMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public GenreMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public GenreMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs index c94d365301..9e4d910198 100644 --- a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs @@ -1,25 +1,41 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.LiveTv +namespace MediaBrowser.Providers.LiveTv; + +/// +/// Service to manage live TV metadata. +/// +public class LiveTvMetadataService : MetadataService { - public class LiveTvMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LiveTvMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public LiveTvMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 50bbf0974a..c4d4e775ac 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Threading; @@ -12,7 +13,9 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -26,13 +29,24 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo, new() where TIdType : ItemLookupInfo, new() { - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) + protected MetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger> logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) { ServerConfigurationManager = serverConfigurationManager; Logger = logger; ProviderManager = providerManager; FileSystem = fileSystem; LibraryManager = libraryManager; + PathManager = pathManager; + KeyframeManager = keyframeManager; + MediaSegmentManager = mediaSegmentManager; ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); } @@ -48,6 +62,12 @@ namespace MediaBrowser.Providers.Manager protected ILibraryManager LibraryManager { get; } + protected IPathManager PathManager { get; } + + protected IKeyframeManager KeyframeManager { get; } + + protected IMediaSegmentManager MediaSegmentManager { get; } + protected virtual bool EnableUpdatingPremiereDateFromChildren => false; protected virtual bool EnableUpdatingGenresFromChildren => false; @@ -303,6 +323,55 @@ namespace MediaBrowser.Providers.Manager updateType |= ItemUpdateType.MetadataImport; } + // Cleanup extracted files if source file was modified + var itemPath = item.Path; + if (!string.IsNullOrEmpty(itemPath)) + { + var info = FileSystem.GetFileSystemInfo(itemPath); + var modificationDate = info.LastWriteTimeUtc; + var itemLastModifiedFileSystem = item.DateModified; + if (info.Exists && itemLastModifiedFileSystem != modificationDate) + { + Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath); + + item.DateModified = modificationDate; + if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded) + { + item.DateCreated = info.CreationTimeUtc; + } + + var size = info.Length; + if (item is Video video) + { + var videoType = video.VideoType; + var sizeChanged = size != (video.Size ?? 0); + if (videoType == VideoType.BluRay || video.VideoType == VideoType.Dvd || sizeChanged) + { + if (sizeChanged) + { + item.Size = size; + Logger.LogDebug("File size changed from {Then} to {Now}: {Path}", video.Size, size, itemPath); + } + + var validPaths = PathManager.GetExtractedDataPaths(video).Where(Directory.Exists).ToList(); + if (validPaths.Count > 0) + { + Logger.LogInformation("File changed, pruning extracted data: {Path}", itemPath); + foreach (var path in validPaths) + { + Directory.Delete(path, true); + } + } + + KeyframeManager.DeleteKeyframeDataAsync(video.Id, CancellationToken.None).GetAwaiter().GetResult(); + MediaSegmentManager.DeleteSegmentsAsync(item.Id).GetAwaiter().GetResult(); + } + } + + updateType |= ItemUpdateType.MetadataImport; + } + } + return updateType; } @@ -1132,6 +1201,11 @@ namespace MediaBrowser.Providers.Manager target.DateCreated = source.DateCreated; } + if (replaceData || source.DateModified != default) + { + target.DateModified = source.DateModified; + } + if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode)) { target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode; diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 856f33b497..1a29548f24 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Mime; -using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; @@ -24,6 +22,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Lyrics; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 286ba0de0d..4fd3ab9730 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -133,7 +133,6 @@ namespace MediaBrowser.Providers.MediaInfo audio.TotalBitrate = mediaInfo.Bitrate; audio.RunTimeTicks = mediaInfo.RunTimeTicks; - audio.Size = mediaInfo.Size; // Add external lyrics first to prevent the lrc file get overwritten on first scan var mediaStreams = new List(mediaInfo.MediaStreams); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 7947ba9218..7c88a0e7d4 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -214,10 +214,14 @@ namespace MediaBrowser.Providers.MediaInfo mediaAttachments = mediaInfo.MediaAttachments; video.TotalBitrate = mediaInfo.Bitrate; video.RunTimeTicks = mediaInfo.RunTimeTicks; - video.Size = mediaInfo.Size; video.Container = mediaInfo.Container; + var videoType = video.VideoType; + if (videoType == VideoType.BluRay || videoType == VideoType.Dvd) + { + video.Size = mediaInfo.Size; + } - chapters = mediaInfo.Chapters ?? Array.Empty(); + chapters = mediaInfo.Chapters ?? []; if (blurayInfo is not null) { FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo); @@ -234,8 +238,8 @@ namespace MediaBrowser.Providers.MediaInfo } } - mediaAttachments = Array.Empty(); - chapters = Array.Empty(); + mediaAttachments = []; + chapters = []; } var libraryOptions = _libraryManager.GetLibraryOptions(video); @@ -400,7 +404,7 @@ namespace MediaBrowser.Providers.MediaInfo { if (video.Genres.Length == 0 || replaceData) { - video.Genres = Array.Empty(); + video.Genres = []; foreach (var genre in data.Genres.Trimmed()) { @@ -643,7 +647,7 @@ namespace MediaBrowser.Providers.MediaInfo long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; if (runtime <= dummyChapterDuration) { - return Array.Empty(); + return []; } int chapterCount = (int)(runtime / dummyChapterDuration); diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index ba6034ec12..8c673350da 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -130,9 +130,9 @@ namespace MediaBrowser.Providers.MediaInfo if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) { var file = directoryService.GetFile(path); - if (file is not null && file.LastWriteTimeUtc != item.DateModified) + if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size) { - _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path); + _logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path); return true; } } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 938f3cb327..1134baf92d 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Tasks; diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs index 8997ddc644..0779e17bda 100644 --- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs +++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs @@ -1,40 +1,56 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Movies; + +/// +/// Service to manage movie metadata. +/// +public class MovieMetadataService : MetadataService { - public class MovieMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MovieMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - public MovieMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } - /// - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - var sourceItem = source.Item; - var targetItem = target.Item; + var sourceItem = source.Item; + var targetItem = target.Item; - if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName)) - { - targetItem.CollectionName = sourceItem.CollectionName; - } + if (replaceData || string.IsNullOrEmpty(targetItem.CollectionName)) + { + targetItem.CollectionName = sourceItem.CollectionName; } } } diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs index e77d2fa8a3..bf8735ad4b 100644 --- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs +++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs @@ -1,42 +1,58 @@ -#pragma warning disable CS1591 - using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Movies; + +/// +/// Service to manage trailer metadata. +/// +public class TrailerMetadataService : MetadataService { - public class TrailerMetadataService : MetadataService + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TrailerMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) + { + } + + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) { - public TrailerMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || target.Item.TrailerTypes.Length == 0) { + target.Item.TrailerTypes = source.Item.TrailerTypes; } - - /// - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + else { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - - if (replaceData || target.Item.TrailerTypes.Length == 0) - { - target.Item.TrailerTypes = source.Item.TrailerTypes; - } - else - { - target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray(); - } + target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray(); } } } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 64b6273670..cc6d7953d5 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -5,245 +5,252 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Music; + +/// +/// The album metadata service. +/// +public class AlbumMetadataService : MetadataService { /// - /// The album metadata service. + /// Initializes a new instance of the class. /// - public class AlbumMetadataService : MetadataService + /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public AlbumMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IPathManager pathManager, + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, pathManager, keyframeManager, mediaSegmentManager) { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the . - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public AlbumMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } + + /// + protected override bool EnableUpdatingPremiereDateFromChildren => true; - /// - protected override bool EnableUpdatingPremiereDateFromChildren => true; + /// + protected override bool EnableUpdatingGenresFromChildren => true; - /// - protected override bool EnableUpdatingGenresFromChildren => true; + /// + protected override bool EnableUpdatingStudiosFromChildren => true; - /// - protected override bool EnableUpdatingStudiosFromChildren => true; + /// + protected override IReadOnlyList GetChildrenForMetadataUpdates(MusicAlbum item) + => item.GetRecursiveChildren(i => i is Audio); - /// - protected override IReadOnlyList GetChildrenForMetadataUpdates(MusicAlbum item) - => item.GetRecursiveChildren(i => i is Audio); + /// + protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList children, bool isFullRefresh, ItemUpdateType currentUpdateType) + { + var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); - /// - protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList children, bool isFullRefresh, ItemUpdateType currentUpdateType) + // don't update user-changeable metadata for locked items + if (item.IsLocked) { - var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); + return updateType; + } - // don't update user-changeable metadata for locked items - if (item.IsLocked) + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + { + if (!item.LockedFields.Contains(MetadataField.Name)) { - return updateType; - } + var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i)); - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) - { - if (!item.LockedFields.Contains(MetadataField.Name)) + if (!string.IsNullOrEmpty(name) + && !string.Equals(item.Name, name, StringComparison.Ordinal)) { - var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i)); - - if (!string.IsNullOrEmpty(name) - && !string.Equals(item.Name, name, StringComparison.Ordinal)) - { - item.Name = name; - updateType |= ItemUpdateType.MetadataEdit; - } + item.Name = name; + updateType |= ItemUpdateType.MetadataEdit; } - - var songs = children.Cast