您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Complete TradingView script downloading solution with batch processing and status tracking
// ==UserScript== // @name TradingView Script Downloader // @namespace http://tampermonkey.net/ // @version 5.0 // @description Complete TradingView script downloading solution with batch processing and status tracking // @author You // @match https://www.tradingview.com/script/* // @match https://www.tradingview.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-start // @require https://update.greasyfork.org/scripts/528234/1596455/waitForElement.js // @license MIT // ==/UserScript== (async function () { "use strict"; // Global configuration const CONFIG = { targetURL: "https://pine-facade.tradingview.com/pine-facade/get/PUB", selectors: { description: 'div[class^="layout-"] > div[class^="content-"] > div[class^="description-"]', sourceCodeBTN: "#code", username: '[class^="usernameOutline-"]', }, storage: { processedUrls: "tv_processed_urls", queueUrls: "tv_queue_urls", currentIndex: "tv_current_index", }, }; // State variables let descriptionContent = "[NOT FOUND]"; let username = "[NOT FOUND]"; let isProcessing = false; // Utility functions function extractIdFromUrl(url) { try { console.log("[Debug] Extracting ID from URL:", url); const decodedUrl = new URL(decodeURIComponent(url)).pathname; const parts = decodedUrl.split(";"); const extractedId = parts.length > 1 ? parts[1].split("/")[0] : null; console.log("[Debug] Extracted ID:", extractedId); return extractedId; } catch (e) { console.log("[URL Parsing Error]", e); return null; } } function sanitizeFilename(filename) { return filename.replace(/[<>:"\/\\|?*]/g, " ").trim(); } function downloadFile(filename, content) { const safeFilename = sanitizeFilename(filename); const blob = new Blob([content], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = safeFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } function saveProcessedUrl(url) { const processed = GM_getValue(CONFIG.storage.processedUrls, {}); processed[url] = { timestamp: Date.now(), status: "completed", }; GM_setValue(CONFIG.storage.processedUrls, processed); } function isUrlProcessed(url) { const processed = GM_getValue(CONFIG.storage.processedUrls, {}); return !!processed[url]; } function getQueuedUrls() { return GM_getValue(CONFIG.storage.queueUrls, []); } function setQueuedUrls(urls) { GM_setValue(CONFIG.storage.queueUrls, urls); } function getCurrentIndex() { return GM_getValue(CONFIG.storage.currentIndex, 0); } function setCurrentIndex(index) { GM_setValue(CONFIG.storage.currentIndex, index); } function clearQueue() { GM_setValue(CONFIG.storage.queueUrls, []); GM_setValue(CONFIG.storage.currentIndex, 0); } // XHR Hook for capturing script data function hookXHR() { if (window.xhrHooked) return; // Prevent multiple hooks window.xhrHooked = true; const originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.addEventListener("load", function () { if ( !( url?.includes(CONFIG.targetURL) && this.readyState === 4 && this.status === 200 ) ) { return; } try { const responseJSON = JSON.parse(this.responseText); const extractedId = extractIdFromUrl(url); if (responseJSON?.scriptName && extractedId) { console.log( `[Script Found] ${responseJSON.scriptName} | [URL] ${url}` ); // Enhance response with additional data responseJSON.description = descriptionContent; responseJSON.username = username; responseJSON.url = location.href; responseJSON.downloadTime = new Date().toISOString(); const fname = `${responseJSON.scriptName}-${extractedId}.json`; const data = JSON.stringify(responseJSON, null, 2); downloadFile(fname, data); saveProcessedUrl(location.href); GM_notification({ text: `Downloaded: ${responseJSON.scriptName}`, timeout: 3000, }); console.log("+++++++++++DOWNLOAD COMPLETE+++++++++++"); // Process next URL in queue after a delay setTimeout(() => { processNextInQueue(); }, 2000); } } catch (e) { console.log("[XHR JSON Parse Error]", e); } }); return originalXHROpen.apply(this, [method, url, ...rest]); }; } // Process individual script page async function processScriptPage() { const currentURL = location.href; if (isUrlProcessed(currentURL)) { console.log("URL already processed, skipping..."); setTimeout(() => processNextInQueue(), 1000); return; } try { console.log("Getting page elements..."); // Wait for elements with timeout const sourceCodeELM = await Promise.race([ waitForElement(CONFIG.selectors.sourceCodeBTN), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 10000) ), ]); if (!sourceCodeELM) { console.log("Source code button not found!"); setTimeout(() => processNextInQueue(), 1000); return; } // Get description and username try { const descriptionELM = await waitForElement( CONFIG.selectors.description ); if (descriptionELM) { descriptionContent = descriptionELM.textContent.trim(); } } catch (e) { console.log("Description element not found"); } try { const usernameELM = await waitForElement(CONFIG.selectors.username); if (usernameELM) { username = usernameELM.textContent.trim(); } } catch (e) { console.log("Username element not found"); } // Click source code button periodically until XHR is captured let clickCount = 0; const maxClicks = 20; const clickInterval = setInterval(() => { if (clickCount >= maxClicks || isUrlProcessed(currentURL)) { clearInterval(clickInterval); if (!isUrlProcessed(currentURL)) { console.log("Max clicks reached, moving to next..."); setTimeout(() => processNextInQueue(), 2000); } return; } sourceCodeELM.click(); clickCount++; console.log(`Clicked source code button (${clickCount}/${maxClicks})`); }, 1500); } catch (error) { console.log("Error processing script page:", error); setTimeout(() => processNextInQueue(), 2000); } } // Process next URL in queue async function processNextInQueue() { const queuedUrls = getQueuedUrls(); const currentIndex = getCurrentIndex(); if (currentIndex >= queuedUrls.length) { console.log("✅ All URLs processed!"); GM_notification({ text: "All TradingView scripts processed!", timeout: 5000, }); clearQueue(); isProcessing = false; return; } const nextUrl = queuedUrls[currentIndex]; console.log( `🔗 Processing (${currentIndex + 1}/${queuedUrls.length}): ${nextUrl}` ); setCurrentIndex(currentIndex + 1); if (location.href !== nextUrl) { window.location.href = nextUrl; } else { // Already on the page, process it await processScriptPage(); } } // Create batch processing modal function createBatchModal() { if (document.getElementById("tv-batch-modal")) return; const modal = document.createElement("div"); modal.id = "tv-batch-modal"; modal.innerHTML = ` <div style=" position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; "> <div style=" background: #1e1e1e; color: #ffffff; padding: 24px; border-radius: 12px; width: 95%; max-width: 800px; max-height: 90%; overflow: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.6); border: 1px solid #333; "> <h2 style="margin-top:0; color: #4CAF50;">📊 TradingView Script Batch Downloader</h2> <p style="color: #ccc; margin-bottom: 16px;">Paste TradingView script URLs (one per line):</p> <textarea id="tv-links-input" placeholder="https://www.tradingview.com/script/..." style=" width: 100%; height: 300px; font-size: 14px; background: #2d2d2d; color: #fff; border: 1px solid #555; border-radius: 6px; padding: 12px; font-family: monospace; resize: vertical; "></textarea> <div style="margin-top: 20px; display: flex; justify-content: space-between; align-items: center;"> <div style="color: #999; font-size: 12px;"> <div id="tv-status">Ready to process...</div> </div> <div> <button id="tv-start-btn" style=" padding: 12px 24px; margin-right: 12px; font-size: 14px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; ">🚀 Start Processing</button> <button id="tv-cancel-btn" style=" padding: 12px 24px; font-size: 14px; background: #666; color: white; border: none; border-radius: 6px; cursor: pointer; ">❌ Cancel</button> </div> </div> </div> </div> `; document.body.appendChild(modal); const startBtn = document.getElementById("tv-start-btn"); const cancelBtn = document.getElementById("tv-cancel-btn"); const input = document.getElementById("tv-links-input"); const status = document.getElementById("tv-status"); // Load existing queue if any const existingQueue = getQueuedUrls(); if (existingQueue.length > 0) { input.value = existingQueue.join("\n"); status.textContent = `Found ${existingQueue.length} URLs from previous session`; } cancelBtn.onclick = () => { modal.remove(); }; startBtn.onclick = () => { const baseURL = "tradingview.com/script/"; const lines = input.value.split("\n"); const urlSet = new Set(); for (const line of lines) { const trimmed = line.trim(); if (trimmed.includes(baseURL)) { urlSet.add( trimmed.startsWith("https://") ? trimmed : `https://${trimmed}` ); } } const urls = [...urlSet]; console.log(`✅ ${urls.length} unique TradingView script links found.`); if (urls.length === 0) { alert("❌ No valid TradingView script links found!"); return; } // Save URLs to queue and start processing setQueuedUrls(urls); setCurrentIndex(0); isProcessing = true; modal.remove(); GM_notification({ text: `Starting batch download of ${urls.length} scripts...`, timeout: 3000, }); processNextInQueue(); }; } // Main initialization async function init() { const currentURL = location.href; // Hook XHR for all pages hookXHR(); // Register menu commands GM_registerMenuCommand("📊 Batch Download Scripts", createBatchModal); GM_registerMenuCommand("🗑️ Clear Processed URLs", () => { GM_setValue(CONFIG.storage.processedUrls, {}); GM_notification({ text: "Cleared processed URLs history", timeout: 2000, }); }); GM_registerMenuCommand("📋 Show Status", () => { const processed = GM_getValue(CONFIG.storage.processedUrls, {}); const queue = getQueuedUrls(); const currentIndex = getCurrentIndex(); console.log("=== TradingView Downloader Status ==="); console.log(`Processed URLs: ${Object.keys(processed).length}`); console.log(`Queued URLs: ${queue.length}`); console.log(`Current Index: ${currentIndex}`); console.log(`Processing: ${isProcessing}`); GM_notification({ text: `Processed: ${Object.keys(processed).length} | Queue: ${ queue.length }`, timeout: 3000, }); }); // Check if we're on a script page if (currentURL.includes("tradingview.com/script/")) { // Check if we have a queue to process const queuedUrls = getQueuedUrls(); if (queuedUrls.length > 0 && isProcessing !== false) { console.log("Resuming queue processing..."); await processScriptPage(); } else if (!isUrlProcessed(currentURL)) { // Single page processing mode console.log("Processing single script page..."); await processScriptPage(); } else { console.log("Page already processed."); setTimeout(() => processNextInQueue(), 2000); } } // Auto-show modal on main TradingView pages (optional) if ( currentURL === "https://www.tradingview.com/" || currentURL.includes("tradingview.com/u/") ) { setTimeout(() => { if ( confirm( "🤖 Would you like to start batch downloading TradingView scripts?" ) ) { createBatchModal(); } }, 2000); } } // Start the script await init(); })();