Download or share videos & bulk-save favourites on ThisVid. iOS/macOS (Userscripts app), Android, desktop (Tampermonkey/Violentmonkey).
// ==UserScript==
// @name ThisVid Downloader
// @namespace https://thisvid.com/
// @version 1.1
// @description Download or share videos & bulk-save favourites on ThisVid. iOS/macOS (Userscripts app), Android, desktop (Tampermonkey/Violentmonkey).
// @author MaleVoreLover
// @license MIT
// @match https://thisvid.com/videos/*
// @match https://thisvid.com/members/*/favourites*
// @match https://thisvid.com/members/favourites*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setClipboard
// @connect thisvid.com
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const IS_IOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
const IS_SAFARI = IS_IOS || (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent));
const CAN_SHARE = typeof navigator.share === 'function';
const isVideoPage = /^https:\/\/thisvid\.com\/videos\/[^/?#]+\/?$/.test(location.href);
const isFavPage = /\/favourites/.test(location.pathname);
function waitForVideoSrc(timeout = 12000) {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeout;
const check = () => {
const el = document.querySelector('video');
const src = el?.src || el?.currentSrc || '';
if (src.includes('/get_file/')) { el.pause(); return resolve(src); }
if (Date.now() > deadline) return reject(new Error('Could not find video source. Try pressing Play manually first.'));
setTimeout(check, 300);
};
setTimeout(check, 600);
});
}
function createHiddenVideoElement() {
let video = document.getElementById('tv-native-video');
if (!video) {
video = document.createElement('video');
video.id = 'tv-native-video';
Object.assign(video.style, {
position: 'fixed',
top: '0',
left: '0',
width: '1px',
height: '1px',
opacity: '0',
pointerEvents: 'none',
});
video.playsInline = true;
video.muted = true;
video.preload = 'auto';
video.setAttribute('playsinline', '');
video.setAttribute('webkit-playsinline', '');
video.crossOrigin = 'anonymous';
document.body.appendChild(video);
}
return video;
}
async function loadHiddenVideoSrc(src) {
if (!src) return null;
const video = createHiddenVideoElement();
if (video.src !== src) {
video.src = src;
}
video.muted = true;
video.playsInline = true;
video.preload = 'auto';
try { video.load(); } catch {}
return new Promise(resolve => {
const cleanup = () => {
video.removeEventListener('loadedmetadata', onReady);
video.removeEventListener('loadeddata', onReady);
video.removeEventListener('canplay', onReady);
video.removeEventListener('canplaythrough', onReady);
video.removeEventListener('error', onError);
clearTimeout(timer);
};
const onReady = () => { cleanup(); resolve(video.currentSrc || video.src); };
const onError = () => { cleanup(); resolve(video.currentSrc || video.src); };
const timer = setTimeout(() => { cleanup(); resolve(video.currentSrc || video.src); }, 10000);
video.addEventListener('loadedmetadata', onReady);
video.addEventListener('loadeddata', onReady);
video.addEventListener('canplay', onReady);
video.addEventListener('canplaythrough', onReady);
video.addEventListener('error', onError);
video.play().catch(() => {});
});
}
async function loadNativeVideoFallback() {
let src = extractSrcFromHtml(document.documentElement.innerHTML);
if (!src) src = await fetchVideoPageSrc(location.href);
if (!src) return null;
const hiddenSrc = await loadHiddenVideoSrc(src);
return hiddenSrc || src;
}
async function getVideoSrc() {
const v = document.querySelector('video');
if (v?.src?.includes('/get_file/')) return v.src;
if (v?.currentSrc?.includes('/get_file/')) return v.currentSrc;
const extractedSrc = extractSrcFromHtml(document.documentElement.innerHTML);
if (extractedSrc) {
const validated = await loadHiddenVideoSrc(extractedSrc);
if (validated?.includes('/get_file/')) return validated;
return extractedSrc;
}
const btn = document.querySelector('#kt_player .fp-play, .fp-play, .jw-icon-playback, [aria-label="Play"]')
?? document.querySelector('#kt_player, #player, .fp-player, .jwplayer');
btn?.click();
try {
return await waitForVideoSrc();
} catch (err) {
const fallbackSrc = await loadNativeVideoFallback();
if (fallbackSrc) return fallbackSrc;
throw err;
}
}
function triggerDownload(url, filename) {
try {
if (typeof GM_download !== 'undefined') {
GM_download({ url, name: filename });
return;
}
} catch {}
window.open(url, '_blank');
}
function getTitle() {
return (document.querySelector('h1.title, h1, .video-title')?.textContent || document.title)
.trim().replace(/\s*[-|].*$/, '').replace(/[/\\:*?"<>|]/g, '_').substring(0, 100) || 'thisvid-video';
}
async function doCopy(text) {
try { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); return; } } catch {}
try { await navigator.clipboard.writeText(text); return; } catch {}
const ta = Object.assign(document.createElement('textarea'), { value: text });
Object.assign(ta.style, { position: 'fixed', opacity: '0' });
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function toast(msg) {
const t = document.createElement('div');
t.className = 'tv-toast'; t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2200);
}
function injectStyles() {
const s = document.createElement('style');
s.textContent = `
#tv-fab {
position: fixed; bottom: 24px; right: 20px; z-index: 2147483647;
display: flex; align-items: center; gap: 8px;
padding: 0 18px 0 14px; height: 44px;
background: rgba(28,28,30,.95);
-webkit-backdrop-filter: blur(20px) saturate(180%);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255,255,255,.1); border-radius: 22px;
box-shadow: 0 4px 24px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.05) inset;
cursor: pointer; font-family: -apple-system,'Helvetica Neue',system-ui,sans-serif;
font-size: 14px; font-weight: 600; color: #fff; letter-spacing: -.01em;
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
transition: transform .12s, opacity .12s; user-select: none;
}
#tv-fab:active { transform: scale(.93); opacity: .85; }
#tv-fab-dot { width: 8px; height: 8px; border-radius: 50%; background: #ff3b30; flex-shrink: 0; box-shadow: 0 0 6px rgba(255,59,48,.7); }
#tv-backdrop {
position: fixed; inset: 0; z-index: 2147483645;
background: rgba(0,0,0,.45);
-webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
display: none; animation: tvFadeIn .2s ease;
}
#tv-backdrop.open { display: block; }
@keyframes tvFadeIn { from { opacity: 0 } to { opacity: 1 } }
#tv-card {
position: fixed; z-index: 2147483646;
bottom: 80px; right: 20px; width: 300px;
max-height: calc(100svh - 110px);
background: #1c1c1e; border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,.7), 0 1px 0 rgba(255,255,255,.06) inset;
overflow: hidden; display: none; flex-direction: column;
font-family: -apple-system,'Helvetica Neue',system-ui,sans-serif;
animation: tvSlideUp .22s cubic-bezier(.32,1,.23,1);
transform-origin: bottom right;
}
#tv-card.open { display: flex; }
@keyframes tvSlideUp {
from { opacity: 0; transform: scale(.88) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
#tv-card-head {
padding: 18px 44px 14px 20px;
border-bottom: 0.5px solid rgba(255,255,255,.08); flex-shrink: 0;
}
#tv-card-title { font-size: 20px; font-weight: 600; color: #fff; margin: 0 0 3px; }
#tv-card-sub { font-size: 13px; color: rgba(255,255,255,.4); margin: 0; line-height: 1.4; }
#tv-card-close {
position: absolute; top: 16px; right: 14px;
width: 28px; height: 28px; border-radius: 50%;
background: rgba(255,255,255,.1); border: none; cursor: pointer;
color: rgba(255,255,255,.6); display: flex; align-items: center; justify-content: center;
touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background .1s;
}
#tv-card-close:active { background: rgba(255,255,255,.2); }
#tv-card-body { overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; }
.tv-list-row {
display: flex; align-items: center; gap: 14px;
padding: 13px 20px;
border-bottom: 0.5px solid rgba(255,255,255,.06);
background: none; border-left: none; border-right: none; border-top: none;
width: 100%; cursor: pointer; text-align: left;
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
transition: background .1s;
}
.tv-list-row:last-child { border-bottom: none; }
.tv-list-row:active { background: rgba(255,255,255,.06); }
.tv-list-row.disabled { opacity: .45; cursor: default; }
.tv-list-row.hidden { display: none; }
.tv-icon {
width: 36px; height: 36px; border-radius: 9px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.ic-red { background: #c0392b; }
.ic-blue { background: #0a7aff; }
.ic-purple { background: #5856d6; }
.ic-gray { background: #3a3a3c; }
.tv-row-label { font-size: 16px; font-weight: 400; color: #fff; flex: 1; }
.tv-row-chevron { color: rgba(255,255,255,.2); flex-shrink: 0; }
#tv-status {
padding: 10px 20px 14px; font-size: 13px; color: rgba(255,255,255,.35);
line-height: 1.4; display: none; border-top: 0.5px solid rgba(255,255,255,.06);
}
#tv-status.visible { display: block; }
#tv-status.ok { color: #30d158; }
#tv-status.err { color: #ff453a; }
#tv-status.info { color: #0a84ff; }
/* ── Favourites overlay ── */
#tv-fav-overlay {
position: fixed; inset: 0; z-index: 2147483647;
display: flex; flex-direction: column; background: #000;
font-family: -apple-system,'Helvetica Neue',system-ui,sans-serif;
animation: tvFadeIn .18s ease;
}
#tv-fav-head {
display: flex; align-items: center; gap: 10px;
padding: 14px 16px 10px; border-bottom: 0.5px solid rgba(255,255,255,.1); flex-shrink: 0;
}
#tv-fav-head-text { flex: 1; min-width: 0; }
#tv-fav-head h2 { margin: 0; font-size: 17px; font-weight: 700; color: #fff; }
#tv-fav-sub { font-size: 13px; color: rgba(255,255,255,.35); margin-top: 1px; }
.tv-fav-hbtn {
padding: 7px 14px; border: none; border-radius: 10px;
font-size: 13px; font-weight: 600; cursor: pointer;
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
white-space: nowrap; flex-shrink: 0; transition: filter .1s;
}
.tv-fav-hbtn:active { filter: brightness(.75); }
.tv-fav-hbtn:disabled { opacity: .25; cursor: default; }
.tv-fav-hbtn.copy { background: #2c2c2e; color: #0a84ff; }
.tv-fav-hbtn.close { background: #2c2c2e; color: #fff; }
#tv-fav-prog { height: 2px; flex-shrink: 0; background: rgba(255,255,255,.06); }
#tv-fav-bar { height: 100%; width: 0%; background: #c0392b; transition: width .2s; }
#tv-fav-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 12px 16px 40px; }
.tv-fav-group { background: #1c1c1e; border-radius: 12px; overflow: hidden; margin-bottom: 10px; }
.tv-fav-row {
display: flex; align-items: center; padding: 12px 14px; gap: 12px;
border-bottom: 0.5px solid rgba(255,255,255,.06);
}
.tv-fav-row:last-child { border-bottom: none; }
.tv-fav-info { flex: 1; min-width: 0; }
.tv-fav-title {
font-size: 14px; font-weight: 500; color: #fff;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tv-fav-status { font-size: 12px; margin-top: 2px; color: rgba(255,255,255,.28); }
.tv-fav-status.ok { color: #30d158; }
.tv-fav-status.err { color: #ff453a; }
.tv-fav-btn {
flex-shrink: 0; padding: 7px 14px; border: none; border-radius: 8px;
font-size: 13px; font-weight: 600; cursor: pointer;
touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: filter .1s;
}
.tv-fav-btn:active { filter: brightness(.75); }
.tv-fav-btn:disabled { opacity: .25; cursor: default; }
.tv-fav-btn.share { background: #0a7aff; color: #fff; }
.tv-fav-btn.dl { background: #c0392b; color: #fff; }
.tv-fav-btn.muted { background: #2c2c2e; color: rgba(255,255,255,.3); }
#tv-fav-scan {
display: flex; flex-direction: column; align-items: center;
justify-content: center; min-height: 220px; gap: 14px;
color: rgba(255,255,255,.28); font-size: 14px;
}
.tv-spinner {
width: 24px; height: 24px;
border: 2.5px solid rgba(255,255,255,.08); border-top-color: #c0392b;
border-radius: 50%; animation: tvSpin .65s linear infinite;
}
@keyframes tvSpin { to { transform: rotate(360deg) } }
.tv-toast {
position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%);
background: rgba(255,255,255,.93); color: #000;
padding: 9px 18px; border-radius: 20px; font-size: 13px; font-weight: 600;
z-index: 2147483647; pointer-events: none; white-space: nowrap;
box-shadow: 0 2px 14px rgba(0,0,0,.35); animation: tvToast 2.2s ease forwards;
}
@keyframes tvToast {
0% { opacity: 0; transform: translateX(-50%) translateY(6px); }
10% { opacity: 1; transform: translateX(-50%) translateY(0); }
75% { opacity: 1; }
100% { opacity: 0; }
}
`;
document.head.appendChild(s);
}
const ICON = {
dl: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
share: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>`,
copy: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
open: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`,
list: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
chev: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`,
x: `<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>`,
};
function mkIcon(name, bg) {
const d = document.createElement('div');
d.className = `tv-icon ${bg}`; d.innerHTML = ICON[name]; return d;
}
function setStatus(msg, type = '') {
const el = document.getElementById('tv-status');
if (!el) return;
el.textContent = msg; el.className = msg ? `visible ${type}` : '';
}
function showError(msg) {
if (!msg) return;
alert(msg);
setStatus(msg, 'err');
}
function openCard() {
document.getElementById('tv-card')?.classList.add('open');
document.getElementById('tv-backdrop')?.classList.add('open');
if (isVideoPage) prefetchDownloadSrc();
}
function closeCard() {
document.getElementById('tv-card')?.classList.remove('open');
document.getElementById('tv-backdrop')?.classList.remove('open');
setStatus('');
}
function buildCard() {
const backdrop = document.createElement('div');
backdrop.id = 'tv-backdrop';
backdrop.onclick = closeCard;
const card = document.createElement('div');
card.id = 'tv-card';
const head = document.createElement('div');
head.id = 'tv-card-head';
const titleEl = document.createElement('p');
titleEl.id = 'tv-card-title';
titleEl.textContent = isVideoPage ? '1 Video Found' : 'Favourites';
const subEl = document.createElement('p');
subEl.id = 'tv-card-sub';
subEl.textContent = IS_IOS
? 'Hold \u201cDownload\u201d then tap \u201cDownload Linked File\u201d'
: location.hostname;
const closeBtn = document.createElement('button');
closeBtn.id = 'tv-card-close';
closeBtn.innerHTML = ICON.x;
closeBtn.onclick = closeCard;
head.appendChild(titleEl);
head.appendChild(subEl);
head.appendChild(closeBtn);
const body = document.createElement('div');
body.id = 'tv-card-body';
const mkRow = (label, iconName, iconBg, handler) => {
const btn = document.createElement('button');
btn.className = 'tv-list-row';
btn.appendChild(mkIcon(iconName, iconBg));
const lbl = document.createElement('span');
lbl.className = 'tv-row-label'; lbl.textContent = label;
const chev = document.createElement('span');
chev.className = 'tv-row-chevron'; chev.innerHTML = ICON.chev;
btn.appendChild(lbl); btn.appendChild(chev);
btn.addEventListener('click', handler);
return btn;
};
const mkLinkRow = (label, iconName, iconBg, handler) => {
const link = document.createElement('a');
link.className = 'tv-list-row';
link.href = '#';
link.target = '_blank';
link.dataset.ready = 'false';
if (IS_IOS) {
link.title = 'Hold to download, or play first if the link is not ready';
}
link.appendChild(mkIcon(iconName, iconBg));
const lbl = document.createElement('span');
lbl.className = 'tv-row-label'; lbl.textContent = label;
const chev = document.createElement('span');
chev.className = 'tv-row-chevron'; chev.innerHTML = ICON.chev;
link.appendChild(lbl); link.appendChild(chev);
link.addEventListener('click', event => {
if (link.dataset.ready === 'true') {
return;
}
event.preventDefault();
handler();
});
return link;
};
if (isVideoPage) {
if (CAN_SHARE) body.appendChild(mkRow('Share', 'share', 'ic-blue', onShareSingle));
const downloadLabel = IS_IOS ? 'Hold to download' : 'Download';
const downloadPlaceholderLabel = 'Loading...';
const downloadPlaceholder = mkRow(downloadPlaceholderLabel, 'dl', 'ic-red', async () => {
try { await getSrc(); }
catch (e) { showError(e.message); }
});
downloadPlaceholder.id = 'tv-download-placeholder';
downloadPlaceholder.classList.add('disabled');
body.appendChild(downloadPlaceholder);
const downloadRow = mkLinkRow(downloadLabel, 'dl', 'ic-red', onDownloadSingle);
downloadRow.id = 'tv-download-row';
downloadRow.classList.add('hidden');
downloadLinkAnchor = downloadRow;
body.appendChild(downloadRow);
body.appendChild(mkRow('Copy Link', 'copy', 'ic-purple', onCopyLink));
body.appendChild(mkRow('Open in Tab', 'open', 'ic-gray', onOpenTab));
}
if (isFavPage) {
body.appendChild(mkRow('Load & Save Favourites', 'list', 'ic-red', onBulkStart));
}
const statusEl = document.createElement('div');
statusEl.id = 'tv-status';
body.appendChild(statusEl);
card.appendChild(head);
card.appendChild(body);
document.body.appendChild(backdrop);
document.body.appendChild(card);
}
let downloadLinkAnchor = null;
function buildFab() {
const fab = document.createElement('button');
fab.id = 'tv-fab';
fab.innerHTML = `<div id="tv-fab-dot"></div><span>Save Video</span>`;
fab.addEventListener('click', () => {
document.getElementById('tv-card')?.classList.contains('open') ? closeCard() : openCard();
});
document.body.appendChild(fab);
}
function setDownloadLinkHref(src) {
if (!downloadLinkAnchor) return;
downloadLinkAnchor.href = src;
downloadLinkAnchor.dataset.ready = 'true';
downloadLinkAnchor.setAttribute('download', `${getTitle()}.mp4`);
downloadLinkAnchor.classList.remove('hidden');
const placeholder = document.getElementById('tv-download-placeholder');
if (placeholder) placeholder.classList.add('hidden');
}
function prefetchDownloadSrc() {
if (_cachedSrc) return;
getSrc().then(src => setDownloadLinkHref(src)).catch(() => {});
}
let _cachedSrc = null;
async function getSrc() {
if (_cachedSrc) return _cachedSrc;
setStatus('Starting player…', 'info');
_cachedSrc = await getVideoSrc();
setDownloadLinkHref(_cachedSrc);
setStatus('');
return _cachedSrc;
}
async function onShareSingle() {
try {
const src = await getSrc();
await navigator.share({ title: getTitle(), url: src });
setStatus('Shared!', 'ok');
} catch(e) {
if (e.name !== 'AbortError') showError(e.message);
else setStatus('');
}
}
async function onDownloadSingle() {
try {
const src = await getSrc();
triggerDownload(src, `${getTitle()}.mp4`);
setStatus(IS_IOS ? 'Hold the link \u2014 tap \u201cDownload Linked File\u201d' : 'Download started', 'ok');
} catch(e) { showError(e.message); }
}
async function onCopyLink() {
try {
const src = await getSrc();
await doCopy(src);
toast('Link copied'); setStatus('Direct link copied', 'ok');
} catch(e) { showError(e.message); }
}
async function onOpenTab() {
try {
const src = await getSrc();
window.open(src, '_blank');
setStatus('Opened in new tab', 'ok');
} catch(e) { showError(e.message); }
}
function parseVideoLinks(doc) {
return [...new Set(
Array.from(doc.querySelectorAll('a[href*="/videos/"]'))
.map(a => a.href)
.filter(h => /\/videos\/[^/?#]+\/?$/.test(h))
)];
}
async function collectAllFavLinks() {
let links = parseVideoLinks(document);
const pageNums = Array.from(document.querySelectorAll('a[href*="/favourites/"]'))
.map(a => { const m = a.href.match(/\/(\d+)\/?$/); return m ? +m[1] : 0; })
.filter(n => n > 1);
const maxPage = pageNums.length ? Math.max(...pageNums) : 1;
const basePath = location.pathname.replace(/\/\d+\/?$/, '').replace(/\/$/, '');
for (let p = 2; p <= maxPage; p++) {
const url = `${location.origin}${basePath}/${p}/`;
await new Promise(res => {
GM_xmlhttpRequest({
method: 'GET', url, headers: { Referer: location.href },
onload(r) {
links = links.concat(parseVideoLinks(new DOMParser().parseFromString(r.responseText, 'text/html')));
res();
},
onerror: res,
});
});
await sleep(250);
}
return [...new Set(links)];
}
function isPreviewSrc(src) {
const normalized = src.toLowerCase();
return /(?:\.gif|preview|thumbnail|thumb|poster|sprite)/.test(normalized)
|| /\/get_file\/(?:1|2)\//.test(normalized)
|| /\.mp4\.gif/i.test(src)
|| /\.gif\.mp4/i.test(src)
|| /(?:preview|thumb|poster)=/.test(normalized);
}
function scoreVideoSrc(src) {
let score = 0;
if (/([?&])rnd=/.test(src)) score += 100;
const match = src.match(/\/get_file\/(\d+)\//);
if (match) {
if (match[1] === '4') score += 20;
if (match[1] === '1') score -= 20;
if (match[1] === '2') score -= 10;
}
if (/\.mp4\/.+\?/.test(src)) score += 10;
if (/\.mp4$/i.test(src)) score += 5;
return score;
}
function extractSrcCandidatesFromHtml(html) {
const candidates = new Set();
const patterns = [
/['"](https:\/\/(?:[\w-]+\.)?thisvid\.com\/get_file\/[^"']+?\.mp4(?:\/?(?:\?[^"']*)?))['"]/ig,
/src:\s*['"](https:\/\/[^"']+\/get_file\/[^"']+?\.mp4(?:\/?(?:\?[^"']*)?))['"]/ig,
/file:\s*['"](https:\/\/[^"']+\/get_file\/[^"']+?\.mp4(?:\/?(?:\?[^"']*)?))['"]/ig,
];
for (const re of patterns) {
let m;
while ((m = re.exec(html)) !== null) {
const candidate = m[1].replace(/\\u0026/g, '&').replace(/&/g, '&');
let detector = candidate;
try { detector = decodeURIComponent(candidate); } catch {}
if (!isPreviewSrc(detector)) {
candidates.add(candidate);
}
}
}
return [...candidates].map(src => ({ src, score: scoreVideoSrc(src) }))
.sort((a, b) => b.score - a.score)
.map(item => item.src);
}
function extractSrcFromHtml(html) {
return extractSrcCandidatesFromHtml(html)[0] || null;
}
function fetchVideoPageSrc(videoUrl) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET', url: videoUrl, headers: { Referer: 'https://thisvid.com/' },
onload(r) {
const candidates = extractSrcCandidatesFromHtml(r.responseText);
resolve(candidates[0] || null);
},
onerror: () => resolve(null),
});
});
}
async function onBulkStart() {
closeCard();
const ov = document.createElement('div');
ov.id = 'tv-fav-overlay';
ov.innerHTML = `
<div id="tv-fav-head">
<div id="tv-fav-head-text"><h2>Favourites</h2><div id="tv-fav-sub">Scanning…</div></div>
<button class="tv-fav-hbtn copy" id="tv-fav-copyall" disabled>Copy All Links</button>
<button class="tv-fav-hbtn close" id="tv-fav-close">Close</button>
</div>
<div id="tv-fav-prog"><div id="tv-fav-bar"></div></div>
<div id="tv-fav-body">
<div id="tv-fav-scan"><div class="tv-spinner"></div><div>Scanning pages…</div></div>
</div>`;
ov.querySelector('#tv-fav-close').onclick = () => ov.remove();
document.body.appendChild(ov);
const body = ov.querySelector('#tv-fav-body');
const bar = ov.querySelector('#tv-fav-bar');
const sub = ov.querySelector('#tv-fav-sub');
const copyAll = ov.querySelector('#tv-fav-copyall');
let links;
try { links = await collectAllFavLinks(); }
catch(e) { sub.textContent = `Error: ${e.message}`; return; }
if (!links.length) {
body.innerHTML = '<div id="tv-fav-scan"><div>No videos found.</div></div>';
return;
}
sub.textContent = `0 / ${links.length} resolved`;
body.innerHTML = '';
const group = document.createElement('div');
group.className = 'tv-fav-group';
body.appendChild(group);
const rows = links.map(url => {
const slug = url.split('/videos/')[1]?.replace(/\//g, '') || 'video';
const el = document.createElement('div');
el.className = 'tv-fav-row';
el.innerHTML = `
<div class="tv-fav-info">
<div class="tv-fav-title">${slug.replace(/-/g, ' ')}</div>
<div class="tv-fav-status">Pending</div>
</div>`;
const btn = document.createElement('button');
btn.className = 'tv-fav-btn muted'; btn.textContent = '…'; btn.disabled = true;
el.appendChild(btn);
group.appendChild(el);
return { el, slug, title: slug.replace(/-/g, ' ') };
});
const allSrcs = [];
for (let i = 0; i < links.length; i++) {
const src = await fetchVideoPageSrc(links[i]);
const { el, slug, title } = rows[i];
const statusEl = el.querySelector('.tv-fav-status');
const btn = el.querySelector('.tv-fav-btn');
if (!src) {
statusEl.className = 'tv-fav-status err'; statusEl.textContent = 'Not found';
btn.className = 'tv-fav-btn muted'; btn.textContent = 'Open'; btn.disabled = false;
btn.onclick = () => window.open(links[i], '_blank');
} else {
allSrcs.push(src);
statusEl.className = 'tv-fav-status ok'; statusEl.textContent = 'Ready';
if (CAN_SHARE) {
btn.className = 'tv-fav-btn share'; btn.textContent = 'Share'; btn.disabled = false;
btn.onclick = async () => {
try { await navigator.share({ title, url: src }); }
catch(e) { if (e.name !== 'AbortError') { await doCopy(src); toast('Link copied'); } }
};
} else {
btn.className = 'tv-fav-btn dl'; btn.textContent = 'Save'; btn.disabled = false;
btn.onclick = () => triggerDownload(src, `${slug}.mp4`);
}
}
bar.style.width = `${Math.round(((i + 1) / links.length) * 100)}%`;
sub.textContent = `${allSrcs.length} / ${links.length} resolved`;
await sleep(250);
}
if (allSrcs.length) {
copyAll.disabled = false;
copyAll.onclick = async () => { await doCopy(allSrcs.join('\n')); toast(`${allSrcs.length} links copied`); };
}
}
injectStyles();
if (isVideoPage || isFavPage) { buildCard(); buildFab(); }
})();