Merge pull request #8526 from Shadowghost/rating-overhaul

pull/9385/head
Cody Robibero 1 year ago committed by GitHub
commit ecb5c48538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3202,7 +3202,8 @@ namespace Emby.Server.Implementations.Data
return IsAlphaNumeric(value);
}
private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement)
#nullable enable
private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement)
{
if (query.IsResumable ?? false)
{
@ -3677,7 +3678,6 @@ namespace Emby.Server.Implementations.Data
if (statement is not null)
{
nameContains = FixUnicodeChars(nameContains);
statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
}
}
@ -3803,13 +3803,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ArtistIds)
{
var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement is not null)
{
statement.TryBind(paramName, artistId);
}
statement?.TryBind(paramName, artistId);
index++;
}
@ -3824,13 +3819,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.AlbumArtistIds)
{
var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
if (statement is not null)
{
statement.TryBind(paramName, artistId);
}
statement?.TryBind(paramName, artistId);
index++;
}
@ -3845,13 +3835,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ContributingArtistIds)
{
var paramName = "@ArtistIds" + index;
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
if (statement is not null)
{
statement.TryBind(paramName, artistId);
}
statement?.TryBind(paramName, artistId);
index++;
}
@ -3866,13 +3851,8 @@ namespace Emby.Server.Implementations.Data
foreach (var albumId in query.AlbumIds)
{
var paramName = "@AlbumIds" + index;
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
if (statement is not null)
{
statement.TryBind(paramName, albumId);
}
statement?.TryBind(paramName, albumId);
index++;
}
@ -3887,13 +3867,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ExcludeArtistIds)
{
var paramName = "@ExcludeArtistId" + index;
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement is not null)
{
statement.TryBind(paramName, artistId);
}
statement?.TryBind(paramName, artistId);
index++;
}
@ -3908,13 +3883,8 @@ namespace Emby.Server.Implementations.Data
foreach (var genreId in query.GenreIds)
{
var paramName = "@GenreId" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
if (statement is not null)
{
statement.TryBind(paramName, genreId);
}
statement?.TryBind(paramName, genreId);
index++;
}
@ -3929,11 +3899,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.Genres)
{
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
if (statement is not null)
{
statement.TryBind("@Genre" + index, GetCleanValue(item));
}
statement?.TryBind("@Genre" + index, GetCleanValue(item));
index++;
}
@ -3948,11 +3914,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in tags)
{
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
if (statement is not null)
{
statement.TryBind("@Tag" + index, GetCleanValue(item));
}
statement?.TryBind("@Tag" + index, GetCleanValue(item));
index++;
}
@ -3967,11 +3929,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in excludeTags)
{
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
if (statement is not null)
{
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
}
statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
index++;
}
@ -3986,14 +3944,8 @@ namespace Emby.Server.Implementations.Data
foreach (var studioId in query.StudioIds)
{
var paramName = "@StudioId" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
if (statement is not null)
{
statement.TryBind(paramName, studioId);
}
statement?.TryBind(paramName, studioId);
index++;
}
@ -4008,11 +3960,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.OfficialRatings)
{
clauses.Add("OfficialRating=@OfficialRating" + index);
if (statement is not null)
{
statement.TryBind("@OfficialRating" + index, item);
}
statement?.TryBind("@OfficialRating" + index, item);
index++;
}
@ -4020,34 +3968,96 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause);
}
if (query.MinParentalRating.HasValue)
var ratingClauseBuilder = new StringBuilder("(");
if (query.HasParentalRating ?? false)
{
whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating");
if (statement is not null)
ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
if (query.MinParentalRating.HasValue)
{
statement.TryBind("@MinParentalRating", query.MinParentalRating.Value);
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
}
if (query.MaxParentalRating.HasValue)
if (query.MaxParentalRating.HasValue)
{
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
}
else if (query.BlockUnratedItems.Length > 0)
{
whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating");
var paramName = "@UnratedType";
var index = 0;
string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++));
ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))");
if (statement is not null)
{
statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
{
statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
}
}
}
if (query.HasParentalRating.HasValue)
{
if (query.HasParentalRating.Value)
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
whereClauses.Add("InheritedParentalRatingValue > 0");
ratingClauseBuilder.Append(" OR (");
}
else
if (query.MinParentalRating.HasValue)
{
whereClauses.Add("InheritedParentalRatingValue = 0");
ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
if (query.MaxParentalRating.HasValue)
{
if (query.MinParentalRating.HasValue)
{
ratingClauseBuilder.Append(" AND ");
}
ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
ratingClauseBuilder.Append(")");
}
if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
{
ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null");
}
}
else if (query.MinParentalRating.HasValue)
{
ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
if (query.MaxParentalRating.HasValue)
{
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
ratingClauseBuilder.Append(")");
}
else if (query.MaxParentalRating.HasValue)
{
ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
else if (!query.HasParentalRating ?? false)
{
ratingClauseBuilder.Append("InheritedParentalRatingValue is null");
}
var ratingClauseString = ratingClauseBuilder.ToString();
if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase))
{
whereClauses.Add(ratingClauseString + ")");
}
if (query.HasOfficialRating.HasValue)
@ -4089,37 +4099,25 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
if (statement is not null)
{
statement.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
}
statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
if (statement is not null)
{
statement.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
}
statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
if (statement is not null)
{
statement.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
}
statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
if (statement is not null)
{
statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
if (query.HasSubtitles.HasValue)
@ -4169,15 +4167,11 @@ namespace Emby.Server.Implementations.Data
if (query.Years.Length == 1)
{
whereClauses.Add("ProductionYear=@Years");
if (statement is not null)
{
statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
else if (query.Years.Length > 1)
{
var val = string.Join(',', query.Years);
whereClauses.Add("ProductionYear in (" + val + ")");
}
@ -4185,10 +4179,7 @@ namespace Emby.Server.Implementations.Data
if (isVirtualItem.HasValue)
{
whereClauses.Add("IsVirtualItem=@IsVirtualItem");
if (statement is not null)
{
statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
if (query.IsSpecialSeason.HasValue)
@ -4219,31 +4210,22 @@ namespace Emby.Server.Implementations.Data
if (queryMediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
if (statement is not null)
{
statement.TryBind("@MediaTypes", queryMediaTypes[0]);
}
statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
}
else if (queryMediaTypes.Length > 1)
{
var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
whereClauses.Add("MediaType in (" + val + ")");
}
if (query.ItemIds.Length > 0)
{
var includeIds = new List<string>();
var index = 0;
foreach (var id in query.ItemIds)
{
includeIds.Add("Guid = @IncludeId" + index);
if (statement is not null)
{
statement.TryBind("@IncludeId" + index, id);
}
statement?.TryBind("@IncludeId" + index, id);
index++;
}
@ -4253,16 +4235,11 @@ namespace Emby.Server.Implementations.Data
if (query.ExcludeItemIds.Length > 0)
{
var excludeIds = new List<string>();
var index = 0;
foreach (var id in query.ExcludeItemIds)
{
excludeIds.Add("Guid <> @ExcludeId" + index);
if (statement is not null)
{
statement.TryBind("@ExcludeId" + index, id);
}
statement?.TryBind("@ExcludeId" + index, id);
index++;
}
@ -4283,11 +4260,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@ExcludeProviderId" + index;
excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
if (statement is not null)
{
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
}
statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@ -4312,7 +4285,7 @@ namespace Emby.Server.Implementations.Data
}
// TODO this seems to be an idea for a better schema where ProviderIds are their own table
// buut this is not implemented
// but this is not implemented
// hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
// TODO this is a really BAD way to do it since the pair:
@ -4326,11 +4299,7 @@ namespace Emby.Server.Implementations.Data
hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val%
if (statement is not null)
{
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
}
statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
index++;
break;
@ -4407,11 +4376,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length == 1)
{
whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
if (statement is not null)
{
statement.TryBind("@AncestorId", query.AncestorIds[0]);
}
statement?.TryBind("@AncestorId", query.AncestorIds[0]);
}
if (query.AncestorIds.Length > 1)
@ -4424,39 +4389,13 @@ namespace Emby.Server.Implementations.Data
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
if (statement is not null)
{
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
}
statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
}
if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
{
whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
if (statement is not null)
{
statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
}
}
if (query.BlockUnratedItems.Length == 1)
{
whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)");
if (statement is not null)
{
statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString());
}
}
if (query.BlockUnratedItems.Length > 1)
{
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
whereClauses.Add(
string.Format(
CultureInfo.InvariantCulture,
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
inClause));
statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
}
if (query.ExcludeInheritedTags.Length > 0)
@ -4605,6 +4544,7 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
#nullable disable
/// <summary>
/// Formats a where clause for the specified provider.

@ -83,13 +83,14 @@ namespace Emby.Server.Implementations.Dto
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
var returnItems = new BaseItemDto[items.Count];
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
var programTuples = new List<(BaseItem, BaseItemDto)>();
var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
for (int index = 0; index < items.Count; index++)
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = items[index];
var item = accessibleItems[index];
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
@ -25,7 +26,7 @@ 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" };
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" };
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<LocalizationManager> _logger;
@ -86,12 +87,10 @@ namespace Emby.Server.Implementations.Localization
var name = parts[0];
dict.Add(name, new ParentalRating(name, value));
}
#if DEBUG
else
{
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
#endif
}
_allParentalRatings[countryCode] = dict;
@ -184,7 +183,56 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
=> GetParentalRatingsDictionary().Values;
{
var ratings = GetParentalRatingsDictionary().Values.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
// Minimum rating possible
if (!ratings.Any(x => x.Value == 0))
{
ratings.Add(new ParentalRating("Approved", 0));
}
// Matches PG (this has different age restrictions depending on country)
if (!ratings.Any(x => x.Value == 10))
{
ratings.Add(new ParentalRating("10", 10));
}
// Matches PG-13
if (!ratings.Any(x => x.Value == 13))
{
ratings.Add(new ParentalRating("13", 13));
}
// Matches TV-14
if (!ratings.Any(x => x.Value == 14))
{
ratings.Add(new ParentalRating("14", 14));
}
// 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))
{
ratings.Add(new ParentalRating("21", 21));
}
// A lot of countries don't excplicitly have a seperate rating for adult content
if (!ratings.Any(x => x.Value == 1000))
{
ratings.Add(new ParentalRating("XXX", 1000));
}
// A lot of countries don't excplicitly have a seperate rating for banned content
if (!ratings.Any(x => x.Value == 1001))
{
ratings.Add(new ParentalRating("Banned", 1001));
}
return ratings.OrderBy(r => r.Value);
}
/// <summary>
/// Gets the parental ratings dictionary.
@ -194,6 +242,7 @@ namespace Emby.Server.Implementations.Localization
{
var countryCode = _configurationManager.Configuration.MetadataCountryCode;
// Fall back to US ratings if no country code is specified or country code does not exist.
if (string.IsNullOrEmpty(countryCode))
{
countryCode = "us";
@ -205,15 +254,15 @@ namespace Emby.Server.Implementations.Localization
}
/// <summary>
/// Gets the ratings.
/// Gets the ratings for a country.
/// </summary>
/// <param name="countryCode">The country code.</param>
/// <returns>The ratings.</returns>
private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
{
_allParentalRatings.TryGetValue(countryCode, out var value);
_allParentalRatings.TryGetValue(countryCode, out var countryValue);
return value;
return countryValue;
}
/// <inheritdoc />
@ -221,12 +270,14 @@ namespace Emby.Server.Implementations.Localization
{
ArgumentException.ThrowIfNullOrEmpty(rating);
// Handle unrated content
if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Fairly common for some users to have "Rated R" in their rating field
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
var ratingsDictionary = GetParentalRatingsDictionary();
@ -246,18 +297,17 @@ namespace Emby.Server.Implementations.Localization
}
// Try splitting by : to handle "Germany: FSK 18"
var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
}
if (!trimmedRating.IsEmpty)
{
return GetRatingLevel(trimmedRating.ToString());
}
// Remove prefix country code to handle "DE-18"
if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart('-').ToString());
}
// TODO: Further improve by normalizing out all spaces and dashes
return null;
}

