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>
155 lines
9.8 KiB
Markdown
155 lines
9.8 KiB
Markdown
# 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 | **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)
|
|
|
|
- **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.
|