TradingView Script Downloader

Complete TradingView script downloading solution with batch processing and status tracking

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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();
})();