@ -0,0 +1,11 @@
E,0
EC,0
T,7
M,18
AO,18
UR,18
RP,18
X,1000
XX,1000
XXX,1000
XXXX,1000
1 E 0
2 EC 0
3 T 7
4 M 18
5 AO 18
6 UR 18
7 RP 18
8 X 1000
9 XX 1000
10 XXX 1000
11 XXXX 1000

@ -1,7 +1,13 @@
AU-G,1
AU-PG,5
AU-M,6
AU-MA15+,7
AU-R18+,9
AU-X18+,10
AU-RC,11
Exempt,0
G,0
7+,7
M,15
MA,15
MA15+,15
PG,16
16+,16
R,18
R18+,18
X18+,18
18+,18
X,1000

1 AU-G Exempt 1 0
2 AU-PG G 5 0
3 AU-M 7+ 6 7
4 AU-MA15+ M 7 15
5 AU-R18+ MA 9 15
6 AU-X18+ MA15+ 10 15
7 AU-RC PG 11 16
8 16+ 16
9 R 18
10 R18+ 18
11 X18+ 18
12 18+ 18
13 X 1000

@ -1,6 +1,11 @@
BE-AL,1
BE-MG6,2
BE-6,3
BE-9,5
BE-12,6
BE-16,8
AL,0
KT,0
TOUS,0
MG6,6
6,6
9,9
KNT,12
12,12
14,14
16,16
18,18

