Greasy Fork

Greasy Fork is available in English.

Tiktok Timeline Downloader

Download entier profile timeline

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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