您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Safely enhance TikTok web: download videos (with blob fallback via recorder), audio-only (WIP), dark mode, auto loop/mute/scroll, UI tweaks, draggable settings gear/menu w/ backup & import/export — by Eliminater74
// ==UserScript== // @name TikTok Enhancer Plus // @namespace https://greasyfork.org/en/users/123456-eliminater74 // @version 2.4 // @description Safely enhance TikTok web: download videos (with blob fallback via recorder), audio-only (WIP), dark mode, auto loop/mute/scroll, UI tweaks, draggable settings gear/menu w/ backup & import/export — by Eliminater74 // @author Eliminater74 // @license MIT // @match https://www.tiktok.com/* // @icon https://www.tiktok.com/favicon.ico // @grant GM_download // @run-at document-end // ==/UserScript== (function () { 'use strict'; const SETTINGS_KEY = 'tiktokEnhancerSettings'; const UI_GEAR_KEY = 'tiktokEnhancer_ui_gear'; const UI_MENU_KEY = 'tiktokEnhancer_ui_menu'; const defaultSettings = { darkMode: false, autoMute: false, autoLoop: false, autoScroll: false, removeAds: true, wideMode: false, blobRecorder:false // <-- new: enable Record/Stop button + "r" hotkey }; const config = { ...defaultSettings, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') }; const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(config)); // ----------------- utils ----------------- const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const once = (id, nodeMaker) => { let el = document.getElementById(id); if (!el) { el = nodeMaker(); el.id = id; (el.tagName === 'STYLE' ? document.head : document.body).appendChild(el); } return el; }; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const safeTitle = (s) => (s || 'TikTok').replace(/[\\/:*?"<>|]+/g, '').slice(0, 80) || 'TikTok'; // checkbox id map (fixes earlier toggle bugs) const settingToCheckboxId = { darkMode: 'toggle-dark', autoMute: 'toggle-mute', autoLoop: 'toggle-loop', autoScroll: 'toggle-scroll', removeAds: 'toggle-ads', wideMode: 'toggle-wide', blobRecorder: 'toggle-blobrec' }; // -------------- draggable helper -------------- function makeDraggable(el, storageKey, fallbackPos) { el.style.position = 'fixed'; el.style.touchAction = 'none'; // restore position try { const saved = JSON.parse(localStorage.getItem(storageKey) || 'null'); if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) { el.style.left = saved.x + 'px'; el.style.top = saved.y + 'px'; } else if (fallbackPos) { const { x, y } = fallbackPos(); el.style.left = x + 'px'; el.style.top = y + 'px'; } } catch {} let startX, startY, startLeft, startTop, moved = false; const onDown = (e) => { moved = false; const p = e.touches ? e.touches[0] : e; startX = p.clientX; startY = p.clientY; const rect = el.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onUp); }; const onMove = (e) => { const p = e.touches ? e.touches[0] : e; if (e.cancelable) e.preventDefault(); moved = true; const dx = p.clientX - startX; const dy = p.clientY - startY; const newX = clamp(startLeft + dx, 0, window.innerWidth - el.offsetWidth); const newY = clamp(startTop + dy, 0, window.innerHeight - el.offsetHeight); el.style.left = newX + 'px'; el.style.top = newY + 'px'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); const rect = el.getBoundingClientRect(); localStorage.setItem(storageKey, JSON.stringify({ x: rect.left, y: rect.top })); if (moved) { el.dataset.justDragged = '1'; setTimeout(() => delete el.dataset.justDragged, 150); } }; el.addEventListener('mousedown', onDown); el.addEventListener('touchstart', onDown, { passive: false }); } // ---------------- UI: menu + gear ---------------- function createMenu() { const menu = document.createElement('div'); menu.id = 'tiktok-enhancer-menu'; menu.style.cssText = ` position: fixed; background: #222; color: white; padding: 10px; border-radius: 10px; z-index: 999999; font-family: sans-serif; box-shadow: 0 0 10px #000; display: none; line-height: 1.8; user-select: none; width: 230px; `; menu.innerHTML = ` <div style="cursor:move; font-weight:600; margin-bottom:6px;">TikTok Enhancer • Menu</div> <label><input type="checkbox" id="toggle-dark"> Dark Mode</label><br> <label><input type="checkbox" id="toggle-mute"> Auto Mute</label><br> <label><input type="checkbox" id="toggle-loop"> Auto Loop</label><br> <label><input type="checkbox" id="toggle-scroll"> Auto Scroll</label><br> <label><input type="checkbox" id="toggle-ads"> Remove Ads</label><br> <label><input type="checkbox" id="toggle-wide"> Wide Mode</label><br> <label><input type="checkbox" id="toggle-blobrec"> Blob Recorder (WebM)</label><br> <hr style="border-color:#444"> <div style="display:flex; gap:6px; flex-wrap:wrap;"> <button id="save-settings">💾 Backup</button> <button id="load-settings">📂 Restore</button> <button id="export-settings">⬇ Export JSON</button> <button id="import-settings">⬆ Import JSON</button> <input id="import-file" type="file" accept="application/json" style="display:none"> </div> `; document.body.appendChild(menu); makeDraggable(menu, UI_MENU_KEY, () => ({ x: window.innerWidth - 260, y: window.innerHeight - 260 })); const gear = document.createElement('div'); gear.textContent = '⚙️'; gear.style.cssText = ` position: fixed; font-size: 24px; z-index: 999998; cursor: pointer; background: #333; color: white; padding: 5px 10px; border-radius: 50%; box-shadow: 0 0 8px #000; `; document.body.appendChild(gear); makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 })); gear.addEventListener('click', () => { if (gear.dataset.justDragged) return; menu.style.display = (menu.style.display === 'none' || !menu.style.display) ? 'block' : 'none'; }); // bind checkboxes Object.entries(settingToCheckboxId).forEach(([key, id]) => { const el = document.getElementById(id); if (el) { el.checked = !!config[key]; el.addEventListener('change', () => { config[key] = el.checked; saveSettings(); applySettings(); }); } }); // backup / restore $('#save-settings').onclick = () => { localStorage.setItem(SETTINGS_KEY + '_backup', JSON.stringify(config)); alert('Settings backed up locally.'); }; $('#load-settings').onclick = () => { const backup = JSON.parse(localStorage.getItem(SETTINGS_KEY + '_backup') || '{}'); Object.assign(config, backup); saveSettings(); Object.entries(settingToCheckboxId).forEach(([k, id]) => { const el = document.getElementById(id); if (el) el.checked = !!config[k]; }); applySettings(); }; // export / import JSON $('#export-settings').onclick = () => { const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const ts = new Date().toISOString().replace(/[:.]/g, '-'); a.href = url; a.download = `tiktok-enhancer-settings-${ts}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }; const importFile = $('#import-file'); $('#import-settings').onclick = () => importFile.click(); importFile.addEventListener('change', async (e) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await file.text(); const incoming = JSON.parse(text); Object.keys(defaultSettings).forEach(k => { if (typeof incoming[k] === 'boolean') config[k] = incoming[k]; }); saveSettings(); Object.entries(settingToCheckboxId).forEach(([k, id]) => { const el = document.getElementById(id); if (el) el.checked = !!config[k]; }); applySettings(); alert('Settings imported.'); } catch (err) { alert('Failed to import settings: ' + err); } finally { importFile.value = ''; } }); } // -------------- applySettings (styles + timers) -------------- let adSweepTimer = null; function applySettings() { // Dark mode via invert document.documentElement.style.filter = config.darkMode ? 'invert(1) hue-rotate(180deg)' : ''; $$('img, video, canvas').forEach(el => { el.style.filter = config.darkMode ? 'invert(1) hue-rotate(180deg)' : ''; }); // Remove Ads (single managed timer) if (config.removeAds) { if (!adSweepTimer) { adSweepTimer = setInterval(() => { const adSelectors = [ '[data-e2e*="sponsored"]', '[data-e2e="search-hot"]', '[data-e2e="trending"]', '[data-testid*="trending"]', 'a[href*="/sponsor"]', 'a[href*="utm_source="]', '.tiktok-1soki6-DivAdWrapper', '.ad-wrapper', '[class*="ad-"]' ]; adSelectors.forEach(sel => $$(sel).forEach(n => n.remove())); }, 1200); } } else if (adSweepTimer) { clearInterval(adSweepTimer); adSweepTimer = null; } // Wide mode – single style node const style = once('tiktok-enhancer-wide-style', () => document.createElement('style')); style.textContent = config.wideMode ? ` #app, main, body, html { max-width: 100% !important; width: 100% !important; } main > div, #app > div { max-width: 100% !important; padding-left: 0 !important; padding-right: 0 !important; } [data-e2e="feed-list"], [data-e2e="search-video"], [data-e2e="recommend-list"], [class*="feed"] { max-width: 100vw !important; width: 100vw !important; } ` : ''; } // ---------------- Blob Recorder (WebM) ---------------- let activeRecorder = null; let recordedChunks = []; function startRecordingFromVideo(video, filenameBase = 'TikTok_Record') { if (!window.MediaRecorder) { alert('MediaRecorder not supported in this browser.'); return; } if (!video) { alert('No video element found.'); return; } if (activeRecorder) { alert('Already recording.'); return; } try { const stream = video.captureStream ? video.captureStream() : video.mozCaptureStream?.(); if (!stream) throw new Error('Unable to capture stream from the video element.'); // ensure playback (muting the element also mutes the capture, so unmute if needed) if (video.muted) video.muted = false; recordedChunks = []; const mr = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9,opus' }); mr.ondataavailable = (e) => { if (e.data && e.data.size) recordedChunks.push(e.data); }; mr.onstop = () => { const blob = new Blob(recordedChunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const ts = new Date().toISOString().replace(/[:.]/g, '-'); a.href = url; a.download = `${filenameBase}_${ts}.webm`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); activeRecorder = null; recordedChunks = []; }; mr.start(1000); activeRecorder = mr; video.play().catch(()=>{}); } catch (err) { console.error(err); alert('Failed to start recording: ' + err.message); } } function stopRecording() { if (activeRecorder) activeRecorder.stop(); else alert('Not recording.'); } // ---------------- video wiring ---------------- function injectDownload(video) { if (!video || video.dataset.enhanced) return; video.dataset.enhanced = 'true'; if (config.autoMute) video.muted = true; if (config.autoScroll) { video.addEventListener('ended', () => window.scrollBy({ top: window.innerHeight, behavior: 'smooth' }), { once: true }); } if (config.autoLoop) { video.addEventListener('ended', () => { video.currentTime = 0; video.play().catch(()=>{}); }); } const btnId = 'tiktok-download-btn'; const placeButtons = () => { const src = video.currentSrc || video.src; if (!src) return; const existing = document.getElementById(btnId); if (existing) existing.remove(); const wrap = document.createElement('div'); wrap.id = btnId; wrap.style.cssText = ` position: fixed; bottom: 80px; left: 20px; z-index: 999999; `; // Download button (direct URLs only) const dl = document.createElement('button'); dl.textContent = '⬇ Download'; dl.onclick = () => { const url = video.currentSrc || video.src; if (!url) return; if (url.startsWith('blob:')) { alert('Blob stream detected. Use Record to capture as WebM.'); } else { GM_download(url, `TikTok_${Date.now()}.mp4`); } }; dl.style.cssText = `background:#e11;color:white;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;margin-right:5px;`; // Audio-only (placeholder) const audioBtn = document.createElement('button'); audioBtn.textContent = '🎵 Audio'; audioBtn.onclick = () => alert('Audio-only download not yet implemented. Coming soon!'); audioBtn.style.cssText = `background:#333;color:white;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;`; wrap.append(dl, audioBtn); // Record/Stop (if enabled) if (config.blobRecorder) { const recBtn = document.createElement('button'); recBtn.textContent = '⏺ Record'; recBtn.style.cssText = `background:#0a0;color:white;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;margin-left:5px;`; let recording = false; recBtn.onclick = () => { if (!recording) { recording = true; recBtn.textContent = '⏹ Stop'; startRecordingFromVideo(video, safeTitle(document.title)); } else { recording = false; recBtn.textContent = '⏺ Record'; stopRecording(); } }; wrap.appendChild(recBtn); } document.body.appendChild(wrap); }; if (video.readyState >= 2) placeButtons(); else { const onCanPlay = () => { placeButtons(); video.removeEventListener('canplay', onCanPlay); }; video.addEventListener('canplay', onCanPlay); } } function monitorVideos() { $$('video').forEach(injectDownload); const observer = new MutationObserver(() => { $$('video').forEach(injectDownload); }); observer.observe(document.body, { childList: true, subtree: true }); } // ---------------- hotkeys ---------------- function keyShortcuts() { document.addEventListener('keydown', e => { const vid = $('video'); if (!vid) return; if (e.key === 'd') { const url = vid.currentSrc || vid.src; if (url && !url.startsWith('blob:')) GM_download(url, `TikTok_${Date.now()}.mp4`); else alert('Blob stream — use Record (WebM).'); } if (e.key === 'm') vid.muted = !vid.muted; if (e.key === 't') { config.darkMode = !config.darkMode; saveSettings(); applySettings(); } if (e.key === 'r' && config.blobRecorder) { if (!activeRecorder) startRecordingFromVideo(vid, safeTitle(document.title)); else stopRecording(); } }); } // ---------------- init ---------------- createMenu(); applySettings(); monitorVideos(); keyShortcuts(); })();