1 BE-AL AL 1 0
2 BE-MG6 KT 2 0
3 BE-6 TOUS 3 0
4 BE-9 MG6 5 6
5 BE-12 6 6 6
6 BE-16 9 8 9
7 KNT 12
8 12 12
9 14 14
10 16 16
11 18 18

@ -1,6 +1,8 @@
BR-L,1
BR-10,5
BR-12,7
BR-14,8
BR-16,8
BR-18,9
Livre,0
L,0
ER,9
10,10
12,12
14,14
16,16
18,18

1 BR-L Livre 1 0
2 BR-10 L 5 0
3 BR-12 ER 7 9
4 BR-14 10 8 10
5 BR-16 12 8 12
6 BR-18 14 9 14
7 16 16
8 18 18

@ -1,6 +1,20 @@
CA-G,1
CA-PG,5
CA-14A,7
CA-A,8
CA-18A,9
CA-R,10
E,0
G,0
TV-Y,0
TV-G,0
TV-Y7,7
TV-Y7-FV,7
PG,9
TV-PG,9
PG-13,13
13+,13
TV-14,14
14A,14
16+,16
NC-17,17
R,18
TV-MA,18
18A,18
18+,18
A,1000
Prohibited,1001

1 CA-G E 1 0
2 CA-PG G 5 0
3 CA-14A TV-Y 7 0
4 CA-A TV-G 8 0
5 CA-18A TV-Y7 9 7
6 CA-R TV-Y7-FV 10 7
7 PG 9
8 TV-PG 9
9 PG-13 13
10 13+ 13
11 TV-14 14
12 14A 14
13 16+ 16
14 NC-17 17
15 R 18
16 TV-MA 18
17 18A 18
18 18+ 18
19 A 1000
20 Prohibited 1001

