Greasy Fork is available in English.
Automates downloading a batch of TikTok videos in original quality via tikwm.com
// ==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);
}
});
})();