Greasy Fork

Greasy Fork is available in English.

TikWM TikTok Batch Downloader

Automates downloading a batch of TikTok videos in original quality via tikwm.com

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TikWM TikTok Batch Downloader
// @namespace    http://greasyfork.icu/en/users/318296-thomased
// @version      1.0.6
// @description  Automates downloading a batch of TikTok videos in original quality via tikwm.com
// @author       Gemini 3.1 Pro + Claude Sonnet 4.6
// @license      MIT
// @icon         https://www.tikwm.com/favicon.ico
// @match        https://www.tikwm.com/originalDownloader*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    const K = {
        QUEUE:       'tkwm_queue',
        ACTIVE:      'tkwm_active',
        DONE_COUNT:  'tkwm_done_count',
        AUTORUN:     'tkwm_autorun',
        FOLDER:      'tkwm_folder_name',
        APPEND_DESC: 'tkwm_append_desc',
        SESSION_ID:  'tkwm_sessionid',
    };

    const gmGet  = k       => GM_getValue(k, null);
    const gmSet  = (k, v)  => GM_setValue(k, v);
    const getQ   = ()      => JSON.parse(gmGet(K.QUEUE) || '[]');
    const saveQ  = q       => gmSet(K.QUEUE, JSON.stringify(q));

    if (!gmGet(K.FOLDER)) gmSet(K.FOLDER, '#TikTok');
    if (!gmGet(K.APPEND_DESC)) gmSet(K.APPEND_DESC, 'false');

    const storedSession = gmGet(K.SESSION_ID);

    if (storedSession && !window.location.search.includes(`cookie=sessionid=${storedSession}`)) {
        window.location.replace(`https://www.tikwm.com/originalDownloader.html?cookie=sessionid=${storedSession}`);
        return;
    }

    GM_registerMenuCommand('Change Download Folder Name', () => {
        const currentFolder = gmGet(K.FOLDER) || '#TikTok';
        const newFolder = prompt('Enter the base download folder name (inside your browser\'s default Downloads directory):', currentFolder);
        if (newFolder !== null && newFolder.trim() !== '') {
            gmSet(K.FOLDER, newFolder.trim());
            alert(`Download folder changed to: ${newFolder.trim()}`);
        }
    });

    GM_registerMenuCommand('Toggle Description in Filename', () => {
        const currentVal = gmGet(K.APPEND_DESC) === 'true';
        const newVal = !currentVal;
        gmSet(K.APPEND_DESC, newVal ? 'true' : 'false');

        const cb = document.getElementById('tkwm_cb_desc');
        if (cb) cb.checked = newVal;

        alert(`Append Description is now: ${newVal ? 'ON' : 'OFF'}`);
    });

    const PANEL_W = 340;

    GM_addStyle(`
        #tkwm_tab {
            position: fixed; left: 0; top: 50%; width: 18px; height: 44px; margin-top: -22px;
            background: #bdc5c8; border: 1px solid #abb0b3; border-left: none;
            border-radius: 0 6px 6px 0; cursor: pointer; z-index: 2147483646;
            display: flex; align-items: center; justify-content: center; opacity: 0.7;
        }
        #tkwm_tab:hover { opacity: 1; }
        #tkwm_panel {
            position: fixed; left: 0; top: 50%; transform: translate(-100%, -50%);
            width: ${PANEL_W}px; background: rgba(0, 0, 0, 0.85); color: #fff;
            padding: 10px 10px 10px 28px; border-radius: 0 8px 8px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.4);
            transition: transform 140ms linear, opacity 140ms linear;
            opacity: 0.92; z-index: 2147483645;
            font-family: system-ui, Segoe UI, Arial; font-size: 12px;
        }
        #tkwm_panel.show { transform: translate(0, -50%); opacity: 1; }
        #tkwm_header { margin-bottom: 8px; }
        #tkwm_title { font-weight: 700; font-size: 13px; margin-bottom: 2px; color: #4CAF50; }
        #tkwm_info_row { display: flex; justify-content: space-between; opacity: 0.9; font-size: 11px; }
        .tkwm_row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; align-items: center; }
        .tkwm_btn {
            border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.08);
            color: #fff; padding: 4px 10px; border-radius: 999px; cursor: pointer;
            font-size: 12px; line-height: 1.2; flex: 1; text-align: center;
        }
        .tkwm_btn:hover { background: rgba(255,255,255,0.18); }
        #tkwm_btn_start { border-color: rgba(100,220,120,0.6); }
        #tkwm_btn_stop { border-color: rgba(220,100,100,0.6); }
        #tkwm_btn_reset { border-color: rgba(220,200,100,0.6); }
        #tkwm_ta {
            width: 100%; height: 110px; margin-top: 8px; background: rgba(255,255,255,0.1);
            border: 1px solid rgba(255,255,255,0.2); color: #fff; font-size: 11px;
            resize: vertical; padding: 6px; box-sizing: border-box; border-radius: 4px;
            white-space: pre; overflow-x: auto;
        }
        #tkwm_ta:not([readonly]) {
            background: rgba(15, 32, 64, 0.9);
            border-color: #4CAF50;
        }
        #tkwm_settings_row { margin-top: 10px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1); display: flex; flex-direction: column; align-items: stretch; }
        .tkwm_checkbox_label { display: flex; align-items: center; gap: 6px; font-size: 11px; cursor: pointer; opacity: 0.9; margin-bottom: 6px;}
        .tkwm_checkbox_label:hover { opacity: 1; }
        .tkwm_input {
            background: rgba(0, 0, 0, 0.4);
            border: 1px solid rgba(255, 255, 255, 0.2);
            color: #fff; padding: 4px 6px; border-radius: 4px;
            font-size: 11px; flex: 1; width: 100%;
        }
        .tkwm_input:focus { outline: none; border-color: #4CAF50; }
    `);

    const tab = document.createElement('div');
    tab.id = 'tkwm_tab';
    tab.innerHTML = '<svg width="12" height="12"><path id="tkwm_arrow" d="M4 2 L8 6 L4 10 Z" fill="#2b2f33"/></svg>';
    document.body.appendChild(tab);

    const panel = document.createElement('div');
    panel.id = 'tkwm_panel';
    panel.innerHTML = `
        <div id="tkwm_header">
            <div id="tkwm_title">TikWM Batch Downloader</div>
            <div id="tkwm_info_row">
                <span id="tkwm_count">0 in queue</span>
                <span id="tkwm_status">Idle</span>
            </div>
        </div>
        <textarea id="tkwm_ta" readonly placeholder="Queue is empty."></textarea>

        <div id="tkwm_controls_main">
            <div class="tkwm_row">
                <button class="tkwm_btn" id="tkwm_btn_start">Start</button>
                <button class="tkwm_btn" id="tkwm_btn_stop">Stop</button>
                <button class="tkwm_btn" id="tkwm_btn_add">Add</button>
                <button class="tkwm_btn" id="tkwm_btn_edit">Edit</button>
            </div>
            <div class="tkwm_row">
                <button class="tkwm_btn" id="tkwm_btn_reset">Clear Queue</button>
                <button class="tkwm_btn" id="tkwm_import">Import</button>
            </div>
            <div id="tkwm_settings_row">
                <label class="tkwm_checkbox_label">
                    <input type="checkbox" id="tkwm_cb_desc"> Append Description to Filename
                </label>
                <div style="display:flex; gap:6px;">
                    <input type="text" id="tkwm_session_input" class="tkwm_input" placeholder="Private sessionid...">
                    <button class="tkwm_btn" id="tkwm_btn_session" style="flex: 0 0 auto;">Set</button>
                </div>
            </div>
        </div>

        <div id="tkwm_controls_edit" style="display:none;">
            <div class="tkwm_row">
                <button class="tkwm_btn" id="tkwm_btn_save" style="background:#2e7d32;">Save Changes</button>
                <button class="tkwm_btn" id="tkwm_btn_cancel" style="background:#bf360c;">Cancel</button>
            </div>
        </div>
    `;
    document.body.appendChild(panel);

    const fi = document.createElement('input');
    fi.type = 'file';
    fi.id = 'tkwm_file_input';
    fi.style.display = 'none';
    panel.appendChild(fi);

    const arrow = document.getElementById('tkwm_arrow');
    const ta = document.getElementById('tkwm_ta');
    const ctrlMain = document.getElementById('tkwm_controls_main');
    const ctrlEdit = document.getElementById('tkwm_controls_edit');
    const cbDesc = document.getElementById('tkwm_cb_desc');
    const sessionInput = document.getElementById('tkwm_session_input');
    const btnSession = document.getElementById('tkwm_btn_session');

    let isShown = false;
    let hideTimer = null;
    let pinnedUntil = 0;
    let editMode = 'none';
    let isProcessing = false;

    if (storedSession) {
        sessionInput.value = storedSession;
        btnSession.textContent = 'Clear';
        btnSession.style.borderColor = 'rgba(220,100,100,0.6)';
    }

    btnSession.addEventListener('click', () => {
        pinBriefly(3000);
        if (storedSession) {
            gmSet(K.SESSION_ID, '');
            window.location.href = 'https://www.tikwm.com/originalDownloader.html';
        } else {
            const val = sessionInput.value.trim();
            if (val) {
                gmSet(K.SESSION_ID, val);
                window.location.href = `https://www.tikwm.com/originalDownloader.html?cookie=sessionid=${val}`;
            }
        }
    });

    function nowMs() { return Date.now(); }

    function pinBriefly(ms) {
        pinnedUntil = nowMs() + (ms || 2000);
        showPanel(true);
    }

    function shouldBlockHide() {
        return nowMs() <= pinnedUntil || document.activeElement === ta || document.activeElement === sessionInput || editMode !== 'none';
    }

    function setArrow(open) {
        if(arrow) arrow.setAttribute('d', open ? 'M8 2 L4 6 L8 10 Z' : 'M4 2 L8 6 L4 10 Z');
    }

    function showPanel(force) {
        if (hideTimer) clearTimeout(hideTimer);
        if (isShown && !force) return;
        isShown = true;
        panel.classList.add('show');
        setArrow(true);
    }

    function scheduleHide() {
        if (shouldBlockHide()) {
            hideTimer = setTimeout(scheduleHide, 500);
            return;
        }
        hideTimer = setTimeout(() => {
            if (shouldBlockHide()) return;
            isShown = false;
            panel.classList.remove('show');
            setArrow(false);
        }, 140);
    }

    tab.addEventListener('mouseenter', () => showPanel(false));
    tab.addEventListener('mouseleave', scheduleHide);
    panel.addEventListener('mouseenter', () => showPanel(true));
    panel.addEventListener('mouseleave', scheduleHide);

    function refreshStatus() {
        const q    = getQ();
        const done = parseInt(gmGet(K.DONE_COUNT) || '0');
        const act  = gmGet(K.ACTIVE);

        const countEl = document.getElementById('tkwm_count');
        if (countEl) countEl.innerHTML = `${q.length} in queue | ${done} done`;

        const statusEl = document.getElementById('tkwm_status');
        if (statusEl) {
            if (act) {
                statusEl.innerHTML = `[...] ...${act.slice(-25)}`;
            } else {
                statusEl.innerHTML = gmGet(K.AUTORUN) === 'true' ? 'Running...' : 'Idle';
            }
        }

        if (editMode === 'none') {
            ta.value = q.join('\n');
            ta.scrollTop = ta.scrollHeight;
        }
    }

    function toast(msg, bg) {
        const d = document.createElement('div');
        d.style.cssText = `position:fixed;top:12px;right:12px;z-index:99999;
            background:${bg};color:#fff;padding:10px 16px;border-radius:6px;
            font:13px/1.4 monospace;max-width:380px;word-break:break-all;
            box-shadow:0 2px 12px rgba(0,0,0,.45)`;
        d.textContent = msg;
        document.body.appendChild(d);
        setTimeout(() => { if(d.parentNode) d.parentNode.removeChild(d); }, 4000);
    }

    cbDesc.addEventListener('change', (e) => {
        gmSet(K.APPEND_DESC, e.target.checked ? 'true' : 'false');
    });

    document.getElementById('tkwm_btn_add').onclick = () => {
        pinBriefly(3000);
        editMode = 'add';
        ta.readOnly = false;
        ta.value = '';
        ta.placeholder = 'Paste new TikTok URLs here...';
        ctrlMain.style.display = 'none';
        ctrlEdit.style.display = 'block';
        ta.focus();
    };

    document.getElementById('tkwm_btn_edit').onclick = () => {
        pinBriefly(3000);
        editMode = 'edit';
        ta.readOnly = false;
        ta.value = getQ().join('\n');
        ctrlMain.style.display = 'none';
        ctrlEdit.style.display = 'block';
        ta.focus();
    };

    document.getElementById('tkwm_btn_cancel').onclick = () => {
        pinBriefly(3000);
        editMode = 'none';
        ta.readOnly = true;
        ta.placeholder = 'Queue is empty.';
        ctrlMain.style.display = 'block';
        ctrlEdit.style.display = 'none';
        refreshStatus();
    };

    document.getElementById('tkwm_btn_save').onclick = () => {
        pinBriefly(3000);
        const urls = ta.value.split('\n')
            .map(u => u.trim())
            .filter(u => /^https?:\/\//.test(u));

        if (editMode === 'add') {
            const q = getQ();
            const qSet = new Set(q);
            urls.forEach(u => qSet.add(u));
            saveQ([...qSet]);
        } else if (editMode === 'edit') {
            const qSet = new Set(urls);
            saveQ([...qSet]);
        }

        editMode = 'none';
        ta.readOnly = true;
        ta.placeholder = 'Queue is empty.';
        ctrlMain.style.display = 'block';
        ctrlEdit.style.display = 'none';
        refreshStatus();
    };

    document.getElementById('tkwm_btn_start').onclick = () => {
        pinBriefly(3000);
        gmSet(K.AUTORUN, 'true');
        processNext();
    };

    document.getElementById('tkwm_btn_stop').onclick = () => {
        pinBriefly(3000);
        gmSet(K.AUTORUN, 'false');
        gmSet(K.ACTIVE, null);
        document.getElementById('tkwm_status').innerHTML = 'Stopped';
    };

    document.getElementById('tkwm_btn_reset').onclick = () => {
        pinBriefly(3000);
        if (!confirm('Clear queue and reset counter?')) return;
        saveQ([]);
        gmSet(K.DONE_COUNT, '0');
        gmSet(K.ACTIVE, null);
        gmSet(K.AUTORUN, 'false');
        refreshStatus();
    };

    document.getElementById('tkwm_import').onclick = () => {
        pinBriefly(3000);
        fi.click();
    };

    fi.onchange = (evt) => {
        const f = evt.target.files[0];
        if (!f) return;

        const r = new FileReader();

        r.onload = e => {
            const lines = e.target.result.split(/\r?\n/)
                .map(s => s.trim())
                .filter(u => /^https?:\/\//.test(u));

            if (!lines.length) return alert('No valid URLs found in file.');

            const q = getQ();
            const qSet = new Set(q);
            lines.forEach(u => qSet.add(u));

            saveQ([...qSet]);

            evt.target.value = '';
            refreshStatus();
            pinBriefly(3000);
        };
        r.readAsText(f);
    };

    function goNext() {
        if (gmGet(K.AUTORUN) === 'true' && getQ().length > 0) {
            location.reload();
        } else if (gmGet(K.AUTORUN) === 'true' && getQ().length === 0) {
            gmSet(K.AUTORUN, 'false');
            toast('[DONE] Queue empty - finished!', '#1565c0');
        }
    }

    function waitForResult(originalUrl) {
        let attempts = 0;
        let rateLimitDetected = false;
        let rateLimitAttemptCount = 0;

        const interval = setInterval(() => {
            attempts++;
            const resultDiv = document.querySelector('.result');
            const tipsDiv = document.querySelector('.tips');

            const links = resultDiv ? resultDiv.querySelectorAll('a.btn-success') : [];
            if (links.length > 0) {
                clearInterval(interval);
                let playUrl = links[0].href;
                startDownload(playUrl, originalUrl);
                return;
            }

            if (tipsDiv && tipsDiv.textContent.trim().length > 0 && window.getComputedStyle(tipsDiv).display !== 'none') {
                const errMsg = tipsDiv.textContent.trim();

                if (errMsg.toLowerCase().includes('limit') || errMsg.toLowerCase().includes('request/second')) {
                    if (!rateLimitDetected) {
                        rateLimitDetected = true;
                        rateLimitAttemptCount = attempts;
                    }
                } else {
                    clearInterval(interval);

                    let cleanMsg = errMsg;
                    if (errMsg.toLowerCase().includes('url parsing is failed')) {
                        cleanMsg = 'Private video blocked by TikTok / TikWM backend error';
                    }

                    toast('[!] ' + cleanMsg + ' (Skipping)', '#e65100');
                    gmSet(K.ACTIVE, null);
                    setTimeout(goNext, 3500);
                    return;
                }
            }

            if (rateLimitDetected && (attempts - rateLimitAttemptCount > 4)) {
                clearInterval(interval);
                toast('[!] Rate Limit Hit. Retrying in 6s...', '#e65100');
                const q = getQ();
                q.unshift(originalUrl);
                saveQ(q);
                gmSet(K.ACTIVE, null);
                setTimeout(goNext, 6000);
                return;
            }

            if (attempts > 60) {
                clearInterval(interval);
                toast('[!] Timeout waiting for API result. Skipping...', '#e65100');
                gmSet(K.ACTIVE, null);
                setTimeout(goNext, 2000);
            }
        }, 500);
    }

    function startDownload(dlUrl, originalUrl) {
        let username = "unknown_user";
        let videoId = "manual_" + Date.now();

        const match = originalUrl.match(/tiktok\.com\/@([^\/]+)\/video\/(\d+)/);
        if (match) {
            username = match[1];
            videoId = match[2];
        } else {
            const idMatch = originalUrl.match(/\/video\/(\d+)/);
            if (idMatch) videoId = idMatch[1];
        }

        let descText = "";
        if (gmGet(K.APPEND_DESC) === 'true') {
            const descElement = document.querySelector('.result h4');
            if (descElement && !descElement.textContent.includes('@')) {
                let rawText = descElement.textContent;

                let cleanText = rawText
                    .replace(/_/g, ' ')
                    .replace(/[^a-zA-Z0-9\säöåÄÖÅ]/g, '')
                    .replace(/\s+/g, ' ')
                    .trim();

                if (cleanText.length > 0) {
                    descText = " " + cleanText.substring(0, 64).trim();
                }
            }
        }

        const baseFolder = gmGet(K.FOLDER) || '#TikTok';
        const filename = `${baseFolder}/${username}/${videoId}${descText}.mp4`;

        toast('Starting HD download...', '#ff9800');

        GM_download({
            url: dlUrl,
            name: filename,
            saveAs: false,
            onload: function() {
                gmSet(K.ACTIVE, null);
                const cnt = parseInt(gmGet(K.DONE_COUNT) || '0') + 1;
                gmSet(K.DONE_COUNT, String(cnt));
                toast('[OK] Downloaded (' + cnt + '): ' + filename, '#2e7d32');
                setTimeout(goNext, 1200);
            },
            onerror: function(err) {
                console.error("GM_download error:", err);
                toast('[!] Browser download failed. Skipping...', '#e65100');
                gmSet(K.ACTIVE, null);
                setTimeout(goNext, 4500);
            }
        });
    }

    function processNext() {
        if (isProcessing) return;
        isProcessing = true;

        const q = getQ();
        if (q.length === 0) {
            gmSet(K.AUTORUN, 'false');
            document.getElementById('tkwm_status').innerHTML = '[DONE] All downloaded!';
            isProcessing = false;
            return;
        }

        const url = q.shift();
        saveQ(q);
        gmSet(K.ACTIVE, url);
        refreshStatus();

        const input = document.getElementById('params');
        if (!input) {
            document.getElementById('tkwm_status').innerHTML = '[ERROR] Input field not found!';
            isProcessing = false;
            return;
        }

        input.value = url;
        input.dispatchEvent(new Event('input', { bubbles: true }));

        setTimeout(() => {
            const submitBtn = document.querySelector('.btn-submit');
            if (submitBtn) {
                submitBtn.click();
                waitForResult(url);
            } else {
                toast('[!] Submit button not found!', '#e65100');
            }
        }, 800);
    }

    window.addEventListener('load', () => {
        refreshStatus();

        if (cbDesc) {
            cbDesc.checked = gmGet(K.APPEND_DESC) === 'true';
        }

        if (location.hash.startsWith('#tt_paths=')) {
            const raw = location.hash.replace('#tt_paths=', '');
            const paths = decodeURIComponent(raw).split(',');
            if (paths.length > 0 && paths[0] !== '') {
                const newUrls = paths.map(p => 'https://www.tiktok.com' + p);
                const q = getQ();
                const qSet = new Set(q);
                newUrls.forEach(u => qSet.add(u));
                saveQ([...qSet]);
                refreshStatus();
                toast('[OK] Imported ' + paths.length + ' links from TikTok!', '#1565c0');
                history.replaceState(null, null, ' ');
            }
        }

        if (gmGet(K.AUTORUN) === 'true' && !gmGet(K.ACTIVE)) {
            if (getQ().length > 0) {
                setTimeout(processNext, 2500);
            } else {
                gmSet(K.AUTORUN, 'false');
                document.getElementById('tkwm_status').innerHTML = '[DONE] Queue empty!';
            }
        } else if (gmGet(K.AUTORUN) === 'true' && gmGet(K.ACTIVE)) {
            setTimeout(processNext, 3000);
        }
    });

})();