Hướng dẫn
TL;DR — Server của bạn quét mọi trang của
GET /videosbằng key của app, giữ toàn bộ kết quả, và phục vụ link từ đó cho tớirefresh_before. Chỉ dừng khinext_cursor === null, không bao giờ dừng vì một trang ngắn. Retry429/503; nếu một lượt quét hỏng, tiếp tục phục vụ danh sách trước đó.
Trước khi bắt đầu
- Mỗi app một key, có
videos:read, gắn với app đó (Open video). - Đồng bộ theo trường nào:
id, không bao giờ theotitle.
Lượt quét
- Gọi
GET /videos?limit=200, rồi gọi lại vớicursor=<next_cursor>(URL-encode, giữ nguyên) cho tới khinext_cursorlànull. Một trang ngắn hay rỗng không có nghĩa là đã hết: các video chưa sẵn sàng phát bị bỏ qua. - Đặt khoá dòng theo
id. Lưu cặporder_code↔idmột lần nếu bạn ghép với một tệp đơn hàng; không bao giờ ghép theotitle(tên có thể trùng). - Lấy
refresh_beforesớm nhất trong mọi trang: các trang trước hết hạn trước. - Chỉ thay danh sách app của bạn thấy khi lượt quét đã hoàn tất. Nếu lượt quét hỏng, tiếp tục phục vụ danh sách trước đó (link của nó vẫn hợp lệ tới khi chính nó hết hạn) và thử lại sau một phút.
- Quét lại trước
refresh_beforesớm nhất.
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 ?? [];Retry diễn ra bên trong call, theo chính sách retry chuẩn. CI của docs chạy code này với một API giả có trang đầu trả 429, để chứng minh lượt quét không kết thúc với danh sách rỗng.
Các trường
Mọi trường của một dòng: Liệt kê open video. Những trường hay làm người ta vấp:
mp4.p720có thể lànull: không có bản 720p. Đừng thay bằngp480.order_codeluôn có mặt và có thể lànull.difficultyvàkcal_esthiện luôn lànull.
Xong khi
- Lượt quét có trang đầu trả
429vẫn kết thúc với đầy đủ danh sách. - App lấy link từ server của bạn, không bao giờ lấy từ bộ nhớ lưu trữ.
- Danh sách được làm mới trước
refresh_beforesớm nhất.
Liên quan
Cập nhật 05/10/2026 · Lịch sử thay đổi (1)