Lihuelworks's Tiktok - Get list of tiktok links (Updated!)

Adds a button to TikTok to get a list of all tiktok video links (e.g from a tiktok profile) to use in yt-dlp (Scroll an list link download code is by Dinoosauro https://github.com/Dinoosauro/tiktok-to-ytdlp)

// ==UserScript==
// @name         Lihuelworks's Tiktok - Get list of tiktok links (Updated!)
// @namespace    Violentmonkey Scripts
// @version      0.2
// @license CC-BY-NC-SA-4.0
// @run-at document-end
// @icon https://www.tiktok.com/favicon.ico
// @homepageURL https://github.com/lihuelworks/tiktok-to-ytdlp-userscript
// @description  Adds a button to TikTok to get a list of all tiktok video links (e.g from a tiktok profile) to use in yt-dlp (Scroll an list link download code is by Dinoosauro https://github.com/Dinoosauro/tiktok-to-ytdlp)
// @author       Lihuelworks (with code from Dinoosauro's https://github.com/Dinoosauro/tiktok-to-ytdlp)
// @match        https://www.tiktok.com/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  "use strict";

  // Add custom styles including reset and Material Design
  GM_addStyle(`
        #lihuelworks-tiktok-links-download-button {
            border: none;
            margin: 0;
            padding: 0;
            width: auto;
            overflow: visible;
            background: transparent;
            color: inherit;
            font: inherit;
            line-height: normal;
            -webkit-font-smoothing: inherit;
            -moz-osx-font-smoothing: inherit;
            -webkit-appearance: none;
        }

        #lihuelworks-tiktok-links-download-button::-moz-focus-inner {
            border: 0;
            padding: 0;
        }

        /* Material Design styles */
        #lihuelworks-tiktok-links-download-button {
            padding: 12px 24px;
            background-color: #6200ea;
            color: white;
            font-size: 14px;
            font-weight: 500;
            border-radius: 4px;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
            cursor: pointer;
            transition: background-color 0.3s ease, transform 0.3s ease;
        }

        #lihuelworks-tiktok-links-download-button:hover {
            background-color: #3700b3;
            transform: scale(1.05);
        }

        #lihuelworks-tiktok-links-download-button:focus {
            outline: none;
            box-shadow: 0 0 0 2px rgba(98, 0, 234, 0.5);
        }

        #lihuelworks-tiktok-links-download-button:active {
            background-color: #03dac6;
        }
    `);

  // Function to create and append the download button
  function addDownloadButton() {
    // Create a button element
    let downloadButton = document.createElement("button");
    downloadButton.textContent = "Get list of TikTok links";
    downloadButton.style.order = "-1";
    downloadButton.style.zIndex = "9999999";
    downloadButton.id = "lihuelworks-tiktok-links-download-button";

    // Get the first element with a class that contains "DivSearchWrapper"
    let searchWrapper = document.querySelectorAll(
      '[class*="DivSearchWrapper"]'
    )[0];

    if (searchWrapper) {
      // Insert the button above the first element with class containing DivSearchWrapper
      searchWrapper.parentElement.insertBefore(downloadButton, searchWrapper);
      console.log("Button added!");
    } else {
      console.log("DivSearchWrapper not found!");
    }

    // Add click event listener to the button
    downloadButton.addEventListener("click", function () {
      // Call the function to start downloading TikTok videos
      tiktoktoytdlp();
    });
  }

  // Function to start downloading TikTok videos
  function tiktoktoytdlp() {
    // Using var in the global part of the script so that the script can be re-used also in WebKit & Gecko
    var scriptOptions = {
      scrolling_min_time: 1300, // Change the mininum time the script will try to refresh the page
      scrolling_max_time: 2100, // Change the maxinum time the script will try to refresh the page
      min_views: -1, // If a video has fewer views than this, it won't be included in the script.
      delete_from_next_txt: true, // Delete all the items put in the previous .txt file when asking for a new one. Useful only if you want to obtain a .txt file while scrolling.
      output_name_type: 2, // Put a string to specify a specific name of the file. Put 0 for trying to fetching it using data tags, 1 for fetching it from the window title, 2 for fetching it from the first "h1" element. _Invalid_ inputs will use the standard "TikTokLinks.txt". This will be edited if a different value is passed from the startDownload() function.
      adapt_text_output: true, // Replace characters that are prohibited on Windows
      allow_images: true, // Save also TikTok Image URLs
      export_format: "txt", // Put "json" to save everything as a JSON file.
      exclude_from_json: [], // If you plan to export the content in a JSON file, here you can exclude some properties from the JSON output. You can exclude "url", "views", "caption".
      advanced: {
        get_array_after_scroll: false, // Gets the item links after the webpage is fully scrolled, and not after every scroll.
        get_link_by_filter: true, // Get the website link by inspecting all the links in the container div, instead of looking for data references.
        check_nullish_link: true, // Check if a link is nullish and, if true, try with the next video.
        log_link_error: true, // Write in the console if there's an error when fetching the link.
        maximum_downloads: Infinity, // Change this to a finite number to fetch only a specific number of values. Note that a) more elements might be added to the final file if available; and b) "get_array_after_scroll" must be set to false.
        delete_from_dom: false, // Automatically delete the added items from the DOM. This works only if "get_array_after_scroll" is disabled. This is suggested only if you need to download a page with lots of videos
        get_video_container_from_e2e: false, // Use the [data-e2e] attributes for getting the video container, instead of the normal CSS class.
      },
      node: {
        resolve: null,
        isNode: false,
        isResolveTime: false,
      },
    };
    /**
     * A function that is able to read a double array, composed with `[["the property name", "the property value"]]`, and change the value of the scriptOptions array
     * @param {string[][]} customTypes the double array
     */
    function nodeElaborateCustomArgs(customTypes) {
      if ((customTypes ?? "") !== "") {
        // If the provided value isn't nullish
        customTypes.forEach((e) => {
          // Get each value
          var optionChange = e[0].split("=>"); // The arrow (=>) is used to indicate that the property is in a nested object (ex: advanced=>log_link_error).
          optionChange.length === 1
            ? (scriptOptions[e[0]] = e[1])
            : (scriptOptions[optionChange[0]][optionChange[1]] = e[1]); // If the length is 1, just change the option. Otherwise, look for the nested object and change its value
        });
      }
    }
    // SCRIPT START:
    var height = document.body.scrollHeight;
    /**
     * A Map that contains the video URL as its key, and the video views and caption as its value
     */
    var containerMap = new Map([]);
    /**
     * The array of video links to skip
     */
    var skipLinks = [];
    /**
     * Scroll the webpage
     */
    function loadWebpage() {
      if (
        document.querySelectorAll(".tiktok-qmnyxf-SvgContainer").length === 0
      ) {
        // Checks if the SVG loading animation is present in the DOM
        !scriptOptions.advanced.get_array_after_scroll &&
          scriptOptions.advanced.delete_from_dom &&
          window.scrollTo({
            top:
              document.body.scrollHeight -
              window.outerHeight * (window.devicePixelRatio || 1),
            behavior: "smooth",
          }); // If items from the DOM are removed, the page must be scrolled a little bit higher, so that the TikTok refresh is triggered
        setTimeout(
          () => {
            window.scrollTo({
              top: document.body.scrollHeight,
              behavior: "smooth",
            }); // Scroll to the bottom of the page
            setTimeout(() => {
              if (height !== document.body.scrollHeight) {
                // The webpage has scrolled the previous time, so we can try another scroll
                if (!scriptOptions.advanced.get_array_after_scroll) {
                  addArray();
                  if (
                    scriptOptions.advanced.maximum_downloads <
                    Array.from(containerMap).length + skipLinks.length
                  ) {
                    // If the number of fetched items is above the permitted one, download the script and don't do anything.
                    ytDlpScript();
                    return;
                  }
                }
                setTimeout(() => {
                  height = document.body.scrollHeight;
                  loadWebpage();
                }, Math.floor(Math.random() * scriptOptions.scrolling_max_time + scriptOptions.scrolling_min_time));
              } else {
                setTimeout(() => {
                  if (
                    document.querySelectorAll(".tiktok-qmnyxf-SvgContainer")
                      .length === 0 &&
                    height == document.body.scrollHeight
                  ) {
                    // By scrolling, the webpage height doesn't change, so let's download the txt file
                    scriptOptions.node.isResolveTime = true;
                    ytDlpScript();
                    skipLinks = []; // Restore so that the items can be re-downloaded
                  } else {
                    // The SVG animation is still there, so there are other contents to load.
                    loadWebpage();
                  }
                }, 3500);
              }
            }, 150);
          },
          !scriptOptions.advanced.get_array_after_scroll &&
            scriptOptions.advanced.delete_from_dom
            ? Math.floor(Math.random() * 600 + 600)
            : 1
        );
      } else {
        // Let's wait 1 second, so that TikTok has time to load content.
        setTimeout(function () {
          loadWebpage();
        }, 1000);
      }
    }
    /**
     * Elaborate items in the page
     */
    function addArray() {
      const e2eLinks =
        "[data-e2e=user-liked-item], [data-e2e=music-item], [data-e2e=user-post-item], [data-e2e=favorites-item], [data-e2e=challenge-item], [data-e2e=search_top-item]";
      let container = document.querySelectorAll(
        scriptOptions.advanced.get_video_container_from_e2e
          ? e2eLinks
          : ".tiktok-1uqux2o-DivItemContainerV2, .css-ps7kg7-DivThreeColumnItemContainer, .tiktok-x6y88p-DivItemContainerV2, .css-1uqux2o-DivItemContainerV2, .css-x6y88p-DivItemContainerV2, .css-1soki6-DivItemContainerForSearch, .css-ps7kg7-DivThreeColumnItemContainer"
      ); // Class of every video container
      if (scriptOptions.advanced.get_video_container_from_e2e)
        container = Array.from(container).map((item) => item.parentElement);
      for (const tikTokItem of container) {
        if (!tikTokItem) continue; // Skip nullish results
        const getLink = scriptOptions.advanced.get_link_by_filter
          ? Array.from(tikTokItem.querySelectorAll("a")).filter(
              (e) =>
                e.href.indexOf("/video/") !== -1 ||
                e.href.indexOf("/photo/") !== -1
            )[0]?.href
          : tikTokItem
              .querySelector(`[data-e2e=user-post-item-desc], ${e2eLinks}`)
              ?.querySelector("a")?.href; // If the new filter method is selected, the script will look for the first link that contains a video link structure. Otherwise, the script'll look for data tags that contain the video URL.
        if (!scriptOptions.allow_images && getLink.indexOf("/photo/") !== -1)
          continue; // Avoid adding photo if the user doesn't want to.
        if (
          scriptOptions.advanced.check_nullish_link &&
          (getLink ?? "") === ""
        ) {
          // If the script needs to check if the link is nullish, and it's nullish...
          if (scriptOptions.advanced.log_link_error)
            console.log("SCRIPT ERROR: Failed to get link!"); // If the user wants to print the error in the console, write it
          continue; // And, in general, continue with the next link.
        }
        if (skipLinks.indexOf(getLink) === -1) {
          const views =
            tikTokItem.querySelector(
              ".css-cralc2-SpanPlayCount, [data-e2e=video-views]"
            )?.innerHTML ?? "0";
          const caption =
            tikTokItem.querySelector(".css-vi46v1-DivDesContainer a span")
              ?.textContent ??
            tikTokItem.querySelector(".css-a3te33-AVideoContainer picture img")
              ?.alt ??
            "";
          containerMap.set(getLink, {
            views: `${views
              .replace(".", "")
              .replace("K", "00")
              .replace("M", "00000")}${
              (views.indexOf("K") !== -1 || views.indexOf("M") !== -1) &&
              views.indexOf(".") === -1
                ? "0"
                : ""
            }`,
            caption,
          });
        }
      }
      if (
        !scriptOptions.advanced.get_array_after_scroll &&
        scriptOptions.advanced.delete_from_dom
      ) {
        // Delete all the items from the DOM. Only the last 20 items will be kept.
        for (const item of Array.from(container).slice(
          0,
          container.length - 20
        ))
          item.remove();
      }
    }
    /**
     * Replace a name with allowed Windows characters.
     * @param {string} name
     * @returns the "sanitized" string
     */
    function sanitizeName(name) {
      return name
        .replaceAll("<", "‹")
        .replaceAll(">", "›")
        .replaceAll(":", "∶")
        .replaceAll('"', "″")
        .replaceAll("/", "∕")
        .replaceAll("\\", "∖")
        .replaceAll("|", "¦")
        .replaceAll("?", "¿")
        .replaceAll("*", "");
    }
    /**
     * Delete the keys that the user doesn't want in the output JSON
     * @param {any} obj
     * @returns the object without those keys
     */
    function deleteUnrequestedContent(obj) {
      for (const key in obj)
        if (scriptOptions.exclude_from_json.indexOf(key) !== -1)
          delete obj[key];
      if (Object.keys(obj).length === 1) return obj[Object.keys(obj)[0]];
      return obj;
    }
    /**
     * Generate the output file
     * @returns if running on Node, a string array or an Object. If running on the console, undefined.
     */
    function ytDlpScript() {
      addArray(); // Add the last elements in the DOM, or all the elements if get_array_after_scroll is set to true.
      // Create the txt file with all of the TikTok links.
      let ytDlpScript = scriptOptions.export_format === "json" ? [] : "";
      for (const [url, obj] of Array.from(containerMap)) {
        if (+obj.views < scriptOptions.min_views) continue;
        scriptOptions.export_format === "json"
          ? ytDlpScript.push(deleteUnrequestedContent({ ...obj, url }))
          : (ytDlpScript += `${url}\n`);
      }
      if (scriptOptions.node.isNode && !scriptOptions.node.isResolveTime)
        return getWhatToReturn(ytDlpScript);
      else
        downloadScript(
          typeof ytDlpScript === "object"
            ? JSON.stringify(ytDlpScript)
            : ytDlpScript
        ); // If the user has requested from Node to get the array, get it
    }
    /**
     * Get if a JSON object array should be returned, or if a splitted string.
     * @param {any | string} content the content that needs to be returned
     * @returns the content to return
     */
    function getWhatToReturn(content) {
      return typeof content === "object" ? content : content.split("\n");
    }
    /**
     * Download the script text to a file
     * @param {string} script the content of the output file
     * @param {boolean} force force download of the script, even if on Node
     */
    function downloadScript(script, force) {
      if (scriptOptions.node.isNode && !force) {
        if (scriptOptions.node.isResolveTime)
          scriptOptions.node.resolve(getWhatToReturn(script));
        else return getWhatToReturn(script);
        scriptOptions.node.resolve = null;
        scriptOptions.node.isResolveTime = false;
        return;
      }
      const blob = new Blob([script], { type: "text/plain" }); // Create a blob with the text
      const link = document.createElement("a");
      let name = `TikTokLinks.${scriptOptions.export_format}`; // Set the standard name
      switch (
        scriptOptions.output_name_type // Look at the type of the name
      ) {
        case 0: // Fetch name from data tags
          name =
            document
              .querySelector("[data-e2e=user-title]")
              ?.textContent.trim() ??
            document
              .querySelector("[data-e2e=browse-username]")
              ?.firstChild?.textContent.trim() ??
            document
              .querySelector("[data-e2e=browse-username]")
              ?.textContent.trim() ??
            document
              .querySelector("[data-e2e=challenge-title]")
              ?.textContent.trim() ??
            document
              .querySelector("[data-e2e=music-title]")
              ?.textContent.trim() ??
            `TikTokLinks.${scriptOptions.export_format}`;
          break;
        case 1: // Fetch name from the website title
          name = `${document.title.substring(
            0,
            document.title.indexOf(" | TikTok")
          )}.${scriptOptions.export_format}`;
          break;
        case 2: // Fetch name from the first "h1" element on the page
          name = `${
            document.querySelector("h1")?.textContent.trim() ?? "TikTokLinks"
          }.${scriptOptions.export_format}`;
          break;
      }
      if (typeof scriptOptions.output_name_type === "string")
        name = scriptOptions.output_name_type; // If it's a string, apply it to the output name
      if (scriptOptions.adapt_text_output) name = sanitizeName(name); // If the user wants to use safe characters only, adapt the string name.
      link.href = URL.createObjectURL(
        new File([blob], name, {
          type:
            scriptOptions.export_format === "json"
              ? "application/json"
              : "text/plain",
        })
      );
      link.download = name;
      link.click();
      URL.revokeObjectURL(link.href);
    }
    /**
     * Write requestTxtNow() in the console to obtain the .txt file while converting. Useful if you have lots of items, and you want to start downloading them.
     * @returns the current script
     */
    function requestTxtNow() {
      const value = ytDlpScript();
      if (scriptOptions.delete_from_next_txt) {
        // If delete_from_next_txt is enabled, delete the old items, so that only the newer ones will be downloaded.
        skipLinks.push(...Array.from(containerMap).map((item) => item[0]));
        containerMap = new Map([]);
      }
      return value;
    }
    function startDownload(name) {
      containerMap = new Map([]);
      skipLinks = [];
      if ((name ?? "") !== "") scriptOptions.output_name_type = name; // Update the file name type if it's provided a non-nullish value
      if (scriptOptions.node.isNode) {
        return new Promise((resolve) => {
          scriptOptions.node.resolve = resolve;
          loadWebpage();
        });
      } else loadWebpage(); // And start scrolling the webpage
    }
    nodeElaborateCustomArgs();
    startDownload(); // Add as an argument a custom file name (or a custom file type value), or edit it from the scriptOptions.output_name_type
  }

  // Run the addDownloadButton function to add the button to the page
  window.addEventListener("load", function () {
    addDownloadButton();
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址