Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Grok Media Asset Manager v4.4 (toggle select + configurable delays)
- // @namespace http://tampermonkey.net/
- // @version 4.4
- // @description Сканирует видео/изображения, умный выбор, настраиваемые задержки
- // @author Tim Tom, Eugene Novikov, Liang Wenfeng
- // @match https://grok.com/files*
- // @connect assets.grok.com
- // @grant GM_xmlhttpRequest
- // @run-at document-end
- // ==/UserScript==
- (function() {
- 'use strict';
- const INTER_FILE_DELAY_SEC = 1.5; // пауза между файлами (сек)
- const RETRY_DELAY_403_BASE = 30; // базовое ожидание при 403 (умножается на номер попытки)
- const RETRY_DELAY_OTHER_BASE = 15; // базовое ожидание при 429/503 и т.п.
- const MAX_RETRIES = 3; // максимум попыток на файл
- const fileList = [];
- let accountId = null;
- let paused = false;
- let aborted = false;
- let isScanning = false;
- let isDownloading = false;
- let totalSelected = 0;
- let completed = 0;
- function getExtension(mimeType) {
- const map = {
- 'video/mp4': '.mp4', 'video/webm': '.webm', 'video/quicktime': '.mov',
- 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif',
- 'image/webp': '.webp', 'image/avif': '.avif'
- };
- return map[mimeType] || '';
- }
- function getTypeIcon(type) { return type === 'video' ? '🎬' : type === 'image' ? '🖼️' : '📄'; }
- function getFileName(fileObj) { return fileObj.id + (fileObj.ext || ''); }
- const panel = document.createElement('div');
- panel.style.cssText = `
- position: fixed; bottom: 20px; right: 20px; width: 500px; max-height: 85vh;
- background: #1e1f22; color: #fff; border: 1px solid #404040; border-radius: 12px;
- padding: 0; z-index: 99999; font-family: monospace; font-size: 13px;
- box-shadow: 0 4px 24px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden;
- `;
- panel.innerHTML = `
- <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;">
- <strong style="font-size:14px;">📦 Assets: <span id="counter" style="color:#059669;">0</span></strong>
- <div>
- <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>
- <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>
- <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>
- <button id="select-failed" style="background:#444; color:white; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:11px;">⚠ Failed</button>
- </div>
- </div>
- <div id="body" style="padding:14px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:10px;">
- <div id="account" style="opacity:0.7; font-size:12px; min-height:1.2em;">🔑 Account: not detected</div>
- <button id="scan-btn" style="background:#059669; border:none; color:white; border-radius:6px; padding:6px 14px; cursor:pointer;">🔍 Scan All Media</button>
- <div id="stats" style="font-size:11px; color:#aaa;"></div>
- <div id="file-list" style="overflow-y:auto; max-height:350px; background:#2a2b2e; border-radius:8px; padding:4px; font-size:11px; color:#ccc;"></div>
- <div style="display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
- <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>
- <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>
- <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>
- <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>
- <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>
- </div>
- <div id="progress-area" style="font-size:12px; display:none;">
- <span>Progress: <span id="progress-text">0/0</span></span>
- <div style="background:#404040; border-radius:4px; height:6px; margin-top:4px;">
- <div id="progress-bar" style="background:#059669; height:6px; border-radius:4px; width:0%;"></div>
- </div>
- </div>
- <div id="status" style="font-size:12px; color:#ccc;"></div>
- </div>
- `;
- document.body.appendChild(panel);
- const counterEl = document.getElementById('counter');
- const accountEl = document.getElementById('account');
- const statsEl = document.getElementById('stats');
- const fileListEl = document.getElementById('file-list');
- const scanBtn = document.getElementById('scan-btn');
- const downloadBtn = document.getElementById('download-btn');
- const retryBtn = document.getElementById('retry-btn');
- const pauseBtn = document.getElementById('pause-btn');
- const resumeBtn = document.getElementById('resume-btn');
- const abortBtn = document.getElementById('abort-btn');
- const progressArea = document.getElementById('progress-area');
- const progressText = document.getElementById('progress-text');
- const progressBar = document.getElementById('progress-bar');
- const statusEl = document.getElementById('status');
- let drag = false, offX, offY;
- document.getElementById('header').addEventListener('mousedown', e => {
- if (e.target.tagName === 'BUTTON') return;
- drag = true;
- offX = e.clientX - panel.getBoundingClientRect().left;
- offY = e.clientY - panel.getBoundingClientRect().top;
- panel.style.transition = 'none';
- });
- window.addEventListener('mousemove', e => {
- if (!drag) return;
- panel.style.left = (e.clientX - offX) + 'px';
- panel.style.top = (e.clientY - offY) + 'px';
- panel.style.right = 'auto';
- panel.style.bottom = 'auto';
- });
- window.addEventListener('mouseup', () => { drag = false; panel.style.transition = ''; });
- function statusSymbol(s) { return { 'new':'⬜', 'downloading':'⏳', 'success':'✅', 'error':'❌' }[s] || '⬜'; }
- function updateCounter() { counterEl.textContent = fileList.length; }
- function addFile(id, url, mimeType) {
- if (fileList.find(f => f.id === id)) return;
- const ext = getExtension(mimeType);
- const type = mimeType.startsWith('video/') ? 'video' : 'image';
- const fileObj = { id, url, type, ext, status: 'new', element: null, checkbox: null };
- fileList.push(fileObj);
- renderFileItem(fileObj);
- updateCounter();
- }
- function renderFileItem(fileObj) {
- const row = document.createElement('div');
- row.style.cssText = 'display:flex; align-items:center; gap:6px; padding:2px 0;';
- const cb = document.createElement('input');
- cb.type = 'checkbox';
- cb.checked = (fileObj.status === 'new');
- cb.style.margin = '0';
- const icon = document.createElement('span');
- icon.style.cssText = 'width:24px; text-align:center;';
- icon.textContent = getTypeIcon(fileObj.type);
- const label = document.createElement('span');
- label.style.cssText = 'flex:1; word-break:break-all;';
- label.textContent = getFileName(fileObj);
- const statusSpan = document.createElement('span');
- statusSpan.style.cssText = 'width:20px; text-align:center;';
- statusSpan.textContent = statusSymbol(fileObj.status);
- row.appendChild(cb);
- row.appendChild(icon);
- row.appendChild(label);
- row.appendChild(statusSpan);
- fileListEl.appendChild(row);
- fileObj.element = row;
- fileObj.checkbox = cb;
- row.addEventListener('click', (e) => {
- if (e.target !== cb) {
- cb.checked = !cb.checked;
- if (fileObj.status === 'downloading') cb.checked = false;
- }
- });
- }
- function updateFileStatus(fileObj, newStatus) {
- fileObj.status = newStatus;
- const iconSpan = fileObj.element?.querySelector('span:last-child');
- if (iconSpan) iconSpan.textContent = statusSymbol(newStatus);
- if (fileObj.checkbox) {
- if (newStatus === 'success') {
- fileObj.checkbox.checked = false;
- fileObj.checkbox.disabled = true;
- } else if (newStatus === 'error') {
- fileObj.checkbox.checked = false;
- fileObj.checkbox.disabled = false;
- } else if (newStatus === 'downloading') {
- fileObj.checkbox.checked = true;
- fileObj.checkbox.disabled = true;
- } else if (newStatus === 'new') {
- fileObj.checkbox.checked = true;
- fileObj.checkbox.disabled = false;
- }
- }
- }
- function toggleSelection(predicate) {
- const candidates = fileList.filter(f => predicate(f) && f.checkbox && !f.checkbox.disabled);
- if (candidates.length === 0) {
- statusEl.textContent = 'No matching files to toggle';
- return;
- }
- const allChecked = candidates.every(f => f.checkbox.checked);
- candidates.forEach(f => { f.checkbox.checked = !allChecked; });
- }
- document.getElementById('select-all').addEventListener('click', () => toggleSelection(f => f.status !== 'success'));
- document.getElementById('select-video').addEventListener('click', () => toggleSelection(f => f.type === 'video' && f.status !== 'success'));
- document.getElementById('select-image').addEventListener('click', () => toggleSelection(f => f.type === 'image' && f.status !== 'success'));
- document.getElementById('select-failed').addEventListener('click', () => {
- fileList.forEach(f => { if (f.status === 'error' && f.checkbox) f.checkbox.checked = true; });
- });
- async function scanAllMedia() {
- if (isScanning) return;
- isScanning = true;
- scanBtn.textContent = 'Scanning...';
- scanBtn.disabled = true;
- statusEl.textContent = 'Scanning API for videos and images...';
- fileList.length = 0;
- fileListEl.innerHTML = '';
- accountEl.textContent = '🔑 Account: detecting...';
- statsEl.textContent = '';
- updateCounter();
- let pageToken = '';
- const baseUrl = '/rest/assets?pageSize=50&orderBy=ORDER_BY_LAST_USE_TIME&source=SOURCE_ANY&isLatest=true&includeImagineFiles=true';
- try {
- while (true) {
- let url = baseUrl;
- if (pageToken) url += '&pageToken=' + pageToken;
- const resp = await fetch(url, { credentials: 'include' });
- const data = await resp.json();
- if (!data.assets) break;
- for (const asset of data.assets) {
- const mimeType = asset.mimeType;
- if (!mimeType || (!mimeType.startsWith('video/') && !mimeType.startsWith('image/'))) continue;
- const assetUrl = asset.key ? 'https://assets.grok.com/' + asset.key : null;
- if (!assetUrl) continue;
- if (!accountId) {
- const m = assetUrl.match(/\/users\/([a-f0-9-]+)\/generated\//);
- if (m) {
- accountId = m[1];
- accountEl.textContent = `🔑 Account: ${accountId}`;
- }
- }
- const id = asset.assetId || asset.id || 'unknown';
- addFile(id, assetUrl, mimeType);
- }
- if (data.nextPageToken && data.nextPageToken !== pageToken) {
- pageToken = data.nextPageToken;
- await new Promise(r => setTimeout(r, 200));
- } else break;
- }
- } catch(e) {
- console.error('Scan error:', e);
- statusEl.textContent = 'Scan error: ' + e.message;
- }
- isScanning = false;
- scanBtn.textContent = '🔍 Scan All Media';
- scanBtn.disabled = false;
- const videoCount = fileList.filter(f => f.type === 'video').length;
- const imageCount = fileList.filter(f => f.type === 'image').length;
- statsEl.textContent = `🎬 Videos: ${videoCount} | 🖼️ Images: ${imageCount}`;
- statusEl.textContent = `Found ${fileList.length} assets`;
- downloadBtn.style.display = 'inline-block';
- if (!accountId) accountEl.textContent = '🔑 Account: not auto-detected – click any media on page';
- }
- scanBtn.addEventListener('click', scanAllMedia);
- new MutationObserver(mutations => {
- for (const mut of mutations) for (const node of mut.addedNodes) {
- if (node.nodeName === 'VIDEO' || node.nodeName === 'SOURCE' || node.nodeName === 'IMG') {
- const src = node.src || node.currentSrc || node.querySelector('source')?.src;
- if (src) {
- const m = src.match(/\/users\/([a-f0-9-]+)\/generated\//);
- if (m && !accountId) {
- accountId = m[1];
- accountEl.textContent = `🔑 Account: ${accountId}`;
- }
- }
- }
- }
- }).observe(document.body, { childList: true, subtree: true });
- function downloadFile(fileObj, attempt = 1) {
- return new Promise((resolve) => {
- if (aborted) { updateFileStatus(fileObj, 'error'); resolve(false); return; }
- updateFileStatus(fileObj, 'downloading');
- statusEl.textContent = `Downloading: ${getFileName(fileObj)}...`;
- GM_xmlhttpRequest({
- method: 'GET',
- url: fileObj.url,
- responseType: 'blob',
- onload: function(resp) {
- if (resp.status === 200) {
- const blob = resp.response;
- const blobUrl = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = blobUrl;
- a.download = getFileName(fileObj);
- a.style.display = 'none';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(blobUrl);
- }, 100);
- updateFileStatus(fileObj, 'success');
- completed++;
- updateProgress();
- resolve(true);
- } else if (resp.status === 403 || resp.status === 429 || resp.status === 503) {
- if (aborted) { updateFileStatus(fileObj, 'error'); resolve(false); return; }
- if (attempt < MAX_RETRIES) {
- const baseDelay = resp.status === 403 ? RETRY_DELAY_403_BASE : RETRY_DELAY_OTHER_BASE;
- const delay = baseDelay * attempt * 1000; // прогрессивно
- statusEl.textContent = `${resp.status} – retry in ${delay/1000}s...`;
- setTimeout(() => downloadFile(fileObj, attempt+1).then(resolve), delay);
- } else {
- updateFileStatus(fileObj, 'error');
- resolve(false);
- }
- } else {
- updateFileStatus(fileObj, 'error');
- resolve(false);
- }
- },
- onerror: function() {
- if (aborted) { resolve(false); return; }
- updateFileStatus(fileObj, 'error');
- resolve(false);
- },
- ontimeout: function() {
- if (aborted) { resolve(false); return; }
- updateFileStatus(fileObj, 'error');
- resolve(false);
- }
- });
- });
- }
- function updateProgress() {
- if (totalSelected > 0) {
- progressArea.style.display = 'block';
- progressText.textContent = `${completed} / ${totalSelected}`;
- progressBar.style.width = Math.round((completed / totalSelected) * 100) + '%';
- }
- }
- async function processSelected() {
- const selected = fileList.filter(f => f.checkbox.checked && f.status !== 'success');
- totalSelected = selected.length;
- completed = 0;
- if (totalSelected === 0) {
- statusEl.textContent = 'No files selected';
- return;
- }
- disableControls(true);
- progressArea.style.display = 'block';
- updateProgress();
- for (let i = 0; i < selected.length; i++) {
- while (paused && !aborted) await new Promise(r => setTimeout(r, 500));
- if (aborted) break;
- const fileObj = selected[i];
- await downloadFile(fileObj);
- if (aborted) break;
- if (i < selected.length - 1) {
- const steps = INTER_FILE_DELAY_SEC * 10;
- for (let step = 0; step < steps; step++) {
- if (aborted || paused) break;
- await new Promise(r => setTimeout(r, 100));
- }
- while (paused && !aborted) await new Promise(r => setTimeout(r, 500));
- if (aborted) break;
- }
- }
- finishDownload();
- }
- function disableControls(disable) {
- downloadBtn.style.display = disable ? 'none' : 'inline-block';
- retryBtn.style.display = disable ? 'none' : (fileList.filter(f => f.status === 'error').length > 0 ? 'inline-block' : 'none');
- pauseBtn.style.display = disable ? 'inline-block' : 'none';
- resumeBtn.style.display = 'none';
- abortBtn.style.display = disable ? 'inline-block' : 'none';
- }
- function finishDownload() {
- disableControls(false);
- progressArea.style.display = 'none';
- statusEl.textContent = aborted ? 'Aborted.' : 'All selected downloads completed.';
- paused = false;
- aborted = false;
- isDownloading = false;
- retryBtn.style.display = fileList.filter(f => f.status === 'error').length > 0 ? 'inline-block' : 'none';
- }
- function startDownload() {
- if (!accountId) { alert('Сначала определите ID аккаунта – кликните на любое изображение/видео или сделайте скан'); return; }
- if (fileList.length === 0) { alert('Сначала нажмите Scan All Media'); return; }
- paused = false;
- aborted = false;
- isDownloading = true;
- processSelected();
- }
- downloadBtn.addEventListener('click', startDownload);
- pauseBtn.addEventListener('click', () => {
- paused = true;
- pauseBtn.style.display = 'none';
- resumeBtn.style.display = 'inline-block';
- statusEl.textContent = 'Paused';
- });
- resumeBtn.addEventListener('click', () => {
- paused = false;
- resumeBtn.style.display = 'none';
- pauseBtn.style.display = 'inline-block';
- });
- abortBtn.addEventListener('click', () => {
- aborted = true;
- paused = false;
- });
- retryBtn.addEventListener('click', () => {
- fileList.forEach(f => { if (f.status === 'error' && f.checkbox) f.checkbox.checked = true; });
- paused = false;
- aborted = false;
- isDownloading = true;
- retryBtn.style.display = 'none';
- processSelected();
- });
- statusEl.textContent = 'Ready – click Scan All Media';
- })();
Advertisement
Add Comment
Please, Sign In to add comment