# Open video sync

> **This path is frozen.** It keeps working and apps already on it need change nothing, but Wandura is no longer adding to it. New integrations should use [your own video space](https://docs.wandura.ai/developers/concepts/own-space), where your app reads straight from the video platform in your organisation's space.

> **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](https://docs.wandura.ai/developers/concepts/open-video#how-it-works)).
- 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
```ts
/** 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');
}
```

```ts
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](https://docs.wandura.ai/developers/reference/behaviour#retries). 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](https://docs.wandura.ai/developers/reference/videos-list). 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`.

## Related
- [Open video](https://docs.wandura.ai/developers/concepts/open-video)
