Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled

- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped)
- IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the
  AES-128 key is fetched server-side with the session cookie, segments stay on the CDN
- delivery/info cached per token lifetime and throttled (~2/min)
- config page: paste sails.sid + validate, extra creators, quality, search test
- scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow
- research docs, HAR analysis (HAR itself gitignored), install-pi5.sh

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 09:42:27 +01:00
commit 9e33a343ab
162 changed files with 52004 additions and 0 deletions

152
doc/plan.md Normal file
View File

@@ -0,0 +1,152 @@
# jellyfin-plugin-floatplane — plan (2026-10-25)
> Decisions taken 2026-10-25: Gitea repo `brasse/jellyfin-plugin-floatplane`
> (private), **build for Jellyfin 10.10.x** (Pi5 stays on 10.10.7 for now),
> **cookie session first** (OAuth client id to be requested from Floatplane
> later), **VOD first**, and map/test the search API. Remaining open points
> are listed at the bottom.
## Goal
Make Floatplane subscriptions browsable and playable inside Jellyfin on the
Pi5 (`film.brasse-pc.eu`) with **no changes to any client**: creators and
their sub-channels appear as a "Floatplane" channel under My Media in the
web client, Android/TV apps and Jellyfin Media Player; videos play through
the normal Jellyfin player with watched-state, resume and SyncPlay.
## What the research established
Full reports: [`research/floatplane-api-research.md`](research/floatplane-api-research.md),
[`research/jellyfin-plugin-research.md`](research/jellyfin-plugin-research.md),
HAR findings in [`floatplane-api-notes.md`](floatplane-api-notes.md).
**Jellyfin side**
- Pi5 runs **10.10.7** (image pulled 2025-08-12). `linuxserver/jellyfin:latest`
is now **12.3** (.NET 10). A 10.10 plugin is `NotSupported` on 12.x and
vice versa. Decision needed (Q2).
- 10.10.7 = .NET 8, NuGet `Jellyfin.Controller 10.10.7`, `targetAbi 10.10.0.0`.
Verified locally: the template builds against 10.10.7 with the installed
.NET 8 SDK in 8 s.
- Plugins are IL/AnyCPU → one build runs on x64 and the Pi5 (arm64). No
native deps allowed in the plugin.
- The **channel API (`IChannel` + `IRequiresMediaInfoCallback`) exists and
works** in 10.10.7 *and* 12.3 (checked both NuGet packages). It is the
only way to get a virtual, playable catalogue into every official client.
Reference implementation: chaosflix-jellyfin (copy in `research/reference-sources/chaosflix/`).
- Jellyfin forwards **only User-Agent/Referer** to ffmpeg and its relay —
never cookies or `Authorization`. Therefore the plugin must **proxy**
playback through its own ASP.NET controller with HMAC-signed anonymous
URLs (chaosflix pattern).
- linuxserver image paths: plugins in `/config/data/plugins/<Name>_<ver>/`,
config XML in `/config/data/plugins/configurations/`, logs `/config/log/`.
- Listing cache 3 h (bump `DataVersion`/cache key to refresh), media-info
cache 5 min, one parent per item id, channel `Name` must never change.
**Floatplane side**
- Public, no login: creator info, channel lists, post listings, thumbnails.
- Login needed: `delivery/info` (stream URLs) and the HLS **AES-128 key**
(`/api/video/watchKey`). Segments and playlists only need their 6-hour
signed token.
- Username/password login requires a Cloudflare Turnstile token — verified
today that no User-Agent bypasses it. Two real options:
1. **Pasted `sails.sid` cookie** (what the web client used in the HAR;
works today, zero dependencies, lifetime unknown — must handle expiry).
2. **OAuth2 device-code flow** against Keycloak `auth.floatplane.com`
(what Hydravion/Wasserflug/floatplane-downloader use; access token
30 min, refresh token 90 days sliding). Needs a **client_id registered
by Floatplane** — verified `invalid_client` for an unregistered id.
Obtained by asking Floatplane staff (community Discord).
- `delivery/info` is rate-limited (~2/min) → cache per video until token
expiry, serialise calls.
- `scenario=download` gives plain MP4 URLs (seekable, no key dance) — not
in the capture; test once a session exists. If the token alone is enough
this is the preferred playback path.
## Stack
- C# / .NET 8 class library `Jellyfin.Plugin.Floatplane` (switch to .NET 10
+ Jellyfin 12.x packages if Q2 says upgrade). `System.Text.Json`,
`IHttpClientFactory`, no third-party NuGet packages that ship natives.
- Target OS/arch: linux-x64 (dev/test) + linux-arm64 (Pi5), single AnyCPU build.
- Tests: xUnit against recorded JSON fixtures (shapes from the HAR + spec).
- Packaging: `meta.json` + zip (+ md5) per release; `manifest.json` plugin
repository served from Gitea so Jellyfin's dashboard can install/update.
## Deploy target
Pattern A (playbook): Gitea Actions builds the zip on push to `master`,
publishes rolling `latest` + `vX.Y.Z` releases, and updates the repository
`manifest.json`. Install on the Pi5 by adding the manifest URL under
Dashboard → Plugins → Repositories (or scp into `/config/data/plugins/`).
## Architecture
```
Jellyfin clients ──► Jellyfin server (Pi5)
│ IChannel "Floatplane"
│ root → creators → sub-channels → videos (Episode/Clip)
│ IRequiresMediaInfoCallback
│ → MediaSourceInfo { Path = /Floatplane/stream/<videoId>?t=<hmac>, MediaStreams = h264/aac from delivery meta }
│ FloatplaneStreamController (anonymous, HMAC-signed)
│ → delivery/info (cached) → rewritten HLS playlist (key URI → /Floatplane/key/…)
│ or plain MP4 relay with Range passthrough
│ FloatplaneClient (session: cookie or OAuth tokens, UA, 429 handling, delivery throttle)
│ SyncTask (IScheduledTask, every N h: subscriptions → creators → posts → cache; bumps cache key)
│ Config page: login (cookie paste now, device-code later), quality preference, which creators/channels, sync interval
└──► www.floatplane.com / cdn-vod-drm2 / pbs
```
## Milestones
| # | 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 |
## Roadmap / expansions (to agree)
- **OAuth device-code login** once Floatplane issues a client id (QR code
on the config page, 90-day refresh token, no more cookie copying).
- **Live streams** (WAN Show): `scenario=live` IVS HLS as a live item or a
Live TV tuner; offline detection by polling the playlist.
- **Audio posts / podcasts** as an audio channel.
- **Archive to library**: optional scheduled download of selected channels
to `/media/Floatplane/<Creator>/<Channel>/` with NFO + thumbnails for
trickplay/offline (floatplane-downloader-style layout).
- **Multi-user**: per-Jellyfin-user Floatplane sessions (`IsEnabledFor`).
- **Home-screen "Latest from Floatplane"** via `ISupportsLatestMedia`.
- **Jellyseerr-style request/notification** hook when new posts appear
(notifyr on the Pi5).
## First real-world test (Bjorn, when home)
1. Pin the Pi5 image tag (`lscr.io/linuxserver/jellyfin:10.10.7ubu2404-ls80`)
before anything else, or a `pull` lands on 12.3.
2. Install: Dashboard → Plugins → Repositories → add
`https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/manifest.json`,
install "Floatplane", restart. (Or `install-pi5.sh` from the release.)
3. Settings → paste `sails.sid` → Validate → Save. Creators from the
subscription appear under My Media → Floatplane.
4. Play a 1080p video in the web client; watch `log/` for
`FloatplaneStreamController` lines and the ffmpeg transcode log — the goal
is a stream **copy** (remux), not a re-encode.
5. Try the MP4 experiment (settings → "progressive MP4 versions") and report
whether it plays; if it does it becomes the default (seekable, no key dance).
## Open questions
1. **OAuth client id**: ask Floatplane (community Discord
`https://discord.gg/4xKDGz5M5B`) for a device-flow client id for this
plugin — only Bjorn can make that request. Until then: cookie.
2. **Cookie lifetime**: unknown; the settings page shows "rejected" when it
expires. Measure in practice.
3. **Pi5 upgrade to 12.x**: separate task; the plugin needs a .NET 10 /
12.x build then (`TaskTriggerInfo` enum and package versions are the
known deltas).
4. **Live streams** and the other roadmap items: priority after VOD works.