TikTok Enhancer Plus

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();