aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-02-16 18:50:11 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-02-16 18:50:11 +0100
commit0f75518287f79c4c9aa3c009a93bd4ad65e2bab1 (patch)
tree1867f231583edfdb9dcd4a837c571d27f8197581
parentde32e2eb6fdb86296312213b7254c37fd4a1a6f3 (diff)
Enforce permissions on BoxSets
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs62
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs19
2 files changed, 65 insertions, 16 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index f43677ab11..ed5db0f934 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -716,12 +716,14 @@ public sealed class BaseItemRepository
var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
// Get the last watched episode ID per series (highest season/episode that is played)
- var lastWatchedInfo = context.BaseItems
+ var lastWatchedBase = context.BaseItems
.AsNoTracking()
.Where(e => e.Type == episodeTypeName)
.Where(e => e.SeriesPresentationUniqueKey != null && seriesKeys.Contains(e.SeriesPresentationUniqueKey))
.Where(e => e.ParentIndexNumber != 0)
- .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
+ .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
+ lastWatchedBase = ApplyAccessFiltering(context, lastWatchedBase, filter);
+ var lastWatchedInfo = lastWatchedBase
.GroupBy(e => e.SeriesPresentationUniqueKey)
.Select(g => new
{
@@ -736,12 +738,14 @@ public sealed class BaseItemRepository
Dictionary<string, Guid> lastWatchedByDateInfo = new();
if (includeWatchedForRewatching)
{
- lastWatchedByDateInfo = context.BaseItems
+ var lastWatchedByDateBase = context.BaseItems
.AsNoTracking()
.Where(e => e.Type == episodeTypeName)
.Where(e => e.SeriesPresentationUniqueKey != null && seriesKeys.Contains(e.SeriesPresentationUniqueKey))
.Where(e => e.ParentIndexNumber != 0)
- .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
+ .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
+ lastWatchedByDateBase = ApplyAccessFiltering(context, lastWatchedByDateBase, filter);
+ lastWatchedByDateInfo = lastWatchedByDateBase
.SelectMany(e => e.UserData!.Where(ud => ud.UserId == userId && ud.Played)
.Select(ud => new { Episode = e, ud.LastPlayedDate }))
.GroupBy(x => x.Episode.SeriesPresentationUniqueKey)
@@ -777,6 +781,7 @@ public sealed class BaseItemRepository
.Where(e => e.SeriesPresentationUniqueKey != null && seriesKeys.Contains(e.SeriesPresentationUniqueKey))
.Where(e => e.ParentIndexNumber == 0)
.Where(e => !e.IsVirtualItem);
+ specialsQuery = ApplyAccessFiltering(context, specialsQuery, filter);
specialsQuery = ApplyNavigations(specialsQuery, filter).AsSingleQuery();
foreach (var special in specialsQuery)
@@ -808,13 +813,15 @@ public sealed class BaseItemRepository
// Single query: fetch all unplayed non-virtual non-special episodes for all series.
// Uses NOT EXISTS (via !Any) for the played check, which is more efficient than GroupJoin.
// Only unplayed episodes are loaded (typically ~10% of total), keeping memory usage low.
- var allUnplayedCandidates = context.BaseItems
+ var allUnplayedBase = context.BaseItems
.AsNoTracking()
.Where(e => e.Type == episodeTypeName)
.Where(e => e.SeriesPresentationUniqueKey != null && seriesKeys.Contains(e.SeriesPresentationUniqueKey))
.Where(e => e.ParentIndexNumber != 0)
.Where(e => !e.IsVirtualItem)
- .Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
+ .Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
+ allUnplayedBase = ApplyAccessFiltering(context, allUnplayedBase, filter);
+ var allUnplayedCandidates = allUnplayedBase
.Select(e => new
{
e.Id,
@@ -856,13 +863,15 @@ public sealed class BaseItemRepository
var seriesNextPlayedIdMap = new Dictionary<string, Guid>();
if (includeWatchedForRewatching)
{
- var allPlayedCandidates = context.BaseItems
+ var allPlayedBase = context.BaseItems
.AsNoTracking()
.Where(e => e.Type == episodeTypeName)
.Where(e => e.SeriesPresentationUniqueKey != null && seriesKeys.Contains(e.SeriesPresentationUniqueKey))
.Where(e => e.ParentIndexNumber != 0)
.Where(e => !e.IsVirtualItem)
- .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
+ .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
+ allPlayedBase = ApplyAccessFiltering(context, allPlayedBase, filter);
+ var allPlayedCandidates = allPlayedBase
.Select(e => new
{
e.Id,
@@ -3302,11 +3311,24 @@ public sealed class BaseItemRepository
var max = filter.MaxParentalRating;
var maxScore = max.Score;
var maxSubScore = max.SubScore ?? 0;
+ var linkedChildren = context.LinkedChildren;
maxParentalRatingFilter = e =>
- e.InheritedParentalRatingValue == null ||
- e.InheritedParentalRatingValue < maxScore ||
- (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
+ // Item has a rating: check against limit
+ (e.InheritedParentalRatingValue != null
+ && (e.InheritedParentalRatingValue < maxScore
+ || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))
+ // Item has no rating
+ || (e.InheritedParentalRatingValue == null
+ && (
+ // No linked children (not a BoxSet/Playlist): pass as unrated
+ !linkedChildren.Any(lc => lc.ParentId == e.Id)
+ // Has linked children: at least one child must be within limits
+ || linkedChildren.Any(lc => lc.ParentId == e.Id
+ && (lc.Child!.InheritedParentalRatingValue == null
+ || lc.Child.InheritedParentalRatingValue < maxScore
+ || (lc.Child.InheritedParentalRatingValue == maxScore
+ && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))));
}
if (filter.HasParentalRating ?? false)
@@ -4084,9 +4106,21 @@ public sealed class BaseItemRepository
var maxSubScore = filter.MaxParentalRating.SubScore ?? 0;
baseQuery = baseQuery.Where(e =>
- e.InheritedParentalRatingValue == null ||
- e.InheritedParentalRatingValue < maxScore ||
- (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore));
+ // Item has a rating: check against limit
+ (e.InheritedParentalRatingValue != null
+ && (e.InheritedParentalRatingValue < maxScore
+ || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))
+ // Item has no rating
+ || (e.InheritedParentalRatingValue == null
+ && (
+ // No linked children (not a BoxSet/Playlist): pass as unrated
+ !context.LinkedChildren.Any(lc => lc.ParentId == e.Id)
+ // Has linked children: at least one child must be within limits
+ || context.LinkedChildren.Any(lc => lc.ParentId == e.Id
+ && (lc.Child!.InheritedParentalRatingValue == null
+ || lc.Child.InheritedParentalRatingValue < maxScore
+ || (lc.Child.InheritedParentalRatingValue == maxScore
+ && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore))))));
}
// Apply block unrated items filtering
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index c6579285db..2166a58024 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -158,7 +158,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return base.IsVisible(user, skipAllowedTagsCheck);
}
- if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
+ if (!IsParentalAllowed(user, skipAllowedTagsCheck))
{
return false;
}
@@ -176,7 +176,22 @@ namespace MediaBrowser.Controller.Entities.Movies
return true;
}
- return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
+ if (!userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i)))
+ {
+ return false;
+ }
+
+ // If user has parental controls, hide the BoxSet when all children are restricted
+ if (user.MaxParentalRatingScore.HasValue)
+ {
+ var linkedItems = GetLinkedChildren();
+ if (linkedItems.Count > 0 && linkedItems.All(child => !child.IsParentalAllowed(user, true)))
+ {
+ return false;
+ }
+ }
+
+ return true;
}
public override bool IsVisibleStandalone(User user)