OpenSeek

Seek-preview thumbnails for video players, delivered as sprite sheets and a WebVTT cue track. A player looks up one title, reads one cue for the current playback position, and crops one tile out of a sheet.

What this is

OpenSeek indexes films by metadata id and duration, serves the preview assets, and merges new coverage from players that watch.

SDK

Two packages, both dependency-free:

Source: github.com/Yatin-Code/openseek-sdk. Publishing is prepared but not live: the npm package is named @openseek/sdk and the Maven coordinates are tv.openseek:openseek-sdk-jvm, but neither is on a public registry yet. Until then, read the sources in the repo.

Kotlin — playback

import openseek.OpenSeekClient
import openseek.thumbnailFor

val client = OpenSeekClient("https://openseek.duckdns.org",
                            apiKey = System.getenv("OPENSEEK_KEY"))
val track = client.loadMovieTrack(imdbId = "tt0111161",
                                 durationMs = player.duration) ?: return
// during a scrub, positionMs = current playback position:
val cue = track.thumbnailFor(positionMs) ?: return   // null past coverage
val tile = client.cropTile(client.sheetBytes(cue.imageUrl), cue)

JavaScript — playback

import { loadMovieTrack, paintPeek } from "@openseek/sdk";

const track = await loadMovieTrack("https://openseek.duckdns.org",
  { imdb_id: "tt0111161", duration_ms: video.duration * 1000 });
if (!track) return;                       // not indexed: no preview
// during a scrub, positionMs = current playback position:
const shown = await paintPeek(canvas, track, positionMs);
shown ? popup.show() : popup.hide();      // false = past covered_until_ms

Both packages use floor semantics: the last cue at or before the position, corrected by the scale the registry reports, and bounded by covered_until_ms.

Get API

The running registry is at https://openseek.duckdns.org.

Getting a key

Keys are minted on the registry host and printed once:

python3 mint_key.py <name>

Revoke one by deleting its row:

DELETE FROM keys WHERE key='…';

Lookup example

curl -H 'X-API-Key: <your key>' \
  'https://openseek.duckdns.org/v1/sprites?imdb_id=tt0111161&duration_ms=8280000'

The response:

{
  "media_type": "movie",
  "title": "The Shawshank Redemption",
  "imdb_id": "tt0111161",
  "vtt_url": "https://openseek.duckdns.org/s/tt0111161-v1/thumbnails.vtt",
  "version": 1,
  "source_duration_ms": 8280000,
  "scale": 1.0,
  "status": "complete",
  "covered_until_ms": 8280000
}

scale is the ratio of the requested duration to the indexed source duration, so a player reporting a slightly different runtime still lands on the right cue. status is pending while coverage is partial and complete when it reaches the end.

Limits and current policy

Open-source guide

Run it yourself

Four components:

Shortest path to a working local setup:

# 1. generate previews for a local file (needs ffmpeg + Pillow)
python3 spritegen/spritegen.py --id tt0111161 \
    --in movie.mp4 --out store/tt0111161/ --interval 10

# 2. serve the store (standard library only)
python3 server/server.py --store ./store --port 8080

# 3. or run the registry with its own database
python3 registry/init_db.py
python3 registry/registry.py --port 8081

init_db.py is idempotent: it applies the schema and then any migrations, so it is safe to re-run on an existing database. The generated store/ is a plain directory of files; no database is needed to serve it. The end-to-end test runs on the standard library alone, with no ffmpeg and no Pillow:

python3 tests/test_e2e.py

MIT licensed.

Work on it / contribute

Repositories:

Repo map at a glance: packages/kotlin and packages/js are the SDKs; docs/wire.md pins the request and response contract; docs/PUBLISHING.md holds the release checklist. On the service side the generator, the static server and the registry are separate top-level directories.

How a contribution actually travels:

  1. The player banks tiles as the title plays — one tile per interval step, kept in memory and then on disk.
  2. At the bundle boundary (48 tiles) it flushes them as a multipart POST /v1/contribute carrying a JSON metadata part, thumbnails.vtt and the sheet-*.jpg files.
  3. The registry stores the upload in quarantine. Nothing under /s/ is served from quarantine.
  4. Verification runs on the quarantined copy: the VTT must parse, every cue must reference a sheet that exists, the tile boxes must be inside the sheet image, sampled tiles must decode as valid non-blank JPEGs, cue count must be at least 5 and the interval must be one of 5, 10 or 30 s.
  5. On success the contribution is promoted: it becomes a new version, or it merges slot by slot into an existing version when the title and duration match within ±15 s. Slots are positional (cue i is second i × interval), the first writer wins on overlap, and colliding sheet filenames are renamed rather than rewritten, so bytes a player already cached never change. Coverage extends to the new last cue.
  6. If it cannot merge, it is rejected with a reason. Duplicate submissions, interval mismatch and tile-size mismatch are the three rejections.

Licence and source

MIT licensed. Everything above is open source and self-hostable: run your own generator, your own store and your own registry, and point your players at it.