// ==UserScript== // @name Tiktok - Get list of tiktok links // @namespace Violentmonkey Scripts // @version 0.1 // @license CC-BY-NC-SA-4.0 // @run-at document-end // @icon https://www.tiktok.com/favicon.ico // @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 none // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 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'; // Get the div with id "app-header" let appHeader = document.getElementById('app-header'); // Append the button to the div "app-header" appHeader.appendChild(downloadButton); console.log("Button added!"); // 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 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 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. }, node: { resolve: null, isNode: false, isResolveTime: false } } function nodeElaborateCustomArgs(customTypes) { // 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 if ((customTypes ?? "") !== "") { // If the provided value isn't nullish customTypes.forEach(e => { // Get each value let 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; var containerSets = [[], []]; // Array: [[Video link], [Video views]] var skipLinks = []; // Array: [Video link to skip] function loadWebpage() { if (document.querySelectorAll(".tiktok-qmnyxf-SvgContainer").length === 0) { // Checks if the SVG loading animation is present in the DOM 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(); 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(); } else { // The SVG animation is still there, so there are other contents to load. loadWebpage(); } }, 3500) } }, 150); } else { // Let's wait 1 second, so that TikTok has time to load content. setTimeout(function () { loadWebpage() }, 1000); } } function addArray() { var getClass = document.querySelectorAll(".tiktok-x6y88p-DivItemContainerV2, .css-x6y88p-DivItemContainerV2"); // Class of every video container for (var i = 0; i < getClass.length; i++) { // Simple information scraping: the link (getLink) is put in the first array, while the views (getViews) are put in the second one var getLink = scriptOptions.advanced.get_link_by_filter ? Array.from(getClass[i].querySelectorAll("a")).filter(e => e.href.indexOf("/video/") !== -1)[0]?.href : (getClass[i].querySelector("[data-e2e=user-post-item-desc]") ?? getClass[i].querySelector("[data-e2e=user-liked-item]") ?? getClass[i].querySelector("[data-e2e=music-item]") ?? getClass[i].querySelector("[data-e2e=user-post-item]") ?? getClass[i].querySelector("[data-e2e=favorites-item]") ?? getClass[i].querySelector("[data-e2e=challenge-item]")).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.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 (containerSets[0].indexOf(getLink) === -1 && skipLinks.indexOf(getLink) === -1) { // If the link hasn't been used, add it to the ContainerSets. containerSets[0].push(getLink); containerSets[1].push(((getClass[i].querySelector("[data-e2e=video-views]"))?.innerHTML ?? "0").replace("K", "00").replace("M", "00000")); } } } function sanitizeName(name) { // Replace a name with allowed Windows characters. return name.replaceAll("<", "‹").replaceAll(">", "›").replaceAll(":", "∶").replaceAll("\"", "″").replaceAll("/", "∕").replaceAll("\\", "∖").replaceAll("|", "¦").replaceAll("?", "¿").replaceAll("*", ""); } 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. var ytDlpScript = ""; for (var x = 0; x < containerSets[0].length; x++) { if (parseInt(containerSets[1][x]) < scriptOptions.min_views) continue; ytDlpScript += `${containerSets[0][x]}\n`; } if (scriptOptions.node.isNode && !scriptOptions.node.isResolveTime) return ytDlpScript.split("\n"); else downloadScript(ytDlpScript); // If the user has requested from Node to get the array, get it } function downloadScript(script) { // Download the script text to a file if (scriptOptions.node.isNode) { if (scriptOptions.node.isResolveTime) scriptOptions.node.resolve(script.split("\n")); else return script.split("\n"); return; } var blob = new Blob([script], { type: "text/plain" }); // Create a blob with the text var link = document.createElement("a"); var name = "TikTokLinks.txt"; // 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.txt"; break; case 1: // Fetch name from the website title name = `${document.title.substring(0, document.title.indexOf(" | TikTok"))}.txt`; break; case 2: // Fetch name from the first "h1" element on the page name = `${document.querySelector("h1")?.textContent.trim() ?? "TikTokLinks"}.txt`; 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: "text/plain" })); link.download = name; link.click(); URL.revokeObjectURL(link.href); } function requestTxtNow() { // 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. let 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(...containerSets[0]); containerSets = [[], []]; } return value; } function startDownload(name) { 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 } // console.log("starting") let element_to_observe = document; // Watch Everything... let observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.addedNodes.length == 1) { let top_node = mutation.addedNodes[0]; // The added node if (top_node instanceof Element && top_node.id === "app-header") { // Check if it's an Element console.log(top_node.id) let appHeader = top_node.querySelector("#app-header"); // Look for app-header element addDownloadButton(); } } }); }); observer.observe(element_to_observe, { childList: true, subtree: true }); })();