cap how many people can watch one multi-use link at once

New setting, ten by default, zero for no limit. Single-use links are unaffected,
they are one viewer by definition.

The catch is what happens at the ceiling. Jellyfin throws SecurityException once a
user is at MaxActiveSessions, and that was landing in the generic handler, which
marks the record failed and runs cleanup, which deletes the guest account. So
without care, adding a ceiling would mean the eleventh person to open a link kicks
out the ten already watching and destroys the link. Capacity is caught separately
now: the record goes back to the state it was in, nothing is torn down, and the
new arrival gets a 503 page inviting them to try again.

Worth being honest that this caps how many people can start watching at once, not
how many ever get in: each redemption issues its own session token that keeps
working until the link is revoked or expires. Revoke is still the hard stop.

README picks up the multi-use option, the new setting, and a section on what a
multi-use link does and does not protect, plus the known limits around the token
in the query string, the unthrottled redeem endpoint, and the tag being hidden in
the web UI only.
Cette révision appartient à :
Franciskid
2026-07-26 21:22:20 +02:00
Parent f0b9e8351b
révision 224a79f7e5
7 fichiers modifiés avec 147 ajouts et 21 suppressions
+47 -3
Voir le fichier
@@ -326,13 +326,18 @@ public sealed class ShareLinksController : ControllerBase
return LinkUnavailablePage(Request);
}
var html = await _redemptionService.RedeemAsync(token, Request, cancellationToken).ConfigureAwait(false);
if (html is null)
var result = await _redemptionService.RedeemAsync(token, Request, cancellationToken).ConfigureAwait(false);
if (result.AtCapacity)
{
return LinkBusyPage();
}
if (result.Html is null)
{
return LinkUnavailablePage(Request);
}
return Content(html, "text/html; charset=utf-8");
return Content(result.Html, "text/html; charset=utf-8");
}
private static ContentResult LinkUnavailablePage(HttpRequest request)
@@ -380,6 +385,45 @@ setTimeout(function () { window.location.replace({{redirectUrlJson}}); }, 4000);
};
}
/// <summary>
/// Served when a multi-use link has as many viewers as it is allowed. The link
/// itself is still good, so this deliberately invites a retry instead of
/// looking like a dead link.
/// </summary>
private static ContentResult LinkBusyPage()
{
var html = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Too many viewers</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; min-height: 100vh; display: grid; place-items: center; background: #111827; color: #e5e7eb; }
main { max-width: 36rem; padding: 2rem; }
.muted { color: #9ca3af; }
a { color: #60a5fa; }
</style>
</head>
<body>
<main>
<div>This link is being watched by as many people as it allows right now.</div>
<div class="muted">Ce lien est deja utilise par autant de personnes qu'il l'autorise.</div>
<p><a href="">Try again</a></p>
</main>
</body>
</html>
""";
return new ContentResult
{
StatusCode = StatusCodes.Status503ServiceUnavailable,
ContentType = "text/html; charset=utf-8",
Content = html
};
}
private static ShareLinkAdminRecordDto ToDto(ShareLinkRecord record)
{
return new ShareLinkAdminRecordDto