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;
/// Creates durable ShareLinks records and applies the temporary tag.
public sealed class ShareLinkCreationService
{
private readonly ShareLinkStore _store;
private readonly ShareTokenService _tokenService;
private readonly ItemTagService _itemTagService;
private readonly ILogger _logger;
/// Initializes a new instance of the class.
public ShareLinkCreationService(
ShareLinkStore store,
ShareTokenService tokenService,
ItemTagService itemTagService,
ILogger logger)
{
_store = store;
_tokenService = tokenService;
_itemTagService = itemTagService;
_logger = logger;
}
/// Creates a new share-link record and returns the raw token once.
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;
}
}
}