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

View File

@@ -0,0 +1,131 @@
/**
* Phase 3.0 authenticated API response-shape canary — constants.
*
* OIDC clientId / realm / issuer are taken from frontend archive evidence
* (build 4.5.22-316-d74397b entry JS: baseUrl auth.floatplane.com, realm floatplane,
* clientId fp-tv-app) and public OIDC discovery. Do not invent alternate clients.
*/
export const TOOL_ID = 'fp-api-shape-canary';
export const TOOL_VERSION = '3.1.0';
export const SCHEMA_VERSION = 1;
/** Custom User-Agent for read-only probes (identify the watch project). */
export const USER_AGENT = 'FloatplaneAPI-Watch/3.1 (api-shape-canary; read-only)';
export const API_BASE_URL = 'https://www.floatplane.com';
/**
* Keycloak / OIDC — evidence-backed defaults.
* Discovery: https://auth.floatplane.com/realms/floatplane/.well-known/openid-configuration
*/
export const OIDC = Object.freeze({
issuer: 'https://auth.floatplane.com/realms/floatplane',
wellKnownUrl:
'https://auth.floatplane.com/realms/floatplane/.well-known/openid-configuration',
/** Frontend TV device-auth config: clientId:"fp-tv-app" */
clientId: 'fp-tv-app',
realm: 'floatplane',
baseUrl: 'https://auth.floatplane.com',
/**
* One-shot / interactive default. Scheduled canary prefers scheduleScope.
* offline_access is advertised in discovery scopes_supported (observed 2026-10-11).
*/
defaultScope: 'openid',
/**
* Prefer offline refresh for scheduled runs. Still not "indefinite" — see
* docs/phase-3-1-api-shape-schedule.md for measured refresh_expires_in.
*/
scheduleScope: 'openid offline_access',
grantTypeDeviceCode: 'urn:ietf:params:oauth:grant-type:device_code',
grantTypeRefreshToken: 'refresh_token',
});
/** Separate lane from Phase 1/2 frontend observation branch. */
export const SHAPE_MONITOR_BRANCH = 'cursor/api-shape-observation';
export const SHAPE_DEFAULT_BRANCH = 'main';
export const SHAPE_PR_TITLE_PREFIX = 'API shape canary:';
/** Evidence notes committed beside captures for auditability. */
export const OIDC_EVIDENCE = Object.freeze({
clientIdSource:
'artifacts/frontend/4.5.22-316-d74397b/.../js/index-BZVDPgzb.js — o_={baseUrl:"https://auth.floatplane.com",realm:"floatplane",clientId:"fp-tv-app",usesExtendedSecurity:!0,...}',
deviceGrantSource:
'OIDC discovery grant_types_supported includes urn:ietf:params:oauth:grant-type:device_code; device_authorization_endpoint present (observed 2026-10-02)',
refreshGrantSource:
'OIDC discovery grant_types_supported includes refresh_token; scopes_supported includes offline_access (observed 2026-10-11). Frontend refreshAccessToken requires DPoP when USE_DPOP; setOAuthTokenFromResponse reads refresh_expires_in as finite wall clock.',
});
/**
* Fixed read-only allowlist (Phase 3.0). Order matters for id discovery.
* No writes. No /delivery/info. No Hydravion.
*/
export const ALLOWLIST = Object.freeze([
{
id: 'user-self',
method: 'GET',
path: '/api/v3/user/self',
role: 'token-sanity',
auth: 'bearer',
},
{
id: 'user-subscriptions',
method: 'GET',
path: '/api/v3/user/subscriptions',
role: 'pick-creator',
auth: 'bearer',
},
{
id: 'content-creator',
method: 'GET',
path: '/api/v3/content/creator',
role: 'list-shape + post-id',
auth: 'bearer',
/** Query built at runtime: id, limit=1 */
queryKeys: ['id', 'limit'],
},
{
id: 'content-post',
method: 'GET',
path: '/api/v3/content/post',
role: 'primary-canary',
auth: 'bearer',
queryKeys: ['id'],
},
{
id: 'content-video',
method: 'GET',
path: '/api/v3/content/video',
role: 'optional-attachment',
auth: 'bearer',
optional: true,
queryKeys: ['id'],
},
]);
/** Unauthenticated companion (list shape only; detail post remains 403). */
export const UNAUTH_LIST = Object.freeze({
id: 'content-creator-unauth',
method: 'GET',
path: '/api/v3/content/creator',
role: 'unauth-list-companion',
auth: 'none',
/** Creator id used in assessment unauth sample (Linus Tech Tips). */
defaultCreatorId: '59f94c0bdd241b70349eb72b',
});
export const EXIT = Object.freeze({
SUCCESS: 0,
FAILURE: 1,
/** Structural drift vs baseline */
DRIFT: 2,
/** Missing token / needs human device approval */
NEEDS_AUTH: 3,
});
export const DEFAULT_ARTIFACTS_ROOT = 'artifacts/api-shape';
export const DEFAULT_TOKEN_FILE = 'state/api-shape-token.local';
/** Combined access token + DPoP key material (gitignored). Prefer over bare token file. */
export const DEFAULT_AUTH_FILE = 'state/api-shape-auth.local.json';
export const BASELINES_DIR = 'baselines';
export const CAPTURES_DIR = 'captures';