# player.schule API: guide for AI agents

player.schule hosts videos for schools. A teacher's videos can be uploaded files (converted to MP4
1080p and 720p), YouTube or Vimeo links (embedded behind a click to load) and imported links
(downloaded and converted). Every video has a public page (`play_url`) and an embed code.
You act for one teacher and only ever see and change her own videos.

Machine readable description: https://player.schule/api/v1/openapi.json (OpenAPI 3.1).

## Authentication

The teacher creates a personal API token in her profile (section "API-Zugänge") and gives it
to you. Send it with every request:

    Authorization: Bearer <token>
    Accept: application/json

A token has abilities, chosen when it was made: videos:read, videos:write, videos:delete, animations:write.
`*` grants all of them. Check what yours may do with `GET /me`.

- videos:read   list and read videos
- videos:write  upload, add links, rename, describe, set or remove a password, reprocess
- videos:delete delete videos
- animations:write   create and edit animations (see the animation section, if present)

## Limits

- 60 requests per minute per token, uploads and links (POST /videos) 10 per minute.
  Over the limit you get 429 with a `Retry-After` header (seconds).
- Uploads: at most 512 MB per file, any format ffmpeg reads.

## Errors

Errors are problem details (`Content-Type: application/problem+json`):

    {"type": "about:blank", "title": "Unprocessable Content", "status": 422,
     "detail": "The url field is required.", "errors": {"url": ["The url field is required."]}}

401 no or invalid token, 403 the token lacks the ability, 404 no video of this teacher has
this id (also for other people's videos), 422 invalid input (`errors` per field), 429 too many
requests, 500 server error.

## Endpoints

Base URL: https://player.schule/api/v1

### GET /me
Who the token acts for, the number of videos and the token's abilities.

    curl -H "Authorization: Bearer $TOKEN" https://player.schule/api/v1/me

### GET /videos  (videos:read)
Lists the teacher's videos, newest first. Query parameters (all optional):
`type` (video, youtube, vimeo, animation), `status` (ready, processing), `q` (search in name and
description), `sort` (created_at, updated_at, name, views, duration, -created_at, -updated_at, -name, -views, -duration), `page`, `per_page` (default 25, at most 100).
The answer has `data` (videos), `links` and `meta` (current_page, last_page, total).

    curl -H "Authorization: Bearer $TOKEN" "https://player.schule/api/v1/videos?type=youtube&q=photosynthesis"

### GET /videos/{id}  (videos:read)
One video. `id` is the video's UUID from the list.

    curl -H "Authorization: Bearer $TOKEN" https://player.schule/api/v1/videos/<id>

Fields: id, type, name, description, status (ready or processing), progress (0 to 100),
duration (seconds), views, protected (password set), source_url, play_url (share this link),
embed_url, embed_code (HTML iframe), renditions (e.g. [1080, 720]), created_at, updated_at.

### POST /videos  (videos:write)
Upload a file (multipart, field `file`, optional `name`):

    curl -H "Authorization: Bearer $TOKEN" -F "file=@lesson.mp4" -F "name=Lesson 1" https://player.schule/api/v1/videos

Answer 201 with the video. It is `processing` until the MP4 files are made; poll
GET /videos/{id} until `status` is `ready`.

Or add a link (JSON, field `url`, optional `name`):

    curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
         -d '{"url": "https://www.youtube.com/watch?v=XXXXXXXXXXX"}' https://player.schule/api/v1/videos

YouTube and Vimeo links become an entry at once (201, or 200 if the teacher already had it).
Any other link is imported: answer 202 with `{"data": null, "imported": true}`; the video
shows up in GET /videos when the download and conversion are done.

### PATCH /videos/{id}  (videos:write)
Change `name`, `description` (null clears it) or `password`. Only the keys you send change.
A password (at least 4 characters) protects an uploaded video or animation: viewers must enter it.
`"password": null` removes the protection. YouTube and Vimeo videos cannot be protected.

    curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
         -d '{"name": "Photosynthesis", "password": "class7b"}' https://player.schule/api/v1/videos/<id>

### POST /videos/{id}/reprocess  (videos:write)
Makes the MP4 files of an uploaded video again (e.g. after a failed conversion).
`{"import": true}` downloads an imported video again from its source. Answer 202.

    curl -X POST -H "Authorization: Bearer $TOKEN" https://player.schule/api/v1/videos/<id>/reprocess

### DELETE /videos/{id}  (videos:delete)
Deletes the video and all of its files. This cannot be undone; ask the teacher first.
Answer 204.

    curl -X DELETE -H "Authorization: Bearer $TOKEN" https://player.schule/api/v1/videos/<id>

## MCP server

The same functions are available as an MCP server (Streamable HTTP, JSON-RPC over POST):

    URL:    https://player.schule/mcp
    Header: Authorization: Bearer <token>

Tools: list_videos, get_video, add_video_link, update_video, delete_video, reprocess_video.
Uploading files is only possible through the REST API (POST /videos).

## Good practice

- Never invent ids; take them from GET /videos.
- Share `play_url` with students; use `embed_code` for learning platforms.
- Do not delete or overwrite anything the teacher did not ask you to change.

## Animations (animated explainers in the Hyperframes format)

An animation is a video of type `animation`: one HTML composition plus its files, played raw in a sandbox on a
separate domain at the same play URL as every video (`play_url`, `embed_url`). Write animations with the
token ability `animations:write` (reading also works with `videos:read`).

FORMAT (Hyperframes): one complete HTML document of fixed size, usually 1920×1080 (<meta name="viewport" content="width=1920, height=1080">; html and body exactly that size, overflow hidden). One root element <div id="animation" data-composition-id="animation" data-start="0" data-width="1920" data-height="1080"> holds every clip. A clip is an element (div, img, video, audio) with id, class="clip", data-start (seconds, or another clip's id meaning "when it ends", optionally "id + 2"), data-duration (seconds; may be left out on audio/video) and data-track-index (higher is in front; clips on one track must not overlap); CSS gives .clip position: absolute and visibility: hidden, and the runtime shows each clip during its time. One inline script inside the root builds `const tl = gsap.timeline({ paused: true });`, adds tweens of visual properties only (opacity, x, y, scale, rotation, colour …) at absolute times (`tl.from("#title h1", { opacity: 0, y: 80, duration: 1 }, 0.5)`), extends it to the full length with `tl.set({}, {}, LENGTH)` and registers it: `window.__timelines = window.__timelines || {}; window.__timelines["animation"] = tl;` — the key equals data-composition-id. Never play, pause or seek media and never toggle visibility in a script; the runtime does that, and audio and video play from their data-start. Keep everything in this one file: no data-composition-src, no other HTML file; a nested composition is an inline div with its own data-composition-id, data-start, data-track-index and registered timeline. GSAP (with its plugins as globals) and the Hyperframes runtime are added by the server when the file does not load them. Pictures, sound, video and fonts are the animation's files: upload them and use them by the relative path assets/<name> (e.g. <img class="clip" src="assets/sea.png" …>, <audio data-start="0" data-track-index="9" src="assets/voice.mp3">, @font-face { src: url(assets/font.woff2) }). File names: lower case a-z, 0-9, dot, dash, underscore; at most 60 files. There is no network: no fetch, no image, font or script by URL except the self-hosted libraries below; the browser blocks every other host and check names it. The composition is at most 400 KB. The animation plays in a sandbox without cookies, storage or access to the page around it, and the play page's own control bar drives it.

Self-hosted libraries (their cdnjs, jsDelivr and unpkg addresses work too and are served locally): GSAP 3.15.0 (global gsap, plugins as globals of the same name (register with gsap.registerPlugin)): <script src="/lib/gsap@3.15.0/gsap.min.js">; also CustomEase.min.js, CustomBounce.min.js, CustomWiggle.min.js, EasePack.min.js, MotionPathPlugin.min.js, TextPlugin.min.js, SplitText.min.js, ScrambleTextPlugin.min.js, DrawSVGPlugin.min.js, MorphSVGPlugin.min.js, Flip.min.js, Physics2DPlugin.min.js; HyperFrames runtime 0.8.174 (the composition runtime, added to every animation by the server): <script src="/lib/hyperframes-runtime@0.8.174/hyperframe.runtime.iife.js">.

### Endpoints (prefix /api/v1, Bearer token)

- `POST animations`: JSON `{"name": "...", "html": "<!doctype html>…"}` (html optional: without it a working
  starter is stored), or multipart with `archive` (a ZIP with `index.html` and its files, which are stored
  flat as `assets/<name>`; references are rewritten) and optional `name`. Answers 201 with the animation.
- `GET animations/{id}`: the animation with `animation.html`, `animation.files`, `animation.size` and `animation.checks`.
- `PUT animations/{id}/composition`: `{"html": "..."}` replaces the whole composition.
- `PATCH animations/{id}/composition`: `{"edits": [{"old_str": "...", "new_str": "..."}]}`; each old_str must occur
  exactly once when it is applied, in order, all or none.
- `PUT animations/{id}/assets/{name}`: the raw bytes as the body (or `POST` multipart with `file`); adds or replaces
  `assets/{name}`. Pictures, sound, video and fonts only (checked by extension and content), 200 MB per animation in all.
- `DELETE animations/{id}/assets/{name}`, `DELETE animations/{id}/unused-assets` (deletes every file the composition does not use),
  `GET animations/{id}/check` (what keeps the animation from playing).

Example:

```
curl -X POST https://player.schule-beispiel.test/api/v1/animations \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Wasserkreislauf"}'
curl -X PUT https://player.schule-beispiel.test/api/v1/animations/$ID/assets/sea.png \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: image/png" --data-binary @sea.png
```

Over MCP the same is one tool, `animation`, with the commands create, view, rewrite, update, add_asset,
remove_asset, prune_assets and check.
