share whole series and seasons, not just single titles

Sharing a series or season now tags the entire tree (series, seasons,
episodes) so the guest can browse and play everything inside it, and
strips it all again at cleanup. Redeeming re-tags the tree, so episodes
added after the link was created show up on the next redemption. The
guest lockdown in the web client now asks the server whether a page's
item is visible to the guest instead of hard-coding the single shared
id, so guests can navigate inside the shared tree but nowhere else.
Libraries and collections are still rejected.

Bumps the version to 1.0.1.0.
Cette révision appartient à :
Franciskid
2026-07-08 01:50:57 +02:00
Parent c637558085
révision bbb999842f
9 fichiers modifiés avec 243 ajouts et 43 suppressions
+82 -2
Voir le fichier
@@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
@@ -45,7 +46,7 @@ public sealed class ItemTagService
tags.Add(tag);
item.Tags = tags.ToArray();
await PersistAsync(item, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("ShareLinks: applied temporary tag {Tag} to item {ItemId}.", tag, item.Id);
_logger.LogDebug("ShareLinks: applied temporary tag {Tag} to item {ItemId}.", tag, item.Id);
return true;
}
@@ -71,10 +72,89 @@ public sealed class ItemTagService
item.Tags = tags.ToArray();
await PersistAsync(item, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("ShareLinks: removed temporary tag {Tag} from item {ItemId}.", tag, item.Id);
_logger.LogDebug("ShareLinks: removed temporary tag {Tag} from item {ItemId}.", tag, item.Id);
return true;
}
/// <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.
/// </summary>
public async Task<bool> EnsureTagTreeAsync(BaseItem item, string tag, CancellationToken cancellationToken)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
var targets = BuildTagTreeTargets(item);
var changed = false;
foreach (var target in targets)
{
cancellationToken.ThrowIfCancellationRequested();
changed |= await EnsureTagAsync(target, tag, cancellationToken).ConfigureAwait(false);
}
_logger.LogInformation(
"ShareLinks: ensured tag {Tag} across {Count} item(s) rooted at {ItemId} \"{ItemName}\".",
tag,
targets.Count,
item.Id,
item.Name);
return changed;
}
/// <summary>
/// Removes the supplied tag from the item and, for a season or folder, from the
/// item's related tree (mirrors <see cref="EnsureTagTreeAsync"/>).
/// </summary>
public async Task<bool> RemoveTagTreeAsync(BaseItem item, string tag, CancellationToken cancellationToken)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
var targets = BuildTagTreeTargets(item);
var changed = false;
foreach (var target in targets)
{
cancellationToken.ThrowIfCancellationRequested();
changed |= await RemoveTagAsync(target, tag, cancellationToken).ConfigureAwait(false);
}
_logger.LogInformation(
"ShareLinks: removed tag {Tag} across {Count} item(s) rooted at {ItemId} \"{ItemName}\".",
tag,
targets.Count,
item.Id,
item.Name);
return changed;
}
private static List<BaseItem> BuildTagTreeTargets(BaseItem item)
{
var targets = new List<BaseItem> { item };
if (item is Season season)
{
var series = season.Series ?? season.GetParent() as Series;
if (series is not null)
{
targets.Add(series);
}
}
if (item is Folder folder)
{
targets.AddRange(folder.GetRecursiveChildren());
}
return targets;
}
private async Task PersistAsync(BaseItem item, CancellationToken cancellationToken)
{
var method = _libraryManager.GetType()
+1 -1
Voir le fichier
@@ -128,7 +128,7 @@ public sealed class ShareLinkCleanupService : IShareLinkCleanupService
{
try
{
var removed = await _itemTagService.RemoveTagAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
var removed = await _itemTagService.RemoveTagTreeAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
record.MetadataTouched |= removed;
}
catch (Exception ex)
+1 -1
Voir le fichier
@@ -65,7 +65,7 @@ public sealed class ShareLinkCreationService
{
if (!string.IsNullOrWhiteSpace(record.AllowedTag))
{
record.MetadataTouched = await _itemTagService.EnsureTagAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
record.MetadataTouched = await _itemTagService.EnsureTagTreeAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
}
record.Status = ShareLinkStatus.Active;
+1 -1
Voir le fichier
@@ -88,7 +88,7 @@ public sealed class ShareLinkRedemptionService
if (!string.IsNullOrWhiteSpace(record.AllowedTag))
{
await _itemTagService.EnsureTagAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
await _itemTagService.EnsureTagTreeAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
record.MetadataTouched = true;
}