Guest User

Grok: download all files

a guest
Aug 6th, 2026
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 14.50 KB | Software | 0 0
  1. // ==UserScript==
  2. // @name         Grok: download all files
  3. // @namespace    local.grok-tools
  4. // @version      1.0.0
  5. // @description  Scans every Grok asset page and saves all versions to a folder selected once by the user.
  6. // @match        https://grok.com/files*
  7. // @run-at       document-idle
  8. // @grant        none
  9. // ==/UserScript==
  10.  
  11. (() => {
  12.   'use strict';
  13.  
  14.   const PAGE_SIZE = 50;
  15.   const PARALLEL_DOWNLOADS = 3;
  16.   const RETRIES = 3;
  17.   const LIST_REQUEST_INTERVAL_MS = 1250;
  18.   const MAX_RATE_LIMIT_RETRIES = 8;
  19.   const MANIFEST_FILE = 'grok-download-manifest.json';
  20.  
  21.   /** @type {FileSystemDirectoryHandle | null} */
  22.   let destination = null;
  23.   /** @type {Array<Record<string, unknown>>} */
  24.   let assets = [];
  25.   /** @type {Map<string, { status: string, error?: string }>} */
  26.   const results = new Map();
  27.   let shouldStop = false;
  28.   let isBusy = false;
  29.   let nextListRequestAt = 0;
  30.  
  31.   const root = document.createElement('section');
  32.   root.id = 'grok-all-files-downloader';
  33.   root.innerHTML = `
  34.     <style>
  35.       #grok-all-files-downloader { position: fixed; right: 18px; bottom: 18px; z-index: 2147483647; width: 350px; color: #f4f4f5; font: 13px/1.4 system-ui, sans-serif; }
  36.       #grok-all-files-downloader .box { background: #18181b; border: 1px solid #3f3f46; border-radius: 12px; box-shadow: 0 12px 40px #0009; padding: 14px; }
  37.       #grok-all-files-downloader h2 { margin: 0 0 8px; font-size: 15px; }
  38.       #grok-all-files-downloader p { margin: 7px 0; color: #d4d4d8; }
  39.       #grok-all-files-downloader .folder { overflow-wrap: anywhere; color: #a1a1aa; }
  40.       #grok-all-files-downloader .buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin: 10px 0; }
  41.       #grok-all-files-downloader button { border: 0; border-radius: 7px; padding: 8px; background: #3f3f46; color: white; cursor: pointer; font: inherit; }
  42.       #grok-all-files-downloader button.primary { background: #2563eb; }
  43.       #grok-all-files-downloader button.stop { background: #b91c1c; }
  44.       #grok-all-files-downloader button:disabled { opacity: .48; cursor: not-allowed; }
  45.       #grok-all-files-downloader progress { width: 100%; height: 9px; }
  46.       #grok-all-files-downloader details { margin-top: 9px; }
  47.       #grok-all-files-downloader pre { max-height: 110px; overflow: auto; margin: 6px 0 0; padding: 7px; border-radius: 6px; background: #09090b; white-space: pre-wrap; color: #d4d4d8; }
  48.     </style>
  49.     <div class="box">
  50.       <h2>Grok — выгрузка файлов</h2>
  51.       <p class="folder">Папка не выбрана</p>
  52.       <div class="buttons">
  53.         <button data-action="folder">Выбрать папку</button>
  54.         <button data-action="scan">Сканировать всё</button>
  55.         <button class="primary" data-action="download">Скачать всё</button>
  56.         <button class="stop" data-action="stop" disabled>Остановить</button>
  57.       </div>
  58.       <progress value="0" max="1"></progress>
  59.       <p class="status">Готово. Будут запрошены все версии файлов.</p>
  60.       <details>
  61.         <summary>Журнал</summary>
  62.         <pre></pre>
  63.       </details>
  64.     </div>`;
  65.   document.body.append(root);
  66.  
  67.   const ui = {
  68.     folder: root.querySelector('.folder'),
  69.     status: root.querySelector('.status'),
  70.     progress: root.querySelector('progress'),
  71.     log: root.querySelector('pre'),
  72.     buttons: Object.fromEntries([...root.querySelectorAll('button')].map((button) => [button.dataset.action, button])),
  73.   };
  74.  
  75.   function setBusy(value) {
  76.     isBusy = value;
  77.     ui.buttons.folder.disabled = value;
  78.     ui.buttons.scan.disabled = value;
  79.     ui.buttons.download.disabled = value;
  80.     ui.buttons.stop.disabled = !value;
  81.   }
  82.  
  83.   function setStatus(message) {
  84.     ui.status.textContent = message;
  85.   }
  86.  
  87.   function setProgress(value, maximum) {
  88.     ui.progress.max = Math.max(maximum, 1);
  89.     ui.progress.value = Math.min(value, ui.progress.max);
  90.   }
  91.  
  92.   function log(message) {
  93.     const time = new Date().toLocaleTimeString();
  94.     ui.log.textContent = `[${time}] ${message}\n${ui.log.textContent}`.slice(0, 12000);
  95.   }
  96.  
  97.   function assertNotStopped() {
  98.     if (shouldStop) throw new DOMException('Операция остановлена пользователем.', 'AbortError');
  99.   }
  100.  
  101.   async function waitWithStop(milliseconds) {
  102.     const deadline = Date.now() + milliseconds;
  103.     while (Date.now() < deadline) {
  104.       assertNotStopped();
  105.       await new Promise((resolve) => setTimeout(resolve, Math.min(250, deadline - Date.now())));
  106.     }
  107.   }
  108.  
  109.   function retryAfterMilliseconds(value, retry) {
  110.     if (value) {
  111.       const seconds = Number(value);
  112.       if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
  113.       const date = Date.parse(value);
  114.       if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
  115.     }
  116.     return Math.min(60_000, 5_000 * 2 ** retry);
  117.   }
  118.  
  119.   async function chooseFolder() {
  120.     if (!window.showDirectoryPicker) {
  121.       throw new Error('Этот браузер не поддерживает выбор папки. Нужен Chrome или Edge последней версии.');
  122.     }
  123.     destination = await window.showDirectoryPicker({ mode: 'readwrite' });
  124.     ui.folder.textContent = `Папка: ${destination.name}`;
  125.     log(`Выбрана папка «${destination.name}».`);
  126.   }
  127.  
  128.   function listUrl(isLatest, pageToken) {
  129.     const query = new URLSearchParams({
  130.       pageSize: String(PAGE_SIZE),
  131.       orderBy: 'ORDER_BY_LAST_USE_TIME',
  132.       source: 'SOURCE_ANY',
  133.       isLatest: String(isLatest),
  134.       includeImagineFiles: 'true',
  135.     });
  136.     if (pageToken) query.set('pageToken', pageToken);
  137.     return `/rest/assets?${query}`;
  138.   }
  139.  
  140.   async function fetchPage(isLatest, pageToken) {
  141.     for (let retry = 0; retry <= MAX_RATE_LIMIT_RETRIES; retry += 1) {
  142.       const intervalWait = Math.max(0, nextListRequestAt - Date.now());
  143.       if (intervalWait) {
  144.         setStatus(`Пауза ${Math.ceil(intervalWait / 1000)} с перед следующим запросом списка…`);
  145.         await waitWithStop(intervalWait);
  146.       }
  147.       assertNotStopped();
  148.       nextListRequestAt = Date.now() + LIST_REQUEST_INTERVAL_MS;
  149.       const response = await fetch(listUrl(isLatest, pageToken), { credentials: 'same-origin' });
  150.       if (response.status === 429) {
  151.         if (retry === MAX_RATE_LIMIT_RETRIES) break;
  152.         const wait = retryAfterMilliseconds(response.headers.get('Retry-After'), retry);
  153.         nextListRequestAt = Math.max(nextListRequestAt, Date.now() + wait);
  154.         setStatus(`Grok ограничил частоту запросов. Повтор через ${Math.ceil(wait / 1000)} с…`);
  155.         log(`HTTP 429 для страницы списка; ожидание ${Math.ceil(wait / 1000)} с, попытка ${retry + 1}/${MAX_RATE_LIMIT_RETRIES}.`);
  156.         await waitWithStop(wait);
  157.         continue;
  158.       }
  159.       if (!response.ok) {
  160.         throw new Error(`Не удалось получить список файлов: HTTP ${response.status}. Запрос: ${listUrl(isLatest, pageToken)}`);
  161.       }
  162.       const body = await response.json();
  163.       if (!Array.isArray(body.assets)) throw new Error('Ответ /rest/assets не содержит массива assets.');
  164.       return body;
  165.     }
  166.     throw new Error(`Grok продолжает возвращать HTTP 429 после ${MAX_RATE_LIMIT_RETRIES} повторов. Запустите выгрузку позже.`);
  167.   }
  168.  
  169.   function uniqueAssets(items) {
  170.     const byId = new Map();
  171.     for (const asset of items) {
  172.       if (asset?.assetId) byId.set(asset.assetId, asset);
  173.     }
  174.     return [...byId.values()];
  175.   }
  176.  
  177.   async function scan() {
  178.     shouldStop = false;
  179.     setBusy(true);
  180.     try {
  181.       const collected = [];
  182.       let page = 0;
  183.       // isLatest is an equality filter, not a switch for disabling the filter.
  184.       // Scan both values so the union contains current and historical assets.
  185.       for (const isLatest of [true, false]) {
  186.         const seenTokens = new Set();
  187.         let token = undefined;
  188.         do {
  189.           assertNotStopped();
  190.           page += 1;
  191.           setStatus(`Получение ${isLatest ? 'актуальных' : 'старых'} версий, страница ${page}…`);
  192.           const body = await fetchPage(isLatest, token);
  193.           collected.push(...body.assets);
  194.           setProgress(collected.length, collected.length + (body.nextPageToken ? PAGE_SIZE : 0));
  195.           token = body.nextPageToken || undefined;
  196.           if (token && seenTokens.has(token)) throw new Error(`API вернул повторяющийся nextPageToken (${token}); выгрузка остановлена, чтобы не зациклиться.`);
  197.           if (token) seenTokens.add(token);
  198.         } while (token);
  199.       }
  200.  
  201.       assets = uniqueAssets(collected);
  202.       results.clear();
  203.       setProgress(assets.length, assets.length);
  204.       setStatus(`Найдено ${assets.length} уникальных файлов. Можно запускать скачивание.`);
  205.       log(`Сканирование завершено: страниц ${page}, файлов ${assets.length}.`);
  206.       await writeManifest();
  207.     } finally {
  208.       setBusy(false);
  209.     }
  210.   }
  211.  
  212.   function sanitizeFilename(value) {
  213.     const name = String(value || 'unnamed').replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_').replace(/[. ]+$/g, '');
  214.    return (name || 'unnamed').slice(0, 170);
  215.  }
  216.  
  217.  function filenameFor(asset) {
  218.    const prefix = asset.rRated === true ? 'r_' : '';
  219.    return `${prefix}${asset.assetId}_${sanitizeFilename(asset.name)}`;
  220.  }
  221.  
  222.  async function writeFile(name, blob) {
  223.    const handle = await destination.getFileHandle(name, { create: true });
  224.    const writable = await handle.createWritable();
  225.    try {
  226.      await writable.write(blob);
  227.    } finally {
  228.      await writable.close();
  229.    }
  230.  }
  231.  
  232.  async function writeManifest() {
  233.    if (!destination || !assets.length) return;
  234.    const manifest = {
  235.      generatedAt: new Date().toISOString(),
  236.      filter: { isLatest: [true, false], source: 'SOURCE_ANY', includeImagineFiles: true },
  237.      total: assets.length,
  238.      completed: [...results.entries()].filter(([, value]) => value.status === 'done').length,
  239.      failed: [...results.entries()].filter(([, value]) => value.status === 'failed').length,
  240.      assets,
  241.      results: Object.fromEntries(results),
  242.    };
  243.    await writeFile(MANIFEST_FILE, new Blob([JSON.stringify(manifest, null, 2)], { type: 'application/json' }));
  244.  }
  245.  
  246.  async function downloadOne(asset) {
  247.    assertNotStopped();
  248.    if (!asset.key) throw new Error(`У ${asset.assetId} отсутствует key для скачивания.`);
  249.    const name = filenameFor(asset);
  250.    const existing = await destination.getFileHandle(name, { create: true });
  251.    const existingFile = await existing.getFile();
  252.    // A non-empty file from an earlier run is considered complete. A file that
  253.    // failed during this session is retried and overwritten instead.
  254.    if (existingFile.size > 0 && results.get(asset.assetId)?.status !== 'failed') return 'skipped';
  255.  
  256.    let lastError;
  257.    for (let attempt = 1; attempt <= RETRIES; attempt += 1) {
  258.      try {
  259.        assertNotStopped();
  260.        const response = await fetch(`https://assets.grok.com/${asset.key}`, { credentials: 'include' });
  261.        if (!response.ok) throw new Error(`HTTP ${response.status}`);
  262.        const blob = await response.blob();
  263.        if (!blob.size) throw new Error('сервер вернул пустой файл');
  264.        await writeFile(name, blob);
  265.        return 'done';
  266.      } catch (error) {
  267.        lastError = error;
  268.        if (attempt < RETRIES) await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
  269.      }
  270.    }
  271.    throw lastError;
  272.  }
  273.  
  274.  async function downloadAll() {
  275.    if (!destination) throw new Error('Сначала выберите папку назначения.');
  276.    if (!assets.length) await scan();
  277.    if (!assets.length) return;
  278.  
  279.    shouldStop = false;
  280.    setBusy(true);
  281.    let cursor = 0;
  282.    let done = 0;
  283.    let skipped = 0;
  284.    let failed = 0;
  285.    setProgress(0, assets.length);
  286.  
  287.    async function worker() {
  288.      while (true) {
  289.        assertNotStopped();
  290.        const asset = assets[cursor++];
  291.        if (!asset) return;
  292.        const previous = results.get(asset.assetId);
  293.        if (previous?.status === 'done' || previous?.status === 'skipped') {
  294.          done += 1;
  295.          setProgress(done + skipped + failed, assets.length);
  296.          continue;
  297.        }
  298.        try {
  299.          const outcome = await downloadOne(asset);
  300.          results.set(asset.assetId, { status: outcome });
  301.          if (outcome === 'skipped') skipped += 1;
  302.          else done += 1;
  303.        } catch (error) {
  304.          if (error?.name === 'AbortError') throw error;
  305.          failed += 1;
  306.          results.set(asset.assetId, { status: 'failed', error: String(error?.message || error) });
  307.          log(`Ошибка ${asset.assetId}: ${error?.message || error}`);
  308.        }
  309.        setProgress(done + skipped + failed, assets.length);
  310.        setStatus(`Скачивание: ${done} готовы, ${skipped} уже есть, ${failed} ошибок из ${assets.length}.`);
  311.        if ((done + skipped + failed) % 10 === 0) await writeManifest();
  312.      }
  313.    }
  314.  
  315.    try {
  316.      await Promise.all(Array.from({ length: PARALLEL_DOWNLOADS }, worker));
  317.      await writeManifest();
  318.      setStatus(`Готово: ${done} скачано, ${skipped} уже было, ошибок: ${failed}.`);
  319.      log(`Скачивание завершено. Manifest: ${MANIFEST_FILE}`);
  320.    } finally {
  321.      setBusy(false);
  322.    }
  323.  }
  324.  
  325.  root.addEventListener('click', async (event) => {
  326.    const action = event.target.closest('button')?.dataset.action;
  327.    if (!action || isBusy && action !== 'stop') return;
  328.    try {
  329.      if (action === 'folder') await chooseFolder();
  330.      if (action === 'scan') await scan();
  331.      if (action === 'download') await downloadAll();
  332.      if (action === 'stop') {
  333.        shouldStop = true;
  334.        setStatus('Остановка после завершения текущих операций…');
  335.        log('Запрошена остановка.');
  336.      }
  337.    } catch (error) {
  338.      const message = error?.name === 'AbortError' ? 'Операция остановлена.' : `Ошибка: ${error?.message || error}`;
  339.      setStatus(message);
  340.      log(message);
  341.    }
  342.  });
  343. })();
  344.  
Tags: #grok
Advertisement
Add Comment
Please, Sign In to add comment