Tiktok Timeline Downloader

Download entier profile timeline

// ==UserScript==
// @name         Tiktok Timeline Downloader
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Download entier profile timeline
// @match        https://www.tiktok.com/@*
// @match        tiktok.com/@*
// @match        https://discord.com/channels/*
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @license      MIT
// @icon         https://tikwm.com/favicon.ico
// ==/UserScript==

(function () {
  ("use strict");
  const tikTokVideoIdRegex = /\/video\/(\d+)(\/|$)/;
  const discordTikTokUrlRegex = /https:\/\/www.tiktok.com\/(\w+)\/video\/(\d+)/;
  const tikTokPhotoIdRegex = /\/@[\w.]+\/photo\/(\d+)/;
  const tikwmRegex = /https:\/\/.*tiktokcdn\.com.*/;
  const username = window.location.href.match(/\/@([\w.]+)/)[1];

  async function getPhotoLinks(url) {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url,
        onload: function (response) {
          if (response.status === 200) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(
              response.responseText,
              "text/html"
            );
            const anchors = doc.querySelectorAll(
              'a[target="_blank"][href*="tiktokcdn.com"]'
            );
            const links = Array.from(anchors);
            const urls = links.map((link) => link.href);
            resolve(urls); // Return the array of links
          } else {
            console.error(`Failed to fetch ${url}: ${response.status}`);
            resolve([]); // Return empty array on error
          }
        },
        onerror: function (err) {
          console.error(`Error fetching ${url}:`, err);
          resolve([]); // Return empty array on error
        },
      });
    });
  }

  async function download(url) {
    return new Promise(async (resolve) => {
      let isImage = url.match(tikwmRegex);
      if (isImage) {
        let fileName = url.split("/").pop().split("?")[0];
        let name = `@${username}-${fileName}`;
        GM_download({
          url,
          name,
          onload: () => {
            resolve(true);
          },
          onerror: () => {
            resolve(false);
          },
        });
        const delay = Math.floor(Math.random() * (500 - 200 + 1)) + 200;
        await new Promise((resolve) => setTimeout(resolve, delay));
      } else {
        let tikTokVideoIdMatch = url.match(tikTokVideoIdRegex);
        let discordTikTokUrlMatch = url.match(discordTikTokUrlRegex);
        let fileName = "";
        let newUrl = "";
        if (tikTokVideoIdMatch) {
          let videoId = tikTokVideoIdMatch[1];
          fileName = `@${username}-${videoId}.mp4`;
          newUrl = `https://tikwm.com/video/media/hdplay/${videoId}.mp4`;
        } else if (discordTikTokUrlMatch) {
          let videoId = discordTikTokUrlMatch[2];
          fileName = `@${username}-${videoId}.mp4`;
          newUrl = `https://tikwm.com/video/media/hdplay/${videoId}.mp4`;
        }
        GM_download({
          url: newUrl,
          name: fileName,
          onload: () => {
            resolve(true);
          },
          onerror: () => {
            resolve(false);
          },
        });
      }
    });
  }

  // Function to scroll to the bottom of the page slowly
  async function scrollToBottom() {
    return new Promise((resolve) => {
      const interval = 1000; // Time between scrolls (ms)
      const scrollStep = window.innerHeight; // Scroll by one viewport height
      const maxScrollRetries = 50; // Maximum attempts to prevent infinite loop
      let retries = 0;

      const scrollInterval = setInterval(() => {
        const { scrollTop, scrollHeight, clientHeight } =
          document.documentElement;
        const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;

        if (isAtBottom || retries >= maxScrollRetries) {
          clearInterval(scrollInterval);
          resolve();
          console.log("Reached the bottom of the page.");
        } else {
          window.scrollBy(0, scrollStep);
          retries++;
        }
      }, interval);
    });
  }

  // Create "Download Timeline" button
  const button = document.createElement("button");
  button.textContent = "Download Timeline";
  button.style.position = "fixed";
  button.style.bottom = "20px";
  button.style.right = "20px";
  button.style.zIndex = "9999";
  button.style.backgroundColor = "#007BFF";
  button.style.color = "#FFF";
  button.style.border = "none";
  button.style.padding = "10px 20px";
  button.style.fontSize = "16px";
  button.style.borderRadius = "5px";
  button.style.cursor = "pointer";
  button.style.boxShadow = "0px 4px 6px rgba(0, 0, 0, 0.1)";
  button.addEventListener(
    "mouseover",
    () => (button.style.backgroundColor = "#0056b3")
  );
  button.addEventListener(
    "mouseout",
    () => (button.style.backgroundColor = "#007BFF")
  );

  document.body.appendChild(button);

  button.addEventListener("click", async function () {
    button.disabled = true;
    button.textContent = "Scrolling to the bottom of the page...";
    await scrollToBottom();

    button.textContent = "Collecting post items...";
    const postItems = document.querySelectorAll('[data-e2e="user-post-item"]');
    const hrefs = Array.from(postItems).map((post) => {
      const anchor = post.querySelector("a");
      return anchor ? anchor.href : null;
    });

    const urls = [];

    for (const href of hrefs) {
      const isImageUrl = href.match(tikTokPhotoIdRegex);
      if (isImageUrl) {
        const photos = await getPhotoLinks(
          `https://tikwm.com/video/${isImageUrl}.html`
        );
        urls.push(...photos);
        const delay = Math.floor(Math.random() * (500 - 200 + 1)) + 200;
        await new Promise((resolve) => setTimeout(resolve, delay));
      } else {
        urls.push(href);
      }
    }

    let remaining = urls.length;
    let downloaded = 0;
    let failed = 0;

    if (urls.length > 0) {
      console.log(`Found ${postItems.length} user post items.`);
      let i = 1;
      for (const href of urls) {
        button.textContent = `${remaining} Remaining | ${downloaded} Downloaded | ${failed} Failed`;
        console.log(`Downloading ${i} of ${urls.length}`);
        const success = await download(href);
        if (success) {
          downloaded++;
        } else {
          failed++;
        }
        remaining--;
        const delay = Math.floor(Math.random() * (1000 - 500 + 1)) + 500;
        await new Promise((resolve) => setTimeout(resolve, delay));
        i++;
      }

      postItems.forEach((item, index) => {
        console.log(`Post ${index + 1}:`, item);
      });
    } else {
      alert("No user post items found.");
    }

    // Reset button text after all downloads are complete
    button.disabled = false;
    button.textContent = "Download Timeline";
  });
})();