← Mike Higgins Home
Custom Code Experiment •

Reddit JSON Extractor

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reddit JSON Extractor</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism-tomorrow.min.css">
  <style>
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    html,
    body {
      height: 100%;
      font-family: "Inter", system-ui, sans-serif;
      line-height: 1.6;
    }

    body {
      background: #000000;
      color: #ffffff;
      min-height: 100vh;
      padding-bottom: 2rem;
    }

    .blog-content {
      max-width: 900px;
      margin: 0 auto;
      padding: 2rem;
    }

    .page-header {
      text-align: left;
      margin-bottom: 1rem;
      padding: 1rem 0;
    }

    .page-header .tagline {
      color: #cccccc;
      font-size: clamp(1rem, 2vw, 1.3rem);
      opacity: 0.9;
      font-weight: 400;
    }

    .intro-text {
      text-align: left;
      margin-bottom: 2rem;
    }

    .intro-text p {
      margin-bottom: 0.5rem;
      font-size: 1.1rem;
      opacity: 0.9;
      color: #cccccc;
    }

    .intro-text a {
      color: #ffffff;
      text-decoration: none;
      transition: color 0.3s ease;
    }

    .intro-text a:hover {
      color: #cccccc;
      text-decoration: underline;
    }

    .controls-container {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      margin-bottom: 2rem;
      padding: 1.5rem;
      background: rgba(20, 20, 20, 0.9);
      border-radius: 8px;
      border: 1px solid rgba(255, 255, 255, 0.2);
    }

    .input-group {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      text-align: left;
    }

    .input-group label {
      color: #ffffff;
      font-weight: 600;
      font-size: 1.1rem;
    }

    .url-input {
      padding: 0.75rem 1rem;
      font-size: 1rem;
      border-radius: 8px;
      background: rgba(40, 40, 40, 0.9);
      color: #ffffff;
      border: 2px solid rgba(255, 255, 255, 0.3);
      transition: all 0.3s ease;
      font-family: "Inter", system-ui;
      width: 100%;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    }

    .url-input:focus {
      outline: none;
      border-color: #ffffff;
      background: rgba(50, 50, 50, 0.9);
      box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
    }

    .button-group {
      display: flex;
      gap: 1rem;
      flex-wrap: wrap;
    }

    .btn {
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      border-radius: 8px;
      background: #000000;
      color: #ffffff;
      border: 2px solid #ffffff;
      cursor: pointer;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      font-family: "Inter", system-ui;
      font-weight: 600;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
      flex: 1;
      min-width: 150px;
      position: relative;
      overflow: hidden;
    }

    .btn::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 0;
      height: 0;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.1);
      transform: translate(-50%, -50%);
      transition: width 0.6s, height 0.6s;
    }

    .btn:hover::before {
      width: 300px;
      height: 300px;
    }

    .btn:hover {
      background: #ffffff;
      color: #000000;
      transform: translateY(-2px);
      box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3),
                  0 0 0 1px #ffffff,
                  0 0 15px rgba(255, 255, 255, 0.2);
      border-color: #ffffff;
    }

    .btn:active {
      transform: translateY(0);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
      transition: all 0.1s ease;
    }

    .btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
      transform: none;
      background: #333333;
      color: #888888;
      border-color: #666666;
    }

    .btn:disabled:hover {
      background: #333333;
      color: #888888;
      transform: none;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
    }

    .btn:disabled:hover::before {
      width: 0;
      height: 0;
    }

    .btn-secondary {
      background: #000000;
      border: 2px solid #cccccc;
      color: #cccccc;
    }

    .btn-secondary:hover {
      background: #cccccc;
      color: #000000;
      border-color: #ffffff;
      box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3),
                  0 0 0 1px #cccccc,
                  0 0 15px rgba(204, 204, 204, 0.2);
    }

    .json-display-container {
      margin-top: 2rem;
      display: none;
      text-align: left;
    }

    .json-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1rem;
      flex-wrap: wrap;
      gap: 1rem;
    }

    .json-header h2 {
      color: #ffffff;
      font-size: 1.5rem;
    }

    .json-display {
      background: rgba(20, 20, 20, 0.9);
      border-radius: 8px;
      padding: 0;
      border: 1px solid rgba(255, 255, 255, 0.2);
      overflow-x: auto;
      max-height: 600px;
      overflow-y: auto;
    }

    .loading {
      text-align: center;
      padding: 2rem;
      color: #ffffff;
      font-size: 1.2rem;
    }

    .error {
      background: rgba(50, 50, 50, 0.9);
      border: 1px solid rgba(255, 255, 255, 0.3);
      border-radius: 8px;
      padding: 1rem;
      margin-top: 1rem;
      color: #ffffff;
      text-align: left;
    }

    .proxy-info {
      background: rgba(30, 30, 30, 0.9);
      border: 1px solid rgba(255, 255, 255, 0.3);
      border-radius: 8px;
      padding: 1rem;
      margin-top: 1rem;
      font-size: 0.9rem;
      color: #cccccc;
      text-align: left;
    }

    .stats {
      background: rgba(20, 20, 20, 0.9);
      border: 1px solid rgba(255, 255, 255, 0.2);
      border-radius: 8px;
      padding: 1rem;
      margin-bottom: 1rem;
      display: none;
      text-align: left;
    }

    .stats h3 {
      color: #ffffff;
      margin-bottom: 0.5rem;
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 1rem;
    }

    .stat-item {
      text-align: center;
    }

    .stat-value {
      font-size: 1.5rem;
      font-weight: bold;
      color: #ffffff;
    }

    .stat-label {
      font-size: 0.9rem;
      opacity: 0.8;
      color: #cccccc;
    }

    .json-display pre {
      margin: 0;
      padding: 1.5rem;
      white-space: pre-wrap;
      word-wrap: break-word;
      overflow-wrap: break-word;
      max-width: 100%;
    }

    .json-display code {
      white-space: pre-wrap !important;
      word-wrap: break-word !important;
      overflow-wrap: break-word !important;
      display: block;
      max-width: 100%;
    }

    @media (max-width: 700px) {
      .blog-content {
        padding: 1.5rem;
      }

      .page-header {
        margin-bottom: 0.75rem;
        padding: 0.5rem 0;
      }

      .controls-container {
        margin-bottom: 1.5rem;
      }

      .intro-text {
        margin-bottom: 1.5rem;
      }

      .intro-text p {
        font-size: 1rem;
      }

      .button-group {
        flex-direction: column;
      }

      .btn {
        min-width: 100%;
      }

      .json-header {
        flex-direction: column;
        align-items: flex-start;
      }

      .stats-grid {
        grid-template-columns: 1fr 1fr;
      }

      .json-display pre {
        padding: 1rem;
        font-size: 0.85rem;
      }
    }
  </style>
