diff --git a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js index 7dcc33f..07b0746 100644 --- a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js +++ b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js @@ -1,8 +1,8 @@ (function () { var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6'; var copyLabel = 'Copy Stream URL'; - var actionLabel = 'Create guest link'; - var clientVersion = '1.0.0-ui-modal-8'; + var actionLabel = 'ShareLink'; + var clientVersion = '1.0.0-ui-modal-9'; var allowedItemStorageKey = 'sharelinks.allowedItemId'; var guestClassName = 'sharelinks-guest'; var hiddenAttr = 'data-sharelinks-hidden'; @@ -435,6 +435,13 @@ injected.setAttribute('aria-label', actionLabel); injected.setAttribute('title', actionLabel); 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) { event.preventDefault(); event.stopPropagation(); diff --git a/README.md b/README.md index 2e39534..2932820 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,121 @@ -# ShareLinks +# ShareLinks for Jellyfin -ShareLinks is a Jellyfin 10.11 plugin scaffold for issuing expiring guest-share -links to individual items. This staging tree now includes the core dashboard -and web-client wiring that later workers will build on: +Send someone a single movie or episode, without giving them an account, without +them seeing the rest of your library. -- plugin metadata and DI registration -- 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 +> *"here, watch this one film, the link dies tomorrow"* -## 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 -is issued, returned to the caller once, and then forgotten. Persistent storage -keeps only a keyed HMAC hash of the token plus the metadata needed to audit or -clean up the link. +No account for them to create, no password for you to hand out, no permanent +guest user piling up. The link is temporary, the guest is temporary, and when it +expires everything is cleaned up on its own. -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 -- 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 +## What the guest sees -Later workers should wire those settings into the issue / redeem / revoke -pipeline and into the guest-user creation logic. +Just the one title, and the ability to play it. The confinement is real and it +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 -path, in a dedicated `sharelinks` directory. The persistent JSON store keeps -`ShareLinkRecord` entries keyed by id, while the token service stores its secret -key separately in the same directory. +Playback works normally, including transcoding and remuxing if you allow it, and +the player's back button still returns them to the title's page. -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` -- `Lifecycle/StartupCleanupHostedService.cs` +If you run other plugins that inject their own UI into the web client (a search +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 -no-op. That gives later workers a stable seam for: +## Security stance -- expiring old links -- removing one-use links after redemption -- tearing down guest accounts and tokens -- recording cleanup attempts and failures +The design goal is simple: a raw share token exists only at the moment it is +issued, is returned to you once, and is then forgotten. Persistent storage keeps +only a keyed HMAC hash of the token plus the metadata needed to audit and clean +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 -- 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 +All of these live on the plugin's dashboard page: -## 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 -already has its controller, web injection hook, and dashboard page entry point -in place. +## Known limitation: cast and crew + +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.