手动节点切换 + Ping测速 + 应用节点(超紧凑面板)
// ==UserScript==
// @name Iwara Video Node Manual
// @icon https://www.google.com/s2/favicons?sz=64&domain=iwara.tv
// @namespace https://github.com/dawn-lc/
// @version 0.2.3
// @description 手动节点切换 + Ping测速 + 应用节点(超紧凑面板)
// @author dawn-lc + Grok
// @include *://*.iwara.tv/*
// @grant unsafeWindow
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
if (unsafeWindow.IwaraVideoNodeReplacer) return;
unsafeWindow.IwaraVideoNodeReplacer = true;
console.log('[Iwara Node Replacer] 脚本已启动 v0.2.3 (超紧凑面板)');
const locale = unsafeWindow.localStorage.getItem('locale') || 'zh';
const I18N = {
en: { title: 'Video Node', retest: 'Retest', apply: 'Apply', enable: 'On', current: 'Current', custom: 'Custom' },
zh: { title: '视频节点', retest: '重新测速', apply: '应用节点', enable: '开启', current: '当前', custom: '自定义' },
ja: { title: '動画ノード', retest: '再テスト', apply: '適用', enable: '有効', current: '現在', custom: 'カスタム' }
};
const t = I18N[locale] || I18N.zh;
const NODE_KEY = 'UseVideoNode';
const ENABLE_KEY = 'UseVideoNodeEnabled';
let currentNode = unsafeWindow.localStorage.getItem(NODE_KEY) || 'hime';
let isEnabled = unsafeWindow.localStorage.getItem(ENABLE_KEY) !== '0';
let nodeList = ['hime', 'mikoto'];
let pingMap = {};
async function testNodeSpeed(node) {
if (!node) return { node, time: 99999 };
const start = Date.now();
try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 7000);
await fetch(`https://${node}.iwara.tv/favicon.ico`, {
method: 'HEAD', mode: 'no-cors', signal: controller.signal
});
pingMap[node] = Math.min(Date.now() - start, 9999);
} catch {
pingMap[node] = 99999;
}
}
async function updatePing() {
const nodesToTest = [...new Set([...nodeList, currentNode])];
await Promise.all(nodesToTest.map(testNodeSpeed));
}
async function applyNode() {
unsafeWindow.localStorage.setItem(NODE_KEY, currentNode);
updatePanelDisplay();
const video = document.querySelector('video');
if (video?.src) {
const ct = video.currentTime || 0;
const playing = !video.paused;
video.src = video.src.replace(/\/\/[^.]+\.iwara\.tv/g, `//${currentNode}.iwara.tv`);
video.load();
if (playing) setTimeout(() => video.play().catch(() => {}), 300);
if (ct > 0) video.currentTime = ct;
return;
}
location.reload();
}
let panel = null;
function createPanel() {
if (panel) return;
panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
left: 16px;
bottom: 16px;
z-index: 2147483647;
background: rgba(0,0,0,0.9);
color: #fff;
padding: 8px;
border-radius: 8px;
font-size: 13px;
font-family: system-ui;
min-width: 140px;
box-shadow: 0 6px 18px rgba(0,0,0,0.75);
border: 1px solid #555;
`;
panel.innerHTML = `
<div style="font-weight:bold;margin-bottom:5px;font-size:13.5px;">${t.title}</div>
<div style="margin-bottom:6px;font-size:13px;color:#0f0;">
${t.current}: <strong id="currentNodeDisplay">${currentNode}</strong>
</div>
<label style="display:flex;align-items:center;gap:5px;margin-bottom:6px;cursor:pointer;">
<input type="checkbox" id="enable" ${isEnabled ? 'checked' : ''}>
<span style="font-size:13px;">${t.enable}</span>
</label>
<div style="margin:6px 0 7px 0;">
<select id="nodeSelect" style="width:100%;padding:4px 6px;border-radius:4px;background:#2a2a2a;color:#fff;border:1px solid #444;font-size:13px;"></select>
</div>
<div id="customContainer" style="display:none;margin:6px 0;">
<input type="text" id="manual" placeholder="自定义节点"
style="width:100%;padding:4px 6px;border-radius:4px;background:#2a2a2a;color:#fff;border:1px solid #444;font-size:13px;">
</div>
<!-- 更小的纵向按钮 -->
<div style="display:flex;flex-direction:column;gap:5px;margin-top:8px;">
<button id="testBtn" style="padding:5px 8px;background:#0066ff;color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;">
${t.retest}
</button>
<button id="applyBtn" style="padding:5px 8px;background:#00aa00;color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;">
${t.apply}
</button>
</div>
`;
document.documentElement.appendChild(panel);
const select = panel.querySelector('#nodeSelect');
const customContainer = panel.querySelector('#customContainer');
const manualInput = panel.querySelector('#manual');
function refreshSelect() {
select.innerHTML = '';
const nodes = [...new Set([...nodeList, currentNode])];
nodes.forEach(node => {
const ping = pingMap[node] ? ` (${pingMap[node]}ms)` : '';
const opt = new Option(node + ping, node);
if (node === currentNode) opt.selected = true;
select.appendChild(opt);
});
const customOpt = new Option(`→ ${t.custom}`, '__custom__');
select.appendChild(customOpt);
}
refreshSelect();
select.addEventListener('change', () => {
if (select.value === '__custom__') {
customContainer.style.display = 'block';
manualInput.focus();
} else {
customContainer.style.display = 'none';
currentNode = select.value;
unsafeWindow.localStorage.setItem(NODE_KEY, currentNode);
updatePanelDisplay();
}
});
manualInput.addEventListener('blur', () => {
const v = manualInput.value.trim().toLowerCase();
if (/^[a-z0-9.-]+$/.test(v)) {
currentNode = v;
unsafeWindow.localStorage.setItem(NODE_KEY, v);
updatePanelDisplay();
refreshSelect();
}
});
panel.querySelector('#enable').addEventListener('change', e => {
unsafeWindow.localStorage.setItem(ENABLE_KEY, e.target.checked ? '1' : '0');
location.reload();
});
panel.querySelector('#testBtn').addEventListener('click', async () => {
const btn = panel.querySelector('#testBtn');
btn.textContent = '测速中...';
btn.disabled = true;
await updatePing();
refreshSelect();
updatePanelDisplay();
btn.textContent = '✅ 完成';
setTimeout(() => { btn.textContent = t.retest; btn.disabled = false; }, 1200);
});
panel.querySelector('#applyBtn').addEventListener('click', applyNode);
}
function updatePanelDisplay() {
if (panel) {
const el = panel.querySelector('#currentNodeDisplay');
if (el) el.textContent = currentNode;
}
}
// Fetch Hook
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function (input, init) {
if (!isEnabled) return originalFetch(input, init);
const response = await originalFetch(input, init);
let urlStr = typeof input === 'string' ? input : (input?.url || '');
if (urlStr.includes('.iwara.tv') && urlStr.includes('/file')) {
const clone = response.clone();
try {
let body = await clone.json();
if (Array.isArray(body)) {
body.forEach(item => {
if (item.src) {
const regex = /\/\/[^.]+\.iwara\.tv/g;
if (item.src.download) item.src.download = item.src.download.replace(regex, `//${currentNode}.iwara.tv`);
if (item.src.view) item.src.view = item.src.view.replace(regex, `//${currentNode}.iwara.tv`);
}
});
}
return new Response(JSON.stringify(body), { status: clone.status, headers: clone.headers });
} catch (e) {}
}
return response;
};
// 初始化
setTimeout(createPanel, 600);
setTimeout(createPanel, 1500);
document.addEventListener('DOMContentLoaded', createPanel);
})();