Seed wiki: architecture overview + Floatplane API summary
48
Floatplane-API.md
Normal file
48
Floatplane-API.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Floatplane API (unofficial) — what the plugin uses
|
||||
|
||||
Condensed from `doc/floatplane-api-notes.md` and `doc/research/floatplane-api-research.md` in the main repo. Verified with plain `curl` on 2026-10-25 (custom User-Agent, no cookie unless stated). Base URL `https://www.floatplane.com`.
|
||||
|
||||
## Public (no cookie)
|
||||
|
||||
| Endpoint | Notes |
|
||||
|----------|-------|
|
||||
| `GET /api/v3/creator/named?creatorURL[0]=<urlname>` | → `[Creator]` incl. `channels[]`, `liveStream`, `subscriptionPlans` |
|
||||
| `GET /api/v3/creator/channels/list?ids[0]=<creatorId>` | → `[Channel]` (`id, creator, title, urlname, order, icon, cover, card`) |
|
||||
| `GET /api/v3/content/creator?id=<creatorId>&limit=20&fetchAfter=<offset>` | post listing, newest first. Filters: `channel=<channelId>`, `search=<text>` (title/body), `hasVideo=true` (also `hasAudio/hasPicture/hasText`, mutually exclusive), `tags[0]=…`, `fromDate/toDate` (ISO 8601), `fromDuration/toDuration` (seconds), `sort=ASC|DESC`. **`limit` max 20** (`paramValidationError` above), `fetchAfter` is a plain offset; empty array = end. List items carry `metadata` (hasVideo, videoDuration…) and `thumbnail` but **no attachment ids**; `isAccessible:false` = above your tier. |
|
||||
| `GET /api/v3/content/tags?creatorIds[0]=<creatorId>` | → `{ "<tag>": count, … }` |
|
||||
| `https://pbs.floatplane.com/...` | thumbnails / icons / covers / timeline sprites; `childImages[]` carry `_WxH` variants |
|
||||
|
||||
## Login required (cookie `sails.sid`)
|
||||
|
||||
| Endpoint | Notes |
|
||||
|----------|-------|
|
||||
| `GET /api/v3/user/self` | cookie probe → `{id, username, email, profileImage}` |
|
||||
| `GET /api/v3/user/subscriptions?active=true` | → `[{plan{id,title,price}, creator:<creatorId>, startDate, endDate}]` |
|
||||
| `GET /api/v3/creator/info?id=` · `GET /api/v3/creator/list?ids[0]=` | creator details by id |
|
||||
| `POST /api/v3/content/creator/list {ids:[…], limit}` | multi-creator feed with `lastElements` cursors |
|
||||
| `GET /api/v3/content/post?id=<postId>` | full post with `videoAttachments[]` objects (`id, title, duration, levels[], textTracks[]`) and `attachmentOrder` |
|
||||
| `GET /api/v3/content/video?id=<videoId>` | video details, `progress` (resume seconds), `timelineSprite` |
|
||||
| `GET /api/v3/delivery/info?scenario=onDemand\|download\|live&entityId=<videoId>` | `groups[].origins[].url` + `variants[]{name:"1080-avc1", label, url (+JWT token, 6 h), enabled, hidden, meta{video{codec,width,height,fps,bitrate}, audio{…}, common.access.deniedReason}}`. **Rate-limited ≈2/min.** |
|
||||
| `GET /api/video/watchKey?token=…` | HLS AES-128 key (16 bytes) — the only playback request that needs the cookie |
|
||||
| `GET /api/v3/content/progress?ids[0]=&contentType=blogPost` · `POST /api/v3/content/progress {id, contentType:"video", progress:<s>}` | watch progress (read = percent, write = seconds) |
|
||||
|
||||
## Auth facts
|
||||
|
||||
- `POST /api/v3/auth/login {username,password,captchaToken}` → `invalidCaptchaTokenError` without a Cloudflare Turnstile token, for every User-Agent tested (browser, Hydravion, Wasserflug/CFNetwork, Android, okhttp).
|
||||
- OAuth2/OIDC: `https://auth.floatplane.com/realms/floatplane/.well-known/openid-configuration` (device-code grant, DPoP supported). Third-party clients (Hydravion, Wasserflug, floatplane-downloader, Floaty, FloatNative) have Floatplane-registered client ids; an unregistered id gets `invalid_client`. Access token 30 min, refresh token 90 days sliding (measured by the community canary).
|
||||
- Session cookie lifetime: not documented; the plugin reports `invalid` and logs a warning when Floatplane rejects it.
|
||||
|
||||
## Playback (VOD)
|
||||
|
||||
```
|
||||
origin = groups[0].origins[0].url e.g. https://cdn-vod-drm2.floatplane.com
|
||||
playlist = origin + variant.url /Videos/<videoId>/1080.mp4/chunk.m3u8?token=<JWT>
|
||||
chunk.m3u8: #EXT-X-KEY:METHOD=AES-128,URI="https://www.floatplane.com/api/video/watchKey?token=…" ← needs cookie
|
||||
<base64json>.ts?token=<JWT> ← token only, CORS *
|
||||
```
|
||||
|
||||
Variants above the account's tier come back `enabled:false, url:""` with `meta.common.access.deniedReason = isMissingPermission`. Subtitles: `video.textTracks[].src` is an S3-presigned VTT URL valid 15 minutes.
|
||||
|
||||
## Etiquette
|
||||
|
||||
Unique User-Agent (`Jellyfin-Floatplane/<version> (+repo url)`), honour `429`/`Retry-After`, cache `delivery/info` per video for the token lifetime, personal use with your own subscription.
|
||||
69
Home.md
Normal file
69
Home.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# jellyfin-plugin-floatplane
|
||||
|
||||
A Jellyfin **channel plugin** that shows your Floatplane subscriptions (creators → sub-channels → videos) inside Jellyfin and every Jellyfin client, and plays them through Jellyfin's normal player. No client-side changes.
|
||||
|
||||
- Code + plan: https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane (`doc/plan.md`, `doc/floatplane-api-notes.md`, `doc/research/`)
|
||||
- Install: Dashboard → Plugins → Repositories → add `https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/manifest.json`, or run `install-pi5.sh` from the release.
|
||||
- Built for **Jellyfin 10.10.x** (.NET 8, `targetAbi 10.10.0.0`). One AnyCPU DLL for x64 and arm64 (Pi5).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Jellyfin clients (web, Android, TV, JMP)
|
||||
│ normal channel browsing + playback APIs
|
||||
▼
|
||||
Jellyfin server ──► FloatplaneChannel (IChannel, IRequiresMediaInfoCallback, IHasCacheKey, ISupportsLatestMedia)
|
||||
│ folders/items │ media sources
|
||||
▼ ▼
|
||||
CatalogService proxy URLs → FloatplaneStreamController (anonymous, HMAC-signed)
|
||||
(creators, channels, │ delivery/info (cached, throttled) via DeliveryResolver
|
||||
newest posts; cached) │ HLS playlist rewrite (HlsRewriter)
|
||||
│ │ AES-128 key fetched WITH the session cookie
|
||||
▼ ▼
|
||||
FloatplaneApiClient ──────────► www.floatplane.com (REST + key server)
|
||||
cdn-vod-drm2.floatplane.com (playlists + segments, signed token only)
|
||||
pbs.floatplane.com (thumbnails, public)
|
||||
FloatplaneSyncTask (IScheduledTask) refreshes the catalogue (startup + every N hours) and bumps the cache key.
|
||||
FloatplaneAdminController (admin only) backs the settings page: validate cookie, status, refresh, search test, tags.
|
||||
```
|
||||
|
||||
### Folder / item id scheme
|
||||
|
||||
| Level | `ChannelItemInfo.Id` | Source |
|
||||
|-------|----------------------|--------|
|
||||
| root | – | creators = subscriptions (needs cookie) ∪ "extra creators" (public url-names) |
|
||||
| creator | `creator:<creatorId>` | `creator.channels[]` (public) |
|
||||
| sub-channel | `channel:<creatorId>:<channelId>` | `GET /api/v3/content/creator?id&channel&hasVideo=true` paged 20 at a time, newest first, up to *Videos listed per channel* |
|
||||
| video | `post:<postId>` | one item per post; a post only ever lives under its own channel (Jellyfin allows one parent per item) |
|
||||
|
||||
The channel `Name` ("Floatplane") must never change: Jellyfin derives the channel's library id from it.
|
||||
|
||||
### Playback flow
|
||||
|
||||
1. Jellyfin asks `GetChannelItemMediaInfo("post:<id>")` → plugin fetches `GET /api/v3/content/post` (login required, cached 1 h) → one `MediaSourceInfo` per video attachment × quality level (preferred level first), `Path = <proxy base>/Floatplane/Stream/<post>/<video>/<level>/index.m3u8?t=<hmac>`, `IsRemote=false`, `SupportsProbing=false`, declared `h264`/`aac` streams so Jellyfin stream-copies instead of transcoding.
|
||||
2. ffmpeg (Jellyfin's transcode/remux path) requests the playlist. The controller verifies the signature, calls `GET /api/v3/delivery/info?scenario=onDemand&entityId=<video>` (via `DeliveryResolver`: cached until the JWT in the URL expires, ≈6 h, throttled to *Delivery lookups per minute*, default 2), picks the variant (exact level → best ≤ level → best), downloads `chunk.m3u8` from the CDN (no cookie needed) and rewrites it:
|
||||
- `#EXT-X-KEY … URI="https://www.floatplane.com/api/video/watchKey?token=…"` → `…/Floatplane/Stream/<video>/Key?u=<b64url>&t=<hmac>` (the plugin fetches the 16-byte key **with the session cookie**, which Jellyfin itself cannot forward — it only passes User-Agent/Referer to ffmpeg).
|
||||
- segment URIs → absolute CDN URLs (signed per-segment token, CORS open, no cookie).
|
||||
3. Optional experimental MP4 version: `…/video.mp4` relays Floatplane's `scenario=download` URL with Range passthrough.
|
||||
|
||||
Why a proxy at all: Jellyfin forwards no cookies/Authorization to ffmpeg or its static relay, and browsers cannot attach a third-party cookie, so the key request has to come from the server. Why signed URLs: the stream endpoints are anonymous (ffmpeg has no Jellyfin token); the HMAC (process-lifetime secret) limits them to what the plugin itself handed out.
|
||||
|
||||
### Session model
|
||||
|
||||
- Floatplane's password login requires a Cloudflare Turnstile token regardless of User-Agent (verified 2026-10-25), and the OAuth device flow (Keycloak `auth.floatplane.com`) needs a `client_id` registered by Floatplane (`invalid_client` otherwise). So v1 uses a **pasted `sails.sid` cookie** (settings page → "Validate cookie" calls `/api/v3/user/self` + `/user/subscriptions`).
|
||||
- Browsing (creator pages, channel lists, post listings, search, thumbnails) is **public**; `content/post`, `delivery/info` and the key need the cookie. An expired cookie shows up as `SessionState = invalid` on the settings page and as a warning in the log.
|
||||
- Rate limits: `delivery/info` ≈2/min (429 + `Retry-After`, Cloudflare 1015 if hammered). Listing endpoints are not noticeably limited; `limit` max is 20.
|
||||
|
||||
### Jellyfin version note
|
||||
|
||||
The Pi5 runs **10.10.7**; `lscr.io/linuxserver/jellyfin:latest` is already **12.3** (.NET 10, new plugin ABI). A 10.10 plugin is `NotSupported` on 12.x and vice versa. Pin the image tag (`10.10.7ubu2404-ls80`) until the upgrade is done deliberately; the known 12.x deltas for this code are the `TaskTriggerInfo.Type` enum and the NuGet package/TFM bump.
|
||||
|
||||
### Caches (to know when "nothing updates")
|
||||
|
||||
| What | Where | TTL / invalidation |
|
||||
|------|-------|--------------------|
|
||||
| Jellyfin's own channel listing cache | `<cache>/channels/<id>/…json` | 3 h, bypassed when the plugin's cache key (generation counter) changes → "Refresh now" or the scheduled task |
|
||||
| Jellyfin's media-info cache | memory | 5 min per item |
|
||||
| Plugin creators + post lists | memory | *Listing cache (minutes)*, default 30 |
|
||||
| Plugin post details | memory | 1 h |
|
||||
| Plugin delivery variants | memory | until token `exp` − 5 min |
|
||||
Reference in New Issue
Block a user