@ -1,8 +1,7 @@
CO-T,1
CO-7,5
CO-12,7
CO-15,8
CO-18,10
CO-X,100
CO-BANNED,15
CO-E,15
T,0
7,7
12,12
15,15
18,18
X,1000
Prohibited,1001

1 CO-T T 1 0
2 CO-7 7 5 7
3 CO-12 12 7 12
4 CO-15 15 8 15
5 CO-18 18 10 18
6 CO-X X 100 1000
7 CO-BANNED Prohibited 15 1001
CO-E 15

@ -1,10 +1,12 @@
DE-0,1
FSK-0,1
DE-6,5
FSK-6,5
DE-12,7
FSK-12,7
DE-16,8
FSK-16,8
DE-18,9
FSK-18,9
Educational,0
Infoprogramm,0
FSK-0,0
0,0
FSK-6,6
6,6
FSK-12,12
12,12
FSK-16,16
16,16
FSK-18,18
18,18

1 DE-0 Educational 1 0
2 FSK-0 Infoprogramm 1 0
3 DE-6 FSK-0 5 0
4 FSK-6 0 5 0
5 DE-12 FSK-6 7 6
6 FSK-12 6 7 6
7 DE-16 FSK-12 8 12
8 FSK-16 12 8 12
9 DE-18 FSK-16 9 16
10 FSK-18 16 9 16
11 FSK-18 18
12 18 18

@ -1,4 +1,7 @@
DA-A,1
DA-7,5
DA-11,6
DA-15,8
F,0
A,0
7,7
11,11
12,12
15,15
16,16

1 DA-A F 1 0
2 DA-7 A 5 0
3 DA-11 7 6 7
4 DA-15 11 8 11
5 12 12
6 15 15
7 16 16

@ -1,6 +1,24 @@
ES-A,1
ES-APTA,1
ES-7,3
ES-12,6
ES-16,8
ES-18,11
A,0
A/fig,0
A/i,0
A/fig/i,0
APTA,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

1 ES-A A 1 0
2 ES-APTA A/fig 1 0
3 ES-7 A/i 3 0
4 ES-12 A/fig/i 6 0
5 ES-16 APTA 8 0
6 ES-18 TP 11 0
7 0+ 0
8 6+ 6
9 7/fig 7
10 7/i 7
11 7/i/fig 7
12 7 7
13 9+ 9
14 10 10
15 12 12
16 12/fig 12
17 13 13
18 14 14
19 16 16
20 16/fig 16
21 18 18
22 18/fig 18
23 X 1000
24 Banned 1001

