الخطة
القائمة لا تُحسب سحبًا، والسجل يُحسب. لذلك:
- امشِ على صفحات القائمة كلها، واحفظ بطاقة كل جهة:
slugوالاسم والدولة والصورة المصغرة. هذا يكفي لقائمة وبحث عندك. - اقرأ سجل الجهة حين يفتحها مستخدم أول مرة، واحفظه.
- مرة في اليوم، امشِ على القائمة بـ
sort=updated(الأحدث تعديلًا أولًا)، وأعد قراءة السجلات التي حفظتها، حتى تصل إلى سجلupdatedAtفيه أقدم من آخر مرة، ثم توقف.
هكذا لا تُصرف السحوبات إلا على جهات فتحها مستخدموك، أو تغيّرت بعد أن حفظتها.
المرور على القائمة
walk.js
// Every card of the archive, page by page. Lists are not pulls.const base = 'https://new.wasmenia.com';const headers = { Authorization: `Bearer ${process.env.WASMENIA_API_KEY}` };const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));export async function* allBrands(filters = {}) { for (let page = 1; ; page++) { const url = new URL('/api/v1/brands', base); for (const [k, v] of Object.entries({ ...filters, limit: 50, page })) url.searchParams.set(k, String(v)); const res = await fetch(url, { headers }); if (res.status === 429) { await sleep(Number(res.headers.get('Retry-After') ?? 60)); page--; // the same page again continue; } if (!res.ok) throw new Error(`HTTP ${res.status}`); const { items, pageCount } = await res.json(); yield* items; // { slug, name, country, category, verified, versions, thumbnailUrl, url } if (page >= pageCount || page >= 500) return; }}for await (const brand of allBrands({ country: 'SA' })) { await db.cards.upsert(brand.slug, brand);}حدود المرور
limit حتى 50، وpage حتى 500، فأقصى ما تمر عليه القائمة بفلاتر واحدة 25,000 جهة. إن كان total أكبر من ذلك، قسّم المرور بفلتر: دولة بعد دولة (country) أو قطاعًا بعد قطاع (category).
حد القائمة 60 طلبًا في الدقيقة بلا مفتاح، وحد باقتك بمفتاح. الكود أعلاه ينتظر Retry-After حين يُرد 429 ثم يعيد الصفحة نفسها.
التحديث اليومي
refresh.js
// Once a day: re-read what changed since the last run, newest change first.const base = 'https://new.wasmenia.com';const headers = { Authorization: `Bearer ${process.env.WASMENIA_API_KEY}` };export async function refresh(since /* Date of the last run */) { for (let page = 1; page <= 500; page++) { const list = await fetch(`${base}/api/v1/brands?sort=updated&limit=50&page=${page}`, { headers }); if (!list.ok) throw new Error(`HTTP ${list.status}`); const { items, pageCount } = await list.json(); for (const card of items) { if (!(await db.records.has(card.slug))) continue; // never opened: nothing to refresh const res = await fetch(`${base}/api/v1/brands/${card.slug}`, { headers }); // a pull if (res.status === 402) return; // the month's pulls are used: try after X-Quota-Reset if (res.status === 404) { await db.records.delete(card.slug); continue; } if (!res.ok) throw new Error(`HTTP ${res.status}`); const brand = await res.json(); // A renamed brand answers under its new slug (308, followed by fetch). if (brand.slug !== card.slug) await db.records.rename(card.slug, brand.slug); await db.records.put(brand.slug, brand); if (new Date(brand.updatedAt) < since) return; // older than the last run: done } if (page >= pageCount) return; }}حين يتغيّر المعرّف
إن غيّرت جهة slug في الأرشيف، يحوّل العنوان القديم إلى الجديد بـ 308، ويتبعه fetch وrequests دون أن تفعل شيئًا. قارن slug في السجل بما طلبته: إن اختلفا، انقل ما حفظته إلى المعرّف الجديد. والجهة التي يُرد عليها 404 لم تعد منشورة: احذفها من نسختك.
ما تعرضه من نسختك
اعرض من نسختك ما حفظته كما هو، ومعه تاريخ آخر قراءة إن احتاج القارئ أن يعرف. واجعل الصور من thumbnailUrl وurl كما جاءت، فهي ثابتة على دومين الملفات ولا تُحسب تحميلًا. تفاصيل الملفات في الملفات والتحميل.