Merge pull request #11 from Franciskid/fix/viewer-ceiling-refusal
Refuse an over-the-ceiling viewer without destroying the link
Cette révision appartient à :
@@ -6,9 +6,9 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>Jellyfin.Plugin.ShareLinks</RootNamespace>
|
||||
<AssemblyName>Jellyfin.Plugin.ShareLinks</AssemblyName>
|
||||
<Version>1.0.3.0</Version>
|
||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||
<FileVersion>1.0.3.0</FileVersion>
|
||||
<Version>1.0.3.1</Version>
|
||||
<AssemblyVersion>1.0.3.1</AssemblyVersion>
|
||||
<FileVersion>1.0.3.1</FileVersion>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -112,6 +112,19 @@ public sealed class ShareLinkRedemptionService
|
||||
return new ShareLinkRedemptionResult();
|
||||
}
|
||||
|
||||
// Turn an over-the-ceiling viewer away before anything is written: no tag
|
||||
// work, no status change, no guest account touched. Jellyfin enforces the
|
||||
// same limit itself when the session is created, but it does so by throwing,
|
||||
// and a throw here would land in the failure path below and tear the whole
|
||||
// share down on the people already watching.
|
||||
if (IsAtViewerCeiling(record))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ShareLinks: record {RecordId} is at its viewer ceiling; turning a viewer away.",
|
||||
record.Id);
|
||||
return new ShareLinkRedemptionResult { AtCapacity = true };
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(record.ItemId, out var itemId))
|
||||
{
|
||||
await HandleFailureAsync(record, "Shared item snapshot is invalid.", cancellationToken).ConfigureAwait(false);
|
||||
@@ -176,15 +189,17 @@ public sealed class ShareLinkRedemptionService
|
||||
record.CleanupError = null;
|
||||
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SecurityException ex)
|
||||
catch (MediaBrowser.Controller.Net.SecurityException ex)
|
||||
{
|
||||
// The link is fine, the guest account has simply reached its viewer
|
||||
// ceiling. Leave the record and the guest alone: marking this failed
|
||||
// would tear down the account and throw out everyone already watching.
|
||||
// Backstop for the race between the ceiling check above and this call.
|
||||
// Jellyfin's own type, NOT System.Security.SecurityException. The link is
|
||||
// fine, so leave the record and the guest exactly as they were: marking
|
||||
// this failed would tear the account down and throw out everyone already
|
||||
// watching.
|
||||
_logger.LogInformation(ex, "ShareLinks: record {RecordId} hit its viewer ceiling while creating the session.", record.Id);
|
||||
record.Status = record.RedeemedAtUtc.HasValue ? ShareLinkStatus.Redeemed : ShareLinkStatus.Active;
|
||||
record.CleanupError = null;
|
||||
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation(ex, "ShareLinks: record {RecordId} is at its viewer ceiling; turning a viewer away.", record.Id);
|
||||
return new ShareLinkRedemptionResult { AtCapacity = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -200,6 +215,32 @@ public sealed class ShareLinkRedemptionService
|
||||
return new ShareLinkRedemptionResult { Html = BuildBootstrapHtml(request, authResult, itemId) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the share already has as many viewers watching as it is
|
||||
/// allowed. Mirrors the limit Jellyfin applies when it creates a session, so we
|
||||
/// can refuse politely instead of letting it throw.
|
||||
/// </summary>
|
||||
private bool IsAtViewerCeiling(ShareLinkRecord record)
|
||||
{
|
||||
if (!record.GuestUserId.HasValue)
|
||||
{
|
||||
// Nobody has redeemed this link yet, so nothing can be at a ceiling.
|
||||
return false;
|
||||
}
|
||||
|
||||
var ceiling = record.OneUse
|
||||
? 1
|
||||
: Math.Max(Plugin.Instance?.Configuration.MaxConcurrentViewers ?? 0, 0);
|
||||
if (ceiling < 1)
|
||||
{
|
||||
// 0 means no limit, the same way Jellyfin reads it.
|
||||
return false;
|
||||
}
|
||||
|
||||
var guestUserId = record.GuestUserId.Value;
|
||||
return _sessionManager.Sessions.Count(session => session.UserId.Equals(guestUserId)) >= ceiling;
|
||||
}
|
||||
|
||||
private async Task HandleTerminalRecordAsync(ShareLinkRecord record, ShareLinkStatus terminalStatus, string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
record.Status = terminalStatus;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"guid": "68540b76-ee74-436d-85ff-2abc884bbea6",
|
||||
"name": "ShareLinks",
|
||||
"version": "1.0.3.0",
|
||||
"version": "1.0.3.1",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"framework": "net9.0",
|
||||
"owner": "Franciskid",
|
||||
"overview": "Secure expiring guest-share links for Jellyfin items.",
|
||||
"description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.",
|
||||
"category": "General",
|
||||
"timestamp": "2026-07-26T19:30:00.0000000Z"
|
||||
"timestamp": "2026-07-26T20:00:00.0000000Z"
|
||||
}
|
||||
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur