Skip to content
zaentrumPublic

About

chino-api — catalog/playback API for chino on the zaentrum platform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

80 Commits

Folders and files

Repository files navigation

chino-api

Backend BFF for the chino product of the zaentrum platform. A read-only Go API that fronts the catalog and streaming services for the chino web, mobile, and TV clients: browse, search, continue-watching, watchlists, playback progress, telemetry, and a bug-report pipeline into OpenProject.

Stack

  • Go + chi router + slog
  • OIDC JWT validation via go-oidc
  • Postgres (pgx) for user-state (playback progress, watch history, watchlists)
  • Prometheus metrics
  • Distroless container image

chino-api is a pure read consumer: it talks to the catalog service for metadata and chino-stream for playback bytes. Writes never originate here.

Endpoints

The full surface is the embedded OpenAPI spec, served at GET /api/openapi.yaml and rendered from internal/http/openapi.yaml. Highlights:

Path Auth Notes
GET /api/healthz none liveness / readiness
GET /api/openapi.yaml none OpenAPI 3 spec
GET /api/config none client app config
GET /api/v1/me bearer JWT echoes the caller's sub
DELETE /api/v1/me bearer JWT (in the header) deletes the signed-in person's data and their account (below)
GET /api/v1/items bearer JWT catalog browse
GET /api/v1/items/{id} bearer JWT item detail, cast and crew by role, its trailers and extras
GET /api/v1/search bearer JWT one search: films, series and episodes ranked together, and people apart (below)
GET /api/v1/people bearer JWT people search by name
GET /api/v1/people/{id} bearer JWT a person, their details and filmography
GET /api/v1/people/{id}/profile bearer JWT or stream token a person's portrait (profile_url)
GET /api/v1/me/continue-watching bearer JWT resume list
POST /api/v1/me/available bearer JWT which of the titles a client keeps the viewer may watch now, in one request (below)
GET /api/v1/me/watchlists bearer JWT named watchlists
GET/POST /api/v1/items/{id}/progress bearer JWT playback progress, a file's (below); the resume point, none for a finished title (below); a late position never over a newer one (below)
GET /api/v1/items/{id}/subtitles bearer JWT an item's sidecar subtitles at their URLs, a styled one's script beside it, and the fonts of its package (below)
GET /api/v1/play/subs/{id}.vtt · .ass bearer JWT or stream token a subtitle's file; its styled original (ASS or SSA)
GET /api/v1/play/fonts/{id}.{ext} bearer JWT or stream token a font of the package, for a player that draws styled subtitles
GET /api/v1/subtitles/languages bearer JWT the instance's subtitle languages, from katalog-manager, kept 5 minutes (below)
POST /api/v1/play/events bearer JWT or stream token playback telemetry (the stream token for sendBeacon)
GET /api/v1/events bearer JWT or stream token live catalog notifications (SSE)
GET /api/v1/notices bearer JWT what addons told the signed-in person, from portal-api — best effort (below)
POST /api/v1/notices/{noticeId}/read · POST /api/v1/notices/read-all · DELETE /api/v1/notices/{noticeId} bearer JWT one notice read, all read, one deleted — forwarded to portal-api
POST /api/v1/feedback bearer JWT bug report → OpenProject (503 when unconfigured)
POST /api/v1/admin/items/{id}/package bearer JWT with the admin role an item's packaging, forwarded to katalog-manager with the bearer
GET /api/v1/admin/items/{id}/package bearer JWT with the admin role the item's processing steps, from katalog-manager
DELETE /api/v1/admin/accounts/{sub}/data bearer JWT with the admin role and the account deletion token what chino keeps of an account an admin deletes on the portal's People page, for portal-api
GET /api/v1/items/{id}/play/info bearer JWT or stream token how the item plays for the client's caps; a packaged title's qualities
GET /api/v1/items/{id}/play/master.m3u8 bearer JWT or stream token the HLS master for the client's caps and q
POST /api/v1/items/{id}/play/prewarm bearer JWT or stream token warm the variant the client starts on
GET /api/v1/items/{id}/play/{vN|aN|sN}/playlist.m3u8 bearer JWT or stream token packaged video / audio / WebVTT rendition, its segments next to it
GET /api/v1/items/{id}/extras/{extraId}/play/master.m3u8 bearer JWT or stream token an extra's HLS master, its play_path (below)
GET /api/v1/items/{id}/extras/{extraId}/play/info bearer JWT or stream token how an extra plays for the client's caps, as a title's /play/info
GET /api/v1/items/{id}/extras/{extraId}/play/{vN|aN|sN}/playlist.m3u8 bearer JWT or stream token an extra's renditions, as a packaged title's

Search

GET /api/v1/search?q= is katalog-api's search for the viewer: the titles (films, series and episodes) ranked together by how they match the query, best first, and the people whose names match it, ranked apart, in one response: {"items": [...], "people": [...]}. types= names the types of title (movie,series,episode by default; a client that shows no episode results asks for movie,series), limit= how many titles (30, at most 100), people= how many people (12, at most 50; 0 none). Each title comes as the lists send it, poster URLs and watched_at included, with match: how it matched (exact, prefix, words, alternative, similar, plot; the plot only when no title matched). An episode names its series (id, title, poster_url, and matched: the series matched by its own title too), so that a client can show the two as one card. A person comes as /api/v1/people sends them, with match. Against a katalog-api without the search, the types' lists of the query come one after the other, each of up to limit titles, and the people as /api/v1/people finds them.

Parental controls

A viewer whose access token carries max_rating, a whole number of years, is capped at that age (a kid's account); a token without the claim is not, and a claim that is no whole number of years holds its viewer to the strictest cap, 0 (chino-api says so once in its log). chino-api passes the cap to katalog-api as max_rating on every catalog request, so the lists, search, people, continue watching and history are served what the cap allows. A stream token minted for a capped viewer carries the cap in its user part (<subject>;max_rating=<age>, signed with the rest; chino-stream and katalog-manager verify it as before), so the media routes hold the viewer to it too.

Every route of one title (detail, segments, more like this, subtitles, a series' episodes and next episode, artwork, playback, trickplay, a sidecar subtitle, its styled original and a package's font by the title they belong to, and the viewer's own progress, watched, watchlist and like of the title) answers a capped viewer 404 "not found" for a title its cap does not allow, and for an id that names none, before anything goes upstream: it asks katalog-api's GET /api/v1/visible, kept for 30 seconds. The lists chino-api keeps (watchlists and their counts, likes, memberships) and chino-stream's Zap feed and packaged ids leave such titles out. When katalog-api cannot say what a cap allows, a capped viewer is served nothing (502). A viewer without a cap is served as before. Items carry min_age, certification and certification_country as katalog-api sends them, for a badge.

The tests of chino-api's own lists need a PostgreSQL in which they may create and drop schemas, named by CHINO_API_TEST_DATABASE_URL, and are skipped without it.

Deleting an account

A person deletes their own account from the app — the app stores ask every app that makes accounts to offer it — with DELETE /api/v1/me and their bearer, in the Authorization header (a ?token= bearer is refused: a link someone could be sent). chino-api deletes what it keeps of them — playback progress, watch history, named watchlists and their items, likes, the legacy watchlist rows — and asks portal-api, with the person's bearer and the account deletion token (ACCOUNT_DELETION_TOKEN, sent as X-Account-Deletion-Token), to delete their account in the platform's realm (DELETE /api/portal/me). The rows are deleted in a transaction committed only once portal-api has deleted the account, or found it gone already:

Answer When Deleted
200 {"account":"deleted","deleted":{…}} portal-api deleted the account the data and the account; deleted counts the rows
200 {"account":"gone",…} the account was gone already the data
409 {"error":"refused","message":…} portal-api refuses: the last admin, an administrator of Keycloak itself nothing; message says why, for the person
502 {"error":"account_not_deleted",…} portal-api did not answer, or refused the token nothing: asking again starts over
501 {"error":"account_deletion_unavailable",…} no token: accounts here live in an identity provider the platform does not manage nothing

The other direction: an admin deleting someone on the portal's People page has portal-api ask for that person's data first, with the admin's bearer and the same token: DELETE /api/v1/admin/accounts/{sub}/data. Bug reports already sent to OpenProject are not deleted with an account, and the request log keeps no bearer (it shows REDACTED).

A client offers it as Delete Account, asks first and says what goes (their history, lists and progress, and their account), sends the request with its bearer, and signs out on 200; on 409 it shows message, on 501 that accounts are deleted by whoever runs the server.

Notices

An addon can tell one person something — "your title is ready". portal-api keeps the notices and shows each person their own; the apps read and change them here, and chino-api forwards the viewer's bearer to portal-api's /api/portal/me/notices and keeps nothing of them. It is best effort, as the slots are: GET /api/v1/notices always answers 200 — {"notices": [...], "unread": n, "available": true}, or with no portal-api, or one that does not answer, an empty list and "available": false — so a home screen that shows notices never fails for them.

Route Answers
GET /api/v1/notices the person's notices, newest first (they keep their newest 100), unread, available
POST /api/v1/notices/{noticeId}/read 200 {"unread": n}; reading it again keeps when it was first read
POST /api/v1/notices/read-all 200 {"read": n, "unread": 0}
DELETE /api/v1/notices/{noticeId} 204

A notice the person does not have is 404 {"error":"not_found"} — someone else's is as one there is not; a change portal-api did not make is 502 (it did not answer) or 503 (none configured), {"error":"notices_unavailable"}, and the notice stays as it was. A notice carries id, addon, addonTitle and addonIcon (whom it is from), title (at most 80 characters), body (at most 280, line breaks allowed), link and itemId ("" for none), createdAt, and readAt (null while unread); the spec has the schema.

A client shows the title, the body and whom it is from as plain text, never as markup; follows link only to its own server — a path, or an http(s) URL on the server's origin; opens itemId with the item routes, which hold a capped viewer to their cap; and shows the unread count where the person looks first.

Playback

The play routes proxy chino-stream, query and all: the stream token, caps (what the client decodes, e.g. avc:2160,hvc:2160,aac,eac3) and q ride on to every playlist and segment, and so does v=<versionId>, which chino-stream writes onto a package's URIs to pin the session to that version. For a packaged title chino-stream serves each client one codec family (HEVC when it decodes it, else H.264) at the heights its decoder takes, the audio groups it decodes and the subtitle group the package has; q=<name> from /play/info's qualities serves one rung, auto (the default) the ladder. The rules and the /play/info shape are in chino-stream's README and in the spec (PlayInfo, PlayQuality). A quality menu shows qualities when it has two or more entries, by label, and reloads the master with q=<name>. pictureArea (PictureArea) says where the picture is in a frame that keeps black bars in it - left, top, width and height, fractions of the frame served - for a player that fits the picture to the screen; it is left out where there is none to say.

A web player on another origin, a Cast receiver among them, plays from the routes that take a stream token (playlists, segments, subtitles, artwork, portraits): their answers, errors too, carry Access-Control-Allow-Origin: *, and they answer a preflight (GET, the Range header) without a credential. Their credential is in the URL, never a cookie, so this lets no other site use a viewer's session. The routes that take a bearer in a header send no CORS headers, and chino-api passes no upstream's Access-Control-* headers on.

Styled subtitles

GET /api/v1/items/{id}/subtitles lists the subtitles of the file an item plays, each at its url in the format it serves, which every client plays. A styled subtitle (an ASS or an SSA script) is packaged as WebVTT like every other, and its package keeps the script as it was beside it, with the fonts the original carried. A player that draws styles (a libass renderer such as JASSUB on the web) opts into them; every other client reads the list as before.

  • An entry whose package keeps its styled original offers it in styled: {"url": "/api/v1/play/subs/<id>.ass", "format": "ass"}.
  • fonts are the fonts of the package, in their order, each its id, its format (ttf, otf, ttc, woff, woff2; omitted for none of them), the names the script asks for it by, and its url, /api/v1/play/fonts/<id>.<format> (.bin for a font of no format). A JASSUB player hands them on as availableFonts, each name lower-cased.
  • Both fields are omitted where there is nothing to offer — fonts also when no subtitle of the list offers styled, the fonts being for those alone — so a title without them is listed byte for byte as before. The item (GET /api/v1/items/{id}) says a subtitle's styled_format as katalog-api does, and has no fonts.
  • The script and the fonts are proxied to chino-stream in the stream-token group, as a subtitle's .vtt is, and a capped viewer is held to the title each belongs to. chino-stream serves each from its title's package alone, cached by the browser alone (private) — a script for good, a font for a day, then revalidated by its ETag — and never sniffed (nosniff).
{"subtitles": [
  {"id": "3c1f…", "lang": "en", "label": "English", "format": "webvtt", "default": true,
   "url": "/api/v1/play/subs/3c1f….vtt",
   "styled": {"url": "/api/v1/play/subs/3c1f….ass", "format": "ass"}}],
 "fonts": [
  {"id": "9b0e…", "format": "ttf", "names": ["Example Sans", "Example Sans Regular", "ExampleSans-Regular"],
   "url": "/api/v1/play/fonts/9b0e….ttf"}]}

Subtitle languages

GET /api/v1/subtitles/languages answers the instance's subtitle languages, the languages every title is to have a subtitle in as katalog-manager's settings name them: {"languages": ["en", "de", "zh-Hans"]}, BCP 47 tags as katalog-manager gives them, in the setting's order, and [] when none is set. Nothing else is in the answer.

  • They are the instance's, not the viewer's: chino-api asks katalog-manager's GET /api/library/subtitles/languages with the viewer's bearer and keeps the answer for every viewer for 5 minutes, so an admin's change of the setting reaches the clients within them.
  • After the 5 minutes each request asks again. When katalog-manager does not answer then (an error, no answer within 5 seconds, a status other than 2xx, a body that is not its answer), the last answer is served while it is less than an hour old. A failure is never kept, and does not make the last answer younger.
  • With no answer of the last hour the answer is 502, a line of text and no list. [] says none is set; a 502 says nothing of the setting.

Playback telemetry

POST /api/v1/play/events takes the players' events in batches, {"sessionId": "…", "events": [{"ts": <ms>, "kind": "…", "itemId": "…", "payload": {…}}]}, and keeps none of them: each is a JSON line in the log (client_ts, kind, itemId, each payload field as p_<field>, credentials REDACTED) and is counted on GET /metrics, the cluster-internal scrape. Every kind counts in chino_player_events_total{kind}, a kind the server does not know too. A payload value is a JSON number or its string, as the TV and mobile apps send every value.

Five kinds measure how playback goes, the baseline the live rungs of ADR-0013 are judged against:

Kind What the server reads Metric
first_frame startup_ms (the player opening, or Try again, to its first frame), mode, resume; with hidden or click_to_play true it is logged, not observed chino_player_startup_seconds{mode,resume}, buckets 0.25, 0.5, 1, 2, 3, 5, 8, 12, 20, 30 s
startup_timeout mode, phase: what the player still waited for when it gave up chino_player_startup_timeouts_total{mode,phase}
rebuffer mode, duration_ms: a stall that began mid-play chino_player_rebuffers_total{mode}, each one; chino_player_rebuffer_seconds{mode}, buckets 0.1, 0.25, 0.5, 1, 2, 4, 8, 15, 30, 60 s
bandwidth bps, the player's estimate while it plays, which leaves out the wait for a segment's first byte; frag_ttfb_ms, that wait for the last video segment (the round trip and the origin's time to answer) chino_player_bandwidth_mbps{device}, buckets 1, 2, 4, 8, 15, 25, 40, 80 Mbit/s; chino_player_segment_ttfb_seconds{device}, buckets 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8 s
level_switch nothing: the rendition on screen changed chino_player_events_total only
  • Labels come from fixed sets, never a user, an item or an address. mode is packaged, passthrough, remux or transcode; resume true or false; phase request (the stream not asked for yet), playlist, init, first_segment or decode (the segments in, no frame); anything else, or nothing, is unknown. device is tv, mobile or desktop: by the client the event names (the TV and mobile apps send one), else by the request's User-Agent; what is not recognised as a TV, a phone or a tablet is desktop.
  • Values are checked: a negative one, or one that is no number, is not observed (the rebuffer is still counted); one beyond reason is observed at a cap: 600 s for a start or a first byte, 3600 s for a stall, 10 Gbit/s.
  • Besides: chino_player_media_errors_total{code} (media_error), chino_player_quality_switches_total{from,to} (quality_switch), chino_player_resume_outcomes_total{outcome} (resume_accept, resume_decline), chino_player_stall_recoveries_total (stall_recover), chino_player_progress_saves_total, and chino_api_http_requests_total and chino_api_http_request_duration_seconds by route and status.

What chino-web sends, and how often, is in its README.

Extras

A movie's or a series' extras - its trailers, teasers, featurettes and other bonus material, each a file of its own that the catalog has packaged for streaming - come with its detail, GET /api/v1/items/{id}, beside trailers, in the order a viewer sees them:

{
  "id": "9c4e7a12-3b5d-4f60-8a91-0e2d4c6b8f13", "type": "movie", "title": "A Film",
  "trailers": [{"site": "YouTube", "external_id": "x1", "url": "https://www.youtube.com/watch?v=x1", "title": "Official Trailer"}],
  "extras": [
    {"id": "1b5c2a8e-6f0d-4c3e-9a51-2d7f0c4b8e01", "kind": "trailer", "title": "Trailer", "language": "en",
     "duration_ms": 33000, "local": true,
     "play_path": "/api/v1/items/9c4e7a12-3b5d-4f60-8a91-0e2d4c6b8f13/extras/1b5c2a8e-6f0d-4c3e-9a51-2d7f0c4b8e01/play/master.m3u8"}
  ]
}
  • trailers stays what it was: the title's links to online videos, url and all. Installed clients read every entry as such a link (mobile and TV need url, every client opens it outside the app), so a trailer this server plays is never in it: it is an extra.
  • An extra is id, kind (trailer, teaser, featurette, behind-the-scenes, making-of, deleted-scene, interview, gag-reel, short, other; a client skips kinds it does not know), title and, when known, language and duration_ms; a series' extra of one season has season_number (0 the specials). local is always true. extras is omitted when none plays, and while the catalog has no extras yet.
  • play_path is the extra's HLS master. A client asks for it as for a title's master, with ?stream=<token>&caps=<caps> (q=<rung id> for one rung), and the master's URIs carry the query on. chino-stream serves it from the extra's package, HEVC only as a title's is, and only under its own title; a client that decodes none of its rungs gets the on-the-fly master, transcoded from the package, its rung (high, medium or low) and audio tracks under the extra's own routes. Its routes are in the spec.
  • The routes of a title's extras hold a capped viewer to the title's rating: the 404 of the title there too.
  • Its /play/info (/api/v1/items/{id}/extras/{extraId}/play/info, the same query) is chino-stream's of the extra, as a title's is: how it plays for the client's caps and q, packaged or on the fly, and its own pictureArea.
  • An extra has no progress, watched, segments, trickplay or /play/prewarm: a player of extras sends none of them, and Continue Watching is not touched.
  • When an extra is packaged, GET /api/v1/events sends its title a note, phase extra.packaged, and the detail lists it on the next fetch. The extras' topic (<prefix>catalog.extra.packaged) is tailed by a consumer group of its own once it exists on the cluster, asked every minute until then, so a cluster not provisioned for extras yet loses none of the other notes.

One file, several episodes

A file can hold several episodes - a double-length finale listed as two - and is never split: it is packaged once, and the catalog names it on the episodes it holds. The first of them, the holder, keeps the file; each other is covered, keeps its own title, overview and artwork, and plays the holder's package. An episode's detail, the item, a series' episodes and every list carry it under the catalog's names:

{"id": "e15", "type": "episode", "title": "Finale (1)", "season_number": 5, "episode_number": 15,
 "covers": ["e16"], "episodeEnd": 16}
{"id": "e16", "type": "episode", "title": "Finale (2)", "season_number": 5, "episode_number": 16,
 "coveredBy": "e15"}
  • covers (the others, in episode order) and episodeEnd (the last episode number the file holds) are on the holder, coveredBy (the holder's id) on each covered episode. Each is omitted on every other item, and while the catalog sends none of them.
  • Progress and watched are the file's. A write for any episode of the file - POST /api/v1/items/{id}/progress, POST and DELETE /api/v1/me/items/{id}/watched - is written for each of them, the same position, the same watched or unwatched. Reads stay per episode, and each has the file's state. Which episodes a file holds chino-api asks the catalog, and keeps a minute; when it cannot say, the write is the episode's alone, as before.
  • The next episode after a file is the one after its last episode: GET /api/v1/series/{id}/next-episode, and the up-next cards of Continue Watching, never offer an episode of the file just watched. Continue Watching has one card for a file: the holder's while it is unfinished.
  • Playback needs nothing new: a covered episode's play routes, with its own id, serve the holder's package, and a capped viewer is held to the covered episode's rating.
  • A client can show the holder as S05E15–16 (episode_number to episodeEnd), and a covered episode as part of it.
  • In a series' episode list (GET /api/v1/series/{id}/episodes) the holder carries file_duration_ms: how long its file is, as chino-stream measured it from the package. A client shows the file as one entry of that length around the episodes it holds; duration_ms stays each episode's own.

Finished titles

A title whose saved position is in its last 5 %, or its last 2 minutes where that is longer, is finished - never by more than its second half, so a short is not finished at its first second, and never where its length is not known. One rule (store.Finished) decides it wherever a resume point or the watched state is served:

  • POST /api/v1/items/{id}/progress of a finished position marks the title watched, as the players' own POST /api/v1/me/items/{id}/watched does, whichever client played it - but with "dismiss": true: a client taking a title off Continue Watching saves it at its end, unwatched.
  • GET /api/v1/items/{id}/progress has no resume point for it: position_sec is 0, with "finished": true, and the clients show Play from the start and the watched mark - no Resume, no Start over.
  • GET /api/v1/me/continue-watching drops a finished film, and puts the next episode in a finished one's place: with that one's own resume point where it has one, a card like any other in progress; else at 0, up_next.

Downloads

A phone keeps downloads (packaged titles fetched from the playback routes) and plays them without a connection; GET /api/config's features.downloads says whether it may offer them. Two routes serve it.

  • POST /api/v1/me/available with {"ids": [...]} (at most 500) answers {"ids": [...]}: the ids asked about that the caller may watch now, in their order, each once. A capped viewer is held to its cap as on every route of one title - a title above it, an unrated one the catalog's setting hides and an id of no title are left out; for a viewer without a cap the catalog decides, a title it holds. The phone asks it as the app starts and comes to the front, and before a download plays, and deletes the downloads the answer leaves out: the library decides, for every account, and a parent lowering a child's cap takes what is above it off the child's phone. An answer that leaves out every id asked also says how many titles of the library the caller may watch, {"ids": [], "titles": n}: above 0 the empty answer is the library's word, even for an account whose only download it is; 0 is a catalog that shows the caller nothing (imported anew, reset), and the phone does not take a title off on that alone. When the catalog cannot say, the answer is 502 and no ids, and the phone deletes nothing.
  • POST /api/v1/items/{id}/progress takes played_ms_ago, the age of the position when it is sent, in milliseconds: a position the phone kept while it had no connection. It is the position at now() minus the age (the phone's clock does not matter), written only where the saved one is older, and a finished one marks the title watched only where it was written. A film finished on the TV stays finished when the phone's earlier second comes in later. Without the field a position is now's, written over any, as every other client's is.
  • POST /api/v1/me/items/{id}/watched takes the same played_ms_ago, as an optional JSON body: a watched mark the phone kept while it had no connection. The title is watched at now() minus the age, or later where it was watched again since. Without a body the mark is now's, as before.

Local development

go run ./cmd/server
# in another shell
curl -sS http://localhost:8080/api/healthz

Disable OIDC for local poking:

OIDC_ENABLED=false go run ./cmd/server
curl -sS http://localhost:8080/api/v1/items

Where a client cannot set an Authorization header (an <img> or <video> URL, sendBeacon, an EventSource), it authenticates with ?stream=<token>, a stream token from POST /api/v1/me/stream-token. The bearer in the URL (?token=) is deprecated: it is still accepted in this release and goes in the next. The request log shows credential values as REDACTED.

When PG_URL is empty, progress and telemetry endpoints answer gracefully but do not persist — keeps local dev simple. When OPENPROJECT_TOKEN is empty the feedback endpoint answers 503 and clients keep the feature off: GET /api/config says so in features.feedback (true only with a token), so a client can hide its bug-report entry points before anyone tries.

features.downloads says whether a client may offer downloads, a packaged title kept on a phone to play without a connection: true unless DOWNLOADS_ENABLED is false (or 0, no, off). A download is fetched from the playback routes, as a stream is, so the switch changes nothing else on the server; a client offers no download while it is false.

Configuration

Configured entirely through environment variables (see internal/config):

Var Purpose
ADDR listen address (default :8080)
OIDC_ISSUER / OIDC_AUDIENCE / OIDC_ENABLED OIDC JWT validation
KATALOG_BASE_URL catalog metadata service
STREAM_BASE_URL chino-stream (HLS / trickplay / play info)
ARTWORK_BASE_URL katalog-manager, for artwork
KATALOG_MANAGER_URL katalog-manager, where the admin packaging routes go with the admin's bearer and the subtitle languages are asked for with the viewer's (default http://katalog-manager-api; ANALYZER_BASE_URL, its former name, is read when it is unset)
PG_URL Postgres URL for user-state (optional)
ADMIN_ROLE the realm role (realm_access.roles of the access token) that opens /api/v1/admin/* (default zaentrum-admin, as katalog-manager and the portal)
ADMIN_SUBJECTS deprecated: comma-separated OIDC sub values let through /api/v1/admin/* besides the role; empty by default, and it goes in a coming release
OPENPROJECT_URL / OPENPROJECT_TOKEN / OPENPROJECT_PROJECT_ID / OPENPROJECT_BUG_TYPE_ID feedback pipeline (optional)
DOWNLOADS_ENABLED whether /api/config lets the apps offer downloads (features.downloads; default true)
STREAM_SIGNING_KEY shared HMAC secret for signed ?stream= URLs (optional)
ACCOUNT_DELETION_TOKEN the token chino-api and portal-api delete an account with (Secret zaentrum-people, key deletion-token, on the platform); empty: DELETE /api/v1/me answers 501
PORTAL_BASE_URL portal-api, for addon slots, notices and account deletion (default http://portal-api)

Layout

cmd/server/main.go                  process entry
internal/config/                    env wiring
internal/http/router.go             chi router + middleware
internal/http/openapi.{go,yaml}     embedded OpenAPI spec
internal/http/                      browse / watchlists / progress / feedback / ... handlers
internal/auth/                      Bearer JWT verifier + stream-token middleware
internal/katalog/                   catalog + people read clients
internal/store/                     Postgres store + SQL migrations
internal/openproject/               minimal OpenProject client (feedback)
internal/metrics/                   Prometheus surface
k8s/                                sample Deployment / Service / Route / ServiceMonitor / Dashboard
Dockerfile                          distroless multi-stage build

Build the container

docker build -t zaentrum/chino-api .

The k8s/ manifests are samples. Build and push the image to your own registry, adjust the env values and hostnames for your environment, and deploy.

License

MPL-2.0.

About

chino-api — catalog/playback API for chino on the zaentrum platform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages