Eugene666

grok mass media download webworker

May 14th, 2026 (edited)
219
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Grok Media Asset Manager v4.7 (worker-driven delay, background safe)
  3. // @namespace    http://tampermonkey.net/
  4. // @version      4.7
  5. // @description  Сканирует видео/изображения, выбирает, загружает с точной паузой (worker)
  6. // @author       Tim Tom, Eugene Novikov, Liang Wenfeng
  7. // @match        https://grok.com/files*
  8. // @connect      assets.grok.com
  9. // @grant        GM_xmlhttpRequest
  10. // @run-at       document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14.     'use strict';
  15.  
  16.     const INTER_FILE_DELAY_SEC = 1;           // точная пауза между файлами (сек), работает в фоне
  17.     const RETRY_DELAY_403_BASE = 30;
  18.     const RETRY_DELAY_OTHER_BASE = 15;
  19.     const MAX_RETRIES = 3;
  20.  
  21.     const fileList = [];
  22.     let accountId = null;
  23.     let paused = false;
  24.     let aborted = false;
  25.     let isScanning = false;
  26.     let isDownloading = false;
  27.     let totalSelected = 0;
  28.     let completed = 0;
  29.     let worker = null;
  30.  
  31.     function createWorker() {
  32.         const code = `
  33.             self.onmessage = function(e) {
  34.                 if (e.data === 'wait') {
  35.                     const delay = ${INTER_FILE_DELAY_SEC * 1000};
  36.                     setTimeout(() => { self.postMessage('go'); }, delay);
  37.                 } else if (e.data === 'stop') {
  38.                     self.close();
  39.                 }
  40.             };
  41.         `;
  42.         const blob = new Blob([code], { type: 'application/javascript' });
  43.         return new Worker(URL.createObjectURL(blob));
  44.     }
  45.  
  46.     function destroyWorker() {
  47.         if (worker) {
  48.             worker.postMessage('stop');
  49.             worker.terminate();
  50.             worker = null;
  51.         }
  52.     }
  53.  
  54.     function getExtension(mimeType) {
  55.         const map = {
  56.             'video/mp4': '.mp4', 'video/webm': '.webm', 'video/quicktime': '.mov',
  57.             'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif',
  58.             'image/webp': '.webp', 'image/avif': '.avif'
  59.         };
  60.         return map[mimeType] || '';
  61.     }
  62.     function getTypeIcon(type) { return type === 'video' ? '🎬' : type === 'image' ? '🖼️' : '📄'; }
  63.     function getFileName(fileObj) { return fileObj.id + (fileObj.ext || ''); }
  64.  
  65.     const panel = document.createElement('div');
  66.     panel.style.cssText = `
  67.         position: fixed; bottom: 20px; right: 20px; width: 500px; max-height: 85vh;
  68.         background: #1e1f22; color: #fff; border: 1px solid #404040; border-radius: 12px;
  69.         padding: 0; z-index: 99999; font-family: monospace; font-size: 13px;
  70.         box-shadow: 0 4px 24px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden;
  71.     `;
  72.     panel.innerHTML = `
  73.         <div id="header" style="background:#2a2b2e; padding:10px 14px; cursor:move; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #404040;">
  74.             <strong style="font-size:14px;">📦 Assets: <span id="counter" style="color:#059669;">0</span></strong>
  75.             <div>
  76.                 <button id="select-all" style="background:#444; color:white; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:11px; margin-right:4px;">☑ All</button>
  77.                 <button id="select-video" style="background:#444; color:white; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:11px; margin-right:4px;">🎬 Vid</button>
  78.                 <button id="select-image" style="background:#444; color:white; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:11px; margin-right:4px;">🖼️ Img</button>
  79.                 <button id="select-failed" style="background:#444; color:white; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:11px;">⚠ Failed</button>
  80.             </div>
  81.         </div>
  82.         <div id="body" style="padding:14px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:10px;">
  83.             <div id="account" style="opacity:0.7; font-size:12px; min-height:1.2em;">🔑 Account: not detected</div>
  84.             <button id="scan-btn" style="background:#059669; border:none; color:white; border-radius:6px; padding:6px 14px; cursor:pointer;">🔍 Scan All Media</button>
  85.             <div id="stats" style="font-size:11px; color:#aaa;"></div>
  86.             <div id="file-list" style="overflow-y:auto; max-height:350px; background:#2a2b2e; border-radius:8px; padding:4px; font-size:11px; color:#ccc;"></div>
  87.             <div style="display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
  88.                 <button id="download-btn" style="background:#2563eb; color:white; border:none; border-radius:6px; padding:6px 12px; cursor:pointer; font-weight:bold; display:none;">⬇️ Download Selected</button>
  89.                 <button id="retry-btn" style="background:#eab308; color:black; border:none; border-radius:6px; padding:6px 12px; cursor:pointer; font-weight:bold; display:none;">🔄 Retry Errors</button>
  90.                 <button id="pause-btn" style="background:#eab308; color:black; border:none; border-radius:6px; padding:6px 12px; cursor:pointer; font-weight:bold; display:none;">⏸️ Pause</button>
  91.                 <button id="resume-btn" style="background:#059669; color:white; border:none; border-radius:6px; padding:6px 12px; cursor:pointer; font-weight:bold; display:none;">▶️ Resume</button>
  92.                 <button id="abort-btn" style="background:#dc2626; color:white; border:none; border-radius:6px; padding:6px 12px; cursor:pointer; font-weight:bold; display:none;">⏹️ Abort</button>
  93.             </div>
  94.             <div id="progress-area" style="font-size:12px; display:none;">
  95.                 <span>Progress: <span id="progress-text">0/0</span></span>
  96.                 <div style="background:#404040; border-radius:4px; height:6px; margin-top:4px;">
  97.                     <div id="progress-bar" style="background:#059669; height:6px; border-radius:4px; width:0%;"></div>
  98.                 </div>
  99.             </div>
  100.             <div id="status" style="font-size:12px; color:#ccc;"></div>
  101.         </div>
  102.     `;
  103.     document.body.appendChild(panel);
  104.  
  105.     const counterEl = document.getElementById('counter');
  106.     const accountEl = document.getElementById('account');
  107.     const statsEl = document.getElementById('stats');
  108.     const fileListEl = document.getElementById('file-list');
  109.     const scanBtn = document.getElementById('scan-btn');
  110.     const downloadBtn = document.getElementById('download-btn');
  111.     const retryBtn = document.getElementById('retry-btn');
  112.     const pauseBtn = document.getElementById('pause-btn');
  113.     const resumeBtn = document.getElementById('resume-btn');
  114.     const abortBtn = document.getElementById('abort-btn');
  115.     const progressArea = document.getElementById('progress-area');
  116.     const progressText = document.getElementById('progress-text');
  117.     const progressBar = document.getElementById('progress-bar');
  118.     const statusEl = document.getElementById('status');
  119.     const selectAllBtn = document.getElementById('select-all');
  120.     const selectVideoBtn = document.getElementById('select-video');
  121.     const selectImageBtn = document.getElementById('select-image');
  122.     const selectFailedBtn = document.getElementById('select-failed');
  123.  
  124.     let drag = false, offX, offY;
  125.     document.getElementById('header').addEventListener('mousedown', e => {
  126.         if (e.target.tagName === 'BUTTON') return;
  127.         drag = true;
  128.         offX = e.clientX - panel.getBoundingClientRect().left;
  129.         offY = e.clientY - panel.getBoundingClientRect().top;
  130.         panel.style.transition = 'none';
  131.     });
  132.     window.addEventListener('mousemove', e => {
  133.         if (!drag) return;
  134.         panel.style.left = (e.clientX - offX) + 'px';
  135.         panel.style.top = (e.clientY - offY) + 'px';
  136.         panel.style.right = 'auto';
  137.         panel.style.bottom = 'auto';
  138.     });
  139.     window.addEventListener('mouseup', () => { drag = false; panel.style.transition = ''; });
  140.  
  141.     function statusSymbol(s) { return { 'new':'⬜', 'downloading':'⏳', 'success':'✅', 'error':'❌' }[s] || '⬜'; }
  142.     function updateCounter() { counterEl.textContent = fileList.length; }
  143.  
  144.     function addFile(id, url, mimeType) {
  145.         if (fileList.find(f => f.id === id)) return;
  146.         const ext = getExtension(mimeType);
  147.         const type = mimeType.startsWith('video/') ? 'video' : 'image';
  148.         const fileObj = { id, url, type, ext, status: 'new', element: null, checkbox: null };
  149.         fileList.push(fileObj);
  150.         renderFileItem(fileObj);
  151.         updateCounter();
  152.     }
  153.  
  154.     function renderFileItem(fileObj) {
  155.         const row = document.createElement('div');
  156.         row.style.cssText = 'display:flex; align-items:center; gap:6px; padding:2px 0;';
  157.         const cb = document.createElement('input');
  158.         cb.type = 'checkbox';
  159.         cb.checked = (fileObj.status === 'new');
  160.         cb.style.margin = '0';
  161.         const icon = document.createElement('span');
  162.         icon.style.cssText = 'width:24px; text-align:center;';
  163.         icon.textContent = getTypeIcon(fileObj.type);
  164.         const label = document.createElement('span');
  165.         label.style.cssText = 'flex:1; word-break:break-all;';
  166.         label.textContent = getFileName(fileObj);
  167.         const statusSpan = document.createElement('span');
  168.         statusSpan.style.cssText = 'width:20px; text-align:center;';
  169.         statusSpan.textContent = statusSymbol(fileObj.status);
  170.  
  171.         row.appendChild(cb);
  172.         row.appendChild(icon);
  173.         row.appendChild(label);
  174.         row.appendChild(statusSpan);
  175.         fileListEl.appendChild(row);
  176.  
  177.         fileObj.element = row;
  178.         fileObj.checkbox = cb;
  179.  
  180.         row.addEventListener('click', (e) => {
  181.             if (e.target !== cb) {
  182.                 cb.checked = !cb.checked;
  183.                 if (fileObj.status === 'downloading') cb.checked = false;
  184.             }
  185.         });
  186.     }
  187.  
  188.     function updateFileStatus(fileObj, newStatus) {
  189.         fileObj.status = newStatus;
  190.         const iconSpan = fileObj.element?.querySelector('span:last-child');
  191.         if (iconSpan) iconSpan.textContent = statusSymbol(newStatus);
  192.         if (fileObj.checkbox) {
  193.             if (newStatus === 'success') {
  194.                 fileObj.checkbox.checked = false;
  195.                 fileObj.checkbox.disabled = true;
  196.             } else if (newStatus === 'error') {
  197.                 fileObj.checkbox.checked = false;
  198.                 fileObj.checkbox.disabled = false;
  199.             } else if (newStatus === 'downloading') {
  200.                 fileObj.checkbox.checked = true;
  201.                 fileObj.checkbox.disabled = true;
  202.             } else if (newStatus === 'new') {
  203.                 fileObj.checkbox.checked = true;
  204.                 fileObj.checkbox.disabled = false;
  205.             }
  206.         }
  207.     }
  208.  
  209.     function toggleSelection(predicate) {
  210.         const candidates = fileList.filter(f => predicate(f) && f.checkbox && !f.checkbox.disabled);
  211.         if (candidates.length === 0) {
  212.             statusEl.textContent = 'No matching files to toggle';
  213.             return;
  214.         }
  215.         const allChecked = candidates.every(f => f.checkbox.checked);
  216.         candidates.forEach(f => { f.checkbox.checked = !allChecked; });
  217.     }
  218.  
  219.     selectAllBtn.addEventListener('click', () => toggleSelection(f => f.status !== 'success'));
  220.     selectVideoBtn.addEventListener('click', () => toggleSelection(f => f.type === 'video' && f.status !== 'success'));
  221.     selectImageBtn.addEventListener('click', () => toggleSelection(f => f.type === 'image' && f.status !== 'success'));
  222.     selectFailedBtn.addEventListener('click', () => {
  223.         fileList.forEach(f => { if (f.status === 'error' && f.checkbox) f.checkbox.checked = true; });
  224.     });
  225.  
  226.     async function scanAllMedia() {
  227.         if (isScanning) return;
  228.         isScanning = true;
  229.         scanBtn.textContent = 'Scanning...';
  230.         scanBtn.disabled = true;
  231.         statusEl.textContent = 'Scanning API for videos and images...';
  232.  
  233.         fileList.length = 0;
  234.         fileListEl.innerHTML = '';
  235.         accountEl.textContent = '🔑 Account: detecting...';
  236.         statsEl.textContent = '';
  237.         updateCounter();
  238.  
  239.         let pageToken = '';
  240.         const baseUrl = '/rest/assets?pageSize=50&orderBy=ORDER_BY_LAST_USE_TIME&source=SOURCE_ANY&isLatest=true&includeImagineFiles=true';
  241.  
  242.         try {
  243.             while (true) {
  244.                 let url = baseUrl;
  245.                 if (pageToken) url += '&pageToken=' + pageToken;
  246.                 const resp = await fetch(url, { credentials: 'include' });
  247.                 const data = await resp.json();
  248.                 if (!data.assets) break;
  249.  
  250.                 for (const asset of data.assets) {
  251.                     const mimeType = asset.mimeType;
  252.                     if (!mimeType || (!mimeType.startsWith('video/') && !mimeType.startsWith('image/'))) continue;
  253.                     const assetUrl = asset.key ? 'https://assets.grok.com/' + asset.key : null;
  254.                     if (!assetUrl) continue;
  255.                     if (!accountId) {
  256.                         const m = assetUrl.match(/\/users\/([a-f0-9-]+)\/generated\//);
  257.                         if (m) {
  258.                             accountId = m[1];
  259.                             accountEl.textContent = `🔑 Account: ${accountId}`;
  260.                         }
  261.                     }
  262.                     const id = asset.assetId || asset.id || 'unknown';
  263.                     addFile(id, assetUrl, mimeType);
  264.                 }
  265.  
  266.                 if (data.nextPageToken && data.nextPageToken !== pageToken) {
  267.                     pageToken = data.nextPageToken;
  268.                     await new Promise(r => setTimeout(r, 200));
  269.                 } else break;
  270.             }
  271.         } catch(e) {
  272.             console.error('Scan error:', e);
  273.             statusEl.textContent = 'Scan error: ' + e.message;
  274.         }
  275.  
  276.         isScanning = false;
  277.         scanBtn.textContent = '🔍 Scan All Media';
  278.         scanBtn.disabled = false;
  279.  
  280.         const videoCount = fileList.filter(f => f.type === 'video').length;
  281.         const imageCount = fileList.filter(f => f.type === 'image').length;
  282.         statsEl.textContent = `🎬 Videos: ${videoCount} | 🖼️ Images: ${imageCount}`;
  283.         statusEl.textContent = `Found ${fileList.length} assets`;
  284.         downloadBtn.style.display = 'inline-block';
  285.         if (!accountId) accountEl.textContent = '🔑 Account: not auto-detected – click any media on page';
  286.     }
  287.  
  288.     scanBtn.addEventListener('click', scanAllMedia);
  289.  
  290.     // fallback account
  291.     new MutationObserver(mutations => {
  292.         for (const mut of mutations) for (const node of mut.addedNodes) {
  293.             if (node.nodeName === 'VIDEO' || node.nodeName === 'SOURCE' || node.nodeName === 'IMG') {
  294.                 const src = node.src || node.currentSrc || node.querySelector('source')?.src;
  295.                 if (src) {
  296.                     const m = src.match(/\/users\/([a-f0-9-]+)\/generated\//);
  297.                     if (m && !accountId) {
  298.                         accountId = m[1];
  299.                         accountEl.textContent = `🔑 Account: ${accountId}`;
  300.                     }
  301.                 }
  302.             }
  303.         }
  304.     }).observe(document.body, { childList: true, subtree: true });
  305.  
  306.     function downloadFile(fileObj, attempt = 1) {
  307.         return new Promise((resolve) => {
  308.             if (aborted) {
  309.                 updateFileStatus(fileObj, 'error');
  310.                 resolve(false);
  311.                 return;
  312.             }
  313.             updateFileStatus(fileObj, 'downloading');
  314.             statusEl.textContent = `Downloading: ${getFileName(fileObj)}...`;
  315.             GM_xmlhttpRequest({
  316.                 method: 'GET',
  317.                 url: fileObj.url,
  318.                 responseType: 'blob',
  319.                 onload: function(resp) {
  320.                     if (resp.status === 200) {
  321.                         const blob = resp.response;
  322.                         const blobUrl = URL.createObjectURL(blob);
  323.                         const a = document.createElement('a');
  324.                         a.href = blobUrl;
  325.                         a.download = getFileName(fileObj);
  326.                         a.style.display = 'none';
  327.                         document.body.appendChild(a);
  328.                         a.click();
  329.                         setTimeout(() => {
  330.                             document.body.removeChild(a);
  331.                             URL.revokeObjectURL(blobUrl);
  332.                         }, 100);
  333.                         updateFileStatus(fileObj, 'success');
  334.                         completed++;
  335.                         updateProgress();
  336.                         resolve(true);
  337.                     } else if (resp.status === 403 || resp.status === 429 || resp.status === 503) {
  338.                         if (aborted) {
  339.                             updateFileStatus(fileObj, 'error');
  340.                             resolve(false);
  341.                             return;
  342.                         }
  343.                         if (attempt < MAX_RETRIES) {
  344.                             const baseDelay = resp.status === 403 ? RETRY_DELAY_403_BASE : RETRY_DELAY_OTHER_BASE;
  345.                             const delay = baseDelay * attempt * 1000;
  346.                             statusEl.textContent = `${resp.status} – retry in ${delay/1000}s...`;
  347.                             setTimeout(() => downloadFile(fileObj, attempt+1).then(resolve), delay);
  348.                         } else {
  349.                             updateFileStatus(fileObj, 'error');
  350.                             resolve(false);
  351.                         }
  352.                     } else {
  353.                         updateFileStatus(fileObj, 'error');
  354.                         resolve(false);
  355.                     }
  356.                 },
  357.                 onerror: function() {
  358.                     if (aborted) { resolve(false); return; }
  359.                     updateFileStatus(fileObj, 'error');
  360.                     resolve(false);
  361.                 },
  362.                 ontimeout: function() {
  363.                     if (aborted) { resolve(false); return; }
  364.                     updateFileStatus(fileObj, 'error');
  365.                     resolve(false);
  366.                 }
  367.             });
  368.         });
  369.     }
  370.  
  371.     function updateProgress() {
  372.         if (totalSelected > 0) {
  373.             progressArea.style.display = 'block';
  374.             progressText.textContent = `${completed} / ${totalSelected}`;
  375.             progressBar.style.width = Math.round((completed / totalSelected) * 100) + '%';
  376.         }
  377.     }
  378.  
  379.     async function processSelected() {
  380.         const selected = fileList.filter(f => f.checkbox.checked && f.status !== 'success');
  381.         totalSelected = selected.length;
  382.         completed = 0;
  383.         if (totalSelected === 0) {
  384.             statusEl.textContent = 'No files selected';
  385.             return;
  386.         }
  387.         disableControls(true);
  388.         progressArea.style.display = 'block';
  389.         updateProgress();
  390.  
  391.         worker = createWorker();
  392.  
  393.         for (let i = 0; i < selected.length; i++) {
  394.             while (paused && !aborted) {
  395.                 await new Promise(r => setTimeout(r, 200));
  396.             }
  397.             if (aborted) break;
  398.  
  399.             const fileObj = selected[i];
  400.             await downloadFile(fileObj);
  401.             if (aborted) break;
  402.  
  403.             if (i < selected.length - 1) {
  404.                 statusEl.textContent = `Waiting ${INTER_FILE_DELAY_SEC}s...`;
  405.                 worker.postMessage('wait');
  406.                 await new Promise((resolve) => {
  407.                     const onMessage = (e) => {
  408.                         if (e.data === 'go') {
  409.                             worker.removeEventListener('message', onMessage);
  410.                             resolve();
  411.                         }
  412.                     };
  413.                     worker.addEventListener('message', onMessage);
  414.                 });
  415.                 // После паузы проверяем паузу/аборт
  416.                 while (paused && !aborted) {
  417.                     await new Promise(r => setTimeout(r, 200));
  418.                 }
  419.                 if (aborted) break;
  420.             }
  421.         }
  422.  
  423.         destroyWorker();
  424.         finishDownload();
  425.     }
  426.  
  427.     function disableControls(disable) {
  428.         downloadBtn.style.display = disable ? 'none' : 'inline-block';
  429.         retryBtn.style.display = disable ? 'none' : (fileList.filter(f => f.status === 'error').length > 0 ? 'inline-block' : 'none');
  430.         pauseBtn.style.display = disable ? 'inline-block' : 'none';
  431.         resumeBtn.style.display = 'none';
  432.         abortBtn.style.display = disable ? 'inline-block' : 'none';
  433.     }
  434.  
  435.     function finishDownload() {
  436.         disableControls(false);
  437.         progressArea.style.display = 'none';
  438.         statusEl.textContent = aborted ? 'Aborted.' : 'All selected downloads completed.';
  439.         paused = false;
  440.         aborted = false;
  441.         isDownloading = false;
  442.         retryBtn.style.display = fileList.filter(f => f.status === 'error').length > 0 ? 'inline-block' : 'none';
  443.         destroyWorker();
  444.     }
  445.  
  446.     function startDownload() {
  447.         if (!accountId) { alert('Сначала определите ID аккаунта – кликните на любое изображение/видео или сделайте скан'); return; }
  448.         if (fileList.length === 0) { alert('Сначала нажмите Scan All Media'); return; }
  449.         paused = false;
  450.         aborted = false;
  451.         isDownloading = true;
  452.         processSelected();
  453.     }
  454.  
  455.     downloadBtn.addEventListener('click', startDownload);
  456.     pauseBtn.addEventListener('click', () => {
  457.         paused = true;
  458.         pauseBtn.style.display = 'none';
  459.         resumeBtn.style.display = 'inline-block';
  460.         statusEl.textContent = 'Paused';
  461.     });
  462.     resumeBtn.addEventListener('click', () => {
  463.         paused = false;
  464.         resumeBtn.style.display = 'none';
  465.         pauseBtn.style.display = 'inline-block';
  466.     });
  467.     abortBtn.addEventListener('click', () => {
  468.         aborted = true;
  469.         paused = false;
  470.         finishDownload();
  471.     });
  472.     retryBtn.addEventListener('click', () => {
  473.         fileList.forEach(f => { if (f.status === 'error' && f.checkbox) f.checkbox.checked = true; });
  474.         paused = false;
  475.         aborted = false;
  476.         isDownloading = true;
  477.         retryBtn.style.display = 'none';
  478.         processSelected();
  479.     });
  480.  
  481.     statusEl.textContent = 'Ready – click Scan All Media';
  482. })();
Advertisement
Add Comment
Please, Sign In to add comment