(async () => { const BASE = 'https://qa6.ru'; const PER_PAGE = 500; // меньше запросов к API const PAGE_CONCURRENCY = 8; // параллельные страницы const IMG_CONCURRENCY = 16; // параллельные фото const ZIP_NAME = 'qa6-models.zip'; const loadScript = (src) => new Promise((res, rej) => { const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); if (!window.JSZip) { await loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.2/dist/jszip.min.js'); } const pool = async (items, limit, fn) => { const out = new Array(items.length); let i = 0; const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); } }); await Promise.all(workers); return out; }; const firstUrl = `${BASE}/api/new-car-kit/admin/1/all-models` + `?per_page=${PER_PAGE}&page=1&sort_by=id_car_model&sort_dir=asc`; const first = await fetch(firstUrl).then((r) => r.json()); const lastPage = first.last_page || 1; console.log(`моделей: ${first.total}, страниц: ${lastPage}`); const pages = [first.data || []]; const rest = Array.from({ length: lastPage - 1 }, (_, i) => i + 2); const restData = await pool(rest, PAGE_CONCURRENCY, async (page) => { const url = `${BASE}/api/new-car-kit/admin/1/all-models` + `?per_page=${PER_PAGE}&page=${page}&sort_by=id_car_model&sort_dir=asc`; const json = await fetch(url).then((r) => r.json()); console.log(`API page ${page}/${lastPage}`); return json.data || []; }); pages.push(...restData); const models = pages.flat().filter((m) => m.image_path && String(m.image_path).trim()); console.log(`с фото: ${models.length}`); const zip = new JSZip(); const folder = zip.folder('models'); let ok = 0; let fail = 0; await pool(models, IMG_CONCURRENCY, async (m, idx) => { const path = String(m.image_path).replace(/^\/+/, ''); const url = `${BASE}/storage/${path}`; const ext = path.match(/\.[a-z0-9]+$/i)?.[0] || '.png'; const name = `${String(m.mark_name || 'mark')}_${String(m.model_name || 'model')}_${m.car_id || idx}` .replace(/[\\/:*?"<>|]+/g, '_') .replace(/\s+/g, '_') + ext; try { const res = await fetch(url); if (!res.ok) throw new Error(res.status); const buf = await res.arrayBuffer(); folder.file(name, buf); ok++; } catch (e) { fail++; console.warn('fail', url, e); } if ((ok + fail) % 50 === 0) { console.log(`фото ${ok + fail}/${models.length} (ok ${ok}, fail ${fail})`); } }); console.log(`качаю zip: ok ${ok}, fail ${fail}`); const blob = await zip.generateAsync({ type: 'blob', compression: 'STORE' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = ZIP_NAME; a.click(); URL.revokeObjectURL(a.href); console.log('готово'); })();