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.
87 lignes
3.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|