From f970ccb0a19b5f710b79faa7749ba36fcfab5f14 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 25 Oct 2026 09:38:51 +0100 Subject: [PATCH] Seed wiki: architecture overview + Floatplane API summary --- Floatplane-API.md | 48 +++++++++++++++++++++++++++++++++ Home.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 Floatplane-API.md create mode 100644 Home.md diff --git a/Floatplane-API.md b/Floatplane-API.md new file mode 100644 index 0000000..f5c1c64 --- /dev/null +++ b/Floatplane-API.md @@ -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]=` | → `[Creator]` incl. `channels[]`, `liveStream`, `subscriptionPlans` | +| `GET /api/v3/creator/channels/list?ids[0]=` | → `[Channel]` (`id, creator, title, urlname, order, icon, cover, card`) | +| `GET /api/v3/content/creator?id=&limit=20&fetchAfter=` | post listing, newest first. Filters: `channel=`, `search=` (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]=` | → `{ "": 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:, 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=` | full post with `videoAttachments[]` objects (`id, title, duration, levels[], textTracks[]`) and `attachmentOrder` | +| `GET /api/v3/content/video?id=` | video details, `progress` (resume seconds), `timelineSprite` | +| `GET /api/v3/delivery/info?scenario=onDemand\|download\|live&entityId=` | `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:}` | 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//1080.mp4/chunk.m3u8?token= +chunk.m3u8: #EXT-X-KEY:METHOD=AES-128,URI="https://www.floatplane.com/api/video/watchKey?token=…" ← needs cookie + .ts?token= ← 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/ (+repo url)`), honour `429`/`Retry-After`, cache `delivery/info` per video for the token lifetime, personal use with your own subscription. diff --git a/Home.md b/Home.md new file mode 100644 index 0000000..a342391 --- /dev/null +++ b/Home.md @@ -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:` | `creator.channels[]` (public) | +| sub-channel | `channel::` | `GET /api/v3/content/creator?id&channel&hasVideo=true` paged 20 at a time, newest first, up to *Videos listed per channel* | +| video | `post:` | 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:")` → plugin fetches `GET /api/v3/content/post` (login required, cached 1 h) → one `MediaSourceInfo` per video attachment × quality level (preferred level first), `Path = /Floatplane/Stream//