Eugene666

grok mass media download

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