v0.2: All videos folder, shared watched state, release-order sorting, official icon, OAuth plumbing, docs
Some checks failed
release / build-release (push) Has been cancelled

- per-creator "All videos" folder (release order across sub-channels); sub-channel copies use
  cpost: ids and UserDataMirror keeps played/resume state in sync
- SortNameUpdater forces release-order sort names so every client lists newest first by default
- official Floatplane icon as channel image (Primary only — a channel Thumb hid video thumbnails
  in Continue Watching) and as 16:9 plugin thumbnail; one-time image replacement on upgrade
- OAuth device-flow login (FloatplaneAuthService + settings UI); needs a Floatplane client id;
  Turnstile verified domain-bound so password login inside Jellyfin is impossible
- generic install.sh (linuxserver / official image / native), thumb.png in package + manifest
- README rewritten for a general audience, doc/usage.md with screenshots, 60 tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:36:32 +01:00
parent 2a883c02de
commit bf476134c2
33 changed files with 1478 additions and 155 deletions

BIN
doc/img/all-videos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
doc/img/channel-root.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
doc/img/creator-folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
doc/img/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
doc/img/plugin-catalog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/img/scheduled-tasks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
doc/img/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
doc/img/video-details.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -103,11 +103,13 @@ Jellyfin clients ──► Jellyfin server (Pi5)
| # | Increment | Done when |
|---|-----------|-----------|
| M0 | Scaffold: csproj (net8/10.10.7), Plugin.cs, config page, `meta.json`, build task, unit-test project, local docker Jellyfin 10.10.7 | **done 2026-10-25** — Active in a local 10.10.7 (LSIO image) |
| M1 | `FloatplaneApiClient`: cookie session, `user/self`, subscriptions, creator/info, content listing with pagination + search/filters, post details; fixtures from the HAR | **code done** — config page validates the cookie; unit tests in `Jellyfin.Plugin.Floatplane.Tests` |
| M2 | Channel browse: root → creators → sub-channels → videos with thumbnails, overview, duration, release date; sync task + cache key bump | **done locally** (web API verified: folders, paging, sorting, latest, images); Android app not yet checked |
| M3 | Playback via proxy controller: delivery/info cache + throttle, HLS playlist rewrite with key proxy (+ optional MP4 relay), `MediaStreams` filled so Jellyfin remuxes instead of transcoding | **code done, untested** — needs a real session cookie (Bjorn, on the Pi5 or locally) |
| M4 | Packaging + CI: Gitea Actions build → zip + md5 + `manifest.json` → releases; install on Pi5 from the repository URL or `install-pi5.sh` | **code done** — first CI run pending the initial push |
| M5 | Polish: watched/resume sync to Floatplane progress API, subtitles (VTT text tracks), locked-tier handling, cookie-expiry warning, Android/TV app check | next |
| M1 | `FloatplaneApiClient`: cookie session, `user/self`, subscriptions, creator/info, content listing with pagination + search/filters, post details; fixtures from the HAR | **done** — config page validates the cookie; 59 unit tests in `Jellyfin.Plugin.Floatplane.Tests` |
| M2 | Channel browse: root → creators → *All videos* + sub-channels → videos with thumbnails, overview, duration, release date; sync task + cache key bump | **done 2026-10-25** — verified in the web client (folders, paging, newest-first default sort, latest, images); *All videos* per creator added, watched state mirrored between the two copies of a video; official Floatplane logo as channel image and plugin thumbnail; Android app not yet checked |
| M3 | Playback via proxy controller: delivery/info cache + throttle, HLS playlist rewrite with key proxy (+ optional MP4 relay), `MediaStreams` filled so Jellyfin remuxes instead of transcoding | **code done, still unverified** — needs a real session cookie on a running server |
| M4 | Packaging + CI: Gitea Actions build → zip + md5 + `manifest.json` + `thumb.png` → releases; install from the repository URL or `install.sh` (generic: LSIO / official image / native) | **done** — CI green, install from the manifest verified locally |
| M5 | Login without cookie copying: OAuth device flow (`Login with Floatplane`) | **plumbing done 2026-10-25** — works up to `invalid_client`; waits for a client id from Floatplane. Password login inside Jellyfin is impossible (Turnstile is domain-bound, error 110200 verified) |
| M6 | Documentation for a general audience: README, `doc/usage.md` with screenshots, settings reference, troubleshooting | **done 2026-10-25** |
| M7 | Polish: watched/resume sync to Floatplane's progress API, subtitles (VTT text tracks), Android/TV app check, Jellyfin 12.x build | next |
## Roadmap / expansions (to agree)

