Deep search filter, 1-click open-in-new-tab, tab manager, infinite scroll, and download panel for supjav.
// ==UserScript==
// @name Supjav Tools
// @namespace supjav-tools
// @version 0.5.3
// @description Deep search filter, 1-click open-in-new-tab, tab manager, infinite scroll, and download panel for supjav.
// @match https://*.supjav.com/*
// @match http://*.supjav.com/*
// @grant GM_download
// @grant GM_info
// @grant GM_xmlhttpRequest
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// Tweak these if the site markup changes. itemSelectors = movie card wrappers,
// paginationSelectors/nextLinkSelectors = pager links, searchFormSelectors = search form.
const CONFIG = {
gridContainerSelectors: ['main', '#main', '.site-main', '.container', '.wrap', '.content', '.list', '.video-list', '.posts', 'body'],
itemSelectors: ['article', '.item', '.post', '.movie', '.box', '.video-card', '.video-item', '.thumb', '.grid-item'],
paginationSelectors: ['.pagination', '.navigation', '.nav-links', '.page-nav', '.pager', '.wp-pagenavi'],
nextLinkSelectors: ['a.next', '.next', 'a.nextpostslink', 'a[rel="next"]', 'li.next a'],
searchFormSelectors: ['form[role="search"]', 'form.search-form', 'form.search', 'form[action*="supjav"]'],
};
// Current code version. VM's GM_info reports the installed copy; the literal
// keeps desktop/extension embeds (where the metadata header is stripped) in sync.
const SCRIPT_VERSION =
(typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ||
'0.5.3';
let mediaFromNetwork = [];
let perfObserverStarted = false;
// The CDN's Mouflon pkey rotates continuously; the freshest token is always
// the one the player's live media-playlist requests carry. Track the newest
// observed token so downloads re-issue the media URL before it rotates out.
const _pkeyAt = {};
let _pkeyLast = null;
function pkeyOf(url) {
const m = typeof url === 'string' ? url.match(/[?&]pkey=([A-Za-z0-9]+)/) : null;
return m ? m[1] : null;
}
function noteMediaUrl(url) {
const pkey = pkeyOf(url);
if (!pkey) return;
const at = Date.now();
_pkeyAt[pkey] = at;
if (!_pkeyLast || _pkeyLast.at <= at) _pkeyLast = { pkey, at };
}
function freshestPkey(url) {
const fromUrl = pkeyOf(url);
const atUrl = fromUrl ? _pkeyAt[fromUrl] || 0 : -1;
if (!_pkeyLast) return fromUrl || null;
return _pkeyLast.at > atUrl ? _pkeyLast.pkey : fromUrl || _pkeyLast.pkey;
}
// DOM element cache: .btn-server/.btn-down/iframe lists are static per page,
// so querying them once avoids repeated full-document scans. Never cache the
// *result* of detectVideoSources() — new HLS streams appear over time and the
// auto-monitor/panel refresh must see them.
let domCache = null;
function getCachedElement(selector) {
if (!domCache) domCache = {};
if (!domCache[selector]) {
domCache[selector] = Array.from(document.querySelectorAll(selector));
}
return domCache[selector];
}
const matches = (root, selectors) => root.querySelector(selectors.join(','));
function startPerformanceObserver() {
if (perfObserverStarted) return;
perfObserverStarted = true;
try {
const obs = new PerformanceObserver((list) => {
list.getEntries().forEach((e) => {
// Only playlists, never MSE segment files (init/chunk .mp4/.m4s).
if (/\.(m3u8|mpd)(\?|$)/i.test(e.name)) {
noteMediaUrl(e.name);
if (!mediaFromNetwork.includes(e.name)) {
mediaFromNetwork.push(e.name);
}
}
});
});
obs.observe({ type: 'resource', buffered: true });
} catch (e) {}
}
const SUPJAV_TOOLS = {
// ---------------------------------------------------------------- source detection
detectVideoSources() {
const sources = [];
const push = (url, kind, label) => {
if (url && typeof url === 'string') {
const clean = url.trim();
if (clean && !sources.some((s) => s.url === clean)) {
let host = '';
try {
host = new URL(clean).hostname;
} catch (e) {}
sources.push({ url: clean, kind, label: label || host });
}
}
};
const mediaKind = (u) => (/\.(m3u8|mpd)(\?|$)/i.test(u) ? 'hls' : 'video');
const videoIframe = document.querySelector('iframe#video, iframe[allowfullscreen]');
if (videoIframe) push(videoIframe.src, 'iframe', 'Player iframe');
const servers = getCachedElement('.btn-server');
servers.forEach((a) => {
push(a.href || a.dataset.src, 'server', `Server ${(a.textContent || '').trim() || 'server'}`);
});
const links = getCachedElement('.btn-down');
links.forEach((a) => {
push(a.href, 'link', `Link ${(a.textContent || '').trim() || 'link'}`);
});
const iframes = getCachedElement('iframe');
iframes.forEach((f) => {
let doc;
try {
doc = f.contentDocument;
} catch (e) {
return;
}
if (!doc) return;
const videos = doc.querySelectorAll('video');
for (const v of videos) {
push(v.currentSrc || v.src, 'video', 'Video tag');
for (const s of v.querySelectorAll('source')) push(s.src, 'video', 'Video tag');
}
const txt = Array.from(doc.scripts).map((s) => s.textContent).join(' ');
const hlsMatches = txt.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/g) || [];
for (const u of hlsMatches) push(u, 'hls', 'HLS playlist');
const mp4Matches = txt.match(/https?:\/\/[^'"\s]+\.mp4[^'"\s]*/g) || [];
for (const u of mp4Matches) push(u, 'video', 'MP4 file');
});
const videos = document.querySelectorAll('video');
for (const v of videos) {
for (const u of [v.currentSrc, v.src]) {
if (/^https?:\/\//i.test(u || '')) push(u, mediaKind(u), 'Video tag');
}
for (const s of v.querySelectorAll('source')) {
if (/^https?:\/\//i.test(s.src || '')) push(s.src, mediaKind(s.src), 'Video tag');
}
}
const scriptText = Array.from(document.scripts).map((s) => s.textContent).join(' ');
const hlsMatches = scriptText.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/g) || [];
for (const u of hlsMatches) push(u, 'hls', 'HLS playlist');
const mp4Matches = scriptText.match(/https?:\/\/[^'"\s]+\.mp4[^'"\s]*/g) || [];
for (const u of mp4Matches) push(u, 'video', 'MP4 file');
for (const k of ['playerData', 'videoData', 'player_config', 'sources']) {
let v;
try {
v = window[k];
} catch (e) {
continue;
}
if (v && typeof v === 'object') {
const raw = JSON.stringify(v);
const hlsMatches = (raw.match(/https?:\/\/[^"\\\s]+\.m3u8[^"\\\s]*/g) || []);
for (const u of hlsMatches) push(u, 'hls', 'Player config');
const mp4Matches = (raw.match(/https?:\/\/[^"\\\s]+\.mp4[^"\\\s]*/g) || []);
for (const u of mp4Matches) push(u, 'video', 'Player config');
}
}
for (const u of mediaFromNetwork) {
noteMediaUrl(u);
push(u, /\.m3u8|\.mpd/i.test(u) ? 'hls' : 'video', 'Live network');
}
const resources = performance.getEntriesByType('resource');
for (const e of resources) {
if (/\.m3u8(\?|$)/i.test(e.name)) {
noteMediaUrl(e.name);
push(e.name, 'hls', 'Network');
}
}
if (window.supjavAPI && typeof window.supjavAPI.getMediaUrls === 'function') {
try {
for (const u of window.supjavAPI.getMediaUrls()) {
push(u, /\.m3u8(\?|$)/i.test(u) ? 'hls' : 'video', 'Desktop capture');
}
} catch (e) {}
}
return sources;
},
// Parses the video code/title/studio from the current page (URL slug + DOM).
// Used for filename templating and Downloads/<folder>/ organization.
videoInfo() {
if (SUPJAV_TOOLS._videoInfo) return SUPJAV_TOOLS._videoInfo;
const info = { code: '', title: '', studio: '' };
const slugMatch = location.pathname.match(/\/video\/([^/]+)/i);
const slug = slugMatch ? decodeURIComponent(slugMatch[1]) : '';
const codeMatch = slug.match(/([a-z]{2,10})[-_ ]?(\d{2,6})/i);
if (codeMatch) {
info.code = `${codeMatch[1]}-${codeMatch[2]}`.toUpperCase();
info.title = slug.replace(codeMatch[0], '').replace(/[-_]+/g, ' ').trim();
} else {
info.title = slug.replace(/[-_]+/g, ' ').trim();
}
if (!info.title) {
const h = document.querySelector('.archive-title h1, h1, .title');
if (h) info.title = h.textContent;
}
const studioEl = document.querySelector(
'.studio a, a[href*="studio"], .director a, a[href*="release"]',
);
if (studioEl) info.studio = studioEl.textContent;
const safe = (s, max) =>
(s || '')
.replace(/\[.*?\]/g, ' ')
.replace(/[^\w\-. ]+/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, max);
info.code = safe(info.code, 20).toUpperCase();
info.title = safe(info.title, 80);
info.studio = safe(info.studio, 40);
SUPJAV_TOOLS._videoInfo = info;
return info;
},
videoFolder() {
return SUPJAV_TOOLS.videoInfo().studio || '';
},
defaultFilename(index, kind) {
const info = SUPJAV_TOOLS.videoInfo();
const base =
[info.code, info.title].filter(Boolean).join('_').replace(/\s+/g, '_') ||
`supjav_${index}`;
const ext = kind === 'hls' ? 'm3u8' : 'mp4';
return `${base}_${index}.${ext}`;
},
download(url, filename) {
const folder = SUPJAV_TOOLS.videoFolder();
if (typeof GM_download === 'function') {
GM_download(url, filename);
} else if (window.supjavAPI && typeof window.supjavAPI.download === 'function') {
window.supjavAPI.download(url, filename, folder);
} else {
const a = document.createElement('a');
a.href = url;
a.download = filename || '';
document.body.appendChild(a);
a.click();
a.remove();
}
},
ensureToast() {
if (SUPJAV_TOOLS._toast && document.contains(SUPJAV_TOOLS._toast)) return SUPJAV_TOOLS._toast;
const t = document.createElement('div');
t.style.cssText =
'position:fixed;left:16px;bottom:16px;z-index:2147483647;font:12px/1.4 system-ui,sans-serif;' +
'color:#eee;background:#1b1b1f;border:1px solid #2e7d32;border-radius:6px;padding:6px 10px;' +
'max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
document.body.appendChild(t);
SUPJAV_TOOLS._toast = t;
return t;
},
copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
return navigator.clipboard.writeText(text).catch(() => SUPJAV_TOOLS.copyTextFallback(text));
} catch (e) {
// Some browsers throw synchronously without clipboard permission/gesture.
return Promise.resolve(SUPJAV_TOOLS.copyTextFallback(text));
}
}
SUPJAV_TOOLS.copyTextFallback(text);
return Promise.resolve();
},
copyTextFallback(text) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} catch (e) {
// execCommand can throw when the page lacks clipboard/focus; nothing to do.
}
ta.remove();
},
sendToJd(urls) {
return SUPJAV_TOOLS.copyText(urls.join('\n')).then(() => {
SUPJAV_TOOLS.ensureToast().textContent =
`Copied ${urls.length} link${urls.length === 1 ? '' : 's'} — JDownloader LinkGrabber will grab it`;
});
},
sendToIdm(url) {
const a = document.createElement('a');
a.className = 'st-idm';
a.href = url;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
SUPJAV_TOOLS.ensureToast().textContent =
'Sent to IDM — starts a browser fallback download if not intercepted';
},
downloadHls(url, filename, row, isAuto) {
if (!window.supjavAPI || typeof window.supjavAPI.downloadHls !== 'function') {
SUPJAV_TOOLS.download(url, filename);
return;
}
const target = row || SUPJAV_TOOLS.ensureToast();
const key = row || SUPJAV_TOOLS._toast;
if (!key._progSubscribed) {
key._progSubscribed = true;
window.supjavAPI.onHlsProgress((p) => {
if (key._progUrl !== url) return;
if (p.complete) {
target.textContent = `Auto-downloaded \u2192 ${p.filename}`;
} else {
const pct = Math.round((p.done / p.total) * 100);
target.textContent = `Auto HLS ${pct}% (${p.done}/${p.total})`;
}
});
}
key._progUrl = url;
target.textContent = 'Auto HLS 0%';
const pkey = freshestPkey(url);
window.supjavAPI
.downloadHls(url, filename, !!isAuto, SUPJAV_TOOLS.videoFolder(), pkey)
.then((res) => {
if (res && res.started) return; // progress events update the row
if (res && res.skipped) target.textContent = `Skipped \u2014 ${res.reason || 'already downloaded'}`;
else if (res && res.ok) target.textContent = `Done \u2192 ${filename}`;
else if (res && res.error) target.textContent = `HLS error: ${res.error}`;
});
},
// ---------------------------------------------------------------- queue auto-mode (desktop)
auto: false,
_autoTried: {},
_autoTimer: null,
setAuto(v) {
SUPJAV_TOOLS.auto = !!v;
if (SUPJAV_TOOLS.auto) {
SUPJAV_TOOLS.startAutoMonitor();
} else if (SUPJAV_TOOLS._autoTimer) {
clearInterval(SUPJAV_TOOLS._autoTimer);
SUPJAV_TOOLS._autoTimer = null;
}
},
startAutoMonitor() {
if (SUPJAV_TOOLS._autoTimer) return;
SUPJAV_TOOLS._autoTimer = setInterval(() => {
if (!SUPJAV_TOOLS.auto) {
clearInterval(SUPJAV_TOOLS._autoTimer);
SUPJAV_TOOLS._autoTimer = null;
return;
}
const sources = SUPJAV_TOOLS.detectVideoSources();
const untried = sources.filter((s) => !SUPJAV_TOOLS._autoTried[s.url]);
const hls = untried.find((s) => s.kind === 'hls');
if (hls) {
SUPJAV_TOOLS._autoTried[hls.url] = true;
SUPJAV_TOOLS.downloadHls(hls.url, SUPJAV_TOOLS.defaultFilename(1, 'hls'), null, true);
return;
}
// Extension-only fallback: with no HLS stream, auto-download the MP4
// directly. The desktop app merges HLS and lets MP4-only videos hit the
// queue watchdog, so this is gated to the extension to stay neutral.
if (window.supjavAPI && window.supjavAPI.runtime === 'extension') {
const mp4 = untried.find((s) => s.kind === 'mp4');
if (mp4) {
SUPJAV_TOOLS._autoTried[mp4.url] = true;
SUPJAV_TOOLS.download(mp4.url, SUPJAV_TOOLS.defaultFilename(1, 'mp4'));
}
}
}, 2000);
},
enqueue(url, title) {
if (window.supjavAPI && typeof window.supjavAPI.enqueue === 'function') {
return window.supjavAPI.enqueue(url, title);
}
return Promise.resolve(null);
},
// ---------------------------------------------------------------- listing page helpers
findCards(root) {
const cards = [];
const seen = new Set();
const push = (el) => {
const linkEl = el.matches('a[href]') ? el : el.querySelector('a[href]');
if (!linkEl || seen.has(linkEl)) return;
const href = linkEl.href;
if (!href || href === location.href || href.startsWith('javascript:')) return;
seen.add(linkEl);
const titleEl = el.querySelector('h1,h2,h3,.title,.name');
const img = el.querySelector('img');
const title = (titleEl ? titleEl.textContent : img ? img.alt : '').trim();
cards.push({ el, linkEl, href, title, img });
};
root.querySelectorAll(CONFIG.itemSelectors.join(',')).forEach(push);
if (!cards.length) {
root.querySelectorAll('a[href] img').forEach((img) => {
const a = img.closest('a[href]');
if (a) push(a);
});
}
return cards;
},
isListPage() {
if (document.querySelector('#dz_video')) return false;
return (
SUPJAV_TOOLS.findCards(document).length >= 2 ||
matches(document, CONFIG.searchFormSelectors) !== null ||
SUPJAV_TOOLS.getNextLink() !== null
);
},
_pageNum(href) {
let m = href.match(/[?&]page=(\d+)/);
if (m) return parseInt(m[1], 10);
m = href.match(/[?&]paged=(\d+)/);
if (m) return parseInt(m[1], 10);
m = href.match(/\/page\/(\d+)\/?/);
if (m) return parseInt(m[1], 10);
return null;
},
getNextLink(root) {
root = root || document;
const current = location.href;
for (const sel of CONFIG.paginationSelectors) {
const pag = root.querySelector(sel);
if (!pag) continue;
for (const sel2 of CONFIG.nextLinkSelectors) {
const a = pag.querySelector(sel2);
if (a && a.href && a.href !== current) return a.href;
}
const any = pag.querySelector('a[href]');
if (any && any.href && any.href !== current) return any.href;
}
const curPage = SUPJAV_TOOLS._pageNum(current) || 1;
const candidates = [];
root.querySelectorAll('a[href]').forEach((a) => {
const href = a.href;
if (!href || href === current) return;
const page = SUPJAV_TOOLS._pageNum(href);
if (page !== null) {
candidates.push({ href, page });
return;
}
const label = [a.textContent, a.getAttribute('aria-label'), a.getAttribute('title'), a.rel]
.join(' ')
.toLowerCase();
if (/\bnext\b|»|nächste|suivant/.test(label)) {
candidates.push({ href, page: curPage + 1 });
}
});
candidates.sort((x, y) => x.page - y.page);
const next = candidates.find((c) => c.page === curPage + 1);
if (next) return next.href;
const later = candidates.find((c) => c.page > curPage);
if (later) return later.href;
if (candidates.length) return candidates[0].href;
return null;
},
getContainer() {
if (SUPJAV_TOOLS._containerSel) return document.querySelector(SUPJAV_TOOLS._containerSel);
for (const sel of CONFIG.gridContainerSelectors) {
const el = document.querySelector(sel);
if (el && el.querySelector(CONFIG.itemSelectors.join(','))) {
SUPJAV_TOOLS._containerSel = sel;
return el;
}
}
return document.body;
},
// ---------------------------------------------------------------- card buttons + filters
enhanceCards(root) {
SUPJAV_TOOLS.findCards(root).forEach((c) => {
if (!c.el.style.position) c.el.style.position = 'relative';
if (!c.el.querySelector('.st-open')) {
const b = document.createElement('button');
b.className = 'st-open';
b.textContent = 'OPEN';
b.title = 'Open video in this tab';
b.style.cssText =
'position:absolute;right:4px;top:4px;z-index:5;background:#1a73e8;color:#fff;' +
'border:0;border-radius:4px;padding:2px 8px;font:600 11px/1.6 system-ui,sans-serif;' +
'cursor:pointer;';
b.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
location.href = c.href;
});
c.el.appendChild(b);
}
if (!c.el.querySelector('.st-queue') && window.supjavAPI && typeof window.supjavAPI.enqueue === 'function') {
const q = document.createElement('button');
q.className = 'st-queue';
q.textContent = 'QUEUE';
q.title = 'Add to desktop download queue';
q.style.cssText =
'position:absolute;left:4px;top:4px;z-index:5;background:#2e7d32;color:#fff;' +
'border:0;border-radius:4px;padding:2px 8px;font:600 11px/1.6 system-ui,sans-serif;' +
'cursor:pointer;';
q.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const r = SUPJAV_TOOLS.enqueue(c.href, c.title || '');
if (r && typeof r.then === 'function') {
r.then((res) => {
if (res && res.rejected) q.textContent = 'Downloaded';
else if (res && res.queued) q.textContent = 'Added';
else q.textContent = 'In queue';
setTimeout(() => (q.textContent = 'QUEUE'), 1200);
}).catch(() => {});
} else {
q.textContent = 'Added';
setTimeout(() => (q.textContent = 'QUEUE'), 1200);
}
});
c.el.appendChild(q);
}
c.el.classList.add('st-card');
});
},
filters: { q: '', include: [], exclude: [], yearMin: '', yearMax: '' },
applyFilters() {
const f = SUPJAV_TOOLS.filters;
const inc = f.include.map((s) => s.trim().toLowerCase()).filter(Boolean);
const exc = f.exclude.map((s) => s.trim().toLowerCase()).filter(Boolean);
document.querySelectorAll('.st-card').forEach((el) => {
const title = (el.textContent || '').toLowerCase();
let show = true;
if (inc.length && !inc.every((k) => title.includes(k))) show = false;
if (show && exc.length && exc.some((k) => title.includes(k))) show = false;
if (show && (f.yearMin || f.yearMax)) {
const years = (title.match(/(19|20)\d{2}/g) || []).map(Number);
if (years.length) {
if (f.yearMin && years.every((y) => y < f.yearMin)) show = false;
if (f.yearMax && years.every((y) => y > f.yearMax)) show = false;
}
}
el.style.display = show ? '' : 'none';
});
},
// ---------------------------------------------------------------- infinite scroll
_nextHref: null,
_loading: false,
_loadedPages: new Set(),
startInfiniteScroll() {
if (SUPJAV_TOOLS._sentinel) return;
SUPJAV_TOOLS._nextHref = SUPJAV_TOOLS.getNextLink();
if (!SUPJAV_TOOLS._nextHref) {
if (!SUPJAV_TOOLS._retry) {
SUPJAV_TOOLS._retry = true;
const iv = setInterval(() => {
SUPJAV_TOOLS._nextHref = SUPJAV_TOOLS.getNextLink();
if (SUPJAV_TOOLS._nextHref) {
clearInterval(iv);
SUPJAV_TOOLS.startInfiniteScroll();
}
}, 1000);
setTimeout(() => clearInterval(iv), 15000);
}
return;
}
const container = SUPJAV_TOOLS.getContainer();
if (!container) return;
const sentinel = document.createElement('div');
sentinel.className = 'st-sentinel';
sentinel.style.cssText = 'height:1px;';
container.appendChild(sentinel);
SUPJAV_TOOLS._sentinel = sentinel;
new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) SUPJAV_TOOLS.loadNextPage();
}, { rootMargin: '1200px' }).observe(sentinel);
},
async loadNextPage() {
if (SUPJAV_TOOLS._loading || !SUPJAV_TOOLS._nextHref) return;
if (SUPJAV_TOOLS._loadedPages.has(SUPJAV_TOOLS._nextHref)) {
SUPJAV_TOOLS._nextHref = null;
return;
}
SUPJAV_TOOLS._loading = true;
const container = SUPJAV_TOOLS.getContainer();
const indicator = document.createElement('div');
indicator.className = 'st-loading';
indicator.textContent = 'Loading more…';
indicator.style.cssText =
'padding:10px;text-align:center;color:#999;font:13px system-ui,sans-serif;';
if (container && SUPJAV_TOOLS._sentinel)
container.insertBefore(indicator, SUPJAV_TOOLS._sentinel);
try {
const res = await fetch(SUPJAV_TOOLS._nextHref, { credentials: 'same-origin' });
if (!res.ok) throw new Error(String(res.status));
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
const source = SUPJAV_TOOLS._containerSel
? doc.querySelector(SUPJAV_TOOLS._containerSel) || doc.body
: doc.body;
const cards = SUPJAV_TOOLS.findCards(source);
if (!cards.length) {
SUPJAV_TOOLS._nextHref = null;
return;
}
const existing = new Set(
Array.from((container || document).querySelectorAll('a[href]')).map((a) => a.href)
);
const frag = document.createDocumentFragment();
cards.forEach((c) => {
if (existing.has(c.href)) return;
frag.appendChild(c.el.cloneNode(true));
});
if (container && SUPJAV_TOOLS._sentinel)
container.insertBefore(frag, SUPJAV_TOOLS._sentinel);
SUPJAV_TOOLS.enhanceCards(container);
SUPJAV_TOOLS.applyFilters();
SUPJAV_TOOLS._loadedPages.add(SUPJAV_TOOLS._nextHref);
SUPJAV_TOOLS._nextHref = SUPJAV_TOOLS.getNextLink(doc);
} catch (e) {
SUPJAV_TOOLS._nextHref = null;
} finally {
if (indicator && indicator.parentNode) indicator.parentNode.removeChild(indicator);
SUPJAV_TOOLS._loading = false;
}
},
// ---------------------------------------------------------------- search + filter bar
buildFilterBar() {
const bar = document.createElement('div');
bar.style.cssText =
'position:sticky;top:0;z-index:9;background:#1b1b1f;color:#eee;padding:8px 10px;' +
'border-bottom:1px solid #333;font:13px/1.5 system-ui,sans-serif;';
const row = (label, input, extra) => {
const w = document.createElement('div');
w.style.cssText = 'display:inline-flex;align-items:center;gap:6px;margin-right:10px;';
const l = document.createElement('label');
l.textContent = label;
l.style.cssText = 'color:#999;';
w.appendChild(l);
w.appendChild(input);
if (extra) w.appendChild(extra);
return w;
};
const inputStyle =
'background:#2a2a30;color:#eee;border:1px solid #444;border-radius:4px;padding:4px 6px;font:inherit;';
const q = document.createElement('input');
q.placeholder = 'Search site...';
q.style.cssText = inputStyle + 'width:180px;';
const go = document.createElement('button');
go.textContent = 'Search';
go.style.cssText = btnStyle();
go.addEventListener('click', () => {
const v = q.value.trim();
location.href = location.origin + '/?s=' + encodeURIComponent(v);
});
q.addEventListener('keydown', (e) => {
if (e.key === 'Enter') go.click();
});
const inc = document.createElement('input');
inc.placeholder = 'include words';
inc.style.cssText = inputStyle + 'width:140px;';
const exc = document.createElement('input');
exc.placeholder = 'exclude words';
exc.style.cssText = inputStyle + 'width:140px;';
const yMin = document.createElement('input');
yMin.type = 'number';
yMin.placeholder = 'Year from';
yMin.style.cssText = inputStyle + 'width:80px;';
const yMax = document.createElement('input');
yMax.type = 'number';
yMax.placeholder = 'Year to';
yMax.style.cssText = inputStyle + 'width:80px;';
const apply = () => {
SUPJAV_TOOLS.filters.include = inc.value.split(/[\s,]+/);
SUPJAV_TOOLS.filters.exclude = exc.value.split(/[\s,]+/);
SUPJAV_TOOLS.filters.yearMin = yMin.value ? parseInt(yMin.value, 10) : '';
SUPJAV_TOOLS.filters.yearMax = yMax.value ? parseInt(yMax.value, 10) : '';
SUPJAV_TOOLS.applyFilters();
};
const applyBtn = document.createElement('button');
applyBtn.textContent = 'Filter';
applyBtn.style.cssText = btnStyle();
applyBtn.addEventListener('click', apply);
const reset = document.createElement('button');
reset.textContent = 'Reset';
reset.style.cssText = btnStyle();
reset.addEventListener('click', () => {
inc.value = exc.value = yMin.value = yMax.value = '';
SUPJAV_TOOLS.filters = { q: '', include: [], exclude: [], yearMin: '', yearMax: '' };
SUPJAV_TOOLS.applyFilters();
});
bar.appendChild(row('', q, go));
bar.appendChild(row('Include', inc));
bar.appendChild(row('Exclude', exc));
bar.appendChild(row('Year', yMin, yMax));
bar.appendChild(applyBtn);
bar.appendChild(reset);
if (window.supjavAPI && typeof window.supjavAPI.enqueueAll === 'function') {
const qAll = document.createElement('button');
qAll.textContent = 'Queue all visible';
qAll.style.cssText = btnStyle() + 'margin-left:10px;';
qAll.addEventListener('click', () => {
const items = [];
document.querySelectorAll('.st-card').forEach((el) => {
if (el.style.display === 'none') return;
const a = el.querySelector('a[href]');
if (!a) return;
const titleEl = el.querySelector('h1,h2,h3,.title,.name');
items.push({
url: a.href,
title: (titleEl ? titleEl.textContent : a.getAttribute('title') || '').trim().slice(0, 100),
});
});
if (!items.length) {
qAll.textContent = 'No visible cards';
setTimeout(() => (qAll.textContent = 'Queue all visible'), 1500);
return;
}
window.supjavAPI.enqueueAll(items).then((r) => {
if (r && typeof r.added === 'number') {
qAll.textContent = `Queued ${r.added} (${r.skipped || 0} dupes)`;
} else {
qAll.textContent = 'Queue unavailable';
}
setTimeout(() => (qAll.textContent = 'Queue all visible'), 2000);
}).catch(() => {
qAll.textContent = 'Queue unavailable';
setTimeout(() => (qAll.textContent = 'Queue all visible'), 1500);
});
});
bar.appendChild(qAll);
}
function btnStyle() {
return (
'background:#3a3a42;color:#eee;border:1px solid #555;border-radius:4px;' +
'padding:4px 10px;cursor:pointer;font:inherit;'
);
}
return bar;
},
// Serializes everything the tool "sees" on the current page so it can be
// pasted back to verify live detection without automated site access.
collectDebugReport() {
const sources = SUPJAV_TOOLS.detectVideoSources();
return {
scriptVersion: SCRIPT_VERSION,
url: location.href,
title: (document.title || '').slice(0, 160),
isList: !!SUPJAV_TOOLS.isListPage(),
next: SUPJAV_TOOLS.getNextLink ? SUPJAV_TOOLS.getNextLink() : null,
sourceCount: sources.length,
sources: sources.map((s) => ({ kind: s.kind, url: s.url, label: s.label })),
extMarker: document.documentElement.getAttribute('data-st-ext'),
supjavApi:
!!window.supjavAPI && typeof window.supjavAPI.enqueue === 'function',
};
},
// ---------------------------------------------------------------- floating panel (sources + tabs)
buildPanel() {
const panel = document.createElement('div');
panel.style.cssText =
'position:fixed;right:16px;bottom:16px;z-index:2147483647;font:13px/1.5 system-ui,sans-serif;' +
'color:#eee;background:#1b1b1f;border:1px solid #333;border-radius:8px;padding:10px 12px;' +
'box-shadow:0 4px 20px rgba(0,0,0,.5);width:340px;max-height:70vh;overflow:auto;';
const title = document.createElement('div');
title.textContent = 'Supjav Tools';
title.style.cssText = 'font-weight:600;margin-bottom:8px;color:#fff;';
panel.appendChild(title);
const label = (text) => {
const d = document.createElement('div');
d.textContent = text;
d.style.cssText = 'font-weight:600;margin:10px 0 4px;color:#bbb;';
return d;
};
const list = document.createElement('div');
panel.appendChild(label('Video sources'));
panel.appendChild(list);
const refresh = () => {
const sources = SUPJAV_TOOLS.detectVideoSources();
if (JSON.stringify(sources) === panel._lastKey) return;
panel._lastKey = JSON.stringify(sources);
list.replaceChildren();
if (!sources.length) {
const empty = document.createElement('div');
empty.textContent = 'No video sources detected on this page.';
empty.style.cssText = 'color:#999;';
list.appendChild(empty);
return;
}
sources.forEach((s, i) => {
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;gap:4px;margin:4px 0;';
const row = document.createElement('button');
row.textContent = `${i + 1}. ${s.kind.toUpperCase()} — ${s.label}`;
row.title = s.url;
row.style.cssText =
'flex:1;padding:6px 8px;text-align:left;background:#2a2a30;color:#eee;' +
'border:1px solid #444;border-radius:6px;cursor:pointer;overflow:hidden;' +
'text-overflow:ellipsis;white-space:nowrap;';
row.addEventListener('click', () => {
const filename = SUPJAV_TOOLS.defaultFilename(i + 1, s.kind);
if (s.kind === 'hls') {
SUPJAV_TOOLS.downloadHls(s.url, filename, row);
} else {
SUPJAV_TOOLS.download(s.url, filename);
}
});
wrap.appendChild(row);
const mini = (label, title, fn) => {
const b = document.createElement('button');
b.textContent = label;
b.title = title;
b.style.cssText =
'padding:6px 8px;background:#1b1b1f;color:#eee;border:1px solid #444;' +
'border-radius:6px;cursor:pointer;font:600 11px/1.4 system-ui,sans-serif;';
b.addEventListener('click', fn);
wrap.appendChild(b);
};
mini(
'IDM',
'Send to Internet Download Manager (IDM browser extension must be installed to intercept)',
() => SUPJAV_TOOLS.sendToIdm(s.url),
);
mini(
'JD',
'Copy link to clipboard for JDownloader LinkGrabber (enable clipboard monitoring in JDownloader settings)',
() => SUPJAV_TOOLS.sendToJd([s.url]),
);
list.appendChild(wrap);
});
};
// auto-monitor section
const autoBox = document.createElement('div');
autoBox.style.cssText = 'display:flex;align-items:center;gap:6px;margin:4px 0;';
const autoChk = document.createElement('input');
autoChk.type = 'checkbox';
autoChk.checked = SUPJAV_TOOLS.auto;
autoChk.addEventListener('change', () => {
SUPJAV_TOOLS.setAuto(autoChk.checked);
localStorage.setItem('st_auto', autoChk.checked ? '1' : '0');
});
const autoLbl = document.createElement('span');
autoLbl.textContent = 'Auto-monitor: auto-download video when a stream appears';
autoLbl.style.cssText = 'color:#ccc;';
autoBox.appendChild(autoChk);
autoBox.appendChild(autoLbl);
panel.appendChild(label('Auto-monitor'));
panel.appendChild(autoBox);
if (window.supjavAPI && typeof window.supjavAPI.enqueue === 'function') {
const q = document.createElement('button');
q.textContent = 'Add this video to queue';
q.style.cssText = btnStyle() + 'width:100%;margin-top:8px;';
q.addEventListener('click', () => {
const t = (document.querySelector('.archive-title h1') || document.body)
.textContent.trim()
.slice(0, 100);
SUPJAV_TOOLS.enqueue(location.href, t).then((r) => {
if (r && r.queued) q.textContent = `Queued (${r.queue} in queue)`;
else if (r && r.rejected) q.textContent = 'Already downloaded';
else if (r && !r.queued) q.textContent = 'Already queued';
else q.textContent = 'Queue unavailable';
setTimeout(() => (q.textContent = 'Add this video to queue'), 2000);
});
});
panel.appendChild(q);
}
// history section (desktop only)
let historyBox = null;
if (window.supjavAPI && typeof window.supjavAPI.getHistory === 'function') {
historyBox = document.createElement('div');
historyBox.style.cssText = 'color:#999;font:12px/1.4 system-ui,sans-serif;';
const clearHist = document.createElement('button');
clearHist.textContent = 'Clear history';
clearHist.style.cssText = btnStyle() + 'margin-left:6px;';
clearHist.addEventListener('click', () => {
window.supjavAPI
.clearHistory()
.then(() => renderHistory())
.catch(() => {});
});
const histLabel = label('History');
histLabel.style.display = 'flex';
histLabel.style.justifyContent = 'space-between';
histLabel.style.alignItems = 'center';
histLabel.appendChild(clearHist);
panel.appendChild(histLabel);
panel.appendChild(historyBox);
const renderHistory = () => {
window.supjavAPI
.getHistory()
.then((items) => {
historyBox.replaceChildren();
if (!items || !items.length) {
historyBox.textContent = 'No downloads yet.';
return;
}
items.slice(0, 8).forEach((h) => {
const row = document.createElement('div');
row.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
const when = new Date(h.time).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
row.textContent = `${when} ${h.folder ? h.folder + '/' : ''}${h.filename} [${h.status}]`;
row.title = h.url || '';
historyBox.appendChild(row);
});
})
.catch(() => {});
};
renderHistory();
SUPJAV_TOOLS._renderHistory = renderHistory;
}
const foot = document.createElement('div');
foot.style.cssText =
'margin-top:10px;padding-top:8px;border-top:1px solid #333;font:11px system-ui,sans-serif;' +
'color:#777;display:flex;justify-content:space-between;align-items:center;';
const ver = document.createElement('span');
ver.textContent = `Supjav Tools v${SCRIPT_VERSION}`;
foot.appendChild(ver);
if (SUPJAV_TOOLS._updateInfo) {
const up = document.createElement('a');
up.textContent = `Update v${SUPJAV_TOOLS._updateInfo.from} \u2192 v${SUPJAV_TOOLS._updateInfo.to}`;
up.href = 'http://127.0.0.1:28719/';
up.target = '_blank';
up.style.cssText = 'color:#81c784;';
foot.appendChild(up);
}
panel.appendChild(foot);
const dbg = document.createElement('button');
dbg.textContent = 'Copy debug report';
dbg.title = 'Copy what this page looks like to the tool (URL, sources, pagination) so it can be verified without automated site access';
dbg.style.cssText = btnStyle() + 'width:100%;margin-top:8px;';
dbg.addEventListener('click', () => {
SUPJAV_TOOLS.copyText(JSON.stringify(SUPJAV_TOOLS.collectDebugReport(), null, 2)).then(() => {
SUPJAV_TOOLS.ensureToast().textContent =
'Debug report copied \u2014 paste it back to the assistant';
});
});
panel.appendChild(dbg);
const close = document.createElement('button');
close.textContent = 'Close';
close.style.cssText =
'margin-top:10px;width:100%;padding:5px;background:transparent;color:#999;' +
'border:1px solid #444;border-radius:6px;cursor:pointer;';
close.addEventListener('click', () => {
panel._timer && clearInterval(panel._timer);
panel.remove();
SUPJAV_TOOLS.fab.style.display = 'flex';
});
panel.appendChild(close);
panel._timer = setInterval(() => {
refresh();
if (SUPJAV_TOOLS._renderHistory) SUPJAV_TOOLS._renderHistory();
}, 2000);
refresh();
if (SUPJAV_TOOLS._renderHistory) SUPJAV_TOOLS._renderHistory();
return panel;
function btnStyle() {
return (
'background:#3a3a42;color:#eee;border:1px solid #555;border-radius:4px;' +
'padding:3px 8px;cursor:pointer;font:inherit;'
);
}
},
versionGt(a, b) {
const left = String(a || '')
.split('.')
.map((n) => parseInt(n, 10) || 0);
const right = String(b || '')
.split('.')
.map((n) => parseInt(n, 10) || 0);
for (let i = 0; i < Math.max(left.length, right.length); i++) {
const x = left[i] || 0;
const y = right[i] || 0;
if (x !== y) return x > y;
}
return false;
},
// The installer server (install/installer.js) answers /version; GM_xmlhttpRequest
// bypasses the mixed-content block so an https supjav page can reach http://127.0.0.1.
checkForUpdate() {
if (typeof GM_xmlhttpRequest !== 'function') return;
try {
const last = parseInt(localStorage.getItem('st_update_check') || '0', 10);
if (Date.now() - last < 6 * 3600 * 1000) {
SUPJAV_TOOLS._updateInfo = JSON.parse(localStorage.getItem('st_update_info') || 'null');
SUPJAV_TOOLS.renderUpdateChip();
return;
}
} catch (e) {
// localStorage disabled or corrupt; fall through to a live check.
}
GM_xmlhttpRequest({
method: 'GET',
url: 'http://127.0.0.1:28719/version',
timeout: 3000,
onload: (res) => SUPJAV_TOOLS.handleUpdateResponse(res.responseText),
onerror: () => SUPJAV_TOOLS.renderUpdateChip(),
ontimeout: () => SUPJAV_TOOLS.renderUpdateChip(),
});
},
handleUpdateResponse(text) {
const available = String(text || '').trim().replace(/^v/, '');
SUPJAV_TOOLS._updateInfo =
available && SUPJAV_TOOLS.versionGt(available, SCRIPT_VERSION)
? { from: SCRIPT_VERSION, to: available }
: null;
try {
localStorage.setItem('st_update_check', String(Date.now()));
localStorage.setItem('st_update_info', JSON.stringify(SUPJAV_TOOLS._updateInfo));
} catch (e) {
// localStorage disabled; the chip still renders for this session.
}
SUPJAV_TOOLS.renderUpdateChip();
},
renderUpdateChip() {
const id = 'st-update-chip';
const old = document.getElementById(id);
if (old) old.remove();
if (!SUPJAV_TOOLS._updateInfo) return;
const chip = document.createElement('div');
chip.id = id;
chip.textContent = `Supjav Tools update: v${SUPJAV_TOOLS._updateInfo.from} \u2192 v${SUPJAV_TOOLS._updateInfo.to}`;
chip.style.cssText =
'position:fixed;top:12px;right:16px;z-index:2147483647;background:#1b1b1f;color:#81c784;' +
'border:1px solid #2e7d32;border-radius:6px;padding:6px 10px;font:12px system-ui,sans-serif;' +
'cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.5);';
chip.addEventListener('click', () => window.open('http://127.0.0.1:28719/', '_blank'));
document.body.appendChild(chip);
},
ensureFab() {
if (SUPJAV_TOOLS.fab) return;
const fab = document.createElement('div');
fab.textContent = 'DL';
fab.style.cssText =
'position:fixed;right:16px;bottom:16px;z-index:2147483646;width:44px;height:44px;' +
'display:flex;align-items:center;justify-content:center;background:#e11;color:#fff;' +
'border-radius:50%;font:700 14px system-ui,sans-serif;cursor:pointer;' +
'box-shadow:0 4px 16px rgba(0,0,0,.5);user-select:none;';
fab.title = 'Supjav Tools';
fab.dataset.stVersion = SCRIPT_VERSION;
fab.addEventListener('click', () => {
fab.style.display = 'none';
document.body.appendChild(SUPJAV_TOOLS.buildPanel());
});
document.body.appendChild(fab);
SUPJAV_TOOLS.fab = fab;
},
// On video pages, if the player iframe is still about:blank, click the first
// server button so the embed loads (desktop/extension both call this from the
// top frame before/alongside autoplay in the subframe).
primeVideoPlayer() {
if (!location.pathname.includes('/video/')) return;
const iframe = document.querySelector('iframe#video, iframe[allowfullscreen], iframe');
const src = iframe && iframe.getAttribute('src');
if (!src || src === 'about:blank') {
const servers = getCachedElement('.btn-server');
if (servers.length) servers[0].click();
}
},
// Runs only in cross-origin embed subframes (extension content script with
// all_frames). Supjav players fetch the stream only after a play action, so
// synthesize one: click a play-looking element until a <video> appears, then
// play() directly (muted first so autoplay is never blocked).
startEmbedAutoplay() {
if (SUPJAV_TOOLS._embedStarted) return;
SUPJAV_TOOLS._embedStarted = true;
const boot = () => {
const v = document.querySelector('video');
if (v) {
try {
v.muted = true;
const p = v.play();
if (p && p.then) p.then(() => { try { v.muted = false; } catch (e) {} }).catch(() => {});
} catch (e) {}
return;
}
const cands = Array.from(
document.querySelectorAll('button, [role=button], a, .play, .vjs-big-play-button, [class*=play i], [id*=play i]'),
);
const btn =
cands.find((el) => {
const r = el.getBoundingClientRect();
return r.width > 24 && r.height > 24;
}) || cands[0];
if (btn) btn.click();
};
boot();
const iv = setInterval(boot, 1000);
setTimeout(() => clearInterval(iv), 90000);
window.__supjavAutoplay = true;
},
start() {
// If the extension is installed, its content script (isolated world) owns
// the UI and embed autoplay; a co-installed Violentmonkey copy has no
// supjavAPI but can see the extension's DOM marker, so it must step aside.
if (!window.supjavAPI && document.documentElement.getAttribute('data-st-ext') === '1') {
return;
}
// In cross-origin embed subframes only run autoplay; never show UI there.
let isTop = true;
try {
isTop = window.top === window;
} catch (e) {}
if (!isTop) {
SUPJAV_TOOLS.startEmbedAutoplay();
return;
}
startPerformanceObserver();
SUPJAV_TOOLS.ensureFab();
SUPJAV_TOOLS.primeVideoPlayer();
SUPJAV_TOOLS.checkForUpdate();
if (localStorage.getItem('st_auto') === '1') SUPJAV_TOOLS.setAuto(true);
if (SUPJAV_TOOLS.isListPage()) {
const bar = SUPJAV_TOOLS.buildFilterBar();
const container = SUPJAV_TOOLS.getContainer();
container.parentNode.insertBefore(bar, container);
SUPJAV_TOOLS.enhanceCards(document);
SUPJAV_TOOLS.startInfiniteScroll();
}
const observer = new MutationObserver(() => {
if (!document.contains(SUPJAV_TOOLS.fab)) SUPJAV_TOOLS.ensureFab();
});
observer.observe(document.body, { childList: true, subtree: true });
},
};
window.__SUPJAV_TOOLS__ = SUPJAV_TOOLS;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => SUPJAV_TOOLS.start());
} else {
SUPJAV_TOOLS.start();
}
})();