@ -1,10 +1,10 @@
FI-S,1
FI-T,1
FI-7,4
FI-12,5
FI-16,8
FI-18,9
FI-K7,4
FI-K12,5
FI-K16,8
FI-K18,9
S,0
T,0
K7,7
7,7
K12,12
12,12
K16,16
16,16
K18,18
18,18

1 FI-S S 1 0
2 FI-T T 1 0
3 FI-7 K7 4 7
4 FI-12 7 5 7
5 FI-16 K12 8 12
6 FI-18 12 9 12
7 FI-K7 K16 4 16
8 FI-K12 16 5 16
9 FI-K16 K18 8 18
10 FI-K18 18 9 18

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

1 FR-U Public Averti 1 0
2 FR-10 Tous Publics 5 0
3 FR-12 U 7 0
4 FR-16 0+ 9 0
5 FR-18 6+ 10 6
6 9+ 9
7 10 10
8 12 12
9 14+ 14
10 16 16
11 18 18
12 X 1000

@ -1,7 +1,22 @@
GB-U,1
GB-PG,5
GB-12,6
GB-12A,7
GB-15,8
GB-18,9
GB-R18,15
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

1 GB-U All 1 0
2 GB-PG E 5 0
3 GB-12 G 6 0
4 GB-12A U 7 0
5 GB-15 0+ 8 0
6 GB-18 6+ 9 6
7 GB-R18 7+ 15 7
8 PG 8
9 9+ 9
10 12 12
11 12+ 12
12 12A 12
13 Teen 13
14 13+ 13
15 14+ 14
16 15 15
17 16 16
18 Caution 18
19 18 18
20 Mature 1000
21 Adult 1000
22 R18 1000

@ -1,6 +1,9 @@
IE-G,1
IE-PG,5
IE-12A,7
IE-15A,8
IE-16,9
IE-18,10
G,4
PG,12
12,12
12A,12
12PG,12
15,15
15A,15
16,16
18,18

1 IE-G G 1 4
2 IE-PG PG 5 12
3 IE-12A 12 7 12
4 IE-15A 12A 8 12
5 IE-16 12PG 9 12
6 IE-18 15 10 15
7 15A 15
8 16 16
9 18 18

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

1 JP-G A 1 0
2 JP-PG12 G 7 0
3 JP-15+ B 8 12
4 JP-18+ PG12 10 12
5 C 15
6 15+ 15
7 R15+ 15
8 16+ 16
9 D 17
10 Z 18
11 18+ 18

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

1 KZ-6- K 0
2 KZ-6+ БА 6 12
3 KZ-12+ Б14 12 14
4 KZ-14+ E16 14 16
5 KZ-16+ E18 16 18
6 KZ-18+ HA 18
KZ-21+ 21

@ -1,6 +1,6 @@
MX-AA,1
MX-A,5
MX-B,7
MX-B-15,8
MX-C,9
MX-D,10
A,0
AA,0
B,12
B-15,15
C,18
D,1000

1 MX-AA A 1 0
2 MX-A AA 5 0
3 MX-B B 7 12
4 MX-B-15 B-15 8 15
5 MX-C C 9 18
6 MX-D D 10 1000

@ -1,6 +1,8 @@
NL-AL,1
NL-MG6,2
NL-6,3
NL-9,5
NL-12,6
NL-16,8
AL,0
MG6,6
6,6
9,9
12,12
14,14
16,16
18,18

1 NL-AL AL 1 0
2 NL-MG6 MG6 2 6
3 NL-6 6 3 6
4 NL-9 9 5 9
5 NL-12 12 6 12
6 NL-16 14 8 14
7 16 16
8 18 18

@ -1,6 +1,9 @@
NO-A,1
NO-6,3
NO-9,4
NO-12,5
NO-15,8
NO-18,9
A,0
6,6
7,7
9,9
11,11
12,12
15,15
18,18
Not approved,1001

1 NO-A A 1 0
2 NO-6 6 3 6
3 NO-9 7 4 7
4 NO-12 9 5 9
5 NO-15 11 8 11
6 NO-18 12 9 12
7 15 15
8 18 18
9 Not approved 1001

@ -1,11 +1,15 @@
NZ-G,1
NZ-PG,5
NZ-M,6
NZ-R13,7
NZ-RP13,7
NZ-R15,8
NZ-RP16,9
NZ-R16,9
NZ-R18,10
NZ-R,10
NZ-MA,10
Exempt,0
G,0
GY,13
PG,13
R13,13
RP13,13
R15,15
M,16
R16,16
RP16,16
GA,18
R18,18
MA,1000
R,1001
Objectionable,1001

