Fichiers
jellyfin-plugin-sharelinks/Jellyfin.Plugin.ShareLinks/Services/ShareLinkCreationService.cs
T
Franciskid bbb999842f 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.
2026-07-08 01:50:57 +02:00

87 lignes
3.1 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.ShareLinks.Models;
using Jellyfin.Plugin.ShareLinks.Storage;
using MediaBrowser.Controller.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.ShareLinks.Services;
/// <summary>Creates durable ShareLinks records and applies the temporary tag.</summary>
public sealed class ShareLinkCreationService
{
private readonly ShareLinkStore _store;
private readonly ShareTokenService _tokenService;
private readonly ItemTagService _itemTagService;
private readonly ILogger<ShareLinkCreationService> _logger;
/// <summary>Initializes a new instance of the <see cref="ShareLinkCreationService"/> class.</summary>
public ShareLinkCreationService(
ShareLinkStore store,
ShareTokenService tokenService,
ItemTagService itemTagService,
ILogger<ShareLinkCreationService> logger)
{
_store = store;
_tokenService = tokenService;
_itemTagService = itemTagService;
_logger = logger;
}
/// <summary>Creates a new share-link record and returns the raw token once.</summary>
public async Task<(ShareLinkRecord Record, string RawToken)> CreateAsync(
BaseItem item,
Guid createdByUserId,
int expiryHours,
bool oneUse,
CancellationToken cancellationToken)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
var token = await _tokenService.GenerateAsync(cancellationToken).ConfigureAwait(false);
var now = DateTimeOffset.UtcNow;
var record = new ShareLinkRecord
{
Id = Guid.NewGuid(),
TokenHash = token.TokenHash,
ItemId = item.Id.ToString("D"),
ItemNameSnapshot = item.Name ?? string.Empty,
CreatedByUserId = createdByUserId == Guid.Empty ? null : createdByUserId,
CreatedAtUtc = now,
ExpiresAtUtc = now.AddHours(expiryHours),
Status = ShareLinkStatus.Pending,
OneUse = oneUse,
AllowedTag = $"sharelinks-{Guid.NewGuid():N}",
CleanupAttempts = 0
};
await _store.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
try
{
if (!string.IsNullOrWhiteSpace(record.AllowedTag))
{
record.MetadataTouched = await _itemTagService.EnsureTagTreeAsync(item, record.AllowedTag!, cancellationToken).ConfigureAwait(false);
}
record.Status = ShareLinkStatus.Active;
record.CleanupError = null;
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
return (record, token.Token);
}
catch (Exception ex)
{
record.Status = ShareLinkStatus.Failed;
record.CleanupError = ex.Message;
record.MetadataTouched = true;
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
_logger.LogWarning(ex, "ShareLinks: failed to finish creation for record {RecordId}.", record.Id);
throw;
}
}
}