stop a shared season handing over the rest of the series

Sharing one season let the guest see every season of that show. Sharing a whole
series or a single episode was fine, which is what made it look like a season
specific problem.

The cause is that the tag went upwards as well as downwards. BuildTagTreeTargets
added a season's parent series, so the guest's tag landed on the series itself,
and Jellyfin's GetInheritedTags is an item's own tags plus every ancestor's, with
AllowedTags matched against that. Tagging the series therefore made every other
season and episode under it inherit the tag and become visible. Sharing a series
looked correct because the whole show is meant to be visible, and sharing an
episode looked correct because nothing above it was ever tagged.

Tagging now only ever goes down: the shared item, and its descendants when it is a
folder. A guest sent one season gets that season and its episodes, and the series
page is not theirs to open, which is what sharing a season should mean.

Removal deliberately still reaches up to the parent series. Links created by
earlier builds put the tag there, and taking a tag off can only remove access, so
those get cleaned up when the link expires or is revoked instead of being stranded
on the series forever.
Cette révision appartient à :
Franciskid
2026-07-27 00:34:55 +02:00
Parent a811095784
révision ad17b17ab4
2 fichiers modifiés avec 48 ajouts et 19 suppressions
+35 -12
Voir le fichier
@@ -77,10 +77,10 @@ public sealed class ItemTagService
}
/// <summary>
/// Ensures the supplied tag is present on the item and, for a season or folder,
/// on the item's related tree (parent series for a season; all recursive
/// children for a folder such as a series or season) so a guest can browse the
/// whole shared branch instead of only the single node the link was created on.
/// Ensures the supplied tag is present on the item and, when the item is a folder
/// such as a series or a season, on everything underneath it, so a guest can
/// browse down through the shared branch instead of only seeing the single node
/// the link was created on.
/// </summary>
public async Task<bool> EnsureTagTreeAsync(BaseItem item, string tag, CancellationToken cancellationToken)
{
@@ -107,8 +107,8 @@ public sealed class ItemTagService
}
/// <summary>
/// Removes the supplied tag from the item and, for a season or folder, from the
/// item's related tree (mirrors <see cref="EnsureTagTreeAsync"/>).
/// Removes the supplied tag from the item and, when it is a folder, from
/// everything underneath it (mirrors <see cref="EnsureTagTreeAsync"/>).
/// </summary>
public async Task<bool> RemoveTagTreeAsync(BaseItem item, string tag, CancellationToken cancellationToken)
{
@@ -117,7 +117,7 @@ public sealed class ItemTagService
throw new ArgumentNullException(nameof(item));
}
var targets = BuildTagTreeTargets(item);
var targets = BuildTagRemovalTargets(item);
var changed = false;
foreach (var target in targets)
{
@@ -134,10 +134,38 @@ public sealed class ItemTagService
return changed;
}
/// <summary>
/// The items that carry a share's tag: the shared item itself and, when it is a
/// folder, everything under it.
/// </summary>
/// <remarks>
/// Never the parents. Jellyfin's <c>GetInheritedTags</c> is an item's own tags
/// plus every ancestor's, and the guest's AllowedTags policy is matched against
/// that, so tagging a season's parent series would hand the guest every other
/// season of that series as well.
/// </remarks>
private static List<BaseItem> BuildTagTreeTargets(BaseItem item)
{
var targets = new List<BaseItem> { item };
if (item is Folder folder)
{
targets.AddRange(folder.GetRecursiveChildren());
}
return targets;
}
/// <summary>
/// Removal deliberately reaches one level further than tagging does: up to a
/// season's parent series. Builds before this fix put the share's tag on that
/// series, and taking a tag off can only ever remove access, so cleaning those
/// up is safe where applying them was not.
/// </summary>
private static List<BaseItem> BuildTagRemovalTargets(BaseItem item)
{
var targets = BuildTagTreeTargets(item);
if (item is Season season)
{
var series = season.Series ?? season.GetParent() as Series;
@@ -147,11 +175,6 @@ public sealed class ItemTagService
}
}
if (item is Folder folder)
{
targets.AddRange(folder.GetRecursiveChildren());
}
return targets;
}
+13 -7
Voir le fichier
@@ -37,12 +37,15 @@ real user or handing over a login that sees everything.
watch at the same time, ten by default, and the eleventh is asked to try again
later rather than displacing anyone.
2. Behind the scenes the plugin tags the shared item with a unique, random tag
and records the share. Share a series or a season and the tag is applied to
the whole tree underneath it too - series, seasons and episodes - so the
guest can actually browse from the series page down into a season and an
episode, not just see a single locked node. Lookups only ever go through a
keyed HMAC hash of the token, and the link itself is dropped from the record
once it is revoked or expired.
and records the share. Share a series or a season and the tag goes on
everything underneath it as well, so the guest can browse down through what
you shared rather than seeing a single locked node. The tag never goes on
anything above it: Jellyfin treats a parent's tags as belonging to all of its
children, so tagging the series that a shared season sits in would hand over
every other season too. Share one season and that is all the guest gets, and
the series page is not theirs to open. Lookups only ever go through a keyed
HMAC hash of the token, and the link itself is dropped from the record once
it is revoked or expired.
3. Whoever opens the link gets a throwaway guest user created on the spot,
restricted by that tag to the shared item and its tree, and is signed in
automatically. They land on the title's page.
@@ -63,7 +66,10 @@ server, not only in the browser:
- On top of that, the web client is locked down for the guest: the home,
menu and search buttons are hidden, in-page links (cast, studio, genres) are
made inert, any attempt to navigate somewhere outside the shared tree snaps back
to the shared title.
to the shared title. Navigating down within what you shared works normally: a
shared series opens into its seasons and episodes, a shared season into its
episodes. Going up does not, so a guest sent one season cannot reach the series
it belongs to.
Playback works normally, including transcoding and remuxing if you allow it, and
the player's back button still returns them to the title's page.