// ==UserScript== // @name TikWM TikTok Batch Downloader // @namespace https://greasyfork.org/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 // @downloadURL https://update.greasyfork.icu/scripts/572629/TikWM%20TikTok%20Batch%20Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/572629/TikWM%20TikTok%20Batch%20Downloader.meta.js // ==/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 = ''; document.body.appendChild(tab); const panel = document.createElement('div'); panel.id = 'tkwm_panel'; panel.innerHTML = `
TikWM Batch Downloader
0 in queue Idle
`; 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); } }); })();