</head>

<body>
  <div class="blog-content">
    <header class="page-header">
      <p class="tagline">Convert any Reddit post to clean, filtered JSON</p>
    </header>

    <section class="intro-text">
      <p>Paste a Reddit URL below</p>
    </section>

    <div class="controls-container">
      <div class="input-group">
        <label for="reddit-url">Reddit URL:</label>
        <input type="text" id="reddit-url" class="url-input" placeholder="Paste Reddit URL here..." value="https://www.reddit.com/r/OSINT/comments/1lhe2n0/google_maps_metadata/">
      </div>
      <div class="button-group">
        <button id="fetch-btn" class="btn">Fetch Clean JSON</button>
        <button id="download-btn" class="btn btn-secondary" disabled>Download JSON</button>
        <button id="clear-btn" class="btn btn-secondary">Clear</button>
      </div>
    </div>

    <div id="stats" class="stats">
      <h3>Post Statistics</h3>
      <div class="stats-grid">
        <div class="stat-item">
          <div class="stat-value" id="stat-upvotes">0</div>
          <div class="stat-label">Upvotes</div>
        </div>
        <div class="stat-item">
          <div class="stat-value" id="stat-comments">0</div>
          <div class="stat-label">Comments</div>
        </div>
        <div class="stat-item">
          <div class="stat-value" id="stat-ratio">0%</div>
          <div class="stat-label">Upvote Ratio</div>
        </div>
        <div class="stat-item">
          <div class="stat-value" id="stat-age">0d</div>
          <div class="stat-label">Age</div>
        </div>
      </div>
    </div>

    <div id="proxy-info" class="proxy-info" style="display: none;">
      <p><strong>Note:</strong> Using CORS proxy to bypass restrictions. This might be slower than direct requests.</p>
    </div>

    <div id="loading" class="loading" style="display: none;">
      Fetching and cleaning JSON data...
    </div>

    <div id="error-message" class="error" style="display: none;">
      Error: Unable to fetch JSON data. Please check the URL and try again.
    </div>

    <div id="json-display-container" class="json-display-container">
      <div class="json-header">
        <h2>Clean JSON Output</h2>
        <div>
          <button id="copy-btn" class="btn btn-secondary">Copy to Clipboard</button>
        </div>
      </div>
      <div class="json-display">
        <pre><code id="json-output" class="language-json"></code></pre>
      </div>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/prism.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/components/prism-json.min.js"></script>
  <script>
    document.addEventListener("DOMContentLoaded", function () {
      const redditUrlInput = document.getElementById("reddit-url");
      const fetchBtn = document.getElementById("fetch-btn");
      const downloadBtn = document.getElementById("download-btn");
      const clearBtn = document.getElementById("clear-btn");
      const copyBtn = document.getElementById("copy-btn");
      const jsonOutput = document.getElementById("json-output");
      const jsonDisplayContainer = document.getElementById(
        "json-display-container"
      );
      const loadingElement = document.getElementById("loading");
      const errorMessage = document.getElementById("error-message");
      const proxyInfo = document.getElementById("proxy-info");
      const statsElement = document.getElementById("stats");

      let cleanJsonData = null;

      const corsProxies = [
        "https://api.allorigins.win/raw?url=",
        "https://corsproxy.io/?",
        "https://cors-anywhere.herokuapp.com/",
        "https://api.codetabs.com/v1/proxy?quest="
      ];

      fetchBtn.addEventListener("click", fetchJsonData);
      redditUrlInput.addEventListener("keypress", function (e) {
        if (e.key === "Enter") {
          fetchJsonData();
        }
      });

      downloadBtn.addEventListener("click", downloadJson);

      clearBtn.addEventListener("click", clearAll);

      copyBtn.addEventListener("click", copyToClipboard);

      async function fetchJsonData() {
        let url = redditUrlInput.value.trim();

        if (!url) {
          showError("Please enter a Reddit URL");
          return;
        }

        if (!url.endsWith(".json")) {
          url += ".json";
        }

        loadingElement.style.display = "block";
        jsonDisplayContainer.style.display = "none";
        statsElement.style.display = "none";
        errorMessage.style.display = "none";
        proxyInfo.style.display = "block";
        downloadBtn.disabled = true;

        try {
          const rawData = await fetchWithProxies(url);
          cleanJsonData = cleanRedditData(rawData);
          displayJson(cleanJsonData);
          updateStats(cleanJsonData);
          downloadBtn.disabled = false;
        } catch (error) {
          console.error("Error fetching JSON:", error);
          showError(
            "Failed to fetch JSON data. Please check the URL and try again."
          );
        } finally {
          loadingElement.style.display = "none";
        }
      }

      function cleanRedditData(rawData) {
        if (
          !rawData ||
          !rawData[0] ||
          !rawData[0].data ||
          !rawData[0].data.children[0]
        ) {
          throw new Error("Invalid Reddit data structure");
        }

        const postData = rawData[0].data.children[0].data;
        const commentsData = rawData[1]?.data?.children || [];

        const cleanPost = {
          id: postData.id,
          title: postData.title,
          author: postData.author,
          subreddit: postData.subreddit,
          subreddit_name_prefixed: postData.subreddit_name_prefixed,
          selftext: postData.selftext || "",
          score: postData.score,
          upvote_ratio: postData.upvote_ratio,
          total_awards_received: postData.total_awards_received,
          num_comments: postData.num_comments,
          created_utc: postData.created_utc,
          created_human: new Date(postData.created_utc * 1000).toISOString(),
          permalink: `https://reddit.com${postData.permalink}`,
          url: postData.url,
          over_18: postData.over_18 || false,
          spoiler: postData.spoiler || false,
          locked: postData.locked || false,
          stickied: postData.stickied || false,
          edited: postData.edited || false
        };

        const cleanComments = commentsData
          .filter((comment) => comment.kind === "t1")
          .map((comment) => cleanCommentData(comment.data))
          .filter((comment) => comment !== null);

        return {
          metadata: {
            extracted_at: new Date().toISOString(),
            source_url: redditUrlInput.value.trim(),
            data_version: "1.0"
          },
          post: cleanPost,
          comments: cleanComments
        };
      }

      function cleanCommentData(comment) {
        if (
          !comment ||
          comment.body === "[deleted]" ||
          comment.body === "[removed]"
        ) {
          return null;
        }

        const cleanComment = {
          id: comment.id,
          author: comment.author,
          body: comment.body,
          score: comment.score,
          created_utc: comment.created_utc,
          created_human: new Date(comment.created_utc * 1000).toISOString(),
          depth: comment.depth || 0,
          permalink: `https://reddit.com${comment.permalink}`,
          is_submitter: comment.is_submitter || false,
          controversiality: comment.controversiality || 0
        };

        if (
          comment.replies &&
          typeof comment.replies === "object" &&
          comment.replies.data
        ) {
          cleanComment.replies = comment.replies.data.children
            .filter((reply) => reply.kind === "t1")
            .map((reply) => cleanCommentData(reply.data))
            .filter((reply) => reply !== null);
        }

        return cleanComment;
      }

      function updateStats(data) {
        if (!data.post) return;

        document.getElementById(
          "stat-upvotes"
        ).textContent = data.post.score.toLocaleString();
        document.getElementById(
          "stat-comments"
        ).textContent = data.post.num_comments.toLocaleString();
        document.getElementById("stat-ratio").textContent =
          Math.round(data.post.upvote_ratio * 100) + "%";

        const postDate = new Date(data.post.created_utc * 1000);
        const now = new Date();
        const daysOld = Math.floor((now - postDate) / (1000 * 60 * 60 * 24));
        document.getElementById("stat-age").textContent = daysOld + "d";

        statsElement.style.display = "block";
      }

      async function fetchWithProxies(url) {
        for (let i = 0; i < corsProxies.length; i++) {
          const proxy = corsProxies[i];
          const proxyUrl = proxy + encodeURIComponent(url);

          try {
            console.log(`Trying proxy ${i + 1}: ${proxy}`);
            const response = await fetch(proxyUrl, {
              method: "GET",
              headers: {
                Accept: "application/json"
              },
              signal: AbortSignal.timeout(15000)
            });

            if (response.ok) {
              console.log(`Proxy ${i + 1} succeeded`);
              return await response.json();
            } else {
              console.log(`Proxy ${i + 1} failed with status: ${response.status}`);
            }
          } catch (error) {
            console.log(`Proxy ${i + 1} error:`, error.message);
          }
        }

        throw new Error("All proxies failed");
      }

      function displayJson(data) {
        const formattedJson = JSON.stringify(data, null, 2);

        jsonOutput.textContent = formattedJson;
        Prism.highlightElement(jsonOutput);

        jsonDisplayContainer.style.display = "block";
      }

      function downloadJson() {
        if (!cleanJsonData) return;

        const formattedJson = JSON.stringify(cleanJsonData, null, 2);
        const blob = new Blob([formattedJson], { type: "application/json" });
        const url = URL.createObjectURL(blob);

        let filename = "reddit_clean_data.json";
        try {
          const urlParts = redditUrlInput.value.split("/");
          if (urlParts.length > 6) {
            filename = `reddit_${urlParts[4]}_${urlParts[6]}_clean.json`;
          }
        } catch (e) {
          console.error("Error generating filename:", e);
        }

        const a = document.createElement("a");
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      }

      function clearAll() {
        redditUrlInput.value = "";
        jsonOutput.textContent = "";
        jsonDisplayContainer.style.display = "none";
        errorMessage.style.display = "none";
        proxyInfo.style.display = "none";
        statsElement.style.display = "none";
        downloadBtn.disabled = true;
        cleanJsonData = null;
      }

      function copyToClipboard() {
        if (!jsonOutput.textContent) return;

        navigator.clipboard
          .writeText(jsonOutput.textContent)
          .then(() => {
            const originalText = copyBtn.textContent;
            copyBtn.textContent = "Copied!";
            setTimeout(() => {
              copyBtn.textContent = originalText;
            }, 2000);
          })
          .catch((err) => {
            console.error("Failed to copy: ", err);
            showError("Failed to copy to clipboard");
          });
      }

      function showError(message) {
        errorMessage.textContent = `Error: ${message}`;
        errorMessage.style.display = "block";
        jsonDisplayContainer.style.display = "none";
        statsElement.style.display = "none";
      }

      if (!AbortSignal.timeout) {
        AbortSignal.timeout = function (ms) {
          const abortController = new AbortController();
          setTimeout(() => abortController.abort(new Error("Timeout")), ms);
          return abortController.signal;
        };
      }
    });
  </script>
</body>

</html>