281
doc/usage.md Normal file
View File

@@ -0,0 +1,281 @@
# Floatplane for Jellyfin — user guide
Everything below refers to the Jellyfin web client; the Android, Android
TV, iOS and desktop apps show the same channel and folders.
Contents: [Install](#install) · [Log in](#log-in) · [Browse and play](#browse-and-play) ·
[Settings reference](#settings-reference) · [Scheduled task](#scheduled-task) ·
[Troubleshooting](#troubleshooting) · [Privacy and security](#privacy-and-security)
## Install
### From the plugin repository
1. Open **Dashboard → Plugins → Repositories** and press **+**.
Give it any name and enter the URL
```
https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/manifest.json
```
![Repositories page with the Floatplane manifest added](img/plugin-repositories.png)
2. Go to **Dashboard → Plugins → Catalog**, scroll to the *Channels* section
and open **Floatplane**, then press **Install**.
![Catalog, Channels section, Floatplane card](img/plugin-catalog-floatplane.png)
3. Restart Jellyfin. **Dashboard → Plugins → My Plugins** now lists Floatplane
as *Active*. Click the card (or the ⋮ menu → Settings) to open the
settings page.
![My Plugins with the Floatplane card](img/dashboard-plugins.png)
New versions show up in the catalog like for any other plugin.
### With the install script
The script downloads the release, verifies the MD5 checksum, puts the plugin
in the right folder and restarts Jellyfin. It understands the
linuxserver.io image (`<config>/data/plugins`), the official image
(`<config>/plugins`) and native installs (`/var/lib/jellyfin/plugins`).
Run it as a user that may write there and restart Jellyfin (usually with
`sudo`). `--dry-run` only prints what would happen.
```bash
curl -fsSL https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/install.sh | sh -s -- --dry-run
curl -fsSL https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/install.sh | sh
```
| Variable | Meaning | Default |
|----------|---------|---------|
| `TAG` | release to install | `latest` (or `vX.Y.Z`) |
| `JELLYFIN_CONTAINER` | Docker container to inspect and restart; empty = native install | `jellyfin` |
| `JELLYFIN_CONFIG_DIR` | host path of the container's `/config` (or the data dir of a native install) | read from `docker inspect` |
| `PLUGINS_DIR` | plugin directory, overrides the detection | detected |
| `PUID` / `PGID` | owner of the plugin files | from the container, else the directory owner |
### Manually
Download `floatplane_<version>.zip` from the
[releases page](https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases),
unzip it into `<jellyfin data>/plugins/Floatplane_<version>/` (the folder
must contain `Jellyfin.Plugin.Floatplane.dll` and `meta.json`) and restart
Jellyfin.
### Requirements
- Jellyfin **10.10.x**. The plugin targets ABI `10.10.0.0` and .NET 8;
Jellyfin 12.x uses a different ABI and needs a separate build (not yet
available).
- Any CPU architecture Jellyfin runs on: the plugin is one managed DLL
(x64 and arm64 verified).
- A Floatplane account with an active subscription for playback. Browsing
public creators works without logging in.
## Log in
Floatplane's username/password login is protected by a Cloudflare Turnstile
captcha that is bound to floatplane.com (any other site gets error 110200),
so the plugin cannot log you in with your password. Instead, open
**Dashboard → Plugins → Floatplane** and use one of the two methods below.
The status block at the top of the page always tells you which session is
in use and whether Floatplane still accepts it.
![The settings page](img/settings.png)
### Option 1 — paste the session cookie (works today)
1. Log in at [floatplane.com](https://www.floatplane.com) in a browser.
2. Open the browser's developer tools and find the cookie `sails.sid` for
`https://www.floatplane.com`:
- **Chrome / Edge**: F12 → *Application* → *Storage* → *Cookies* →
`https://www.floatplane.com`.
- **Firefox**: F12 → *Storage* → *Cookies* → `https://www.floatplane.com`.
- **Safari**: enable the Develop menu, then *Develop → Show Web Inspector*
→ *Storage* → *Cookies*.
3. Copy the **Value** of `sails.sid` (a long string; `sails.sid=` in front
is fine, the plugin strips it).
4. Paste it into **sails.sid cookie** and press **Validate cookie**. The page
shows your username and active subscriptions.
5. Press **Save**. The creators you subscribe to appear in the channel after
the automatic refresh that follows.
When the cookie expires, the status line changes to *rejected*. Log in at
floatplane.com again, copy the new value and repeat.
### Option 2 — Login with Floatplane (OAuth device flow)
Floatplane's own apps and third-party clients (Hydravion, Wasserflug,
floatplane-downloader) log in through an OAuth 2.0 device flow: you open a
link, enter a short code, and the plugin receives tokens that renew
themselves for about 90 days.
This requires a **client id issued by Floatplane** for this plugin. There is
no self-service registration; ask Floatplane (for example through their
community Discord). Without a registered id the button reports
`invalid_client`.
If you have one: enter it under **OAuth client id**, press **Login with
Floatplane**, open the link shown (or go to `floatplane.com/link`) and enter
the code. The page polls until Floatplane confirms and then shows *Logged in
as …*. The OAuth session replaces the cookie. **Forget OAuth session** drops
the tokens again. Client ids that require DPoP-bound tokens are not
supported.
## Browse and play
The **Floatplane** tile appears under *My Media*. It opens a folder per
creator — your subscriptions plus anything listed under *Extra creators*.
![The Floatplane channel: one tile per creator](img/channel-root.png)
Each creator contains:
- **All videos** — every video from all of the creator's sub-channels in
release order, newest first (up to the number set in the settings).
- **One folder per sub-channel** (for example Linus Tech Tips, TechLinked,
ShortCircuit) with that channel's newest videos.
![Creator folder: All videos first, then the sub-channels](img/creator-folder.png)
![All videos, newest first](img/all-videos.png)
Videos carry the Floatplane thumbnail, the post text as description, the
duration and release date, the sub-channel as *Genre* and the creator as
*Studio*. Press **Play** to watch, or use the tick to mark a video as
played.
![Video page](img/video-details.png)
Good to know:
- **Order.** The plugin gives every video a sort name derived from its
release time, so the default *Name* sort of all Jellyfin clients lists
newest first — no need to change the sort in every folder. Switching the
sort direction flips it to oldest first. (The A–Z jump bar on the right of
the web client therefore does nothing useful in these folders.)
- **Watched state.** A video exists twice — in *All videos* and in its
sub-channel folder. The plugin mirrors played/resume state between the two
copies automatically.
- **Home screen.** *Recently Added in Floatplane* shows the newest videos
across creators; *Continue Watching* shows partially watched videos with
their thumbnails.
- **Search.** Jellyfin's search finds every video the plugin has listed at
least once. Floatplane's own search can be tried on the settings page
(*Search test*).
- **Locked posts.** Posts above your subscription tier are hidden by
default; with *Show posts above my tier* they appear with a 🔒 in the
name and will not play.
- **Playback and quality.** On play, the plugin asks Floatplane for the
stream (this endpoint is rate-limited to about two requests per minute;
the answer is cached for six hours) and serves a rewritten HLS playlist
from your Jellyfin server: the encryption key is fetched with your session,
the video segments come straight from Floatplane's CDN, and Jellyfin's
ffmpeg copies the H.264/AAC stream to the client without re-encoding.
The available quality levels appear as *Versions* in the player, preferred
quality first, up to *Highest quality offered*. 4K needs the matching
Floatplane tier. *Progressive MP4 versions* is an experimental alternative
that has not been verified yet.
## Settings reference
Dashboard → Plugins → Floatplane, in page order.
**Status block** — plugin version, which session is configured (cookie or
OAuth) and whether Floatplane accepted it, the creators and sub-channels
currently loaded, cache counters and the last refresh time.
### Session
| Setting | What it does |
|---------|--------------|
| **sails.sid cookie** | The Floatplane session cookie (see [Option 1](#option-1--paste-the-session-cookie-works-today)). Needed for subscriptions and playback. |
| **Validate cookie** | Checks the pasted value against Floatplane without saving it and shows username and subscriptions. |
| **OAuth client id** | Client id issued by Floatplane for this plugin (see [Option 2](#option-2--login-with-floatplane-oauth-device-flow)). |
| **Login with Floatplane** | Starts the device flow with that client id and shows the link and code to enter. |
| **Forget OAuth session** | Removes the stored OAuth tokens (shown only while an OAuth session exists). |
### Catalogue
| Setting | Default | What it does |
|---------|---------|--------------|
| **Extra creators** | empty | Comma-separated Floatplane url-names (the part after `floatplane.com/channel/`, e.g. `linustechtips`) to list in addition to your subscriptions — or instead of them when you are not logged in. Listings are public; playback still needs a session. |
| **Videos listed in "All videos"** | 200 | How many of the newest videos the *All videos* folder contains. Floatplane returns 20 per request, so 200 = 10 requests per creator at refresh. |
| **Videos listed per sub-channel** | 100 | Same, per sub-channel folder. |
| **Show posts above my tier (locked)** | off | Include posts your subscription cannot access, marked with 🔒. |
| **Listing cache (minutes)** | 30 | How long the plugin keeps a folder listing before asking Floatplane again. |
| **Automatic refresh (hours)** | 6 | Interval of the *Refresh Floatplane catalogue* scheduled task. |
| **Refresh now** | — | Drops the caches, reloads creators and sub-channels and makes Jellyfin re-list the folders. Also happens after every **Save**. |
### Playback
| Setting | Default | What it does |
|---------|---------|--------------|
| **Preferred quality** | 1080p | The version listed first and played by default. |
| **Highest quality offered (pixels)** | 1080 | Versions taller than this are not offered at all (keeps 4K out of the list unless you have that tier). |
| **Delivery lookups per minute** | 2 | Budget for Floatplane's stream-info endpoint. Raise it only if Floatplane changes its rate limit. |
| **Proxy base URL** | empty | How Jellyfin's own ffmpeg reaches the plugin's stream endpoints, e.g. `http://127.0.0.1:8096`. Empty uses Jellyfin's local address, which is right for almost every setup. |
| **Also offer progressive MP4 versions (experimental)** | off | Adds an MP4 version per video using Floatplane's download delivery. Untested; may not play. |
**Save** writes everything and triggers a refresh.
### Search test
Pick a creator, type a query and press **Search** to run the query through
Floatplane's search API and see the matching videos (date, sub-channel,
title, length). This is a diagnostic tool; it does not change anything.
## Scheduled task
The plugin registers **Refresh Floatplane catalogue** under **Dashboard →
Scheduled Tasks**. It runs at server start and every *Automatic refresh*
hours, reloads creators and sub-channels, pre-fetches the newest videos and
tells Jellyfin to re-list the channel. You can run it by hand from that page,
or press *Refresh now* on the settings page.
![Scheduled Tasks with Refresh Floatplane catalogue](img/scheduled-tasks.png)
## Troubleshooting
| Symptom | Cause / fix |
|---------|-------------|
| Playing a video fails with *NoCompatibleStream* or the player never starts; the log says *Cannot play Floatplane post … You must be logged-in* | No session or an expired one. Paste a fresh cookie (or log in with Floatplane) and Save. |
| Playback fails right after several quick starts; log mentions 429 or *rate-limited* | Floatplane's stream-info endpoint allows about two lookups per minute. Wait a minute and try again; results are cached afterwards. |
| New videos do not show up | Press *Refresh now* (or run the scheduled task). The plugin caches listings for *Listing cache* minutes and Jellyfin caches channel listings for three hours; a refresh invalidates both. |
| Creators missing although the cookie is valid | Press *Refresh now*; check the status block for an error message. Creators without a video post yet are skipped. |
| Everything is marked 🔒 | You are browsing without a session, or the session is rejected: without login Floatplane reports every post as locked. |
| *Login with Floatplane* says `invalid_client` | The client id is not registered with Floatplane. Use the cookie method until you have one. |
| Plugin shows *NotSupported* or *Malfunctioned* | Jellyfin version mismatch (this build is for 10.10.x) or a broken plugin folder. Check the server log. |
**Logs.** Everything the plugin writes is tagged `Jellyfin.Plugin.Floatplane`
in Jellyfin's server log (Dashboard → Logs, or `log/log_*.log` in the data
directory — `/config/log/` in the linuxserver.io image). For more detail,
copy `logging.default.json` to `logging.json` in the configuration
directory and add
```json
"Jellyfin.Plugin.Floatplane": "Debug"
```
under `Serilog → MinimumLevel → Override`, then restart.
**Paths (linuxserver.io image).** Plugins live in `/config/data/plugins/`,
the plugin configuration in `/config/data/plugins/configurations/Floatplane.xml`,
logs in `/config/log/`. The official image uses `/config/plugins/` and
`/config/log/`.
## Privacy and security
- The session cookie or OAuth tokens are stored in plain XML in Jellyfin's
plugin configuration folder, readable by Jellyfin administrators and
anyone with access to that directory. Treat it like a password.
- Stream URLs handed to the player are signed with a per-process HMAC
secret, so the plugin's stream endpoints cannot be used as a general
relay; they only serve what the plugin itself selected.
- Requests to Floatplane carry a `Jellyfin-Floatplane/<version>` User-Agent
so Floatplane can identify the client. The plugin keeps to the
documented rate limits.
- This is an unofficial client built on Floatplane's community-documented
API and is not affiliated with or endorsed by Floatplane Media Inc. Use it
for personal viewing with your own subscription.