ShareLinks: rename menu action to ShareLink with a share icon; rewrite README
Rename the injected context-menu action from 'Create guest link' to 'ShareLink' and swap its cloned copy icon (content_copy) for the share icon, matching the other menu items' style. Replace the placeholder scaffold README with a user-facing one describing what the plugin does, the server-side guest confinement, configuration, and the known Jellyfin cast/crew tag bug (jellyfin/jellyfin#14926).
Cette révision appartient à :
@@ -1,8 +1,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6';
|
var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6';
|
||||||
var copyLabel = 'Copy Stream URL';
|
var copyLabel = 'Copy Stream URL';
|
||||||
var actionLabel = 'Create guest link';
|
var actionLabel = 'ShareLink';
|
||||||
var clientVersion = '1.0.0-ui-modal-8';
|
var clientVersion = '1.0.0-ui-modal-9';
|
||||||
var allowedItemStorageKey = 'sharelinks.allowedItemId';
|
var allowedItemStorageKey = 'sharelinks.allowedItemId';
|
||||||
var guestClassName = 'sharelinks-guest';
|
var guestClassName = 'sharelinks-guest';
|
||||||
var hiddenAttr = 'data-sharelinks-hidden';
|
var hiddenAttr = 'data-sharelinks-hidden';
|
||||||
@@ -435,6 +435,13 @@
|
|||||||
injected.setAttribute('aria-label', actionLabel);
|
injected.setAttribute('aria-label', actionLabel);
|
||||||
injected.setAttribute('title', actionLabel);
|
injected.setAttribute('title', actionLabel);
|
||||||
injected.dataset.sharelinksItemId = itemId;
|
injected.dataset.sharelinksItemId = itemId;
|
||||||
|
|
||||||
|
var injectedIcon = injected.querySelector('.material-icons');
|
||||||
|
if (injectedIcon) {
|
||||||
|
injectedIcon.classList.remove('content_copy');
|
||||||
|
injectedIcon.classList.add('share');
|
||||||
|
}
|
||||||
|
|
||||||
injected.addEventListener('click', function (event) {
|
injected.addEventListener('click', function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|||||||
+98
-62
@@ -1,85 +1,121 @@
|
|||||||
# ShareLinks
|
# ShareLinks for Jellyfin
|
||||||
|
|
||||||
ShareLinks is a Jellyfin 10.11 plugin scaffold for issuing expiring guest-share
|
Send someone a single movie or episode, without giving them an account, without
|
||||||
links to individual items. This staging tree now includes the core dashboard
|
them seeing the rest of your library.
|
||||||
and web-client wiring that later workers will build on:
|
|
||||||
|
|
||||||
- plugin metadata and DI registration
|
> *"here, watch this one film, the link dies tomorrow"*
|
||||||
- JSON-backed record storage
|
|
||||||
- token generation and hashing
|
|
||||||
- startup and scheduled cleanup shells
|
|
||||||
- configuration defaults
|
|
||||||
- Jellyfin Web script injection and a dashboard config page shell
|
|
||||||
|
|
||||||
## Current security stance
|
ShareLinks adds a **ShareLink** button (with a little share icon) to the context
|
||||||
|
menu of any movie or episode in the Jellyfin web client. Click it, pick how long
|
||||||
|
the link should live, and you get a URL you can send to anyone. When they open
|
||||||
|
it, they land straight on that one title, already signed in, and they cannot
|
||||||
|
wander off into the rest of your server.
|
||||||
|
|
||||||
The design goal is simple: a raw share token should exist only at the moment it
|
No account for them to create, no password for you to hand out, no permanent
|
||||||
is issued, returned to the caller once, and then forgotten. Persistent storage
|
guest user piling up. The link is temporary, the guest is temporary, and when it
|
||||||
keeps only a keyed HMAC hash of the token plus the metadata needed to audit or
|
expires everything is cleaned up on its own.
|
||||||
clean up the link.
|
|
||||||
|
|
||||||
That means later API work must keep a few rules:
|
I built this for my own server (shared with family and a few friends), because I
|
||||||
|
kept wanting to show someone *one specific film* without either adding them as a
|
||||||
|
real user or handing over a login that sees everything.
|
||||||
|
|
||||||
1. never log raw tokens
|
---
|
||||||
2. never write raw tokens to disk
|
|
||||||
3. only return the token in the initial creation response
|
|
||||||
4. treat token validation as hash comparison only
|
|
||||||
5. keep guest-user creation and teardown behind explicit service calls
|
|
||||||
|
|
||||||
## Configuration plan
|
## How it works
|
||||||
|
|
||||||
The `PluginConfiguration` defaults are deliberately opinionated:
|
1. As an admin you open the context menu on a movie or episode and hit
|
||||||
|
**ShareLink**. You choose an expiry (1 hour up to 30 days) and the plugin
|
||||||
|
hands you a link, copied to your clipboard.
|
||||||
|
2. Behind the scenes the plugin tags that one item with a unique, random tag and
|
||||||
|
records the share. The raw link token is shown to you once and never stored,
|
||||||
|
only a keyed HMAC hash of it is kept.
|
||||||
|
3. Whoever opens the link gets a throwaway guest user created on the spot,
|
||||||
|
restricted by that tag to the single shared item, and is signed in
|
||||||
|
automatically. They land on the title's page.
|
||||||
|
4. When the link expires (or you revoke it), a cleanup pass disables and deletes
|
||||||
|
the guest user and strips the temporary tag. A scheduled task and a startup
|
||||||
|
pass make sure nothing lingers if the server was off at expiry time.
|
||||||
|
|
||||||
- default expiry hours
|
## What the guest sees
|
||||||
- maximum allowed expiry hours
|
|
||||||
- optional public base URL override
|
|
||||||
- guest username prefix
|
|
||||||
- transcoding and remuxing toggles
|
|
||||||
- cleanup interval in minutes
|
|
||||||
- one-use default
|
|
||||||
- guest-mode lockdown enabled by default
|
|
||||||
|
|
||||||
Later workers should wire those settings into the issue / redeem / revoke
|
Just the one title, and the ability to play it. The confinement is real and it
|
||||||
pipeline and into the guest-user creation logic.
|
is enforced on the server, not only in the browser:
|
||||||
|
|
||||||
## Storage layout
|
- The guest's Jellyfin policy only permits the single shared item, so every
|
||||||
|
other movie, show, library and search comes back empty from the API. Even
|
||||||
|
someone poking at the raw API cannot list your other content.
|
||||||
|
- On top of that, the web client is locked down for the guest: the home,
|
||||||
|
menu and search buttons are hidden, in-page links (cast, studio, genres) are
|
||||||
|
made inert, "add to playlist" is removed, and any attempt to navigate away
|
||||||
|
snaps back to the shared title.
|
||||||
|
|
||||||
The staging implementation stores plugin data under Jellyfin's application data
|
Playback works normally, including transcoding and remuxing if you allow it, and
|
||||||
path, in a dedicated `sharelinks` directory. The persistent JSON store keeps
|
the player's back button still returns them to the title's page.
|
||||||
`ShareLinkRecord` entries keyed by id, while the token service stores its secret
|
|
||||||
key separately in the same directory.
|
|
||||||
|
|
||||||
This keeps the plugin portable and avoids any hardcoded filesystem locations.
|
## Managing links
|
||||||
|
|
||||||
## Cleanup architecture
|
The plugin's dashboard page lists every share with its status, the title, a
|
||||||
|
copyable link, the temporary guest name, and an expiry, and lets you revoke any
|
||||||
|
of them on the spot. Revoking runs the same teardown as expiry: guest gone, tag
|
||||||
|
gone.
|
||||||
|
|
||||||
There are two cleanup entry points already wired:
|
## Hiding other plugins from guests
|
||||||
|
|
||||||
- `Tasks/CleanupShareLinksScheduledTask.cs`
|
If you run other plugins that inject their own UI into the web client (a search
|
||||||
- `Lifecycle/StartupCleanupHostedService.cs`
|
bar, a floating button), you probably do not want a guest to see them. The
|
||||||
|
**Guest hidden selectors** setting is a comma-separated list of CSS selectors
|
||||||
|
that get hidden in guest sessions. It ships with a default that hides the
|
||||||
|
[AI Search](https://github.com/Franciskid) button; add any other plugin's
|
||||||
|
selector and it disappears for guests too, no code change needed.
|
||||||
|
|
||||||
Both currently call an `IShareLinkCleanupService` implementation that is a
|
## Security stance
|
||||||
no-op. That gives later workers a stable seam for:
|
|
||||||
|
|
||||||
- expiring old links
|
The design goal is simple: a raw share token exists only at the moment it is
|
||||||
- removing one-use links after redemption
|
issued, is returned to you once, and is then forgotten. Persistent storage keeps
|
||||||
- tearing down guest accounts and tokens
|
only a keyed HMAC hash of the token plus the metadata needed to audit and clean
|
||||||
- recording cleanup attempts and failures
|
up the link. So:
|
||||||
|
|
||||||
## Endpoint audit requirements
|
1. raw tokens are never logged
|
||||||
|
2. raw tokens are never written to disk
|
||||||
|
3. the token is only returned in the creation response
|
||||||
|
4. token validation is a hash comparison
|
||||||
|
5. guest-user creation and teardown live behind explicit service calls
|
||||||
|
6. the real access boundary is the server-side tag policy; the web-client
|
||||||
|
lockdown is convenience on top of it
|
||||||
|
|
||||||
When the API surface is added, it should be audited for:
|
## Configuration
|
||||||
|
|
||||||
- authz on every create/list/redeem/revoke endpoint
|
All of these live on the plugin's dashboard page:
|
||||||
- exact token handling on create and redeem flows
|
|
||||||
- rate limiting for token guesses and redemption retries
|
|
||||||
- whether any response leaks the token hash, raw token, or guest credentials
|
|
||||||
- whether guest-mode lockdown is enforced consistently
|
|
||||||
- whether cleanup can safely run while links are being created or redeemed
|
|
||||||
- whether error messages reveal link existence or status
|
|
||||||
|
|
||||||
## What is intentionally missing
|
| Setting | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Default / maximum expiry | The default the menu offers, and the ceiling a link may be set to |
|
||||||
|
| Public base URL override | Force the host used when building links (otherwise derived from the request) |
|
||||||
|
| Guest username prefix | Prefix for the throwaway guest accounts (default `share-`) |
|
||||||
|
| Allow transcoding / remuxing | Whether guest playback may transcode or remux |
|
||||||
|
| Cleanup interval | How often the background cleanup runs |
|
||||||
|
| One-use default | Whether new links default to single redemption |
|
||||||
|
| Guest lockdown | The web-client confinement described above (on by default) |
|
||||||
|
| Guest hidden selectors | CSS selectors hidden from guests, to suppress other plugins' UI |
|
||||||
|
|
||||||
The remaining work is mostly policy polish and cleanup edge cases. The plugin
|
## Known limitation: cast and crew
|
||||||
already has its controller, web injection hook, and dashboard page entry point
|
|
||||||
in place.
|
Jellyfin has a core bug ([jellyfin/jellyfin#14926](https://github.com/jellyfin/jellyfin/issues/14926))
|
||||||
|
where a user restricted by tags loses the Cast & Crew section entirely, because
|
||||||
|
the tag filter is applied to people as well as to media. Since a ShareLinks
|
||||||
|
guest is tag-restricted, they hit this: the shared title's page shows no
|
||||||
|
actors, director or writer. This is a server-side Jellyfin issue, not something
|
||||||
|
the plugin can style around. A workaround inside the plugin is possible and on
|
||||||
|
the list.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- Jellyfin **10.11** (targetAbi `10.11.0.0`), .NET 9. Tested on 10.11.8.
|
||||||
|
- The UI injection targets the standard Jellyfin web client, and works with both
|
||||||
|
the English and French interface.
|
||||||
|
|
||||||
|
## Credits and license
|
||||||
|
|
||||||
|
Developed by [Franciskid](https://github.com/Franciskid).
|
||||||
|
|
||||||
|
Licensed under the [GPL-3.0](LICENSE), like most Jellyfin plugins.
|
||||||
|
|||||||
Référencer dans un nouveau ticket
Bloquer un utilisateur