let a link be used by several people instead of dying on first open

The create popup now has a "Let several people use this link" box next to the
expiry controls. Tick it and the link stays redeemable by everyone you send it to
until it expires; leave it and you get the old behaviour, where the first person
to open it is the only one who gets in. The plugin setting that used to be the
only control is now just what the box starts out as, and its label on the config
page says so, because "Default one-use links" explained nothing.

Multi-use did not actually work before this. Two things in Jellyfin stopped it,
and both had to change:

Guests were given MaxActiveSessions = 1, and AuthenticateNewSessionInternal throws
SecurityException once a user is at that limit. The second viewer's redemption
would fail, the record would go to Failed, and cleanup would then delete the guest
account, kicking the first viewer out too. Multi-use links now get 0, which is how
Jellyfin spells "no limit" in that check.

The device id was generated once and reused for every redemption, and
GetAuthorizationToken logs out every existing session for the same user and device
before issuing a token. So even under a raised session cap, each new viewer would
have revoked the previous one's token. Multi-use links now mint a device id per
redemption.

Both viewers of a multi-use link share one temporary account, so they also share
playback position and watched state on the shared title.
Cette révision appartient à :
Franciskid
2026-07-26 21:05:54 +02:00
Parent ca0ab96ee9
révision f0b9e8351b
7 fichiers modifiés avec 86 ajouts et 18 suppressions
+66 -8
Voir le fichier
@@ -2,7 +2,7 @@
var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6';
var copyLabel = 'Copy Stream URL';
var actionLabel = 'ShareLink';
var clientVersion = '1.0.2-ui-3';
var clientVersion = '1.0.3-ui-1';
var allowedItemStorageKey = 'sharelinks.allowedItemId';
var guestClassName = 'sharelinks-guest';
var hiddenAttr = 'data-sharelinks-hidden';
@@ -65,6 +65,9 @@
pickDateFirst: 'Pick a date and time first.',
dateInvalid: 'That date is not valid.',
pickFuture: 'Pick a time in the future.',
multiUseLabel: 'Let several people use this link',
multiUseHint: 'The link keeps working for anyone you send it to until it expires, instead of dying once the first person opens it.',
resultMultiUseNote: 'Anyone you send this link to can open it until it expires.',
cannotDetermineItem: 'Could not determine which item to share. Open the item page and retry.',
adminOnly: 'ShareLinks is available to administrators only.',
disabled: 'ShareLinks is disabled.',
@@ -89,6 +92,9 @@
pickDateFirst: 'Choisissez d\'abord une date et une heure.',
dateInvalid: 'Cette date n\'est pas valide.',
pickFuture: 'Choisissez une date dans le futur.',
multiUseLabel: 'Autoriser plusieurs personnes à utiliser ce lien',
multiUseHint: 'Le lien reste valable pour toutes les personnes à qui vous l\'envoyez jusqu\'à son expiration, au lieu de mourir dès la première ouverture.',
resultMultiUseNote: 'Toutes les personnes à qui vous envoyez ce lien peuvent l\'ouvrir jusqu\'à son expiration.',
cannotDetermineItem: 'Impossible de déterminer l\'élément à partager. Ouvrez la page du média et réessayez.',
adminOnly: 'ShareLinks est réservé aux administrateurs.',
disabled: 'ShareLinks est désactivé.',
@@ -1063,11 +1069,11 @@
return;
}
var result = await chooseExpiryHours(config, function (expiryHours) {
var result = await chooseExpiryHours(config, function (expiryHours, multiUse) {
var payload = {
itemId: itemId,
expiryHours: expiryHours,
oneUse: config && config.OneUseDefault !== undefined ? !!config.OneUseDefault : true
oneUse: !multiUse
};
var shareUrlPromise = apiPost('ShareLinks/Admin/Create', payload).then(function (response) {
@@ -1086,7 +1092,8 @@
}).then(function (copied) {
return {
shareUrl: shareUrl,
copied: copied
copied: copied,
multiUse: !!multiUse
};
});
});
@@ -1095,7 +1102,7 @@
return;
}
showShareResult(result.shareUrl, result.copied);
showShareResult(result.shareUrl, result.copied, result.multiUse);
} catch (error) {
notify(extractErrorMessage(error, t('couldNotCreate')));
}
@@ -1134,6 +1141,11 @@
options: options,
onChoose: onChoose,
cancelText: t('cancel'),
toggle: {
label: t('multiUseLabel'),
hint: t('multiUseHint'),
checked: !(config && config.OneUseDefault !== false)
},
datePicker: {
min: toLocalDatetimeValue(minDate),
max: toLocalDatetimeValue(maxDate),
@@ -1193,7 +1205,7 @@
}, 3600);
}
function showShareResult(shareUrl, copied) {
function showShareResult(shareUrl, copied, multiUse) {
ensureShareLinksUi();
var body = document.createElement('div');
var note = document.createElement('p');
@@ -1203,6 +1215,13 @@
: t('notCopiedNote');
body.appendChild(note);
if (multiUse) {
var multiUseNote = document.createElement('p');
multiUseNote.className = 'sharelinks-note';
multiUseNote.textContent = t('resultMultiUseNote');
body.appendChild(multiUseNote);
}
var urlBox = document.createElement('textarea');
urlBox.className = 'sharelinks-url';
urlBox.readOnly = true;
@@ -1276,6 +1295,40 @@
body.appendChild(dateRow);
}
var toggleInput = null;
if (settings.toggle) {
var toggleRow = document.createElement('label');
toggleRow.className = 'sharelinks-toggle-row';
toggleInput = document.createElement('input');
toggleInput.type = 'checkbox';
toggleInput.className = 'sharelinks-toggle-input';
toggleInput.checked = !!settings.toggle.checked;
var toggleText = document.createElement('span');
toggleText.className = 'sharelinks-toggle-text';
var toggleLabel = document.createElement('span');
toggleLabel.className = 'sharelinks-toggle-label';
toggleLabel.textContent = settings.toggle.label;
toggleText.appendChild(toggleLabel);
if (settings.toggle.hint) {
var toggleHint = document.createElement('span');
toggleHint.className = 'sharelinks-toggle-hint';
toggleHint.textContent = settings.toggle.hint;
toggleText.appendChild(toggleHint);
}
toggleRow.appendChild(toggleInput);
toggleRow.appendChild(toggleText);
body.appendChild(toggleRow);
}
function toggleChecked() {
return !!(toggleInput && toggleInput.checked);
}
return new Promise(function (resolve) {
var modal;
var actions = [];
@@ -1310,7 +1363,7 @@
if (modal) {
modal.close();
}
resolve(settings.onChoose ? settings.onChoose(hours) : hours);
resolve(settings.onChoose ? settings.onChoose(hours, toggleChecked()) : hours);
}
});
}
@@ -1341,7 +1394,7 @@
button.addEventListener('click', function () {
modal.close();
if (settings.onChoose) {
resolve(settings.onChoose(option.hours));
resolve(settings.onChoose(option.hours, toggleChecked()));
} else {
resolve(option.hours);
}
@@ -1459,6 +1512,11 @@
'.sharelinks-date-row{margin-top:18px;display:flex;flex-direction:column;gap:8px;}',
'.sharelinks-date-label{color:var(--text-secondary-color,#cfcfcf);font-size:.92rem;line-height:1.4;}',
'.sharelinks-date-input{width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.2);border-radius:6px;background:rgba(0,0,0,.18);color:inherit;padding:10px 12px;min-height:42px;font:inherit;color-scheme:dark;}',
'.sharelinks-toggle-row{display:flex;align-items:flex-start;gap:10px;margin-top:18px;padding-top:16px;border-top:1px solid rgba(255,255,255,.12);cursor:pointer;}',
'.sharelinks-toggle-input{margin:2px 0 0;width:18px;height:18px;flex:0 0 auto;accent-color:var(--theme-primary-color,#00a4dc);cursor:pointer;}',
'.sharelinks-toggle-text{display:flex;flex-direction:column;gap:4px;}',
'.sharelinks-toggle-label{line-height:1.35;}',
'.sharelinks-toggle-hint{color:var(--text-secondary-color,#cfcfcf);font-size:.88rem;line-height:1.4;}',
'.sharelinks-toast{position:fixed;left:24px;bottom:24px;z-index:1000000;max-width:min(460px,calc(100vw - 48px));background:rgba(24,24,24,.96);color:#fff;border:1px solid rgba(255,255,255,.14);border-radius:6px;padding:11px 14px;box-shadow:0 10px 30px rgba(0,0,0,.35);opacity:0;transform:translateY(8px);transition:opacity .18s ease,transform .18s ease;}',
'.sharelinks-toast.is-visible{opacity:1;transform:translateY(0);}',
'@media (max-width:520px){.sharelinks-duration-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.sharelinks-dialog{padding:18px;}.sharelinks-actions{justify-content:stretch;}.sharelinks-action{flex:1;}}'