Bản lưu ngày / Snapshot of 2026-10-15 · Xem bản mới nhất / See the latest version
WanduraDocsBeta
BetaThese docs are in beta and change often. Something wrong, unclear or missing? Tell us.
You are reading an older version of these docs: v1 · Go to the latest (v2)
Guides

Open video sync

Markdown
On this page

TL;DR — Your server sweeps every page of GET /videos with the app's key, keeps the whole result, and serves links from it until refresh_before. Stop only on next_cursor === null, never on a short page. Retry 429/503; if a sweep fails, keep serving the previous list.

Before you start

  • One key per app, with videos:read, bound to that app (Open video).
  • Which fields to sync on: id, never title.

The sweep

  1. Call GET /videos?limit=200, then again with cursor=<next_cursor> (URL-encoded, unchanged) until next_cursor is null. A short or empty page does not mean the end: videos not ready to play are skipped.
  2. Key rows by id. Store the order_code ↔ id pair once if you join against an order file; never join on title (titles repeat).
  3. Take the earliest refresh_before of all pages: earlier pages expire first.
  4. Replace the list your apps see only when the sweep completed. If it failed, keep serving the previous list (its links stay valid until their own expiry) and try again in a minute.
  5. Sweep again before the earliest refresh_before.

Code

/** Every page or an exception (429/503/network are retried inside `call`). Never replace your list with a partial sweep. */
export async function sweep(o: CallOptions = {}): Promise<{ rows: OpenVideo[]; refreshAt: Date }> {
  const rows = new Map<string, OpenVideo>();
  let cursor: string | null = null;
  let refreshAt: Date | null = null;
  for (let page = 0; page < 10_000; page++) {
    const qs: string = `limit=200${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`;
    const { data }: { data: Page } = await call<Page>('GET', `/api/delivery/v1/videos?${qs}`, { policy: BACKGROUND, ...o });
    for (const r of data.rows) rows.set(r.id, r); // key by `id`, never by title
    const rb = new Date(data.refresh_before); // earlier pages expire first: keep the EARLIEST deadline
    if (!refreshAt || rb < refreshAt) refreshAt = rb;
    cursor = data.next_cursor;
    if (cursor === null) return { rows: [...rows.values()], refreshAt }; // a short or empty page is NOT the end
  }
  throw new Error('open video sweep did not end');
}
let current: { rows: OpenVideo[]; refreshAt: Date } | null = null;

/** Keeps `current` fresh. On failure keeps serving the previous list and tries again in a minute. */
export async function refreshLoop(o: CallOptions = {}): Promise<void> {
  let delay = 60_000;
  try {
    current = await sweep(o);
    delay = Math.max(60_000, current.refreshAt.getTime() - Date.now());
  } catch {
    // previous links stay valid until their own expiry
  }
  setTimeout(() => void refreshLoop(o), delay).unref();
}
/** What your app receives: links from your server's cache. Apps never store them. */
export const currentRows = (): OpenVideo[] => current?.rows ?? [];

Retries happen inside call, under the canonical retry policy. The docs CI runs this code against a fake API whose first page answers 429, to prove the sweep does not end empty.

Fields

Every field of a row: List open videos. The ones that trip people:

  • mp4.p720 can be null: there is no 720p version. Do not substitute p480.
  • order_code is always present and may be null.
  • difficulty and kcal_est are currently always null.

Done when

  • A sweep whose first page answers 429 still ends with the full list.
  • Apps get links from your server, never from storage.
  • The list is refreshed before the earliest refresh_before.

Updated 5 Oct 2026 · Change history (1)

↑↓ selectEnter openEsc close

Send feedback

Your message goes straight to the Wandura team. It includes the address of this page.

0 / 4000