# 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/_/`, 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/?t=, 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///` 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.