Sleazy Fork is available in English.
this is my first project, thanks for using it. if you have any question or want to bypass more website, dm to my telegram: @wizard_8080
// ==UserScript==
// @name javjb bypass
// @namespace wi3_avjb_bypass
// @version 1.6.5
// @description this is my first project, thanks for using it. if you have any question or want to bypass more website, dm to my telegram: @wizard_8080
// @author wi3
// @match https://avjb.com/*
// @match https://avjb.cc/*
// @match https://javhd.com/*
// @match https://enter.javhd.com/*
// @match https://caribbeancom.com/*
// @match https://*.caribbeancom.com/*
// @match https://caribbeancompr.com/*
// @match https://*.caribbeancompr.com/*
// @match https://naughtyamerica.com/*
// @match https://*.naughtyamerica.com/*
// @match https://brazzers.com/*
// @match https://*.brazzers.com/*
// @match https://pornhubpremium.com/*
// @match https://*.pornhubpremium.com/*
// @match https://blacked.com/*
// @match https://*.blacked.com/*
// @match https://vixen.com/*
// @match https://*.vixen.com/*
// @match https://tushy.com/*
// @match https://*.tushy.com/*
// @match https://tushyraw.com/*
// @match https://*.tushyraw.com/*
// @match https://blackedraw.com/*
// @match https://*.blackedraw.com/*
// @match https://evilangel.com/*
// @match https://*.evilangel.com/*
// @match https://wicked.com/*
// @match https://*.wicked.com/*
// @match https://digitalplayground.com/*
// @match https://*.digitalplayground.com/*
// @match https://puretaboo.com/*
// @match https://*.puretaboo.com/*
// @match https://teamskeet.com/*
// @match https://*.teamskeet.com/*
// @match https://mofos.com/*
// @match https://*.mofos.com/*
// @match https://realitykings.com/*
// @match https://*.realitykings.com/*
// @match https://bangbros.com/*
// @match https://*.bangbros.com/*
// @connect *.jb-aiwei.cc
// @connect jb-aiwei.cc
// @connect *.imgclh.com
// @connect *.avstatic.com
// @connect avjb.com
// @connect *.cdnjhd.com
// @connect *.cdnjav.com
// @connect javhd.com
// @connect static.javhd.com
// @connect fms.caribbeancom.com
// @connect dl21.caribbeancom.com
// @connect *.naughtycdn.com
// @connect *.project1content.com
// @connect *.phncdn.com
// @connect www.pornhub.com
// @connect pornhub.com
// @connect web.archive.org
// @match https://adulttime.com/*
// @match https://*.adulttime.com/*
// @match https://badteenspunished.com/*
// @match https://*.badteenspunished.com/*
// @match https://bountyhunterporn.com/*
// @match https://*.bountyhunterporn.com/*
// @match https://caughtmycoach.com/*
// @match https://*.caughtmycoach.com/*
// @match https://cheatingsis.com/*
// @match https://*.cheatingsis.com/*
// @match https://cumswappingsis.com/*
// @match https://*.cumswappingsis.com/*
// @match https://daddyslilangel.com/*
// @match https://*.daddyslilangel.com/*
// @match https://detentiongirls.com/*
// @match https://*.detentiongirls.com/*
// @match https://driverxxx.com/*
// @match https://*.driverxxx.com/*
// @match https://momsteachsex.com/*
// @match https://*.momsteachsex.com/*
// @match https://myfamilypies.com/*
// @match https://*.myfamilypies.com/*
// @match https://nubiles-casting.com/*
// @match https://*.nubiles-casting.com/*
// @match https://nubileset.com/*
// @match https://*.nubileset.com/*
// @match https://nubiles-porn.com/*
// @match https://*.nubiles-porn.com/*
// @match https://nubilesunscripted.com/*
// @match https://*.nubilesunscripted.com/*
// @match https://petiteballerinasfucked.com/*
// @match https://*.petiteballerinasfucked.com/*
// @match https://petitehdporn.com/*
// @match https://*.petitehdporn.com/*
// @match https://princesscum.com/*
// @match https://*.princesscum.com/*
// @match https://realitysis.com/*
// @match https://*.realitysis.com/*
// @match https://shesbreedingmaterial.com/*
// @match https://*.shesbreedingmaterial.com/*
// @match https://shesinmybed.com/*
// @match https://*.shesinmybed.com/*
// @match https://stepsiblingscaught.com/*
// @match https://*.stepsiblingscaught.com/*
// @match https://teacherfucksteens.com/*
// @match https://*.teacherfucksteens.com/*
// @match https://youngermommy.com/*
// @match https://*.youngermommy.com/*
// @match https://nubiles.net/*
// @match https://*.nubiles.net/*
// @match https://familyswap.xxx/*
// @match https://*.familyswap.xxx/*
// @match https://smashed.xxx/*
// @match https://*.smashed.xxx/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @license MIT
// @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.1.5/hls.min.js
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ---- site dispatch (this block must stay FIRST: the wi3 trial guard
// ---- is injected immediately after the line above and `return`s out of
// ---- this IIFE, which has to disable BOTH halves at once).
const _wi3Site = (function () {
const h = location.hostname;
if (h === 'avjb.com' || h === 'avjb.cc' || /\.avjb\.(com|cc)$/.test(h)) return 'avjb';
if (h === 'javhd.com' || /\.javhd\.com$/.test(h)) return 'javhd';
if (/(^|\.)caribbeancom(pr)?\.com$/.test(h)) return 'carib';
if (/(^|\.)naughtyamerica\.com$/.test(h)) return 'na';
if (/(^|\.)brazzers\.com$/.test(h)) return 't2gate';
if (/(^|\.)pornhubpremium\.com$/.test(h)) return 'php';
if (/(^|\.)(blacked|vixen|tushy|tushyraw|blackedraw|evilangel|wicked|digitalplayground|puretaboo)\.com$/.test(h)) return 'trlt';
if (/(^|\.)teamskeet\.com$/.test(h)) return 'tskeet';
if (/(^|\.)mofos\.com$|(^|\.)realitykings\.com$|(^|\.)bangbros\.com$/.test(h)) return 't2gate';
if (/(^|\.)adulttime\.com$/.test(h)) return 'trlt';
if (/(^|\.)(nubiles-porn|nubiles-casting|nubileset|nubilesunscripted|nubiles|momsteachsex|stepsiblingscaught|badteenspunished|bountyhunterporn|caughtmycoach|cheatingsis|cumswappingsis|daddyslilangel|detentiongirls|driverxxx|myfamilypies|petiteballerinasfucked|petitehdporn|princesscum|realitysis|shesbreedingmaterial|shesinmybed|teacherfucksteens|youngermommy)\.(com|net)$|(^|\.)(familyswap|smashed)\.xxx$/.test(h)) return 'nubil';
return '';
})();
if (_wi3Site === 'avjb') {
// ==================== AVJB half ====================
// ============================================================
// KNOWN CDN MAP (folder structure: id/1000-floor = folder)
// v1.2.2: 88newline (id<=~121k) + newbox (id>=~122k) serve the FULL
// video HLS with NO signature — only Referer:avjb.com required.
// ============================================================
const SERVERS = [
'https://88newline.jb-aiwei.cc',
'https://newbox.jb-aiwei.cc',
'https://99newline.jb-aiwei.cc',
'https://7senver.jb-aiwei.cc',
'https://r22.jb-aiwei.cc',
'https://bmc2.imgclh.com',
'https://cdn1.bmc2.imgclh.com',
'https://cdn2.bmc2.imgclh.com',
'https://v.bmc2.imgclh.com',
'https://hls.bmc2.imgclh.com',
'https://stream.bmc2.imgclh.com',
'https://back.bbmmic2.com',
'https://lvb.rsso2.com',
];
const MIRRORS = ['https://newbox.jb-aiwei.cc', 'https://88newline.jb-aiwei.cc'];
// v1.3.0 — trang VIP không nhả token (reg-wall): token gắn SESSION chứ
// không gắn video. Trick tay của LO: vô trang FREE lấy CSRF -> quay lại
// trang paid -> spped.php trả file đầy đủ. Ở đây tự động hoá y chang.
const FREE_TOKEN_PAGES = [
'/vi/video/126278/fc2-ppv-4694056-2/',
'/vi/video/5552/9fb255b544c3ac94285563dd04dbfa9f/',
'/vi/video/132865/2fc227f5caa952eaedad49cd3801e7a7/'
];
// ============================================================
// GM REQUEST — CORS-free, gửi được Referer (server bắt buộc
// Referer chứa avjb.com; fetch thường bị CORS chặn vì mirror
// không trả Access-Control-Allow-Origin).
// ============================================================
const GM_XHR = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;
function gmRequest(url, responseType, timeout) {
return new Promise((resolve, reject) => {
if (!GM_XHR) { reject(new Error('no GM_xmlhttpRequest')); return; }
try {
GM_XHR({
method: 'GET',
url: url,
responseType: responseType || 'text',
timeout: timeout || 20000,
headers: { 'Referer': location.origin + '/' },
onload: (r) => resolve(r),
onerror: () => reject(new Error('gm http error: ' + url)),
ontimeout: () => reject(new Error('gm timeout: ' + url)),
onabort: () => reject(new Error('gm abort: ' + url)),
});
} catch (e) { reject(e); }
});
}
// hls.js loader qua GM — manifest + segment đọc được dù mirror không có ACAO
function GmLoader() {
this._gm = null;
this._aborted = false;
this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, end: 0 } };
}
GmLoader.prototype.load = function (context, config, callbacks) {
const self = this;
const url = context.url;
const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
self._aborted = false;
const t0 = performance.now();
const s = self.stats;
s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
s.parsing.start = 0; s.parsing.end = 0;
s.buffering.start = 0; s.buffering.end = 0;
const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
if (!GM_XHR) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
try {
GM_XHR({
method: 'GET', url: url, responseType: rt, timeout: 25000,
headers: { 'Referer': location.origin + '/' },
onload: (r) => {
const t1 = performance.now();
if (self._aborted || self.stats.aborted) return;
if (r.status >= 200 && r.status < 300) {
const data = r.response;
const len = (data && (data.byteLength || data.length)) || 0;
self.stats.loading.end = t1;
self.stats.loaded = len;
self.stats.total = len;
// xhr-loader gọi onProgress với payload trước onSuccess —
// stream-controller cần nó để transmuxer.push(), thiếu là stall
if (typeof callbacks.onProgress === 'function') {
callbacks.onProgress(self.stats, context, data, netStub);
}
callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
} else {
callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
}
},
onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
});
} catch (e) {
callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
}
};
GmLoader.prototype.abort = function () {
this._aborted = true;
this.stats.aborted = true;
};
GmLoader.prototype.destroy = function () {};
const PROBE_DATES = ['20220101','20220301','20220530','20220615','20220801',
'20221001','20221201','20230201','20230501','20230801','20240101',
'20240601','20250101','20250501','20250901','20260101','20260501'];
let capturedUrl = null; // stolen from the site's own player request
let videoLoaded = false;
let isChecking = false;
let loopTimer = null;
let loopMs = 1500;
let apiFails = 0;
let loopN = 0;
// ============================================================
// STAGE 0 — steal the stream URL at document-start
// ============================================================
if (!window.__wi3_avjb_hooked) {
window.__wi3_avjb_hooked = true;
// site's Playerjs lib (newjp.min.js) loads LATER and would clobber a
// plain assignment — so trap it: setter eats the lib's definition,
// getter always hands back our hook. Poll covers `function Playerjs(){}`
// declarations, which replace the accessor outright.
let _nativeP = null;
function _capture(u) {
if (u && typeof u === 'string' && !capturedUrl && !videoLoaded) {
if (/_limt/i.test(u)) { // teaser — paywalled 30s cut, never full
console.log('[wi3-AVJB] skip teaser:', u);
return;
}
capturedUrl = u;
console.log('[wi3-AVJB] captured from Playerjs:', u);
}
}
function _wi3Hook(config) {
try { _capture(config && config.file); } catch (e) {}
// dummy api: site code calls api("time"/"file"/...) — capture source
// switches too, and never let site code crash on a dead player
return {
api: function (cmd, val) {
try { if (cmd === 'file') _capture(val); } catch (e) {}
const v = document.getElementById('wi3Video');
if (cmd === 'time' && v) return v.currentTime || 0;
if (cmd === 'duration' && v) return v.duration || 0;
if (cmd === 'file') return null;
return 0;
}
};
}
const _trap = function () {
Object.defineProperty(window, 'Playerjs', {
configurable: true,
get: function () { return _wi3Hook; },
set: function (fn) {
if (typeof fn === 'function' && fn !== _wi3Hook) _nativeP = fn;
}
});
};
try { _trap(); } catch (e) {}
setInterval(function () {
try {
const cur = window.Playerjs;
if (cur !== _wi3Hook) {
if (typeof cur === 'function') _nativeP = cur;
_trap();
}
} catch (e) {}
}, 50);
}
function isBasicProbe(url) {
const onKnown = SERVERS.some(s => url.startsWith(s));
const signed = /md5=|[a-zA-Z0-9_-]{30,}/.test(url);
return onKnown && !signed;
}
function grabIfM3u8(url) {
if (!url || capturedUrl || videoLoaded) return;
if (!/\.m3u8(\?|$)/i.test(url)) return;
if (isBasicProbe(url)) return;
capturedUrl = url;
console.log('[wi3-AVJB] captured from network:', url);
}
const _fetch = window.fetch;
window.fetch = function (input, init) {
try {
const method = (init && init.method || 'GET').toUpperCase();
if (method !== 'HEAD') {
grabIfM3u8(typeof input === 'string' ? input : input && input.url);
}
} catch (e) {}
return _fetch.apply(this, arguments);
};
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
try {
if (String(method).toUpperCase() !== 'HEAD') grabIfM3u8(url);
} catch (e) {}
return _open.apply(this, arguments);
};
// ============================================================
// STAGE 0b — custom player DOM (built early, injected later)
// ============================================================
const playerDiv = document.createElement('div');
playerDiv.id = 'wi3Player';
playerDiv.innerHTML = `
<div class="wi3-content">
<video id="wi3Video" controls preload="none" playsinline webkit-playsinline></video>
<div class="wi3-footer">
<span id="wi3Tips">⌛ initializing...</span>
<a id="wi3Dl" href="#" target="_blank" rel="noopener">⏬ download</a>
</div>
</div>`;
const video = playerDiv.querySelector('#wi3Video');
const tipsEl = playerDiv.querySelector('#wi3Tips');
const dlEl = playerDiv.querySelector('#wi3Dl');
let hls = null;
function setTips(text, color) {
tipsEl.innerText = text;
if (color) tipsEl.style.color = color;
}
// double-click left/right = -10s / +10s
let lastClick = 0;
video.addEventListener('click', (e) => {
const now = Date.now();
const dt = now - lastClick;
if (dt < 300 && dt > 30) {
const r = video.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
if (y > 0.9) { lastClick = now; return; }
e.preventDefault(); e.stopPropagation();
if (x < 0.5) video.currentTime = Math.max(0, video.currentTime - 10);
else video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
}
lastClick = now;
});
// ============================================================
// STAGE 1 — load an m3u8 into our player
// ============================================================
function loadStream(url) {
if (videoLoaded || !url) return false;
videoLoaded = true;
console.log('[wi3-AVJB] loading stream:', url);
dlEl.dataset.u = url;
dlEl.href = url; // mp4 tay; m3u8 se bi click handler chop lai tai truc tiep
if (/\.mp4(\?|#|\/|$)/i.test(url) && !/\.m3u8/i.test(url)) {
// direct mp4 (currentVideoURL fallback) — hls.js can't parse it
video.src = url;
video.addEventListener('loadedmetadata', () => {
setTips('✅ unlocked — click to play', '#2ed573');
stopLoop();
}, { once: true });
return true;
}
// ============================================================
// TAI TRUC TIEP (v1.6.5) — khong qua trang dich vu. HLS: doc
// playlist -> tai het segment GM_xhr (CORS-free) -> noi thanh
// 1 file (.ts / .mp4) -> luu bang a.download. Co progress.
// ============================================================
async function wi3SaveDirect() {
const gmBuf = async (u2) => {
const r = await gmRequest(u2, 'arraybuffer', 30000);
return r.response;
};
const fail = (m) => { setTips('❌ ' + m, '#ff4757'); console.warn('[wi3-dl]', m); };
try {
setTips('⬇ đọc playlist...', '#ffa502');
const u = dlEl.dataset.u || url;
if (!/\.m3u8(\?|$)/i.test(u)) { // khong phai HLS: link direct luu ngay
dlEl.href = u; return; // de browser xu ly binh thuong
}
let text = await gmRequest(u, 'text', 20000).then(r => r.response);
let listUrl = u;
// master playlist -> chon variant chat nhat (bandwidth cao nhat)
if (/^#EXT-X-STREAM-INF/m.test(text)) {
const lines = text.split(/\r?\n/);
let best = null, bestBw = -1;
for (let i = 0; i < lines.length - 1; i++) {
if (!/^#EXT-X-STREAM-INF/m.test(lines[i])) continue;
const bw = parseInt((lines[i].match(/BANDWIDTH=(\d+)/) || [])[1] || '0', 10);
const cand = lines[i + 1].trim();
if (cand && !cand.startsWith('#') && bw > bestBw) { bestBw = bw; best = cand; }
}
if (!best) throw new Error('master khong co variant');
listUrl = new URL(best, u).href;
text = await gmRequest(listUrl, 'text', 20000).then(r => r.response);
}
if (!/^#EXTM3U/m.test(text)) throw new Error('khong phai playlist hop le');
// lay segment (bo comment + tag), giu thu tu
const segs = [];
let mapUri = null;
text.split(/\r?\n/).forEach(ln => {
const s = ln.trim();
if (!s) return;
if (s.startsWith('#EXT-X-MAP')) {
const mm = s.match(/URI="([^"]+)"/); if (mm) mapUri = mm[1];
} else if (s && !s.startsWith('#')) {
segs.push(s);
}
});
if (!segs.length) throw new Error('playlist rong');
const parts = [];
let got = 0;
if (mapUri) {
const mu = new URL(mapUri, listUrl).href;
parts.push(await gmBuf(mu));
got++;
}
for (const s of segs) {
const su = new URL(s, listUrl).href;
const buf = await gmBuf(su);
if (buf && buf.byteLength) { parts.push(buf); got++; }
setTips('⬇ ' + got + '/' + segs.length + ' segment (' +
Math.round(got / segs.length * 100) + '%) ' +
Math.round(parts.reduce((a, b) => a + (b.byteLength || b), 0) / 1048576) + ' MB', '#ffa502');
}
if (!parts.length) throw new Error('tai segment that bai');
const isMp4 = !!mapUri;
const blob = new Blob(parts, { type: isMp4 ? 'video/mp4' : 'video/mp2t' });
const name = ((document.title || 'video').replace(/[\\/:*?"<>|]/g, '_').slice(0, 90)) + (isMp4 ? '.mp4' : '.ts');
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 60000);
setTips('✅ đã tải: ' + name + ' (' + Math.round(blob.size / 1048576) + ' MB) — mở bằng VLC/mpv', '#2ed573');
} catch (e) { fail('tải lỗi: ' + (e && e.message || e)); }
}
dlEl.onclick = (ev) => {
const cu = dlEl.dataset.u || '';
if (!/\.m3u8(\?|$)/i.test(cu)) return true; // mp4: link direct, browser luu
ev.preventDefault();
wi3SaveDirect();
return false;
};
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
if (hls) hls.destroy();
const crossOrigin = (() => { try { return new URL(url, location.href).hostname !== location.hostname; } catch (e) { return false; } })();
hls = new Hls({
manifestLoadingTimeOut: 20000,
manifestLoadingMaxRetry: 4,
levelLoadingTimeOut: 20000,
levelLoadingMaxRetry: 4,
xhrSetup: (xhr) => { xhr.withCredentials = false; },
enableWorker: true,
// mirror không có CORS header + chặn hotlink → đọc qua GM
loader: (crossOrigin && GM_XHR) ? GmLoader : Hls.DefaultConfig.loader,
});
hls.on(Hls.Events.MANIFEST_LOADING, () => setTips('⌛ loading manifest...'));
hls.on(Hls.Events.MANIFEST_PARSED, () => {
setTips('✅ unlocked — click to play', '#2ed573');
stopLoop();
});
hls.on(Hls.Events.ERROR, (_e, data) => {
if (!data.fatal) return;
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError(); return; }
setTips('❌ ' + (data.details || data.type), '#ff4757');
videoLoaded = false; // let the waterfall continue with next source
kick();
});
hls.loadSource(url);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
video.addEventListener('loadedmetadata', () => {
setTips('✅ unlocked — click to play', '#2ed573');
stopLoop();
}, { once: true });
}
return true;
}
// ============================================================
// STAGE 2a — ACCOUNT: detect session -> auto-login -> unlock
// (video mới = reg-wall: trang chỉ nhả full URL khi đã đăng nhập)
// ============================================================
const CFG = { USER: '', PASS: '' }; // ← muốn hardcode tk thì điền 2 dòng này
const accState = { tried: false, ok: false, fpN: 0 };
function looksMember(h) {
return /\/(logout|my-profile|my-settings|my-account|dashboard)(\/|["'#?])/.test(h);
}
// chẩn đoán: cái gì trong HTML match là "member" + trạng thái tường
function fingerprint(label, h) {
if (accState.fpN >= 2) return;
accState.fpN++;
try {
const mm = h.match(/\/(logout|my-profile|my-settings|my-account|dashboard)(\/|["'#?])/);
const ctx = mm ? h.substr(Math.max(0, mm.index - 70), 160).replace(/\s+/g, ' ') : '-';
const pwTitle = (h.match(/class="pw-title"[^>]*>([^<]{0,100})/) || [])[1] || '-';
const files = (h.match(/file\s*[:=]\s*["'][^"']+["']/g) || []).slice(0, 4);
const sm3u8 = (h.replace(/\\\//g, '/').match(/https?:[^"'\\\s]+index\.m3u8\?[^"'\\\s]+/) || [])[0];
const loginLink = /href="[^"]*\/login\/"/.test(h);
console.log('[wi3-AVJB] FP(' + label + ') len=' + h.length +
' member=' + looksMember(h) + ' loginLink=' + loginLink +
' paywall=' + (h.match(/paywall/g) || []).length +
' token=' + /CSRF/i.test(h) + ' curURL=' + /currentVideoURL/.test(h));
console.log('[wi3-AVJB] FP memberMatch:', mm ? mm[0] : null, '| ctx:', ctx);
console.log('[wi3-AVJB] FP pwTitle:', pwTitle, '| fileUrls:', files, '| signedM3u8:', sm3u8 || null);
if (label === 'fresh' && pwTitle !== '-') setTips('⚠ tường đang giữ: ' + pwTitle, '#ffa502');
} catch (e) { console.warn('[wi3-AVJB] fp error', e); }
}
function getCreds() {
if (CFG.USER && CFG.PASS) return { u: CFG.USER, p: CFG.PASS };
try {
const u = GM_getValue('avjb_user', '');
const p = GM_getValue('avjb_pass', '');
if (u && p) return { u, p };
} catch (e) {}
return null;
}
function askCreds(force) {
try {
if (!force && sessionStorage.getItem('wi3_asked')) return null;
sessionStorage.setItem('wi3_asked', '1');
const u = prompt('AVJB — username hoặc email:');
if (!u) return null;
const p = prompt('AVJB — password:');
if (!p) return null;
try { GM_setValue('avjb_user', u); GM_setValue('avjb_pass', p); } catch (e) {}
console.log('[wi3-AVJB] creds saved for', u);
return { u, p };
} catch (e) { return null; }
}
async function ensureLogin(html) {
if (accState.tried) return accState.ok;
accState.tried = true;
fingerprint('live', html);
if (looksMember(html)) { // đã đăng nhập sẵn trong browser
accState.ok = true;
console.log('[wi3-AVJB] member session detected — skip login');
setTips('✅ session detected — unlocking...', '#2ed573');
return true;
}
let c = getCreds();
if (!c) {
setTips('⚠ cần tài khoản avjb — đang hỏi...', '#ffa502');
c = askCreds(false);
}
if (!c) {
setTips('⚠ nhập tk qua menu Tampermonkey 🍅', '#ffa502');
return false;
}
setTips('⌛ logging in as ' + c.u + '...');
try {
const body = new URLSearchParams({
username: c.u, pass: c.p, remember_me: '1',
action: 'login', email_link: location.origin + '/email/'
}).toString();
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 10000);
const res = await fetch('/login/', {
method: 'POST', credentials: 'same-origin', cache: 'no-store',
signal: ctrl.signal,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
},
body: body
});
clearTimeout(timer);
const txt = await res.text();
console.log('[wi3-AVJB] login HTTP', res.status, '|', txt.slice(0, 200));
const m = txt.match(/<failure>[\s\S]*?type="([^"]+)"/) || txt.match(/"error"\s*:\s*"([^"]+)"/);
if (m) {
const err = m[1];
if (err === 'antispam' || err === 'captcha' || err === 'code') {
setTips('❌ login cần captcha — login tay 1 lần trong tab, script tự nhận session', '#ff4757');
} else if (err === 'not_found' || err === 'wrong_password' || err === 'invalid_password') {
setTips('❌ sai mật khẩu — sửa qua menu TM 🍅', '#ff4757');
} else {
setTips('❌ login lỗi: ' + err, '#ff4757');
}
return false;
}
accState.ok = true;
setTips('✅ login ok — unlocking...', '#2ed573');
return true;
} catch (e) {
setTips('❌ login: ' + (e && e.message), '#ff4757');
console.warn('[wi3-AVJB] login error:', e);
return false;
}
}
// ============================================================
// STAGE 2 — API: fresh CSRF -> spped.php -> try EVERY source
// (v2.11.0 only tried defaultKey; we walk the whole list)
// ============================================================
async function fetchText(url, ms) {
console.log('[wi3-AVJB] fetchText:', url);
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { credentials: 'same-origin', cache: 'no-store', signal: ctrl.signal });
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.text();
} finally { clearTimeout(t); }
}
// bất kỳ *CSRF* var nào cũng được — prefer PLAYER_CSRF, else dài nhất
function grabToken(h) {
const ms = [...h.matchAll(/([A-Za-z_]\w*CSRF\w*)\s*=\s*["']([^"']{20,})["']/g)];
if (!ms.length) return null;
const pref = ms.find(m => m[1] === 'PLAYER_CSRF');
ms.sort((a, b) => b[2].length - a[2].length);
return (pref || ms[0])[2];
}
let stealLastTry = 0;
async function stealToken() {
if (Date.now() - stealLastTry < 12000) return null; // throttle 12s
stealLastTry = Date.now();
setTips('⌛ trang paid thiếu token — lấy từ trang free...', '#ffa502');
for (const path of FREE_TOKEN_PAGES) {
try {
const freeHtml = await fetchText(path, 7000);
const tok = grabToken(freeHtml);
console.log('[wi3-AVJB] steal free page', path, '->', tok ? 'token OK' : 'no token');
if (!tok) continue;
// "quay lại" trang paid — server set video hiện tại là trang này
try {
console.log('[wi3-AVJB] steal DEBUG — back-nav fetch starting:', location.href);
const back = await fetchText(location.href, 7000);
console.log('[wi3-AVJB] steal DEBUG — back-nav done, len=' + (back || '').length);
const paidTok = grabToken(back);
if (paidTok) {
console.log('[wi3-AVJB] token appeared after back-nav — dùng token paid');
return paidTok;
}
} catch (e) {
console.warn('[wi3-AVJB] back-nav refetch failed:', e && e.message);
}
return tok;
} catch (e) {
console.warn('[wi3-AVJB] steal fail:', path, e && e.message);
}
}
return null;
}
// nguồn từ video khác (get_file /126000/126278/...) → không bấm nhầm
function urlMatchesVideo(u, folder, vid) {
if (u.includes('/' + vid + '/') || u.includes('/' + vid + '_') ||
u.includes('/' + folder + '/') || u.includes(vid + 'video') ||
u.includes(vid + '_')) return true;
const m = u.match(/\/(\d{3,7})\/(\d{4,7})\//);
if (m) return +m[1] === folder || +m[2] === vid;
return true;
}
async function playSources(data, strict) {
if (!data || !Array.isArray(data.sources) || !data.sources.length) return false;
const vid = idFromPath();
const folder = vid ? Math.floor(vid / 1000) * 1000 : 0;
const ordered = [...data.sources].sort((a, b) =>
(b.key === data.defaultKey) - (a.key === data.defaultKey));
for (const src of ordered) {
if (!src || !src.url) continue;
if (strict && vid && !urlMatchesVideo(src.url, folder, vid)) {
console.log('[wi3-AVJB] skip other-video source:', src.url);
continue;
}
console.log('[wi3-AVJB] trying source key:', src.key);
if (loadStream(src.url)) return true;
await new Promise(r => setTimeout(r, 2500));
if (videoLoaded) return true;
}
return false;
}
async function apiBypass() {
setTips('⌛ extracting token...');
// token lives in inline <script>s — read from live DOM, zero network.
// (Refetching the full page used to hang behind streaming connections.)
let raw = document.documentElement.innerHTML || '';
let token = grabToken(raw);
if (!token) {
const texts = [];
document.querySelectorAll('script').forEach(s => { if (s.textContent) texts.push(s.textContent); });
token = grabToken(texts.join('\n'));
}
if (!token) {
// v1.2 — trang không token = reg-wall: tự detect session / auto-login trước
await ensureLogin(raw);
}
if (!token) {
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 6000);
const res = await fetch(location.href, {
cache: 'no-store', credentials: 'same-origin', signal: ctrl.signal
});
const fresh = await res.text();
clearTimeout(timer);
token = grabToken(fresh);
raw = fresh; // giữ HTML mới cho stage 2b/2c
fingerprint('fresh', fresh);
if (token) {
console.log('[wi3-AVJB] token found after login/refetch');
} else if (accState.ok && !looksMember(fresh)) {
setTips('❌ login chưa có hiệu lực — login tay rồi F5', '#ff4757');
}
} catch (e) {
console.warn('[wi3-AVJB] refetch failed:', e && e.message);
}
}
// v1.3.0 — vẫn không token? ăn cắp từ trang free rồi quay lại (cách của LO)
let stolen = false;
if (!token) {
token = await stealToken();
stolen = !!token;
if (token) console.log('[wi3-AVJB] stolen token from free page');
}
if (token) {
console.log('[wi3-AVJB] CSRF ok — calling spped.php');
setTips('⌛ requesting sources...');
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 8000);
const res = await fetch('/player/spped.php?csrf=' + encodeURIComponent(token),
{ credentials: 'same-origin', cache: 'no-store', signal: ctrl.signal });
clearTimeout(timer);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (await playSources(data, stolen)) return true;
} catch (e) {
console.warn('[wi3-AVJB] api failed:', e);
}
} else {
console.warn('[wi3-AVJB] no CSRF token anywhere (refetch + steal) — direct fallback');
setTips('⚠ chưa có token — loop chậm chờ server cấp...', '#ffa502');
}
// STAGE 2b — the site's own no-CSRF path: direct file URL in page source
const dm = (raw.match(/currentVideoURL\s*=\s*["']([^"']+)["']/) || [])[1];
if (dm) {
const clean = dm.replace(/^function\/0\//, '');
if (/\.mp4|\.m3u8/.test(clean)) {
console.log('[wi3-AVJB] direct URL fallback:', clean);
if (loadStream(clean)) return true;
}
}
// STAGE 2c — trang đăng nhập có thể nhúng file thật thẳng vào Playerjs
const fmAll = raw.match(/file\s*[:=]\s*["']([^"']+)["']/g) || [];
for (const f of fmAll) {
const u = (f.match(/["']([^"']+)["']/) || [])[1];
if (!u || !/\.(mp4|m3u8)/i.test(u)) continue;
if (/_limt/i.test(u)) {
console.log('[wi3-AVJB] inline file = teaser (skip):', u);
continue;
}
console.log('[wi3-AVJB] inline file fallback:', u);
if (loadStream(u)) return true;
}
// STAGE 2d — signed m3u8 anywhere in page source
const sm = (raw.replace(/\\\//g, '/').match(/https?:[^"'\\\s]+index\.m3u8\?[^"'\\\s]+/) || [])[1];
if (sm) {
console.log('[wi3-AVJB] signed m3u8 in page:', sm);
if (loadStream(sm)) return true;
}
return false;
}
// ============================================================
// STAGE 2.5 — MIRROR FAST PATH (v1.2.2)
// 88newline + newbox nhả full HLS unsigned (chỉ cần Referer avjb.com
// — browser tự gửi khi cross-origin fetch). 2 URL song song, ~200ms.
// ============================================================
let mirrorLastTry = 0;
function mirrorProbe(videoID) {
if (!videoID || videoLoaded) return Promise.resolve(false);
const now = Date.now();
if (now - mirrorLastTry < 8000) return Promise.resolve(false);
mirrorLastTry = now;
const folder = Math.floor(videoID / 1000) * 1000;
let done = false;
console.log('[wi3-AVJB] mirror probe — id:', videoID, 'folder:', folder);
const jobs = MIRRORS.map(base => {
const url = `${base}/videos/${folder}/${videoID}/index.m3u8`;
const grab = GM_XHR
? gmRequest(url, 'text', 12000).then(r => ({ ok: r.status >= 200 && r.status < 300, text: r.response || '' }))
: fetch(url, { cache: 'no-store', credentials: 'omit' })
.then(r => r.ok ? r.text().then(t => ({ ok: true, text: t })) : ({ ok: false, text: '' }))
.catch(() => ({ ok: false, text: '' }));
return grab
.then(res => {
if (!res.ok || done || videoLoaded || !/^#EXTM3U/.test((res.text || '').trim())) return null;
done = true;
console.log('[wi3-AVJB] mirror hit:', url, '(' + res.text.length + ' bytes)');
loadStream(url);
return url;
})
.catch(() => null);
});
return Promise.all(jobs).then(() => done);
}
// ============================================================
// STAGE 3 — legacy CDN probe (HEAD-scan folder structure)
// ============================================================
function idFromPath() {
const m = location.pathname.match(/\/video\/(\d+)/);
if (m) return parseInt(m[1], 10);
const meta = document.querySelector('meta[property="og:image"]');
const hay = meta ? meta.content : '';
const p = hay.split('/');
for (let i = 0; i < p.length - 1; i++) {
if (/^\d+000$/.test(p[i]) && /^\d+$/.test(p[i + 1])) return parseInt(p[i + 1], 10);
}
return null;
}
function legacyProbe(videoID) {
if (!videoID || videoLoaded) return;
const folder = Math.floor(videoID / 1000) * 1000;
const cacheKey = 'wi3_date_' + videoID;
const cached = localStorage.getItem(cacheKey);
const jobs = [];
console.log('[wi3-AVJB] legacy probe — id:', videoID, 'folder:', folder);
SERVERS.forEach(base => {
const urls = [];
if (base.includes('bbmmic2.com')) {
if (cached) urls.push(`${base}/data/${cached}/${videoID}/hls/index.m3u8`);
PROBE_DATES.forEach(d => { if (d !== cached) urls.push(`${base}/data/${d}/${videoID}/hls/index.m3u8`); });
} else if (base.includes('imgclh.com')) {
urls.push(
`${base}/contents/videos/${folder}/${videoID}/index.m3u8`,
`${base}/contents/videos/${folder}/${videoID}/master.m3u8`,
`${base}/contents/videos_hls/${folder}/${videoID}/index.m3u8`,
`${base}/contents/hls/${folder}/${videoID}/index.m3u8`,
`${base}/videos/${folder}/${videoID}/index.m3u8`,
`${base}/hls/${folder}/${videoID}/index.m3u8`);
} else {
urls.push(`${base}/videos/${folder}/${videoID}/index.m3u8`);
}
urls.forEach(url => jobs.push(
fetch(url, { method: 'HEAD', cache: 'no-store' })
.then(r => {
if (r.ok && !videoLoaded) {
const d = url.match(/\/data\/(\d{8})\//);
if (d) { try { localStorage.setItem(cacheKey, d[1]); } catch (e) {} }
console.log('[wi3-AVJB] legacy hit:', url);
loadStream(url);
}
}).catch(() => {})
));
});
return Promise.all(jobs);
}
// ============================================================
// PAGE GATE — chi trang video (/vi/video/<id>/) moi duoc che/phau thuat.
// Trang listing (home/album/the loai...) chi dedupe + thumbnail.
// ============================================================
function isVideoPage() { return /\/video\/\d+/.test(location.pathname); }
// ============================================================
// PAYWALL NUKER
// ============================================================
function nuke(root) {
root = root || document;
if (!isVideoPage()) return; // khong che/kill/ inject tren listing
try {
const kill = (sel) => root.querySelectorAll(sel).forEach(el => el.remove());
if (root.getElementById) {
const l2 = root.getElementById('layer2');
if (l2) l2.remove();
}
kill('.paywall-guest, .paywall-v2, .no-player, .pw-headline, .pw-guest-actions, .pw-btn-primary, .pw-btn-secondary, .pw-title, .message');
root.querySelectorAll('*').forEach(el => {
if (el.id === 'wi3Player' || (el.closest && el.closest('#wi3Player'))) return;
if (el.children.length) return;
const t = (el.textContent || '').trim();
if (['Sign Up to Watch', 'Register Free Now', 'Already a member? Log In', 'Full video']
.some(s => t === s) || t.includes('Get 5 Free Watches')) {
let n = el;
for (let i = 0; i < 6 && n.parentElement; i++) {
const c = typeof n.className === 'string' ? n.className : '';
if (/paywall|no-player|message/.test(c)) { n.remove(); break; }
n = n.parentElement;
}
}
});
} catch (e) {}
// an player site goc (re-inject sau khi wipe holder) — chi giu wi3 player
try {
root.querySelectorAll('video').forEach(v => {
if (v === video || (v.closest && v.closest('#wi3Player'))) return;
v.style.setProperty('display', 'none', 'important');
try { v.pause(); } catch (e) {}
let n = v, hide = v;
for (let i = 0; i < 4 && n.parentElement; i++) {
n = n.parentElement;
if (n === document.body || n.contains(playerDiv)) break;
const cls = (typeof n.className === 'string' ? n.className : '') + ' ' + (n.id || '');
if (/player|video|stream|holder|html5|media/i.test(cls)) hide = n;
}
if (hide !== v && !hide.contains(playerDiv)) {
hide.style.setProperty('display', 'none', 'important');
}
});
} catch (e) {}
if (root === document) injectPlayer();
}
function injectPlayer() {
if (!isVideoPage()) return; // gate: chi video page moi chen
playerDiv.className = 'wi3-inline';
const holder = document.querySelector('.player-holder');
if (holder) {
// KHONG wipe holder (site js se wipe lai -> dau rang -> mat ca 2).
// An holder di, chet player ra TRUOC holder. Reposition: luc DOM chua
// render no da body-prepend len tren header -> holder xuat h thi doi xuong dung cho.
holder.style.setProperty('display', 'none', 'important');
if (playerDiv.nextElementSibling !== holder) {
holder.parentNode.insertBefore(playerDiv, holder);
if (!videoLoaded) setTips('⌛ waiting for stream...');
}
return;
}
// holder chua render: nao co anchor thi chen truoc anchor,
// DOM con trong thi cho lan sau — KHONG prepend len body (loi player treo tren header)
const anchor = document.querySelector('.video-info, .video-title, .entry-content, h1, main, article');
if (anchor && anchor.parentNode) {
if (playerDiv.nextElementSibling !== anchor) {
anchor.parentNode.insertBefore(playerDiv, anchor);
if (!videoLoaded) setTips('⌛ waiting for stream...');
}
return;
}
// parse xong ma khong holder/anchor (reg-wall hang le) → fallback cuoi
if (!playerDiv.isConnected && document.readyState !== 'loading') {
document.body.prepend(playerDiv);
if (!videoLoaded) setTips('⌛ waiting for stream...');
}
}
// ============================================================
// DEDUPE — an video trung lap tren grid (ban goc + "Quang ba" cung id)
// ============================================================
function dedupeVideos() {
try {
const anchors = document.querySelectorAll('a[href*="/video/"]');
const byId = new Map(); // videoId -> card giu lai
const done = new Set(); // card da xu ly
anchors.forEach(a => {
const m = ((a.getAttribute('href') || '').split(/[?#]/)[0]).match(/\/video\/(\d+)/);
if (!m) return;
const key = m[1];
// leo tim "card": node gan nhat chua dung 1 video rieng
let n = a, card = null;
for (let i = 0; i < 5 && n.parentElement && n.parentElement !== document.body; i++) {
const p = n.parentElement;
const ids = new Set();
p.querySelectorAll('a[href*="/video/"]').forEach(x => {
const mm = ((x.getAttribute('href') || '').split(/[?#]/)[0]).match(/\/video\/(\d+)/);
if (mm) ids.add(mm[1]);
});
if (ids.size > 1) break;
n = p;
if (ids.size === 1) card = p;
}
if (!card || done.has(card)) return;
done.add(card);
if (byId.has(key)) {
const keep = byId.get(key);
if (keep === card || keep.contains(card) || card.contains(keep)) return;
card.style.setProperty('display', 'none', 'important');
} else {
byId.set(key, card);
}
});
} catch (e) {}
}
function setupNuker() {
if (window.__wi3Nuker) return;
window.__wi3Nuker = true;
if (!document.body) return;
new MutationObserver(() => nuke()).observe(document.body, { childList: true, subtree: true });
setInterval(() => nuke(), 500);
document.addEventListener('click', (e) => {
const t = e.target;
if (t.closest && t.closest('.paywall-v2, .paywall-guest, .no-player, .pw-btn-primary, .pw-btn-secondary, .pw-guest-actions')) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
}
}, true);
}
// ============================================================
// THUMBNAILS
// ============================================================
function fixThumbs() {
document.querySelectorAll('a[href^="/video/"] img').forEach(img => {
if (img.src && !/placeholder|blank|lazy/.test(img.src)) return;
const link = img.closest('a[href^="/video/"]');
const id = link && link.href.split('/video/')[1].split('/')[0];
if (!id) return;
const folder = Math.floor(parseInt(id, 10) / 1000) * 1000;
img.src = `https://bmc2.imgclh.com/contents/videos_screenshots/${folder}/${id}/preview.jpg`;
});
document.querySelectorAll('img').forEach(img => {
img.style.opacity = '1';
img.style.visibility = 'visible';
img.loading = 'eager';
img.classList.remove('lazy', 'lazyload', 'b-lazy');
});
}
// ============================================================
// MAIN WATERFALL — captured -> api -> legacy, retries until life
// ============================================================
async function attempt() {
if (videoLoaded || isChecking) return;
// chi trang video co id — reg-wall cung van la /vi/video/<id>/ (holder khong can thiet)
if (!isVideoPage()) return;
isChecking = true;
setupNuker();
injectPlayer();
fixThumbs();
try {
if (capturedUrl) {
setTips('⌛ using captured stream...');
loadStream(capturedUrl);
if (videoLoaded) { apiFails = 0; return; }
}
// v1.2.2 — mirror unsigned HLS trước, không cần login/CSRF
if (!videoLoaded) {
setTips('⌛ probing mirrors...');
if (await mirrorProbe(idFromPath())) { apiFails = 0; return; }
}
if (!videoLoaded && await apiBypass()) { apiFails = 0; return; }
if (!videoLoaded) {
apiFails++;
setTips('⚠ api failed ×' + apiFails + ' — chờ server cấp token rồi thử lại...', '#ff4757');
// legacy: rare — CDNs 404 on new videos and send no CORS headers
if (apiFails >= 2 && loopN % 20 === 0) await legacyProbe(idFromPath());
}
} finally {
isChecking = false;
loopN++;
// v1.3.0 — fail nhiều lần = server cần thời gian ("chờ 1 lúc"):
// chuyển sang loop chậm 5s thay vì spam mỗi 1.5s
const want = (!videoLoaded && apiFails >= 3) ? 5000 : 1500;
if (want !== loopMs) setLoopMs(want);
}
}
function setLoopMs(ms) {
loopMs = ms;
if (loopTimer) clearInterval(loopTimer);
loopTimer = setInterval(attempt, ms);
console.log('[wi3-AVJB] loop interval ->', ms + 'ms');
}
function kick() {
if (!loopTimer) loopTimer = setInterval(attempt, loopMs);
attempt();
}
function stopLoop() {
if (loopTimer) { clearInterval(loopTimer); loopTimer = null; }
}
// ============================================================
// STYLE
// ============================================================
GM_addStyle(`
img { opacity: 1 !important; visibility: visible !important; }
.thumb, .video-thumb { background: none !important; }
.paywall-guest, .paywall-v2, .no-player, .pw-headline, .pw-guest-actions,
.pw-btn-primary, .pw-btn-secondary, .pw-title, .message {
display: none !important; visibility: hidden !important; opacity: 0 !important;
pointer-events: none !important; height: 0 !important; width: 0 !important;
position: absolute !important; left: -99999px !important; top: -99999px !important;
}
#wi3Player.wi3-inline {
position: relative !important; width: 100% !important; max-width: 100vw !important;
background: #000; z-index: 999999; display: block !important;
visibility: visible !important; opacity: 1 !important;
}
#wi3Player .wi3-content { display: flex; flex-direction: column; width: 100%; }
#wi3Player #wi3Video { width: 100%; max-height: 56.25vw; aspect-ratio: 16/9; background: #000; display: block; }
#wi3Player .wi3-footer { display: flex; justify-content: space-between; align-items: center;
padding: 8px 12px; background: #1a1a1a; font-size: 13px; font-family: Arial, sans-serif; }
#wi3Tips { color: #ccc; }
#wi3Dl { color: #ff4757; text-decoration: none; font-weight: bold; font-size: 13px; white-space: nowrap; }
#wi3Dl:hover { color: #ff6b81; text-decoration: underline; }
`);
// ============================================================
// BOOT
// ============================================================
function boot() {
if (!document.body) { setTimeout(boot, 10); return; }
console.log('[wi3-AVJB] v1.3.4 active on', location.pathname);
try {
GM_registerMenuCommand('Nhập / sửa tài khoản avjb', function () {
try { sessionStorage.removeItem('wi3_asked'); } catch (e) {}
accState.tried = false; accState.ok = false;
apiFails = 0;
const c = askCreds(true);
if (c) { alert('Đã lưu tài khoản: ' + c.u); attempt(); }
});
GM_registerMenuCommand('Xóa tài khoản đã lưu', function () {
try { GM_setValue('avjb_user', ''); GM_setValue('avjb_pass', ''); } catch (e) {}
alert('Đã xóa tài khoản đã lưu.');
});
GM_registerMenuCommand('Lưu HTML trang (debug) — drop vào D:\\ccac', function () {
fetch(location.href, { cache: 'no-store', credentials: 'same-origin' })
.then(r => r.text())
.then(t => {
const blob = new Blob([t], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'avjb_page.html';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
})
.catch(e => alert('Lỗi tải HTML: ' + e.message));
});
} catch (e) {}
setupNuker();
fixThumbs();
dedupeVideos();
setInterval(dedupeVideos, 1500);
kick();
new MutationObserver(() => setTimeout(fixThumbs, 100))
.observe(document.body, { childList: true, subtree: true });
let st;
window.addEventListener('scroll', () => {
clearTimeout(st); st = setTimeout(fixThumbs, 150);
}, { passive: true });
}
boot();
} else if (_wi3Site === 'javhd') {
// ==================== JAVHD half ====================
// ============================================================
// CDN MAP
// v1.0.0: c1 + c4 (cdnjhd / cdnjav) serve the FULL mp4 with NO
// signature prefix and NO Referer requirement. c3/c2 require the
// `<token>,<epoch>` path prefix the site mints for members, c5
// answers 200 on everything with Content-Length: 0 (junk), the
// rest 404. Accept-Ranges: bytes + Access-Control-Allow-Origin: *
// so the plain <video> seeks natively.
// ============================================================
const HOSTS = [
'https://c1.cdnjhd.com',
'https://c4.cdnjhd.com',
'https://c1.cdnjav.com',
'https://c4.cdnjav.com',
];
// suffix -> label, ordered best-first
const QUALITIES = [
{ q: '_sh', label: '1080p', res: 1080 },
{ q: '_hq', label: '720p', res: 720 },
{ q: '_med', label: '480p', res: 480 },
{ q: '_low', label: '240p', res: 240 },
];
const GM_XHR = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;
function gmHead(url, timeout) {
return new Promise((resolve) => {
if (!GM_XHR) { resolve(null); return; }
try {
GM_XHR({
method: 'HEAD',
url: url,
timeout: timeout || 12000,
headers: { 'Referer': location.origin + '/' },
onload: (r) => resolve(r),
onerror: () => resolve(null),
ontimeout: () => resolve(null),
onabort: () => resolve(null),
});
} catch (e) { resolve(null); }
});
}
function gmGet(url, responseType, timeout) {
return new Promise((resolve, reject) => {
if (!GM_XHR) { reject(new Error('no GM_xmlhttpRequest')); return; }
try {
GM_XHR({
method: 'GET',
url: url,
responseType: responseType || 'text',
timeout: timeout || 15000,
headers: { 'Referer': location.origin + '/' },
onload: (r) => resolve(r),
onerror: () => reject(new Error('gm http error: ' + url)),
ontimeout: () => reject(new Error('gm timeout: ' + url)),
onabort: () => reject(new Error('gm abort: ' + url)),
});
} catch (e) { reject(e); }
});
}
// ============================================================
// STATE
// ============================================================
let videoLoaded = false;
let isChecking = false;
let loopTimer = null;
let loopMs = 2000;
let failN = 0;
let loopN = 0;
let currentRes = 0;
let lastVid = null; // SPA navigation: detect "we're on a different video now"
// SLOT LOCK (v1.4.3) — this JAVHD block ALSO ships as the standalone
// javhd-bypass.user.js script. Both @match javhd.com with separate
// sandboxes, so window.__wi3JavhdNuker does NOT stop the other copy:
// both inject #wi3Player -> two players on screen, and each script's
// MutationObserver re-inserts whatever the other removed (thrash).
// Claim the slot through the PAGE window, which every instance shares;
// the loser stands down instead of fighting.
const PAGE_WIN = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
const MY_SLOT = {};
function claimSlot() {
try {
const owner = PAGE_WIN.__wi3JavhdSlot;
if (owner && owner.token !== MY_SLOT && owner.el && owner.el.isConnected) {
// lost the slot — if we already injected (boot race), retract so
// exactly one player survives; our observer will retry and stand
// down here on every pass.
if (playerDiv.isConnected) playerDiv.remove();
return false;
}
PAGE_WIN.__wi3JavhdSlot = { token: MY_SLOT, el: playerDiv };
return true;
} catch (e) { return true; } // no page window access (safe mode) -> old behavior
}
// ============================================================
// PAGE HELPERS
// ============================================================
// Two live URL families: /en/id/<n>/<slug> and /en/studio/room/<x>/video/<slug>.
// The studio slug carries no id — videoIdFromDom() reads content-id from the DOM.
function isVideoPage() {
return /\/id\/\d+/.test(location.pathname)
|| /\/studio\/room\/[^/]+\/video\//.test(location.pathname);
}
function videoIdFromDom() {
const pc = document.querySelector('player-component');
if (pc) {
const cid = pc.getAttribute('content-id');
if (cid && /^\d+$/.test(cid)) return cid;
const cp = pc.getAttribute('content-path') || '';
const m = cp.match(/(?:videoId=|\/player\/)(\d+)/);
if (m) return m[1];
}
const m = location.pathname.match(/\/id\/(\d+)/);
if (m) return m[1];
// v1.2.0 — hydrate removes <player-component> and studio/room pages
// carry no /id/<n> in the path — pull the id from the inline
// globicaParams (pageid: '<n>') / the join link (cms_id=<n>).
try {
const H = document.documentElement.innerHTML;
const g = H.match(/pageid:\s*'(\d+)'/) || H.match(/cms_id=(\d+)/);
if (g) return g[1];
} catch (e) {}
return null;
}
function contentPathFromDom() {
const pc = document.querySelector('player-component');
return pc ? (pc.getAttribute('content-path') || '') : '';
}
// poster may live in the component's `:empty-player` binding (guest
// pages) or only in the player JSON (free/premiumFree pages, which
// ship no empty-player at all).
// v1.2.0 — after hydration the site replaces <player-component> with a
// plain div.player whose poster moved into the CSS var --playerPoster
// (and the <img class="jplayer__poster-img">). Probe all three.
function posterFromDom() {
const pc = document.querySelector('player-component');
if (pc) {
const blob = pc.outerHTML || '';
const m = blob.match(/poster:\s*['"]([^'"]+)['"]/);
if (m) return m[1].replace(/\\\//g, '/');
const m2 = blob.match(/poster=["']([^"']+)["']/);
if (m2) return m2[1];
}
const cont = document.querySelector('.jplayer__player-container, .player__container, .player');
if (cont) {
const st = cont.getAttribute('style') || '';
const m3 = st.match(/--playerPoster:\s*url\(([^)]+)\)/);
if (m3) return m3[1].trim().replace(/^['"]|['"]$/g, '');
}
const img = document.querySelector('.jplayer__poster-img, .player__poster img, .jplayer__poster img');
if (img && img.src) return img.src;
return '';
}
function posterFromApi(vid) {
const endpoints = [
'/en/player_api?videoId=' + vid + '&is_trailer=1',
'/en/player/' + vid + '?is_trailer=1',
];
// content-path already tells us which family the page uses — try it first
const cp = contentPathFromDom();
if (cp.indexOf('player_api') >= 0) endpoints.reverse();
return (async () => {
for (let i = 0; i < endpoints.length; i++) {
try {
const r = await gmGet(location.origin + endpoints[i], 'text', 10000);
const t = String(r && r.responseText || r && r.response || '');
const m = t.match(/"poster"\s*:\s*"([^"]+)"/);
if (m) return m[1].replace(/\\\//g, '/');
} catch (e) {}
}
return '';
})();
}
// poster forms:
// .../content-01/thumbs/<slug>-p/images/940x530/146s.jpg -> strip -p
// .../content-01/thumbs/<slug>/thumbs/thumb1/.../t1.jpg -> keep as-is
// We probe BOTH the stripped and the raw segment, so an honest slug
// that happens to end in "-p" still resolves.
function slugCandidates(poster) {
if (!poster) return [];
const m = poster.match(/^(https?:\/\/[^/]+)\/([^/]+)\/thumbs\/([^/?#]+)/);
if (!m) return [];
const origin = m[1];
const root = m[2]; // content-01 (etc.)
let seg = decodeURIComponent(m[3]);
const out = [];
if (seg.length > 2 && seg.slice(-2) === '-p') {
out.push({ origin: origin, root: root, slug: seg.slice(0, -2) });
}
out.push({ origin: origin, root: root, slug: seg });
return out;
}
// GM exposes response headers as a single blob (`responseHeaders` in
// Tampermonkey/Violentmonkey; some builds call it `headers`).
function hdrLen(r) {
const blob = String((r && (r.responseHeaders || r.headers)) || '');
const m = blob.match(/content-length:\s*(\d+)/i);
return m ? parseInt(m[1], 10) : 0;
}
// ============================================================
// PROBE — HEAD the first quality; if it answers with a real body,
// HEAD the remaining qualities on that same host+slug.
// ============================================================
async function probeHostSlug(hint) {
const base = `${hint.origin}/${hint.root}/contents/${hint.slug}/videos/${hint.slug}`;
const first = await gmHead(base + '_hq.mp4');
if (!first || first.status < 200 || first.status >= 300) return null;
const cl = hdrLen(first);
if (!cl) return null; // c5-style empty 200
const found = await Promise.all(QUALITIES.map(async (Q) => {
if (Q.q === '_hq') return { label: Q.label, res: Q.res, size: cl, url: base + Q.q + '.mp4' };
const r = await gmHead(base + Q.q + '.mp4');
if (!r || r.status < 200 || r.status >= 300) return null;
const len = hdrLen(r);
if (!len) return null;
return { label: Q.label, res: Q.res, size: len, url: base + Q.q + '.mp4' };
}));
const list = found.filter(Boolean);
if (!list.length) return null;
console.log('[wi3-JAVHD] unsigned CDN hit:', hint.origin, hint.slug, list.length + ' qualities');
return list;
}
async function unsignedSources(poster) {
const cands = slugCandidates(poster);
for (let i = 0; i < cands.length; i++) {
for (let h = 0; h < HOSTS.length; h++) {
const hit = await probeHostSlug({ origin: HOSTS[h], root: cands[i].root, slug: cands[i].slug });
if (hit) return hit;
// same bucket, other host — keep origin fixed to the CDN the
// poster itself came from only as a last try
}
}
// poster origin last (it's the host the site already trusts)
for (let i = 0; i < cands.length; i++) {
const hit = await probeHostSlug(cands[i]);
if (hit) return hit;
}
return null;
}
// ============================================================
// FALLBACK 1 — /en/player_api?videoId=X (no trailer flag)
// Full signed sources, but ONLY for free / premiumFree rows;
// premium rows answer `[]`.
// ============================================================
async function playerApiSources(vid) {
try {
const r = await gmGet(location.origin + '/en/player_api?videoId=' + vid, 'text', 10000);
const raw = String(r && r.responseText || r && r.response || '').replace(/\\\//g, '/');
if (raw.indexOf('"src"') < 0) return null;
const srcs = [];
const re = /"label"\s*:\s*"([^"]+)"[^}]*?"res"\s*:\s*"?(\d+)"?[^}]*?"src"\s*:\s*"([^"]+)"/g;
let m;
while ((m = re.exec(raw)) !== null) {
const u = m[3];
if (/black_cap\.mp4$/i.test(u)) continue; // guest placeholder
srcs.push({ label: m[1], res: parseInt(m[2], 10), url: u });
}
if (!srcs.length) return null;
srcs.sort((a, b) => b.res - a.res);
console.log('[wi3-JAVHD] player_api returned', srcs.length, 'sources');
return srcs;
} catch (e) { return null; }
}
// ============================================================
// FALLBACK 2 — 1-minute trailer (guest JSON from /en/player/<id>)
// ============================================================
async function trailerSources(vid) {
try {
const r = await gmGet(location.origin + '/en/player/' + vid + '?is_trailer=1', 'text', 10000);
const raw = String(r && r.responseText || r && r.response || '').replace(/\\\//g, '/');
if (raw.indexOf('black_cap.mp4') >= 0) return null;
const srcs = [];
const re = /"label"\s*:\s*"([^"]+)"[^}]*?"res"\s*:\s*"?(\d+)"?[^}]*?"src"\s*:\s*"([^"]+)"/g;
let m;
while ((m = re.exec(raw)) !== null) {
const u = m[3];
if (/black_cap\.mp4$/i.test(u)) continue;
srcs.push({ label: m[1], res: parseInt(m[2], 10), url: u });
}
if (!srcs.length) return null;
srcs.sort((a, b) => b.res - a.res);
console.log('[wi3-JAVHD] trailer-only:', srcs[0].url);
return srcs;
} catch (e) { return null; }
}
// ============================================================
// PLAYER DOM
// ============================================================
const playerDiv = document.createElement('div');
playerDiv.id = 'wi3Player';
playerDiv.innerHTML = `
<div class="wi3-content">
<video id="wi3Video" controls preload="metadata" playsinline webkit-playsinline></video>
<div class="wi3-footer">
<span id="wi3Tips">⌛ initializing...</span>
<span id="wi3Qual"></span>
<a id="wi3Dl" href="#" target="_blank" rel="noopener">⏬ download</a>
</div>
</div>`;
const video = playerDiv.querySelector('#wi3Video');
const tipsEl = playerDiv.querySelector('#wi3Tips');
const qualEl = playerDiv.querySelector('#wi3Qual');
const dlEl = playerDiv.querySelector('#wi3Dl');
let sourceSet = []; // [{label,res,url,size}]
let currentIndex = 0;
function setTips(text, color) {
tipsEl.innerText = text;
if (color) tipsEl.style.color = color;
}
function fmtSize(n) {
if (!n) return '';
if (n > 1e9) return (n / 1e9).toFixed(2) + ' GB';
if (n > 1e6) return (n / 1e6).toFixed(0) + ' MB';
return Math.round(n / 1e3) + ' KB';
}
function renderQual() {
if (!sourceSet.length) { qualEl.innerHTML = ''; return; }
qualEl.innerHTML = sourceSet.map((s, i) =>
`<button class="wi3-q${i === currentIndex ? ' on' : ''}" data-i="${i}">${s.label}</button>`
).join('');
}
qualEl.addEventListener('click', (e) => {
const b = e.target.closest('button[data-i]');
if (!b) return;
const i = parseInt(b.getAttribute('data-i'), 10);
if (i === currentIndex) return;
const t = video.currentTime;
const wasPlaying = !video.paused;
currentIndex = i;
applySource(true, t, wasPlaying);
});
function applySource(keepTime, at, autoplay) {
const s = sourceSet[currentIndex];
if (!s) return;
currentRes = s.res;
video.src = s.url;
dlEl.href = s.url;
dlEl.download = '';
renderQual();
const onMeta = () => {
if (keepTime && at && isFinite(at)) {
try { video.currentTime = Math.min(at, (video.duration || at) - 0.5); } catch (e) {}
}
if (autoplay) video.play().catch(() => {});
setTips('✅ unlocked — ' + s.label + (s.size ? ' · ' + fmtSize(s.size) : ''), '#2ed573');
};
video.addEventListener('loadedmetadata', onMeta, { once: true });
video.addEventListener('error', () => {
if (currentIndex < sourceSet.length - 1) {
currentIndex++;
setTips('⚠ ' + s.label + ' failed — falling back...', '#ff4757');
applySource(false, 0, false);
} else {
setTips('⚠ all qualities failed', '#ff4757');
}
}, { once: true });
}
function loadSources(list) {
if (!list || !list.length || videoLoaded) return false;
videoLoaded = true;
sourceSet = list;
currentIndex = 0;
applySource(false, 0, false);
stopLoop();
console.log('[wi3-JAVHD] player armed with', list.length, 'sources');
return true;
}
// double-click left/right = -10s / +10s
let lastClick = 0;
video.addEventListener('click', (e) => {
const now = Date.now();
const dt = now - lastClick;
if (dt < 300 && dt > 30) {
const r = video.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
if (y > 0.9) { lastClick = now; return; }
e.preventDefault(); e.stopPropagation();
if (x < 0.5) video.currentTime = Math.max(0, video.currentTime - 10);
else video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
}
lastClick = now;
});
// ============================================================
// PAYWALL NUKER — guest pages blur the poster behind a
// .player-join-cap ("Full video available only for members").
// ============================================================
// v1.4.5 — the join funnel: category cards on the home/listing pages link
// to /en/join?category_dir=... (the site's paywall). Rewrite them to the
// real listing URL so no click ever lands on a paywall.
function fixJoinLinks(root) {
// v1.4.6 — ANY internal join link (header Join, /en/join?cms_id= video
// CTAs, /en/join?category_dir= category cards, locale /vi/join ...) is a
// paywall hop: category ones go to the real listing, the rest bounce home.
try {
root.querySelectorAll('a[href*="/join"]').forEach(a => {
const h = a.getAttribute('href') || '';
if (!/^(\/|https?:\/\/[^\/]*javhd\.com\/)/.test(h) || h.indexOf('/join') < 0) return;
const cm = h.match(/category_dir=([\w\-]+)/);
a.setAttribute('href', cm
? 'https://javhd.com/en/japanese-porn-videos/category/' + cm[1]
: 'https://javhd.com/en/');
});
} catch (e) {}
}
// v1.4.5/1.4.6 — already ON a paywall? Swap it for real content before
// anything paints. Covers: javhd.com/en/join (rare — usually server-302s
// first), enter.javhd.com/signup?...category_dir= and the /track/ hop.
function joinRedirect() {
try {
if (!/(^|\.)javhd\.com$/.test(location.hostname)) return;
const cm = location.search.match(/category_dir=([\w\-]+)/);
if (cm) {
location.replace('https://javhd.com/en/japanese-porn-videos/category/' + cm[1]);
} else if (/\/join\/?$/.test(location.pathname) || /\/(signup|track)\//.test(location.pathname)) {
location.replace('https://javhd.com/en/');
}
} catch (e) {}
}
function nuke(root) {
root = root || document;
// v1.4.5 — the viewing-limit overlay (.player-caption-limit: "YOU HAVE
// REACHED YOUR VIEWING LIMITS" / "GET FULL VIDEO") and the join CTAs
// live on ANY page (home, category, video): run this pass site-wide.
// Only the site-player hiding below stays gated to video pages.
try {
const kill = (sel) => root.querySelectorAll(sel).forEach(el => el.remove());
// v1.2.0 — site renamed its player classes (player__* -> jplayer__*)
// so the old kill-list matched nothing; cover both generations.
// v1.4.5 — + .player-caption-limit and .player-join-cap (paywalls).
kill('.player-caption-limit, .player-join-cap, .player-join-cap__btn, .player__poster, .player__overlay-play,' +
' .jplayer__poster, .jplayer__overlay-play, .jplayer__overlay-button,' +
' .jplayer__big-play, .jplayer__ad, .player__overlay, .jplayer__notice');
root.querySelectorAll('*').forEach(el => {
if (el.id === 'wi3Player' || (el.closest && el.closest('#wi3Player'))) return;
if (el.children.length) return;
const t = (el.textContent || '').trim();
if (t === 'Join JAVHD' || t === 'Full video available only for members'
|| t === 'Sign up to watch' || t === 'Already a member? Log in'
|| t.indexOf('available only for members') >= 0
|| t.indexOf('YOU HAVE REACHED YOUR VIEWING LIMITS') >= 0) {
let n = el;
for (let i = 0; i < 6 && n.parentElement; i++) {
const c = typeof n.className === 'string' ? n.className : '';
if (/join-cap|caption-limit|poster|blurred|overlay|limit/i.test(c)) { n.remove(); break; }
n = n.parentElement;
}
}
});
} catch (e) {}
fixJoinLinks(root);
if (!isVideoPage()) return;
// hide the site's own player so ours is the only one on screen
try {
root.querySelectorAll('video').forEach(v => {
if (v === video || (v.closest && v.closest('#wi3Player'))) return;
// v1.4.3 — ONLY the site's main player box (and its ancestors)
// may be hidden. The old walk matched ANY ancestor whose class
// contained "video"/"container" — recommend cards live in
// .related-videos / .video-grid style wrappers, so the whole
// "Recommended for you" section got whitewashed along the way.
const box = v.closest('.player, player-component, .content__general--player,' +
' .jplayer__player-container, .player__container, .player__video');
if (!box) return; // card preview / ad / thumbnail video — leave it alone
v.style.setProperty('display', 'none', 'important');
try { v.pause(); } catch (e) {}
if (box !== v && !box.contains(playerDiv)) {
box.style.setProperty('display', 'none', 'important');
}
let n = box, hide = box;
for (let i = 0; i < 5 && n.parentElement; i++) {
n = n.parentElement;
if (n === document.body || n.contains(playerDiv)) break;
const cls = (typeof n.className === 'string' ? n.className : '') + ' ' + (n.id || '');
// player-specific only, and never a recommend/related/suggest wrapper
if (/player|stream|jplayer/i.test(cls) && !/recommend|related|suggest/i.test(cls)) hide = n;
}
// v1.2.0 — after hydrate the site's box sits INSIDE
// .content__general--player which ALSO holds our injected player
// (a sibling of .player). The old `hide.contains(playerDiv)`
// check failed, so hide() whitewashed OUR player too — the
// symptom "site player overlays our clone". Skip any ancestor
// that contains wi3Player; kill only the real site box.
if (hide !== v && !hide.contains(playerDiv)) {
hide.style.setProperty('display', 'none', 'important');
}
});
// pre-hydrate the component still exists (no <video> inside yet)
const pc = root.querySelector && root.querySelector('player-component');
if (pc && !pc.contains(playerDiv)) {
pc.style.setProperty('display', 'none', 'important');
}
// post-hydrate: plain div.player box has no <video> until click —
// hide the whole box directly so its poster/overlay never sits
// over our player.
const sp = root.querySelector && root.querySelector('.player');
if (sp && !sp.contains(playerDiv)) {
sp.style.setProperty('display', 'none', 'important');
}
} catch (e) {}
if (root === document) injectPlayer();
}
function injectPlayer() {
if (!isVideoPage()) return;
if (!claimSlot()) return; // other script instance already owns the slot
// v1.4.4 — sweep foreign copies: a stale TM update bundle can inject an
// old slot-unaware build alongside this one (2 players). We own the
// slot, so remove any #wi3Player that isn't ours; our nuke loop
// re-runs this pass so a re-inserted foreign copy gets swept again.
for (const el of document.querySelectorAll('#wi3Player')) {
if (el !== playerDiv) el.remove();
}
playerDiv.className = 'wi3-inline';
if (playerDiv.isConnected) {
// first inject can fire before Vue mounts .player — once the real
// box exists, walk our player into it (only before playback starts,
// so a running video is never reparented).
const box = document.querySelector('.content__general--player');
if (!videoLoaded && box && playerDiv.parentElement !== box) {
box.insertBefore(playerDiv, box.firstElementChild);
}
return;
}
// v1.2.0 — the player's real box lives inside .content__general--player;
// anchoring on bare h1/main pushed our player below the whole
// description block. Prefer the box, then the old anchors.
const anchor = document.querySelector('player-component, .content__general--player, .player,' +
' .video-info, .video-title, h1, main, article');
if (anchor && anchor.parentNode) {
if (anchor.classList && anchor.classList.contains('content__general--player')) {
anchor.insertBefore(playerDiv, anchor.firstElementChild);
} else {
anchor.parentNode.insertBefore(playerDiv, anchor);
}
if (!videoLoaded) setTips('⌛ waiting for sources...');
return;
}
if (document.body && document.readyState !== 'loading') {
document.body.prepend(playerDiv);
if (!videoLoaded) setTips('⌛ waiting for sources...');
}
}
function setupNuker() {
if (window.__wi3JavhdNuker) return;
window.__wi3JavhdNuker = true;
if (!document.body) return;
new MutationObserver(() => nuke()).observe(document.body, { childList: true, subtree: true });
setInterval(() => nuke(), 600);
document.addEventListener('click', (e) => {
const t = e.target;
// v1.2.0 — cover the new jplayer play overlay (the one the guest
// clicks that would otherwise mount the site's black_cap player).
if (t.closest && t.closest('.player-join-cap, .player-join-cap__btn,' +
' .jplayer__overlay-play, .jplayer__overlay-button, .jplayer__big-play')) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
}
}, true);
}
// ============================================================
// MAIN WATERFALL
// ============================================================
async function attempt() {
if (videoLoaded || isChecking) return;
if (!isVideoPage()) return;
const vid = videoIdFromDom();
if (!vid) return;
// SPA route change to a different video — drop the old state and start over
if (lastVid !== null && lastVid !== vid) {
videoLoaded = false; failN = 0; sourceSet = []; currentRes = 0;
try { video.removeAttribute('src'); video.load(); } catch (e) {}
}
lastVid = vid;
isChecking = true;
setupNuker();
injectPlayer();
try {
// 1. poster -> slug -> unsigned CDN (works for premium AND free)
setTips('⌛ probing CDN...');
let poster = posterFromDom();
if (!poster) poster = await posterFromApi(vid);
if (poster) {
const unsigned = await unsignedSources(poster);
if (unsigned && loadSources(unsigned)) { failN = 0; return; }
}
// 2. player_api (signed URLs, free/premiumFree rows only)
setTips('⌛ trying player_api...');
const api = await playerApiSources(vid);
if (api && loadSources(api)) { failN = 0; return; }
// 3. 1-minute trailer so the page is never dead
setTips('⌛ trying trailer...');
const tr = await trailerSources(vid);
if (tr && loadSources(tr)) {
setTips('⚠ trailer only — full source unavailable', '#ffa502');
failN = 0;
return;
}
failN++;
setTips('⚠ no source yet ×' + failN + ' — retrying...', '#ff4757');
} finally {
isChecking = false;
loopN++;
const want = (!videoLoaded && failN >= 3) ? 6000 : 2000;
if (want !== loopMs) setLoopMs(want);
}
}
function setLoopMs(ms) {
loopMs = ms;
if (loopTimer) clearInterval(loopTimer);
loopTimer = setInterval(attempt, ms);
console.log('[wi3-JAVHD] loop interval ->', ms + 'ms');
}
function kick() {
if (!loopTimer) loopTimer = setInterval(attempt, loopMs);
attempt();
}
function stopLoop() {
if (loopTimer) { clearInterval(loopTimer); loopTimer = null; }
}
// ============================================================
// STYLE
// ============================================================
GM_addStyle(`
.player-join-cap, .player-join-cap__btn, .player__poster, .player__overlay-play,
.jplayer__poster, .jplayer__overlay-play, .jplayer__big-play, .jplayer__ad {
display: none !important; visibility: hidden !important; opacity: 0 !important;
pointer-events: none !important; height: 0 !important; width: 0 !important;
position: absolute !important; left: -99999px !important; top: -99999px !important;
}
#wi3Player.wi3-inline {
position: relative !important; width: 100% !important; max-width: 100vw !important;
background: #000; z-index: 999999; display: block !important;
visibility: visible !important; opacity: 1 !important;
}
#wi3Player .wi3-content { display: flex; flex-direction: column; width: 100%; }
#wi3Player #wi3Video {
width: 100%; max-height: 70vh; aspect-ratio: 16/9; background: #000; display: block;
}
#wi3Player .wi3-footer {
display: flex; justify-content: space-between; align-items: center; gap: 12px;
padding: 8px 12px; background: #1a1a1a; font-size: 13px; font-family: Arial, sans-serif;
}
#wi3Tips { color: #ccc; }
#wi3Qual { display: flex; gap: 6px; }
#wi3Qual .wi3-q {
background: #2a2a2a; color: #ccc; border: 1px solid #444; border-radius: 3px;
padding: 3px 9px; font-size: 12px; cursor: pointer;
}
#wi3Qual .wi3-q.on { background: #ff4757; color: #fff; border-color: #ff4757; }
#wi3Qual .wi3-q:hover { border-color: #888; }
#wi3Dl {
color: #ff4757; text-decoration: none; font-weight: bold; font-size: 13px; white-space: nowrap;
}
#wi3Dl:hover { color: #ff6b81; text-decoration: underline; }
`);
// ============================================================
// BOOT
// ============================================================
function boot() {
// v1.4.5 — never show the join funnel (the site's paywall): a category
// request lands on the real listing, a bare join bounces home.
joinRedirect();
if (!document.body) { setTimeout(boot, 10); return; }
console.log('[wi3-JAVHD] v1.2.0 active on', location.pathname);
try {
GM_registerMenuCommand('JAVHD — force reload sources', function () {
videoLoaded = false; failN = 0; sourceSet = [];
try { video.removeAttribute('src'); video.load(); } catch (e) {}
setLoopMs(2000);
kick();
});
GM_registerMenuCommand('JAVHD — save page HTML (debug)', function () {
fetch(location.href, { cache: 'no-store', credentials: 'same-origin' })
.then(r => r.text())
.then(t => {
const blob = new Blob([t], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'javhd_page.html';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
})
.catch(e => alert('HTML fetch failed: ' + e.message));
});
} catch (e) {}
setupNuker();
kick();
// Vue re-renders can evict our player on SPA navigation — always watch
new MutationObserver(() => { if (!playerDiv.isConnected) injectPlayer(); })
.observe(document.body, { childList: true, subtree: true });
// SPA route change (pushState) — kick immediately instead of waiting
['pushState', 'replaceState'].forEach(function (m) {
const orig = history[m];
history[m] = function () {
const r = orig.apply(this, arguments);
try { window.dispatchEvent(new Event('wi3-route')); } catch (e) {}
return r;
};
});
window.addEventListener('popstate', function () { window.dispatchEvent(new Event('wi3-route')); });
window.addEventListener('wi3-route', function () { joinRedirect(); attempt(); });
}
boot();
} else if (_wi3Site === 'carib') {
// ============================================================
// CARIBBEANCOM / CARIBBEANCOMPR half — v1.5.1
// Verified live 07/10: /r/member playlists are public (any u=),
// but /hls/ FRAGMENTS return 401 without a member session token
// (u= comes from $.getScript(getauthjw7key) -> authjw7Key.u, i.e.
// D2Pass member login; commercial tools confirm the same gate).
// So the flow is gated:
// - member page (window.User set): wait for authjw7Key.u, take
// over the site videojs box with our player + quality switcher
// (real token -> full 240p..1080p ladder streams fine).
// - guest: probe ONE fragment once. 200 -> take over with the
// dummy token; 401/err -> leave the site player alone (trailer
// keeps working) and log the pending free-unlock status.
// ============================================================
const CARIB_BRAND = location.hostname.replace(/^(www|m|en|cn|de|fr|es|it|ru|ko|zh|tv)\./, '');
const CARIB_FMS = 'https://fms.caribbeancom.com';
const CARIB_LABELS = ['1080p', '720p', '480p', '360p', '240p'];
const C_GM = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;
// page world (window.User / authjw7Key live there, not in sandbox)
const C_PW = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
function caribId() {
const m = location.pathname.match(/\/moviepages\/([0-9]{6}-[0-9]{3})\//);
return m ? m[1] : null;
}
function caribMaster(id) {
return CARIB_FMS + '/r/member/' + CARIB_BRAND + '/' + id + '/mb.m3u8?u=' + encodeURIComponent(cToken);
}
let cDiv = null, cVideo = null, cHls = null, cQual = null, cTimer = null, cGmRetry = false;
let cToken = 'wi3'; // swapped for authjw7Key.u on member pages
// GM loader — same proven shape as the AVJB half. Fallback when the
// plain XHR path dies (Origin not whitelisted, e.g. a subdomain).
function GmLoader() {
this._aborted = false;
this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, end: 0 } };
}
GmLoader.prototype.load = function (context, config, callbacks) {
const self = this;
const url = context.url;
const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
self._aborted = false;
const t0 = performance.now();
const s = self.stats;
s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
s.parsing.start = 0; s.parsing.end = 0;
s.buffering.start = 0; s.buffering.end = 0;
const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
if (!C_GM) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
try {
C_GM({
method: 'GET', url: url, responseType: rt, timeout: 25000,
headers: { 'Referer': location.origin + '/' },
onload: (r) => {
const t1 = performance.now();
if (self._aborted || self.stats.aborted) return;
if (r.status >= 200 && r.status < 300) {
const data = r.response;
const len = (data && (data.byteLength || data.length)) || 0;
self.stats.loading.end = t1;
self.stats.loaded = len;
self.stats.total = len;
// xhr-loader calls onProgress before onSuccess —
// stream-controller needs it to transmux()
if (typeof callbacks.onProgress === 'function') {
callbacks.onProgress(self.stats, context, data, netStub);
}
callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
} else {
callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
}
},
onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
});
} catch (e) {
callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
}
};
GmLoader.prototype.abort = function () { this._aborted = true; this.stats.aborted = true; };
GmLoader.prototype.destroy = function () {};
function caribStatus(t) {
const s = cDiv && cDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
// levels sorted bandwidth-desc -> fixed ladder labels (the master
// playlist serves exactly 240/360/480/720/1080 when it has 5 entries)
function caribQualBuild() {
if (!cQual || !cHls) return;
let lv = (cHls.levels || []).map((l, i) => ({ i: i, b: l.bandwidth || 0 }));
if (!lv.length) return;
lv.sort((a, b) => b.b - a.b);
cQual.innerHTML = '';
lv.forEach((e, rank) => {
const label = (lv.length === CARIB_LABELS.length)
? CARIB_LABELS[rank]
: Math.round(e.b / 1000) + 'k';
const b = document.createElement('button');
b.className = 'wi3-q' + (rank === 0 ? ' on' : '');
b.textContent = label;
b.onclick = function () {
try { cHls.currentLevel = e.i; } catch (err) {}
Array.prototype.forEach.call(cQual.children, function (x) { x.classList.remove('on'); });
b.classList.add('on');
};
cQual.appendChild(b);
});
}
function caribCreate(id, useGm) {
try { if (cHls) { cHls.destroy(); cHls = null; } } catch (e) {}
if (typeof Hls === 'undefined' || !Hls.isSupported()) {
// no MSE (or @require failed) -> native HLS (Safari) or nothing
try { cVideo.src = caribMaster(id); caribStatus('native hls'); } catch (e) {}
return;
}
cHls = new Hls(useGm ? { startLevel: 0, loader: GmLoader } : { startLevel: 0 });
cHls.on(Hls.Events.MANIFEST_PARSED, function () {
caribQualBuild();
caribStatus('full video ready - ' + (cHls.levels ? cHls.levels.length : 0) + ' qualities');
});
cHls.on(Hls.Events.ERROR, function (ev, data) {
if (!data || !data.fatal) return;
if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !cGmRetry && C_GM && !useGm) {
// Origin not whitelisted for this subdomain -> GM xhr retry
cGmRetry = true;
caribStatus('retry via GM xhr...');
caribCreate(id, true);
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
try { cHls.recoverMediaError(); } catch (e) {}
} else {
caribStatus('error: ' + (data.details || 'fatal'));
}
});
cHls.loadSource(caribMaster(id));
cHls.attachMedia(cVideo);
}
// site player (videojs: #player_outer > #player > video#vjs-player)
// is guest-gated — hide it entirely, ours is the only player.
function caribNuke() {
try {
const box = document.getElementById('player_outer');
if (box && box !== cDiv) box.style.setProperty('display', 'none', 'important');
document.querySelectorAll('video.video-js').forEach(function (v) {
v.style.setProperty('display', 'none', 'important');
});
} catch (e) {}
}
function caribInject() {
const id = caribId();
if (!id) return;
if (cDiv && document.contains(cDiv)) return;
const anchor = document.getElementById('player_outer')
|| document.getElementById('player')
|| document.querySelector('video.video-js');
if (!anchor || !anchor.parentNode) return;
// sweep foreign copies (stale TM bundle) — never 2 players
document.querySelectorAll('#wi3Player').forEach(function (el) { if (el !== cDiv) el.remove(); });
cDiv = document.createElement('div');
cDiv.id = 'wi3Player';
cDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · Caribbeancom unlocked</span>' +
'<span class="wi3-status">connecting...</span></div>' +
'<video id="wi3CaribVid" controls playsinline></video>' +
'<div id="wi3Qual"></div>';
anchor.parentNode.insertBefore(cDiv, anchor);
cVideo = cDiv.querySelector('video');
cQual = cDiv.querySelector('#wi3Qual');
caribCreate(id, false);
}
function caribTakeover(id, how) {
if (cDiv && document.contains(cDiv)) return;
try {
GM_addStyle(
'#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3CaribVid{display:block;width:100%;background:#000;max-height:70vh;}' +
'#wi3Qual{display:flex;gap:6px;padding:8px 10px;background:#111;flex-wrap:wrap;}' +
'#wi3Qual .wi3-q{background:#222;color:#ccc;border:1px solid #444;border-radius:4px;' +
'padding:4px 10px;font:12px sans-serif;cursor:pointer;}' +
'#wi3Qual .wi3-q.on{background:#ff4757;color:#fff;border-color:#ff4757;}' +
'#wi3Qual .wi3-q:hover{border-color:#888;}'
);
} catch (e) {}
caribInject();
if (!cTimer) {
cTimer = setInterval(function () {
try {
caribNuke();
if (!cDiv || !document.contains(cDiv)) caribInject();
} catch (e) {}
}, 600);
}
console.log('[wi3-CARIB] v1.5.1 active on', location.pathname, '| brand', CARIB_BRAND, '|', how);
}
// Gate: fragments 401 for guests (member token required). Two paths:
// member page -> wait briefly for $.getScript(getauthjw7key) to set
// window.authjw7Key.u (real token) -> takeover
// guest -> probe ONE fragment with dummy u once; only if the
// server actually serves it (200/206) do we take over
function caribGate(id) {
const t0 = Date.now();
let probed = false, stops = 0;
const tick = function () {
if (cDiv && document.contains(cDiv)) return;
let key = null, isMember = false;
try {
if (C_PW) {
if (C_PW.authjw7Key && C_PW.authjw7Key.u) key = String(C_PW.authjw7Key.u);
if (C_PW.User) isMember = true;
}
} catch (e) {}
if (key) {
cToken = key;
caribTakeover(id, 'member token');
return;
}
const elapsed = Date.now() - t0;
if (isMember) {
// member session: authjw7Key arrives via $.getScript - wait up to 6s
if (elapsed < 6000 && ++stops < 40) { setTimeout(tick, 200); }
else { console.log('[wi3-CARIB] member page but no authjw7Key - leaving site player'); }
return;
}
if (probed) return;
if (elapsed < 1500) { setTimeout(tick, 200); return; } // grace: let inline scripts define User
probed = true;
if (!C_GM) {
console.log('[wi3-CARIB] guest, no GM xhr to probe - site player untouched');
return;
}
const probe = CARIB_FMS + '/hls/member/' + CARIB_BRAND + '/' + id +
'/1080p.mp4Frag1Num0.ts?u=wi3&d=.ts';
C_GM({
method: 'GET',
url: probe,
headers: { 'Referer': location.href, 'Origin': location.origin },
timeout: 15000,
onload: function (r) {
if (r.status >= 200 && r.status < 300) {
caribTakeover(id, 'dummy u ok, status ' + r.status);
} else {
console.log('[wi3-CARIB] guest fragment status ' + r.status +
' - full video needs member login; site player untouched');
}
},
onerror: function (r) {
console.log('[wi3-CARIB] guest fragment probe failed' +
(r && r.status ? ' (status ' + r.status + ')' : '') +
' - site player untouched');
},
ontimeout: function () {
console.log('[wi3-CARIB] guest fragment probe timeout - site player untouched');
}
});
};
tick();
}
function caribBoot() {
if (!document.body) { setTimeout(caribBoot, 20); return; }
const id = caribId();
if (id) caribGate(id);
}
caribBoot();
} else if (_wi3Site === 'na') {
// ============================================================
// NAUGHTYAMERICA half -- v1.5.3 (Tier 2, verified live 08/10)
// Guest /scene/<slug> pages ship a PUBLIC trailer HLS on the
// videojs box #triggerPlay:
// abr2.naughtycdn.com/hls/mshf/trailers_apt/<scene>/<name>_
// ,1080,720,480,360,r.mp4.urlset/master.m3u8
// Verified 08/10: master manifest CORS-open (plain fetch returns
// #EXTM3U), 4-rung ladder whose top rungs are HEVC (hvc1) ->
// pick the first h264 rung so Chromium MSE plays it; segments
// serve in-page (206 init-f3 + m4s seen while playing).
// Member session: the same <video> carries the full scene ->
// identical takeover, status flips to 'full stream ready'.
// Guest full scene stays membership-gated -> honest note line.
// ============================================================
const NA_LABELS = ['1080p', '720p', '480p', '360p'];
const NA_GM = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;
let nDiv = null, nVideo = null, nHls = null, nQual = null, nTimer = null, nGmRetry = false;
// GM loader -- same proven shape as the carib half; fallback when
// the CDN answers manifests CORS-open but segments insist on a
// real Referer (location.href is what the site player sends).
function GmLoader() {
this._aborted = false;
this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, first: 0, end: 0 },
buffering: { start: 0, first: 0, end: 0 } };
}
GmLoader.prototype.load = function (context, config, callbacks) {
const self = this;
const url = context.url;
const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
self._aborted = false;
const t0 = performance.now();
const s = self.stats;
s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
s.parsing.start = 0; s.parsing.end = 0;
s.buffering.start = 0; s.buffering.end = 0;
const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
if (!NA_GM) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
try {
NA_GM({
method: 'GET', url: url, responseType: rt, timeout: 25000,
headers: { 'Referer': location.href },
onload: (r) => {
const t1 = performance.now();
if (self._aborted || self.stats.aborted) return;
if (r.status >= 200 && r.status < 300) {
const data = r.response;
const len = (data && (data.byteLength || data.length)) || 0;
self.stats.loading.end = t1;
self.stats.loaded = len;
self.stats.total = len;
if (typeof callbacks.onProgress === 'function') {
callbacks.onProgress(self.stats, context, data, netStub);
}
callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
} else {
callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
}
},
onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
});
} catch (e) {
callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
}
};
GmLoader.prototype.abort = function () { this._aborted = true; this.stats.aborted = true; };
GmLoader.prototype.destroy = function () {};
function naStatus(t) {
const s = nDiv && nDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function naUrl() {
try {
const v = document.getElementById('triggerPlay_html5_api')
|| document.querySelector('video.vjs-tech, video[src*="m3u8"]');
const s = v && (v.currentSrc || v.getAttribute('src') || '');
if (s && /\.m3u8/i.test(s)) return s;
const h = (document.documentElement.innerHTML || '').replace(/\\u002F/g, '/');
const m = h.match(/https?:\/\/abr\d*\.naughtycdn\.com\/[^"'\\\s<>]+master\.m3u8/);
if (m) return m[0];
} catch (e) {}
return null;
}
function naIsHevc(lv) {
const c = String((lv && lv.attrs && lv.attrs.CODECS) || (lv && lv.videoCodec) || '');
return /hvc1|hev1|h265|hevc/i.test(c);
}
// first rung Chromium MSE can actually play (ladder tops are HEVC)
function naFirstPlayable(hls) {
const ls = (hls && hls.levels) || [];
for (let i = 0; i < ls.length; i++) if (!naIsHevc(ls[i])) return i;
return -1;
}
function naQualBuild() {
if (!nQual || !nHls) return;
let lv = (nHls.levels || []).map((l, i) => ({ i: i, b: l.bandwidth || 0, hevc: naIsHevc(l) }));
if (!lv.length) return;
lv.sort((a, b) => b.b - a.b);
nQual.innerHTML = '';
lv.forEach((e, rank) => {
const label = (lv.length === NA_LABELS.length) ? NA_LABELS[rank]
: (Math.round(e.b / 1000) + 'k');
const b = document.createElement('button');
b.className = 'wi3-q' + (e.hevc ? ' hevc' : '') + (rank === 0 ? ' on' : '');
b.textContent = label + (e.hevc ? ' (hevc)' : '');
b.onclick = function () {
try { nHls.currentLevel = e.i; } catch (err) {}
Array.prototype.forEach.call(nQual.children, function (x) { x.classList.remove('on'); });
b.classList.add('on');
};
nQual.appendChild(b);
});
}
function naCreate(useGm) {
const url = naUrl();
if (!url || !nVideo) return;
try { if (nHls) { nHls.destroy(); nHls = null; } } catch (e) {}
const trailer = /trailers_apt|trailer/i.test(url);
const note = nDiv && nDiv.querySelector('.wi3-note');
if (note) note.textContent = trailer
? 'guest sees the public trailer - the full scene is NA membership-gated'
: 'member session - full scene stream';
if (typeof Hls === 'undefined' || !Hls.isSupported()) {
// no MSE (or @require failed) -> native HLS (Safari) or nothing
try { nVideo.src = url; naStatus('native hls'); } catch (e) {}
return;
}
nHls = new Hls(useGm ? { startLevel: 0, loader: GmLoader } : { startLevel: 0 });
nHls.on(Hls.Events.MANIFEST_PARSED, function () {
const idx = naFirstPlayable(nHls);
if (idx > 0) { try { nHls.startLevel = idx; } catch (e) {} }
naQualBuild();
naStatus((trailer ? 'public trailer ready' : 'full stream ready')
+ ' - ' + (nHls.levels ? nHls.levels.length : 0) + ' qualities');
});
nHls.on(Hls.Events.ERROR, function (ev, data) {
if (!data || !data.fatal) return;
if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !nGmRetry && NA_GM && !useGm) {
nGmRetry = true;
naStatus('retry via GM xhr...');
naCreate(true);
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
// HEVC rung on a box without hw decode -> step to first h264 rung
const cur = (nHls.currentLevel >= 0 && nHls.levels) ? nHls.levels[nHls.currentLevel] : null;
if (cur && naIsHevc(cur)) {
const idx = naFirstPlayable(nHls);
if (idx >= 0) { try { nHls.currentLevel = idx; } catch (e) {} return; }
}
try { nHls.recoverMediaError(); } catch (e) {}
} else {
naStatus('error: ' + (data.details || 'fatal'));
}
});
nHls.loadSource(url);
nHls.attachMedia(nVideo);
}
// site videojs box #triggerPlay holds the same stream behind the
// start card -- hide it and pause its audio, ours is the only player
function naNuke() {
try {
const box = document.getElementById('triggerPlay');
if (box && box !== nDiv) {
box.style.setProperty('display', 'none', 'important');
const sv = box.querySelector('video');
if (sv) { try { sv.pause(); } catch (e) {} }
}
document.querySelectorAll('video.vjs-tech').forEach(function (v) {
if (nVideo && v === nVideo) return;
try { v.pause(); } catch (e) {}
v.style.setProperty('display', 'none', 'important');
});
} catch (e) {}
}
function naInject() {
if (!naUrl()) return;
if (nDiv && document.contains(nDiv)) return;
const siteBox = document.getElementById('triggerPlay');
const anchor = (siteBox && siteBox.parentNode)
|| document.querySelector('.contain-start-card')
|| document.querySelector('.left-scene')
|| (function () { const v = document.querySelector('video.vjs-tech'); return v && v.parentNode; })();
if (!anchor) return;
// sweep foreign copies (stale TM bundle) -- never 2 players
document.querySelectorAll('#wi3Player').forEach(function (el) { if (el !== nDiv) el.remove(); });
nDiv = document.createElement('div');
nDiv.id = 'wi3Player';
nDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · NaughtyAmerica</span>' +
'<span class="wi3-status">connecting...</span></div>' +
'<video id="wi3NaVid" controls playsinline></video>' +
'<div id="wi3Qual"></div>' +
'<div class="wi3-note"></div>';
anchor.insertBefore(nDiv, anchor.firstChild || null);
nVideo = nDiv.querySelector('video');
nQual = nDiv.querySelector('#wi3Qual');
naCreate(false);
}
function naTakeover(how) {
if (nDiv && document.contains(nDiv)) return;
try {
GM_addStyle(
'#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3NaVid{display:block;width:100%;background:#000;max-height:70vh;}' +
'#wi3Qual{display:flex;gap:6px;padding:8px 10px;background:#111;flex-wrap:wrap;}' +
'#wi3Qual .wi3-q{background:#222;color:#ccc;border:1px solid #444;border-radius:4px;' +
'padding:4px 10px;font:12px sans-serif;cursor:pointer;}' +
'#wi3Qual .wi3-q.on{background:#ff4757;color:#fff;border-color:#ff4757;}' +
'#wi3Qual .wi3-q:hover{border-color:#888;}' +
'#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
);
} catch (e) {}
naInject();
if (!nTimer) {
nTimer = setInterval(function () {
try {
naNuke();
if (!nDiv || !document.contains(nDiv)) naInject();
} catch (e) {}
}, 600);
}
console.log('[wi3-NA] v1.5.3 active on', location.pathname, '|', how);
}
function naBoot() {
if (!document.body) { setTimeout(naBoot, 20); return; }
const t0 = Date.now();
let done = false;
const tick = function () {
if (done) return;
const u = naUrl();
if (u) {
done = true;
naTakeover('src ' + (u.indexOf('trailers_apt') >= 0 ? 'trailer' : 'full'));
return;
}
if (Date.now() - t0 > 15000) {
console.log('[wi3-NA] no stream url after 15s (not a /scene/ page or still gated)' +
' - page untouched');
done = true;
return;
}
setTimeout(tick, 400);
};
tick();
}
naBoot();
} else if (_wi3Site === 'php') {
// ============================================================
// PORNHUB PREMIUM guest bypass -- v1.5.4
// The premium domain serves guests a static login wall (zero
// video API calls fire on view_video.php). The SAME viewkey on
// the main site still ships guest flashvars: videoUrl = signed
// phncdn HLS master (h/e/f tokens) + quality ladder
// 180P_225K..720P_4000K + signed progressive ev.phncdn mp4
// (validfrom/validto/ip/hash). GM xhr runs in the extension
// context, so cross-domain GET to www.pornhub.com needs no
// CORS -- buyer networks reach .com fine; probe boxes with a
// sinkholed .com degrade gracefully to the honest note.
// Parse -> hls.js takeover above the wall. Premium-exclusive
// viewkeys (no main-site copy / redirect) -> honest note,
// page untouched -- same discipline as the CC/BC gates.
// ============================================================
(function phpBoot() {
// Viewkey carriers, best-effort: (1) location.search if the server
// ever renders the video doc directly; (2) document.referrer when
// the initiating URL carried viewkey. Verified recon 08/10:
// guest /view_video.php is server-302'd to
// /premium/login?redirect=<opaque ~90-byte blob> (no client key,
// login HTML/JS has zero viewkey traces), Chrome propagates the
// INITIATOR referrer (never the viewkey hop), cookies /
// localStorage / XHRs carry nothing, header+bot-UA+cookie matrices
// all302 -> the dominant paste flow lands here WITHOUT a viewkey.
// Then we show the paste bar: the user still has the link in
// hand. Known-viewkey flow kicks off automatically above it.
const KRE = /[?&]viewkey=([a-z0-9]+)/i;
const vm = location.search.match(KRE) ||
(document.referrer || '').match(KRE);
let anchor = null;
const want = ['main', '#videoPlayerClass', '.video_wrapper', '#root', 'body'];
for (let i = 0; i < want.length; i++) {
anchor = document.querySelector(want[i]);
if (anchor) break;
}
if (!anchor) { setTimeout(phpBoot, 300); return; }
const vk = vm ? vm[1] : '';
let pDiv = null, pHls = null;
function phpStyle() {
try {
GM_addStyle(
'#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3Player .wi3-row{display:flex;gap:6px;padding:6px 10px;background:#111;align-items:center;}' +
'#wi3Player #wi3In{flex:1;min-width:120px;background:#000;color:#ddd;border:1px solid #333;' +
'border-radius:4px;padding:5px 8px;font:12px/1.4 sans-serif;}' +
'#wi3Player button{background:#1a1a1a;color:#5eead4;border:1px solid #333;' +
'border-radius:4px;padding:5px 10px;font:12px/1.4 sans-serif;cursor:pointer;}' +
'#wi3Vid{display:block;width:100%;background:#000;max-height:70vh;}'
);
} catch (e) {}
}
function phpNote(t) {
const s = pDiv && pDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function phpShell(badge, status) {
phpStyle();
document.querySelectorAll('#wi3Player').forEach(function (x) { x.remove(); });
pDiv = document.createElement('div');
pDiv.id = 'wi3Player';
pDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">' + badge + '</span>' +
'<span class="wi3-status">' + status + '</span></div>';
anchor.insertBefore(pDiv, anchor.firstChild || null);
}
function phpPlayer(src, key) {
phpShell('wi3 · premium guest stream', 'connecting...');
const v = document.createElement('video');
v.id = 'wi3Vid';
v.setAttribute('controls', '');
v.setAttribute('playsinline', '');
pDiv.appendChild(v);
let mode = '';
let flips = 0;
const startHls = function () {
if (mode === 'hls' || mode === 'native') return;
if (src.hls && typeof Hls !== 'undefined' && Hls.isSupported()) {
mode = 'hls';
pHls = new Hls({ startLevel: 0 });
pHls.on(Hls.Events.MANIFEST_PARSED, function () {
phpNote('stream ready - main-site flashvars');
});
pHls.on(Hls.Events.ERROR, function (e2, d) {
if (!d || !d.fatal) return;
console.log('[wi3-PHP] hls fatal detail=' + (d.details || '?') +
' code=' + (d.response && d.response.code));
try { if (pHls) { pHls.destroy(); pHls = null; } } catch (x) {}
if (src.mp4 && flips < 3) {
flips++;
mode = 'mp4';
v.src = src.mp4;
phpNote('hls rejected - progressive mp4 fallback');
} else {
phpNote('stream error - free path pending');
}
});
pHls.loadSource(src.hls);
pHls.attachMedia(v);
} else if (src.hls) {
mode = 'native';
v.src = src.hls;
phpNote('playing natively');
} else {
phpNote('stream error - free path pending');
}
};
v.addEventListener('error', function () {
if (mode === 'mp4' && src.hls && flips < 3) {
// progressive source rejected (expired edge / blocked) ->
// recover through hls.js (same flashvars, second rendition)
console.log('[wi3-PHP] mp4 error code=' + (v.error && v.error.code) + ' -> hls fallback');
flips++;
mode = '';
startHls();
} else if (mode === 'native' && src.mp4 && flips < 3) {
// native m3u8 refused (Chrome has no native HLS) -> progressive
console.log('[wi3-PHP] native hls rejected -> mp4 fallback');
flips++;
mode = 'mp4';
v.src = src.mp4;
phpNote('native hls rejected - progressive mp4 fallback');
} else if (mode === 'mp4' || mode === 'native') {
phpNote('stream error - free path pending');
}
});
if (src.mp4 && (src.mp4Ladder || !src.hls)) {
// progressive mp4 first: <video src> needs NO CORS at all and
// does not depend on the @require hls.js bundle being reachable.
// v1.6.2 ladder-first: when mp4 is only the og:video shadow
// (240p ip-bound), flashvars mediaPriority=hls owns quality.
mode = 'mp4';
v.src = src.mp4;
phpNote('progressive mp4 ready - main-site flashvars');
} else {
startHls();
}
console.log('[wi3-PHP] takeover viewkey=' + key +
' mp4=' + String(src.mp4 || '-').slice(0, 70) +
' hls=' + String(src.hls || '-').slice(0, 70));
}
function phpParse(html) {
// returns { hls, mp4 } | null -- walks the flashvars media array
// entries {"group":1,"height":720,"format":"mp4|hls",
// "videoUrl":"https:\/\/..."} and keeps the HIGHEST rendition of
// each type (first-match naive regex would grab 240P). Order of
// keys inside an entry does not matter: the whole entry object is
// parsed with JSON.parse. Legacy raw-URL scans stay as fallback
// for pages whose flashvars use the old single-videoUrl shape.
let hls = null, hlsQ = -1, mp4 = null, mp4Q = -1;
let mp4Ladder = false;
let idx = 0;
while ((idx = html.indexOf('{"group":', idx)) !== -1) {
let depth = 0, inStr = false, esc = false, end = -1;
for (let k = idx; k < html.length && k < idx + 3000; k++) {
const ch = html.charAt(k);
if (inStr) {
if (esc) esc = false;
else if (ch === '\\') esc = true;
else if (ch === '"') inStr = false;
} else if (ch === '"') inStr = true;
else if (ch === '{') depth++;
else if (ch === '}') {
depth--;
if (depth === 0) { end = k + 1; break; }
}
}
if (end < 0) { idx += 9; continue; }
const raw = html.slice(idx, end);
idx = end;
let ent = null;
try { ent = JSON.parse(raw); } catch (x) { continue; }
if (!ent || !ent.videoUrl || !/^https?:\/\//.test(String(ent.videoUrl))) continue;
const u = String(ent.videoUrl).replace(/&/g, '&');
if (/preroll|\/ads?\//i.test(u)) continue;
const q = parseInt(ent.height, 10) || 0;
if (ent.format === 'mp4' && q > mp4Q) { mp4Q = q; mp4 = u; mp4Ladder = true; }
else if (ent.format === 'hls' && q > hlsQ) { hlsQ = q; hls = u; }
}
// legacy fallbacks (old flashvars shape / no {"group": array)
if (!hls || !mp4) {
const flat = html.replace(/\\\//g, '/');
const qOf = function (s) {
const m2 = s.match(/_(\d{3,4})P_(\d+)K_/);
return m2 ? parseInt(m2[2], 10) : 0;
};
if (!hls) {
const all = flat.match(/https?:[^"'\\\s]+\.m3u8[^"'\\\s]*/gi) || [];
for (const cand of all) {
const u2 = cand.replace(/&/g, '&');
const q2 = qOf(u2);
if (q2 > hlsQ) { hlsQ = q2; hls = u2; }
}
if (!hls && all.length) hls = all[0].replace(/&/g, '&');
}
if (!mp4) {
const all = flat.match(/https?:\/\/[^"'\\\s]*phncdn[^"'\\\s]+\.mp4\?[^"'\\\s]*/gi) || [];
for (const cand of all) {
const u2 = cand.replace(/&/g, '&');
if (/\/hls\//i.test(u2) || /preroll|\/ads?\//i.test(u2)) continue;
const q2 = qOf(u2);
if (q2 > mp4Q) { mp4Q = q2; mp4 = u2; }
}
if (!mp4 && all.length) mp4 = all[0].replace(/&/g, '&');
}
}
return (hls || mp4) ? { hls: hls, mp4: mp4, mp4Ladder: mp4Ladder } : null;
}
function phpKick(key) {
phpShell('wi3 · premium bypass', 'fetching main-site copy of ' + key + '...');
const MAIN = 'https://www.pornhub.com/view_video.php?viewkey=' + encodeURIComponent(key);
const noteDead = function () {
phpNote('main site unreachable from this network - free path pending');
};
const adopt = function (body) {
const u = phpParse(body);
if (u) {
phpPlayer(u, key);
} else if (/has been removed|video private|error-code/i.test(body)) {
phpNote('no main-site copy of this viewkey - free path pending');
} else {
phpNote('no flashvars on main-site copy - free path pending');
}
};
// v1.6.2 blocked-ISP path: ISPs that sinkhole www.pornhub.com
// (DNS -> 127.0.0.1, or SNI-reset on the TLS handshake) never
// reach the direct kick. archive.org Save-Page-Now returns the
// FRESH main-site page inline from an unblocked egress. SPN
// rewrites videoUrl through web.archive.org/web/<ts>/ (unwrap
// both the raw and the JSON-escaped form), and the modern h/e/f
// signature carries NO ip= binding -- verified live 08/10: the
// archive-fetched master.m3u8 streamed from a different egress
// (mp4 validfrom/ip/hash IS ip-bound, hence ladder-first above).
// One mirror per 5 min so buyers never trip SPN rate limits.
const remote = function (why) {
let last = 0;
try { last = parseInt(sessionStorage.getItem('wi3PhRm') || '0', 10) || 0; } catch (x) {}
if (Date.now() - last < 300000) { noteDead(); return; }
try { sessionStorage.setItem('wi3PhRm', String(Date.now())); } catch (x) {}
console.log('[wi3-PHP] direct kick failed (' + why + ') -> archive.org mirror');
phpNote('isp mirror warming (archive.org) - about 15s...');
GM_xmlhttpRequest({
method: 'GET',
url: 'https://web.archive.org/save/' + MAIN,
headers: { Referer: MAIN },
timeout: 120000,
onload: function (r) {
let body = String(r.responseText || '');
body = body
.replace(/(?:https?:)\\\/\\\/web\.archive\.org\\\/web\\\/\d+(?:[a-z]{2}_)?\\\//gi, '')
.replace(/https?:\/\/web\.archive\.org\/web\/\d+(?:[a-z]{2}_)?\//gi, '');
if (body.length < 4000 || !/(flashvars|mediaDefinitions|og:video)/i.test(body)) {
noteDead();
return;
}
adopt(body);
},
onerror: noteDead,
ontimeout: noteDead
});
};
GM_xmlhttpRequest({
method: 'GET',
url: MAIN,
headers: { Referer: 'https://www.pornhub.com/' },
timeout: 25000,
onload: function (r) {
const fin = String(r.finalUrl || r.responseURL || '');
const body = String(r.responseText || '');
if (fin && !/viewkey=/i.test(fin)) {
phpNote('main site redirected (no main-site copy) - premium-exclusive, free path pending');
return;
}
if (body.length < 4000 || !/(flashvars|mediaDefinitions|og:video)/i.test(body)) {
remote('unusable-body');
return;
}
adopt(body);
},
onerror: function () { remote('network'); },
ontimeout: function () { remote('timeout'); }
});
}
function phpBar() {
phpStyle();
document.querySelectorAll('#wi3Player').forEach(function (x) { x.remove(); });
pDiv = document.createElement('div');
pDiv.id = 'wi3Player';
pDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · premium bypass</span>' +
'<span class="wi3-status">paste a premium video link - dual-listed scenes play free</span></div>' +
'<div class="wi3-row">' +
'<input id="wi3In" type="text" spellcheck="false" ' +
'placeholder="https://www.pornhubpremium.com/view_video.php?viewkey=...">' +
'<button id="wi3Go" type="button">watch</button>' +
'<button id="wi3Clip" type="button">clip</button></div>';
anchor.insertBefore(pDiv, anchor.firstChild || null);
const inp = pDiv.querySelector('#wi3In');
const go = function () {
const raw = String(inp.value || '').trim();
const m = raw.match(KRE) || raw.match(/\b(ph[0-9a-z]{8,}|[0-9]{6,})\b/i);
if (!m) {
const s = pDiv.querySelector('.wi3-status');
if (s) s.textContent = 'need a link containing viewkey= (or paste just the key)';
return;
}
phpKick(m[1]);
};
pDiv.querySelector('#wi3Go').addEventListener('click', go);
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(); });
pDiv.querySelector('#wi3Clip').addEventListener('click', function () {
try {
navigator.clipboard.readText().then(function (t) {
inp.value = t || '';
if (t) go();
}).catch(function () {
const s = pDiv.querySelector('.wi3-status');
if (s) s.textContent = 'clipboard blocked - paste into the box';
inp.focus();
});
} catch (e2) { inp.focus(); }
});
console.log('[wi3-PHP] paste bar mounted (no viewkey in carriers)');
}
if (vk) phpKick(vk);
else phpBar();
})();
} else if (_wi3Site === 'trlt') {
// ============================================================
// VMG + ANGEL NETWORK guest trailer takeover -- v1.6.0
// (verified live 08/10, headless, UA-bound):
// VMG scene pages (blacked/vixen/tushy/tushyraw/blackedraw)
// mount <video src="cdn.<site>.com/video/mp4_trailer/<id>/
// <SITE>_<id>_480P.mp4?ip=<viewer>&Policy=..&Signature=..">
// CloudFront-signed-per-IP: 60-72s @480p (854w), media
// error 0 verified on blacked + vixen scene pages. Home
// heroes use /previewvideos/ (10s loop) -> excluded by the
// src pattern; visible-but-tiny (<400px) nodes skipped as
// hover thumbs, 0x0 pre-layout nodes accepted (Angel mounts
// its trailer at 0x0 until the player sizes it -- verified
// 08/10: no listing page mounts trailer-pattern srcs).
// Angel Network video pages (evilangel/wicked/
// digitalplayground/puretaboo) mount
// trailers-fame.gammacdn.com/.../tr_<id>_720p.mp4:
// 69-81s @720p (1280w), error 0 verified on evilangel +
// wicked.
// Takeover: adopt the signed trailer url into the wi3 box
// (plain mp4 -- the page-context media fetch carries the same
// IP the signature was minted for, no GM/hls needed), pause +
// hide every site copy, honest membership note. The 600ms
// watcher re-injects after SPA re-renders, refreshes our copy
// on scene swaps, and drops a stale box after 5 misses.
// ============================================================
let tDiv = null, tVid = null, tTimer = null, tMiss = 0;
const TRL_SRC = /mp4_trailer|\/trailers\/|trailers-fame/i;
function trHost() { return location.hostname.replace(/^www\./, ''); }
function trFind() {
const vs = document.querySelectorAll('video');
for (let i = 0; i < vs.length; i++) {
const v = vs[i];
const s = v.currentSrc || v.getAttribute('src') || '';
if (!s || !TRL_SRC.test(s)) continue;
// visible-but-tiny = hover thumbnail; 0x0 = pre-layout main
// player (Angel Network mounts the trailer at 0x0 until its
// player sizes it) -> accept, listings mount no trailer srcs
const w = v.offsetWidth || 0;
if (w > 0 && w < 400) continue;
return v;
}
return null;
}
function trStatus(t) {
const s = tDiv && tDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function trSweep() {
const vs = document.querySelectorAll('video');
for (let i = 0; i < vs.length; i++) {
const v = vs[i];
if (tVid && v === tVid) continue;
const s = v.currentSrc || v.getAttribute('src') || '';
if (!s || !TRL_SRC.test(s)) continue;
try { v.pause(); } catch (e) {}
try { v.muted = true; } catch (e) {}
try { v.style.setProperty('visibility', 'hidden', 'important'); } catch (e) {}
}
}
function trInject() {
const v = trFind();
if (!v) return false;
const src = v.currentSrc || v.getAttribute('src') || '';
if (tDiv && document.contains(tDiv)) {
if (tVid && tVid.getAttribute('src') !== src) {
try { tVid.src = src; } catch (e) {}
trStatus('new scene trailer ready');
}
trSweep();
return true;
}
tDiv = null; tVid = null;
document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
try {
GM_addStyle(
'#wi3Player{position:relative;width:100%;box-sizing:border-box;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3TrVid{display:block;width:100%;background:#000;max-height:70vh;}' +
'#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
);
} catch (e) {}
const box = v.parentElement;
let anchor = (box && box.parentElement) || document.body;
// AdultTime mounts its trailer deep inside a display:none player chain ->
// walk up to the first visible ancestor so the wi3 box is not swallowed
// (VMG/Angel pages stop at the first hop - identical to the old behavior)
let ref = box;
while (anchor && anchor !== document.body) {
const acs = getComputedStyle(anchor);
if (acs.display !== 'none' && acs.visibility !== 'hidden' && anchor.offsetWidth > 0) break;
ref = anchor;
anchor = anchor.parentElement || document.body;
}
while (ref && ref.parentElement && ref.parentElement !== anchor) ref = ref.parentElement;
tDiv = document.createElement('div');
tDiv.id = 'wi3Player';
tDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · ' + trHost() + '</span>' +
'<span class="wi3-status">loading trailer...</span></div>' +
'<video id="wi3TrVid" controls playsinline></video>' +
'<div class="wi3-note">free guest trailer - the full scene is ' + trHost() +
' membership-gated (wi3 keeps the trailer playable)</div>';
anchor.insertBefore(tDiv, (ref && ref.parentElement === anchor) ? ref : null);
tVid = tDiv.querySelector('video');
tVid.addEventListener('loadedmetadata', function () {
trStatus('public trailer ready - ' + Math.round(tVid.duration || 0) + 's @' +
(tVid.videoWidth || '?') + 'p');
});
tVid.addEventListener('error', function () {
trStatus('trailer url rejected (signature/ip) - reload the page');
});
tVid.src = src;
trSweep();
console.log('[wi3-TRLT] trailer adopted on', location.hostname, '|', src.slice(0, 90));
return true;
}
(function trBoot() {
if (!document.body) { setTimeout(trBoot, 20); return; }
trInject();
if (!tTimer) {
tTimer = setInterval(function () {
try {
if (tDiv && !document.contains(tDiv)) { tDiv = null; tVid = null; }
if (trInject()) {
tMiss = 0;
} else {
tMiss++;
if (tDiv && tMiss >= 5) {
try { tDiv.remove(); } catch (e) {}
tDiv = null; tVid = null;
}
}
} catch (e) {}
}, 600);
}
})();
} else if (_wi3Site === 'tskeet') {
// ============================================================
// TEAMSKEET signed-trailer adopt -- v1.6.0 (verified 08/10):
// scene pages (/movies/<slug>) embed the free guest trailer
// as a SIGNED Cloudflare Stream iframe
// (iframe.cloudflarestream.com/<jwt path>); the frame-internal
// <video> attaches an MSE blob with duration 82s for guests --
// no cookie, no join. We CLONE the iframe (same src -> JWT
// reloads within validity) into the wi3 box and hide the site
// host with display:none: the original node stays attached
// (no React/DOM teardown errors) and is unreachable (no double
// audio). The 700ms watcher re-syncs src on SPA scene swaps,
// re-asserts the hidden host, and drops the box after 5
// consecutive misses (navigated back to a listing). Listing
// pages carry no iframe -> idle.
// ============================================================
let kDiv = null, kTimer = null, kMiss = 0;
function ksFind() { return document.querySelector('iframe[src*="cloudflarestream"]'); }
function ksStatus(t) {
const s = kDiv && kDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function ksBuild(f) {
const host = f.parentElement;
const outer = host && host.parentElement;
try {
GM_addStyle(
'#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3Player .wi3-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;}' +
'#wi3Player .wi3-frame iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0;}' +
'#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
);
} catch (e) {}
document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
kDiv = document.createElement('div');
kDiv.id = 'wi3Player';
kDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · TeamSkeet</span>' +
'<span class="wi3-status">trailer embed ready</span></div>' +
'<div class="wi3-frame"></div>' +
'<div class="wi3-note">free guest trailer (~80s) - the full scene is TeamSkeet ' +
'membership-gated (wi3 keeps the trailer playable)</div>';
if (outer) outer.insertBefore(kDiv, host);
else if (host) host.insertBefore(kDiv, host.firstChild);
else document.body.insertBefore(kDiv, document.body.firstChild);
const src = f.getAttribute('src') || '';
const mine = document.createElement('iframe');
mine.setAttribute('src', src);
mine.setAttribute('data-site-src', src);
mine.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture');
mine.setAttribute('allowfullscreen', '');
kDiv.querySelector('.wi3-frame').appendChild(mine);
try { host.style.setProperty('display', 'none', 'important'); } catch (e) {}
console.log('[wi3-TSKEET] trailer iframe adopted on', location.pathname.slice(0, 70));
}
(function ksBoot() {
if (!document.body) { setTimeout(ksBoot, 20); return; }
if (!kTimer) {
kTimer = setInterval(function () {
try {
const f = ksFind();
if (!f) {
kMiss++;
if (kDiv && kMiss >= 5) { try { kDiv.remove(); } catch (e) {} kDiv = null; }
return;
}
kMiss = 0;
const src = f.getAttribute('src') || '';
if (kDiv && !document.contains(kDiv)) kDiv = null;
if (!kDiv) { ksBuild(f); return; }
const mine = kDiv.querySelector('iframe');
if (mine && src && mine.getAttribute('data-site-src') !== src) {
mine.setAttribute('data-site-src', src);
mine.setAttribute('src', src);
ksStatus('trailer embed ready');
}
const host = f.parentElement;
if (host) host.style.setProperty('display', 'none', 'important');
} catch (e) {}
}, 700);
}
})();
} else if (_wi3Site === 'nubil') {
// ============================================================
// NUBILES ENTERTAINMENT NETWORK (26 domains) guest preview
// takeover + preview-quota reset -- v1.6.1, verified 08/10 on
// nubiles-porn.com + momsteachsex.com + stepsiblingscaught.com:
// scene pages /video/watch/<id>/<slug> mount videojs with a
// signed trailer ladder images.<site>/videos/<slug>/videos/
// <site>_<slug>_trailer_{480,640,960,1280}.mp4?st=<sig>&
// e=<unix> (84s @960x540 default; media hotlinks with a
// Referer only, no cookie; each quality carries its own st).
// Guests get 5 previews per SERVER session (PHPSESSID +
// avs_id, both HttpOnly=no -> JS writable): at
// "0 Preview(s) Remaining" the site mounts NO <video> at all.
// Priority: live video sources (site picks 960/1280) -> this
// scene's signed trailer urls left in the HTML -> og:video
// meta (480p) -> rotate both cookies + reload (guarded: one
// rotation per 5 min via sessionStorage, no reload loops).
// Full scene = members-only (unsigned paths answer 403) ->
// honest membership note. Age modal: set the site's own
// 18-plus-modal cookie and drop a live modal.
// ============================================================
let nDiv = null, nVid = null, nTimer = null, nMiss = 0;
const NB_TRAIL = /_trailer_\d+\.mp4/i;
function nbHost() { return location.hostname.replace(/^www\./, ''); }
function nbRndHex(n) {
let s = '';
try {
const a = new Uint8Array(n);
(window.crypto || window.msCrypto).getRandomValues(a);
for (let i = 0; i < n; i++) s += ('0' + a[i].toString(16)).slice(-2);
} catch (e) {
for (let i = 0; i < n; i++) s += ('0' + Math.floor(Math.random() * 256).toString(16)).slice(-2);
}
return s;
}
function nbSid() {
const cs = 'abcdefghijklmnopqrstuvwxyz0123456789';
const a = new Uint32Array(32);
try { (window.crypto || window.msCrypto).getRandomValues(a); } catch (e) {
for (let i = 0; i < 32; i++) a[i] = Math.floor(Math.random() * 4294967296);
}
let s = '';
for (let i = 0; i < 32; i++) s += cs[a[i] % cs.length];
return s;
}
function nbRot() {
const dom = ';domain=.' + nbHost() + ';path=/;max-age=31536000';
try { document.cookie = 'PHPSESSID=;path=/;max-age=0'; } catch (e) {}
try { document.cookie = 'avs_id=;path=/;max-age=0'; } catch (e) {}
try { document.cookie = 'PHPSESSID=' + nbSid() + dom; } catch (e) {}
try { document.cookie = 'avs_id=' + nbRndHex(32) + dom; } catch (e) {}
console.log('[wi3-NUBIL] preview quota rotated on', nbHost());
}
function nbReloadGuarded() {
let last = 0;
try { last = parseInt(sessionStorage.getItem('wi3NbRot') || '0', 10) || 0; } catch (e) {}
if (Date.now() - last < 300000) return false;
try { sessionStorage.setItem('wi3NbRot', String(Date.now())); } catch (e) {}
nbRot();
setTimeout(function () { location.reload(); }, 400);
return true;
}
function nbBest() {
let live = null;
const vs = document.querySelectorAll('video');
for (let i = 0; i < vs.length; i++) {
const s = vs[i].currentSrc || vs[i].getAttribute('src') || '';
if (s && NB_TRAIL.test(s)) { live = vs[i]; break; }
}
if (live) {
const cands = [];
const ss = live.querySelectorAll('source');
for (let i = 0; i < ss.length; i++) {
const u = ss[i].getAttribute('src') || '';
if (u && NB_TRAIL.test(u)) cands.push(u);
}
const cu = live.currentSrc || live.getAttribute('src') || '';
if (cu && NB_TRAIL.test(cu)) cands.push(cu);
let bw = -1, bp = '';
for (let i = 0; i < cands.length; i++) {
const wm = cands[i].match(/_(\d+)\.mp4/i);
const w = wm ? parseInt(wm[1], 10) : 0;
if (w >= bw) { bw = w; bp = cands[i]; }
}
if (bp) return { src: bp, node: live };
}
const slug = (location.pathname.split('/').pop() || '').replace(/-/g, '_').toLowerCase();
const html = document.documentElement.innerHTML || '';
const re = /https?:\/\/[^"'\s\\<>]+?_trailer_(\d+)\.mp4[^"'\s\\<>]*/g;
let mm, hw = -1, hp = '';
while ((mm = re.exec(html)) !== null) {
const u = mm[0].replace(/&/g, '&');
if (slug.length > 4 && u.toLowerCase().indexOf(slug) < 0) continue;
const w = parseInt(mm[1], 10);
if (w >= hw) { hw = w; hp = u; }
}
if (hp) return { src: hp, node: live };
const og = document.querySelector('meta[property="og:video:secure_url"],meta[property="og:video"]');
const ou = og && (og.getAttribute('content') || '');
if (ou && NB_TRAIL.test(ou)) return { src: ou, node: live };
return null;
}
function nbStatus(t) {
const s = nDiv && nDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function nbSweep() {
const vs = document.querySelectorAll('video');
for (let i = 0; i < vs.length; i++) {
const v = vs[i];
if (nVid && v === nVid) continue;
const s = v.currentSrc || v.getAttribute('src') || '';
if (!s || !NB_TRAIL.test(s)) continue;
try { v.pause(); } catch (e) {}
try { v.muted = true; } catch (e) {}
try { v.style.setProperty('visibility', 'hidden', 'important'); } catch (e) {}
}
}
function nbAge() {
try { document.cookie = '18-plus-modal=hidden;path=/;max-age=31536000'; } catch (e) {}
const m = document.querySelector('.eighteen-plus-modal.show');
if (m) { try { m.style.setProperty('display', 'none', 'important'); } catch (e) {} }
}
function nbInject() {
const b = nbBest();
if (!b) {
const body = document.body ? document.body.innerText : '';
const walled = /Preview\(s\)\s*Remaining/.test(body) || /0 Preview/.test(body);
if (walled || !document.querySelector('video')) nbReloadGuarded();
return false;
}
const src = b.src;
if (nDiv && document.contains(nDiv)) {
if (nVid && nVid.getAttribute('src') !== src) {
try { nVid.src = src; } catch (e) {}
nbStatus('new scene trailer ready');
}
nbSweep();
return true;
}
nDiv = null; nVid = null;
document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
try {
GM_addStyle(
'#wi3Player{position:relative;width:100%;box-sizing:border-box;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3TrVid{display:block;width:100%;background:#000;max-height:70vh;}' +
'#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
);
} catch (e) {}
const box = (b.node && b.node.parentElement) ||
document.querySelector('.watch-page-video-wrapper,.watch-page-video-container,#player,.video-player') || null;
const anchor = (box && box.parentElement) || document.body;
nDiv = document.createElement('div');
nDiv.id = 'wi3Player';
nDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · ' + nbHost() + '</span>' +
'<span class="wi3-status">loading trailer...</span></div>' +
'<video id="wi3TrVid" controls playsinline></video>' +
'<div class="wi3-note">free guest trailer - preview quota auto-reset - the full scene is ' + nbHost() +
' membership-gated (wi3 keeps the trailer playable)</div>';
anchor.insertBefore(nDiv, box || null);
nVid = nDiv.querySelector('video');
nVid.addEventListener('loadedmetadata', function () {
nbStatus('public trailer ready - ' + Math.round(nVid.duration || 0) + 's @' +
(nVid.videoWidth || '?') + 'p');
});
nVid.addEventListener('error', function () {
nbStatus('trailer url expired - rotating preview quota...');
if (nbReloadGuarded()) return;
const b2 = nbBest();
if (b2 && b2.src !== nVid.getAttribute('src')) { try { nVid.src = b2.src; } catch (e) {} }
});
nVid.src = src;
nbSweep();
console.log('[wi3-NUBIL] trailer adopted on', location.hostname, '|', src.slice(0, 110));
return true;
}
(function nbBoot() {
if (!document.body) { setTimeout(nbBoot, 20); return; }
nbAge();
nbInject();
if (!nTimer) {
nTimer = setInterval(function () {
try {
if (nDiv && !document.contains(nDiv)) { nDiv = null; nVid = null; }
nbAge();
if (nbInject()) {
nMiss = 0;
} else {
nMiss++;
if (nDiv && nMiss >= 5) {
try { nDiv.remove(); } catch (e) {}
nDiv = null; nVid = null;
}
}
} catch (e) {}
}, 600);
}
})();
} else if (_wi3Site === 't2gate') {
// ============================================================
// AYLO P1 honest guest gate -- v1.6.0 covers the whole family:
// brazzers + mofos + realitykings + bangbros (PH Premium sits
// in the 'php' bypass branch).
// Brazzers verdict (bz1-bz7, 08/10): P1 tour instance
// ZZ_PC_Tour_Gateway_Consolidation_CTRL (id358751),
// playback.scene=false, trailer.isEnabled=false (maxViews3),
// getVideoTrailerSrc omits videos.full.files for guests
// (contentUrl=null); Play button = fake play -> join modal
// (2Day_Trial assets = card trial, PII).
// Family verdict (08/10, mofos/rk/bangbros scene pages hit the
// live PlayerBlock site-api releases/<id>?blockName=PlayerBlock
// directly): every one answers "videos":[],"groups":[],
// "canPlay":true with a type:"scene" metadata child plus a
// mediabook slideshow mp4 only -- no m3u8, no scene mp4, no
// trailer child for guests. canPlay:true + mounted chrome IS
// the trap (join modal).
// Honest gate: watch12s for any guest stream the page reveals
// (future free path), take it over when it appears; otherwise
// leave the page untouched and log the pending status -- same
// discipline as the CC gate.
// ============================================================
const T2_ADS = /static-landing-assets|P1TOUR|Expired_MA|images-assets-ht|topads|player\.mp4|\/ads?\/|mediabook|preview/i;
let tDiv = null, tVideo = null, tHls = null;
function t2StreamEl() {
const vs = document.querySelectorAll('video, source');
for (let i = 0; i < vs.length; i++) {
const s = vs[i].currentSrc || vs[i].getAttribute('src') || '';
if (s && /\.(mp4|m3u8)/i.test(s) && !T2_ADS.test(s)) return vs[i];
}
return null;
}
function t2Status(t) {
const s = tDiv && tDiv.querySelector('.wi3-status');
if (s) s.textContent = t;
}
function t2Takeover(el) {
try {
GM_addStyle(
'#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
'border-radius:6px;overflow:hidden;margin:8px 0;}' +
'#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
'#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
'#wi3Player .wi3-status{color:#aaa;}' +
'#wi3T2Vid{display:block;width:100%;background:#000;max-height:70vh;}'
);
} catch (e) {}
const anchor = el.parentElement || document.body;
document.querySelectorAll('#wi3Player').forEach(function (x) { if (x !== tDiv) x.remove(); });
tDiv = document.createElement('div');
tDiv.id = 'wi3Player';
tDiv.innerHTML =
'<div class="wi3-top"><span class="wi3-badge">wi3 · guest stream</span>' +
'<span class="wi3-status">connecting...</span></div>' +
'<video id="wi3T2Vid" controls playsinline></video>';
anchor.insertBefore(tDiv, anchor.firstChild || null);
tVideo = tDiv.querySelector('video');
const s = el.currentSrc || el.getAttribute('src') || '';
// hide the site copy so only ours makes sound
try { if (el.pause) el.pause(); el.style.setProperty('display', 'none', 'important'); } catch (e) {}
if (/\.mp4/i.test(s)) {
tVideo.src = s;
t2Status('guest mp4 ready');
} else if (/\.m3u8/i.test(s) && typeof Hls !== 'undefined' && Hls.isSupported()) {
tHls = new Hls({ startLevel: 0 });
tHls.on(Hls.Events.MANIFEST_PARSED, function () { t2Status('guest stream ready'); });
tHls.on(Hls.Events.ERROR, function (e2, d) { if (d && d.fatal) t2Status('error: ' + (d.details || 'fatal')); });
tHls.loadSource(s);
tHls.attachMedia(tVideo);
} else {
try { tVideo.src = s; t2Status('playing natively'); } catch (e) {}
}
console.log('[wi3-T2] guest stream surfaced on', location.hostname, '- takeover');
}
function t2Boot() {
if (!document.body) { setTimeout(t2Boot, 20); return; }
const t0 = Date.now();
let done = false;
const tick = function () {
if (done) return;
const el = t2StreamEl();
if (el) { done = true; t2Takeover(el); return; }
if (Date.now() - t0 > 12000) {
done = true;
console.log('[wi3-T2] ' + location.hostname + ': guest wall confirmed (no free stream serves)' +
' - honest gate, page untouched | free-unlock status pending');
return;
}
setTimeout(tick, 500);
};
tick();
}
t2Boot();
}
})();