1 NZ-G Exempt 1 0
2 NZ-PG G 5 0
3 NZ-M GY 6 13
4 NZ-R13 PG 7 13
5 NZ-RP13 R13 7 13
6 NZ-R15 RP13 8 13
7 NZ-RP16 R15 9 15
8 NZ-R16 M 9 16
9 NZ-R18 R16 10 16
10 NZ-R RP16 10 16
11 NZ-MA GA 10 18
12 R18 18
13 MA 1000
14 R 1001
15 Objectionable 1001

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

1 RO-AG AG 1 0
2 AP-12 12
3 N-15 15
4 IM-18 18
5 IM-18-XXX 1000
6 IC 1001

@ -1,5 +1,6 @@
RU-0+,1
RU-6+,3
RU-12+,7
RU-16+,9
RU-18+,10
0+,0
6+,6
12+,12
16+,16
18+,18
Refused classification,1001

1 RU-0+ 0+ 1 0
2 RU-6+ 6+ 3 6
3 RU-12+ 12+ 7 12
4 RU-16+ 16+ 9 16
5 RU-18+ 18+ 10 18
6 Refused classification 1001

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

1 SE-Btl Alla 1 0
2 SE-Barntillåten Barntillåten 1 0
3 SE-7 Btl 3 0
4 SE-11 0+ 5 0
5 SE-15 7 8 7
6 9+ 9
7 10+ 10
8 11 11
9 14 14
10 15 15

@ -1,7 +1,22 @@
UK-U,1
UK-PG,5
UK-12,7
UK-12A,7
UK-15,9
UK-18,10
UK-R18,15
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

1 UK-U All 1 0
2 UK-PG E 5 0
3 UK-12 G 7 0
4 UK-12A U 7 0
5 UK-15 0+ 9 0
6 UK-18 6+ 10 6
7 UK-R18 7+ 15 7
8 PG 8
9 9+ 9
10 12 12
11 12+ 12
12 12A 12
13 Teen 13
14 13+ 13
15 14+ 14
16 15 15
17 16 16
18 Caution 18
19 18 18
20 Mature 1000
21 Adult 1000
22 R18 1000

@ -1,23 +1,50 @@
TV-Y,1
APPROVED,1
G,1
E,1
EC,1
TV-G,1
TV-Y7,3
TV-Y7-FV,4
PG,5
TV-PG,5
PG-13,7
T,7
TV-14,8
R,9
M,9
TV-MA,9
NC-17,10
AO,15
RP,15
UR,15
NR,15
X,15
XXX,100
Approved,0
G,0
TV-G,0
TV-Y,0
TV-Y7,7
TV-Y7-FV,7
PG,10
PG-13,13
TV-PG,13
TV-PG-D,13
TV-PG-L,13
TV-PG-S,13
TV-PG-V,13
TV-PG-DL,13
TV-PG-DS,13
TV-PG-DV,13
TV-PG-LS,13
TV-PG-LV,13
TV-PG-SV,13
TV-PG-DLS,13
TV-PG-DLV,13
TV-PG-DSV,13
TV-PG-LSV,13
TV-PG-DLSV,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

1 TV-Y Approved 1 0
2 APPROVED G 1 0
3 G TV-G 1 0
4 E TV-Y 1 0
5 EC TV-Y7 1 7
6 TV-G TV-Y7-FV 1 7
7 TV-Y7 PG 3 10
8 TV-Y7-FV PG-13 4 13
9 PG TV-PG 5 13
10 TV-PG TV-PG-D 5 13
11 PG-13 TV-PG-L 7 13
12 T TV-PG-S 7 13
13 TV-14 TV-PG-V 8 13
14 R TV-PG-DL 9 13
15 M TV-PG-DS 9 13
16 TV-MA TV-PG-DV 9 13
17 NC-17 TV-PG-LS 10 13
18 AO TV-PG-LV 15 13
19 RP TV-PG-SV 15 13
20 UR TV-PG-DLS 15 13
21 NR TV-PG-DLV 15 13
22 X TV-PG-DSV 15 13
23 XXX TV-PG-LSV 100 13
24 TV-PG-DLSV 13
25 TV-14 14
26 TV-14-D 14
27 TV-14-L 14
28 TV-14-S 14
29 TV-14-V 14
30 TV-14-DL 14
31 TV-14-DS 14
32 TV-14-DV 14
33 TV-14-LS 14
34 TV-14-LV 14
35 TV-14-SV 14
36 TV-14-DLS 14
37 TV-14-DLV 14
38 TV-14-DSV 14
39 TV-14-LSV 14
40 TV-14-DLSV 14
41 NC-17 17
42 R 17
43 TV-MA 17
44 TV-MA-L 17
45 TV-MA-S 17
46 TV-MA-V 17
47 TV-MA-LS 17
48 TV-MA-LV 17
49 TV-MA-SV 17
50 TV-MA-LSV 17

@ -98,7 +98,7 @@ public class ItemUpdateController : BaseJellyfinApiController
}).ToList());
}
UpdateItem(request, item);
await UpdateItem(request, item).ConfigureAwait(false);
item.OnMetadataChanged();
@ -147,7 +147,7 @@ public class ItemUpdateController : BaseJellyfinApiController
var info = new MetadataEditorInfo
{
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
Countries = _localizationManager.GetCountries().ToArray(),
Cultures = _localizationManager.GetCultures().ToArray()
@ -224,7 +224,7 @@ public class ItemUpdateController : BaseJellyfinApiController
return NoContent();
}
private void UpdateItem(BaseItemDto request, BaseItem item)
private async Task UpdateItem(BaseItemDto request, BaseItem item)
{
item.Name = request.Name;
item.ForcedSortName = request.ForcedSortName;
@ -266,9 +266,50 @@ public class ItemUpdateController : BaseJellyfinApiController
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
item.ProductionYear = request.ProductionYear;
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
request.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
item.OfficialRating = request.OfficialRating;
item.CustomRating = request.CustomRating;
if (item is Series rseries)
{
foreach (Season season in rseries.Children)
{
season.OfficialRating = request.OfficialRating;
season.CustomRating = request.CustomRating;
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
foreach (Episode ep in season.Children)
{
ep.OfficialRating = request.OfficialRating;
ep.CustomRating = request.CustomRating;
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}
}
else if (item is Season season)
{
foreach (Episode ep in season.Children)
{
ep.OfficialRating = request.OfficialRating;
ep.CustomRating = request.CustomRating;
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}
else if (item is MusicAlbum album)
{
foreach (BaseItem track in album.Children)
{
track.OfficialRating = request.OfficialRating;
track.CustomRating = request.CustomRating;
track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}
if (request.ProductionLocations is not null)
{
item.ProductionLocations = request.ProductionLocations;

@ -411,6 +411,13 @@ public class ItemsController : BaseJellyfinApiController
query.SeriesStatuses = seriesStatus;
}
// Exclude Blocked Unrated Items
var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
if (blockedUnratedItems is not null)
{
query.BlockUnratedItems = blockedUnratedItems;
}
// ExcludeLocationTypes
if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -72,7 +73,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item returned.</response>
/// <returns>An <see cref="OkResult"/> containing the d item.</returns>
/// <returns>An <see cref="OkResult"/> containing the item.</returns>
[HttpGet("Users/{userId}/Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
@ -86,11 +87,19 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
@ -139,11 +148,19 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
@ -162,7 +179,29 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, true);
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
return MarkFavorite(user, item, true);
}
/// <summary>
@ -176,7 +215,29 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, false);
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
return MarkFavorite(user, item, false);
}
/// <summary>
@ -190,7 +251,29 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return UpdateUserItemRatingInternal(userId, itemId, null);
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
return UpdateUserItemRatingInternal(user, item, null);
}
/// <summary>
@ -205,7 +288,29 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes)
{
return UpdateUserItemRatingInternal(userId, itemId, likes);
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
return UpdateUserItemRatingInternal(user, item, likes);
}
/// <summary>
@ -228,13 +333,20 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions().AddClientFields(User);
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
var dtoOptions = new DtoOptions().AddClientFields(User);
if (item is IHasTrailers hasTrailers)
{
var trailers = hasTrailers.LocalTrailers;
@ -266,11 +378,19 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
var dtoOptions = new DtoOptions().AddClientFields(User);
return Ok(item
@ -385,15 +505,11 @@ public class UserLibraryController : BaseJellyfinApiController
/// <summary>
/// Marks the favorite.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
private UserItemDataDto MarkFavorite(User user, BaseItem item, bool isFavorite)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
@ -408,15 +524,11 @@ public class UserLibraryController : BaseJellyfinApiController
/// <summary>
/// Updates the user item rating.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="likes">if set to <c>true</c> [likes].</param>
private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes)
private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
@ -455,6 +567,13 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
if (item is not UserRootFolder
// Check the item is visible for the user
&& !item.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
}
var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
if (result is not null)
{

@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations
private static readonly Type[] _preStartupMigrationTypes =
{
typeof(PreStartupRoutines.CreateNetworkConfiguration),
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout)
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
typeof(PreStartupRoutines.MigrateRatingLevels)
};
/// <summary>

@ -0,0 +1,86 @@
using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.PreStartupRoutines
{
/// <summary>
/// Migrate rating levels to new rating level system.
/// </summary>
internal class MigrateRatingLevels : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<MigrateRatingLevels> _logger;
private readonly IServerApplicationPaths _applicationPaths;
public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
{
_applicationPaths = applicationPaths;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dataPath = _applicationPaths.DataPath;
var dbPath = Path.Combine(dataPath, DbFilename);
using (var connection = SQLite3.Open(
dbPath,
ConnectionFlags.ReadWrite,
null))
{
// Back up the database before deleting any entries
for (int i = 1; ; i++)
{
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
if (!File.Exists(bakPath))
{
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;
}
}
}
// Migrate parental rating levels to new schema
_logger.LogInformation("Migrating parental rating levels.");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1");
}
}
}
}

@ -554,7 +554,7 @@ namespace MediaBrowser.Controller.Entities
public string OfficialRating { get; set; }
[JsonIgnore]
public int InheritedParentalRatingValue { get; set; }
public int? InheritedParentalRatingValue { get; set; }
/// <summary>
/// Gets or sets the critic rating.
@ -1534,12 +1534,6 @@ namespace MediaBrowser.Controller.Entities
}
var maxAllowedRating = user.MaxParentalAgeRating;
if (maxAllowedRating is null)
{
return true;
}
var rating = CustomRatingForComparison;
if (string.IsNullOrEmpty(rating))
@ -1549,12 +1543,13 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
Logger.LogDebug("{0} has no parental rating set.", Name);
return !GetBlockUnratedValue(user);
}
var value = LocalizationManager.GetRatingLevel(rating);
// Could not determine the integer value
// Could not determine rating level
if (!value.HasValue)
{
var isAllowed = !GetBlockUnratedValue(user);
@ -1567,7 +1562,7 @@ namespace MediaBrowser.Controller.Entities
return isAllowed;
}
return value.Value <= maxAllowedRating.Value;
return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value;
}
public int? GetInheritedParentalRatingValue()
@ -1627,10 +1622,10 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
/// Gets the block unrated value.
/// Gets a bool indicating if access to the unrated item is blocked or not.
/// </summary>
/// <param name="user">The configuration.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
/// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
protected virtual bool GetBlockUnratedValue(User user)
{
// Don't block plain folders that are unrated. Let the media underneath get blocked
@ -2517,7 +2512,7 @@ namespace MediaBrowser.Controller.Entities
var item = this;
var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0;
var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null;
if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
{
item.InheritedParentalRatingValue = inheritedParentalRatingValue;

@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Entities
{
}
public ParentalRating(string name, int value)
public ParentalRating(string name, int? value)
{
Name = name;
Value = value;
@ -28,6 +28,6 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the value.
/// </summary>
/// <value>The value.</value>
public int Value { get; set; }
public int? Value { get; set; }
}
}

@ -46,6 +46,7 @@ namespace MediaBrowser.Model.Users
LoginAttemptsBeforeLockout = -1;
MaxActiveSessions = 0;
MaxParentalRating = null;
EnableAllChannels = true;
EnabledChannels = Array.Empty<Guid>();

@ -83,11 +83,11 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(23, ratings.Count);
Assert.Equal(53, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
Assert.Equal(9, tvma!.Value);
Assert.Equal(17, tvma!.Value);
}
[Fact]
@ -100,21 +100,21 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(10, ratings.Count);
Assert.Equal(18, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
Assert.Equal(7, fsk!.Value);
Assert.Equal(12, fsk!.Value);
}
[Theory]
[InlineData("CA-R", "CA", 10)]
[InlineData("FSK-16", "DE", 8)]
[InlineData("FSK-18", "DE", 9)]
[InlineData("FSK-18", "US", 9)]
[InlineData("TV-MA", "US", 9)]
[InlineData("XXX", "asdf", 100)]
[InlineData("Germany: FSK-18", "DE", 9)]
[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)]
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
{
var localizationManager = Setup(new ServerConfiguration()
@ -135,6 +135,9 @@ 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"));
}

Loading…
Cancel
Save