// ==UserScript==
// @name Bangumi 直接看番
// @namespace http://tampermonkey.net/
// @version 0.8.7
// @description 在Bangumi番剧页面添加直接看番按钮,悬浮播放器播放,支持弹幕和截图
// @author NeetFlix
// @license MIT
// @match https://bgm.tv/subject/*
// @match https://bangumi.tv/subject/*
// @match https://chii.in/subject/*
// @match https://www.7sefun.top/*
// @match https://www.agedm.io/*
// @match https://dmbus.cc/*
// @match *://jx.ejtsyc.com/*
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @connect www.agedm.io
// @connect www.7sefun.top
// @connect dmbus.cc
// @connect api.dandanplay.net
// @connect cas2.dandanplay.net
// @connect bytetos.com
// @connect bytecdn.cn
// @connect bilibili.com
// @connect bilivideo.com
// @connect hdslb.com
// @connect mcdn.bilivideo.cn
// @connect akamaized.net
// @connect cloudfront.net
// @connect cdn.jsdelivr.net
// @run-at document-start
// @downloadURL https://update.greasyfork.icu/scripts/573437/Bangumi%20%E7%9B%B4%E6%8E%A5%E7%9C%8B%E7%95%AA.user.js
// @updateURL https://update.greasyfork.icu/scripts/573437/Bangumi%20%E7%9B%B4%E6%8E%A5%E7%9C%8B%E7%95%AA.meta.js
// ==/UserScript==
(function() {
'use strict';
const PLAYER_MODE_KEY = 'neetflix_player_mode';
const PLAYER_VIDEO_URL_KEY = 'neetflix_video_url';
const PLAYER_TITLE_KEY = 'neetflix_title';
const PLAYER_EPISODE_KEY = 'neetflix_episode';
const PLAYER_BGM_ID_KEY = 'neetflix_bgm_id';
const currentHost = window.location.hostname;
const isBangumi = currentHost.includes('bgm.tv') || currentHost.includes('bangumi.tv') || currentHost.includes('chii.in');
const isIframe = window.self !== window.top;
// 首先注入全局弹幕脚本(所有页面,包括iframe)
injectDanmakuScriptGlobal();
// 检查是否是视频网站页面
const isVideoSite = currentHost.includes('7sefun.top') || currentHost.includes('agedm.io') || currentHost.includes('dmbus.cc');
if (isVideoSite) {
// 视频网站页面需要执行播放模式检测
// iframe里需要延迟检测,因为GM API可能还没准备好
if (isIframe) {
let retryCount = 0;
function tryInitVideoSiteMode() {
const playerMode = GM_getValue(PLAYER_MODE_KEY, '');
if (playerMode || retryCount > 10) {
initVideoSiteMode();
} else {
retryCount++;
setTimeout(tryInitVideoSiteMode, 100);
}
}
tryInitVideoSiteMode();
} else {
initVideoSiteMode();
}
if (!isIframe) return;
}
// 如果是iframe,停止执行后续Bangumi页面逻辑
if (isIframe) {
return;
}
const Plugins = [
{
name: "7sefun",
baseUrl: "https://www.7sefun.top/",
searchUrl: "https://www.7sefun.top/vodsearch/-------------.html?wd=@keyword",
searchList: "//div[2]/div[2]/div[2]/div[2]/div",
searchName: "//div[2]/text()",
searchResult: "//a",
chapterRoads: "//div[2]/div[2]/div[2]/div/div[2]/div[1]/div[2]",
chapterResult: "//a",
useIframe: true,
needsProxy: false
},
{
name: "AGE",
baseUrl: "https://www.agedm.io/",
searchUrl: "https://www.agedm.io/search?query=@keyword",
searchList: "//div[2]/div/section/div/div/div/div",
searchName: "//div/div[2]/h5/a",
searchResult: "//div/div[2]/h5/a",
chapterRoads: "//div[2]/div/section/div/div[2]/div[2]/div[2]/div",
chapterResult: "//ul/li/a",
useIframe: true,
needsProxy: true
},
{
name: "DM84",
baseUrl: "https://dmbus.cc/",
searchUrl: "https://dmbus.cc/s----------.html?wd=@keyword",
searchList: "//div/div[3]/ul/li",
searchName: "//div/a[2]",
searchResult: "//div/a[2]",
chapterRoads: "//div/div[4]/div/ul",
chapterResult: "//li/a",
useIframe: true,
needsProxy: false
},
];
const UserAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
];
let currentPlayerState = {
visible: false,
url: '',
title: '',
episode: '',
roads: [],
currentRoad: 0,
currentEpisode: 0,
pluginName: '',
danmakuEnabled: true,
danmakuOpacity: 0.8,
danmakuFontSize: 24,
danmakuSpeed: 8
};
function getRandomUA() {
return UserAgents[Math.floor(Math.random() * UserAgents.length)];
}
// 全局弹幕脚本注入(在所有页面运行,包括iframe)
function injectDanmakuScriptGlobal() {
const script = document.createElement('script');
script.textContent = `
(function() {
if (window.neetflixDanmakuInjected) return;
window.neetflixDanmakuInjected = true;
var isIframe = (window.self !== window.top);
console.log('[NeetFlix Danmaku] Script loaded, isIframe=' + isIframe);
// ========== IFRAME 逻辑:发送视频时间给父窗口 ==========
if (isIframe) {
var iframeVideo = null;
var iframePaused = true;
var hasVideo = false;
// 监听来自子iframe的时间更新并转发给父窗口
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'neetflixTimeUpdate' && window.parent !== window) {
// 从子iframe收到时间,转发给父窗口
try {
window.parent.postMessage({
type: 'neetflixTimeUpdate',
time: e.data.time,
playbackRate: e.data.playbackRate,
paused: e.data.paused,
_fromChild: true
}, '*');
} catch(err) {}
}
if (e.data && e.data.type === 'neetflixRequestTime' && iframeVideo) {
try {
e.source.postMessage({
type: 'neetflixTimeUpdate',
time: iframeVideo.currentTime,
playbackRate: iframeVideo.playbackRate,
paused: iframeVideo.paused
}, '*');
} catch(err) {}
}
// 转发截图命令到子iframe
if (e.data && e.data.type === 'neetflixScreenshot') {
console.log('[NeetFlix] Forwarding screenshot command to child iframe');
var iframes = document.querySelectorAll('iframe');
iframes.forEach(function(iframe) {
try {
iframe.contentWindow.postMessage({
type: 'neetflixScreenshot'
}, '*');
} catch(err) {
console.log('[NeetFlix] Cannot forward to iframe:', err.message);
}
});
// 尝试在当前页面截图
if (iframeVideo) {
console.log('[NeetFlix] Attempting screenshot on current video');
try {
var canvas = document.createElement('canvas');
canvas.width = iframeVideo.videoWidth || 640;
canvas.height = iframeVideo.videoHeight || 360;
var ctx = canvas.getContext('2d');
ctx.drawImage(iframeVideo, 0, 0, canvas.width, canvas.height);
var dataUrl = canvas.toDataURL('image/png');
console.log('[NeetFlix] Screenshot success, data length:', dataUrl.length);
// 发送截图结果给父窗口
window.parent.postMessage({
type: 'neetflixScreenshotResult',
dataUrl: dataUrl
}, '*');
} catch(err) {
console.log('[NeetFlix] Screenshot failed (CORS):', err.message);
// 通知父窗口截图失败,需要使用特权方式
window.parent.postMessage({
type: 'neetflixScreenshotFailed',
videoSrc: iframeVideo.src,
currentTime: iframeVideo.currentTime
}, '*');
}
}
}
// 转发截图结果到顶层窗口
if (e.data && e.data.type === 'neetflixScreenshotResult') {
console.log('[NeetFlix] Forwarding screenshot result to parent');
window.parent.postMessage({
type: 'neetflixScreenshotResult',
dataUrl: e.data.dataUrl
}, '*');
}
// 转发截图失败消息到顶层窗口
if (e.data && e.data.type === 'neetflixScreenshotFailed') {
console.log('[NeetFlix] Forwarding screenshot failure to parent');
window.parent.postMessage({
type: 'neetflixScreenshotFailed',
videoSrc: e.data.videoSrc,
currentTime: e.data.currentTime
}, '*');
}
});
function sendTime() {
if (iframeVideo && hasVideo) {
try {
window.parent.postMessage({
type: 'neetflixTimeUpdate',
time: iframeVideo.currentTime,
playbackRate: iframeVideo.playbackRate,
paused: iframeVideo.paused
}, '*');
} catch(e) {}
}
requestAnimationFrame(sendTime);
}
function findAndWatchVideo() {
var videos = document.querySelectorAll('video');
if (videos.length > 0) {
iframeVideo = videos[0];
iframePaused = iframeVideo.paused;
hasVideo = true;
iframeVideo.addEventListener('pause', function() { iframePaused = true; });
iframeVideo.addEventListener('play', function() { iframePaused = false; });
sendTime();
console.log('[NeetFlix Danmaku] Iframe: video found, sending time to parent');
return true;
}
return false;
}
function tryFind() {
if (findAndWatchVideo()) return;
// 监听DOM变化,检测动态加载的video和iframe
var observer = new MutationObserver(function(mutations) {
if (findAndWatchVideo()) {
observer.disconnect();
return;
}
// DOM变化了,检查嵌套iframe
checkIframes();
});
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
}
function checkIframes() {
var iframes = document.querySelectorAll('iframe');
iframes.forEach(function(iframe) {
// 监听iframe加载完成
iframe.addEventListener('load', function() {
console.log('[NeetFlix Danmaku] Iframe loaded, checking for video...');
setTimeout(function() {
findAndWatchVideo();
sendRequestToIframe(iframe);
}, 500);
});
// 也立即尝试发送请求
sendRequestToIframe(iframe);
});
}
function sendRequestToIframe(iframe) {
try {
var childWin = iframe.contentWindow;
if (childWin) {
childWin.postMessage({
type: 'neetflixRequestTime',
from: 'parent'
}, '*');
console.log('[NeetFlix Danmaku] Sent time request to iframe');
}
} catch(e) {
console.log('[NeetFlix Danmaku] Cannot access iframe (cross-origin):', e.message);
}
}
// 立即检查一次
checkIframes();
// 持续尝试
var retryCount = 0;
var maxRetries = 20;
function retry() {
if (hasVideo) return;
retryCount++;
checkIframes();
if (retryCount < maxRetries) {
setTimeout(retry, 1000);
} else {
console.log('[NeetFlix Danmaku] Iframe: giving up after ' + maxRetries + ' retries');
}
}
retry();
}
if (document.readyState === 'complete') {
tryFind();
} else {
document.addEventListener('DOMContentLoaded', tryFind);
}
return;
}
// ========== 顶层窗口逻辑:接收时间,渲染弹幕 ==========
var canvas = null;
var ctx = null;
var danmakuList = [];
var danmakuData = {};
var enabled = true;
var opacity = 0.8;
var fontSize = 24;
var speed = 8;
var lineHeight = 1.5;
var lineCount = 12;
var lastTime = 0;
var currentVideoTime = 0;
var currentPlaybackRate = 1;
var currentVideoPaused = false;
var lastVideoSecond = -1;
// 监听来自iframe的时间更新
var messageCount = 0;
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'neetflixTimeUpdate') {
currentVideoTime = e.data.time;
if (e.data.playbackRate) currentPlaybackRate = e.data.playbackRate;
if (e.data.paused !== undefined) currentVideoPaused = e.data.paused;
messageCount++;
if (messageCount <= 5 || messageCount % 60 === 0) {
console.log('[NeetFlix Danmaku] Time update: ' + currentVideoTime.toFixed(2) + 's, paused=' + currentVideoPaused);
}
}
// 接收弹幕数据
if (e.data && e.data.type === 'neetflixLoadDanmaku') {
danmakuData = e.data.data || {};
danmakuList = [];
lastVideoSecond = -1;
console.log('[NeetFlix Danmaku] Loaded ' + Object.keys(danmakuData).length + ' seconds of danmaku');
// 更新窗口模式的弹幕数量
var countSpan = document.getElementById('neetflix-danmaku-count');
if (countSpan) {
var totalCount = 0;
for (var key in danmakuData) {
totalCount += danmakuData[key].length;
}
countSpan.textContent = '(' + totalCount + ')';
}
}
// 接收弹幕开关(悬浮播放器模式)
if (e.data && e.data.type === 'neetflixSetDanmakuEnabled') {
enabled = e.data.enabled;
if (!enabled) danmakuList = [];
}
// 接收弹幕开关(窗口模式)
if (e.data && e.data.type === 'neetflixWindowSetDanmaku') {
enabled = e.data.enabled;
if (!enabled) danmakuList = [];
// 更新按钮状态
var danmakuBtn = document.getElementById('neetflix-danmaku-toggle');
if (danmakuBtn) {
if (enabled) {
danmakuBtn.classList.add('active');
} else {
danmakuBtn.classList.remove('active');
}
}
}
});
// 也尝试在顶层窗口找video
var foundVideo = null;
function findVideoInTop() {
var videos = document.querySelectorAll('video');
if (videos.length > 0) {
foundVideo = videos[0];
console.log('[NeetFlix Danmaku] Top window: found video');
function trackTime() {
if (foundVideo) {
currentVideoTime = foundVideo.currentTime;
currentPlaybackRate = foundVideo.playbackRate;
currentVideoPaused = foundVideo.paused;
}
requestAnimationFrame(trackTime);
}
trackTime();
}
}
var playerContainer = null;
var playerMode = false;
function initCanvas() {
if (!document.body) {
setTimeout(initCanvas, 100);
return;
}
// 检测是否是iframe模式(播放器是否存在)
playerContainer = document.getElementById('neetflix-player');
playerMode = !!playerContainer;
// 如果canvas已存在,需要检查位置是否正确
if (canvas) {
// 如果播放器已创建但canvas不在播放器内,重新定位
if (playerMode) {
var playerBody = document.querySelector('#neetflix-player .neetflix-player-body');
if (playerBody && canvas.parentElement !== playerBody) {
console.log('[NeetFlix Danmaku] Moving canvas to player body');
canvas.remove();
canvas = null;
ctx = null;
}
}
// 如果canvas位置正确,不需要重新初始化
if (canvas) return;
}
canvas = document.createElement('canvas');
canvas.id = 'neetflix-danmaku-canvas';
if (playerMode) {
// iframe模式:canvas放在播放器内部
var playerBody = document.querySelector('#neetflix-player .neetflix-player-body');
if (playerBody) {
canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10;';
playerBody.appendChild(canvas);
console.log('[NeetFlix Danmaku] Canvas added to player body (iframe mode)');
} else {
document.body.appendChild(canvas);
}
} else {
// 窗口模式:canvas覆盖全屏
canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;';
document.body.appendChild(canvas);
}
ctx = canvas.getContext('2d');
resize();
window.addEventListener('resize', function() {
resize();
});
// 监听播放器大小变化
if (playerMode && playerContainer) {
var resizeObserver = new ResizeObserver(function() {
// 拖动时暂停resize,防止弹幕闪烁
if (window.playerDragging) return;
resize();
});
// 观察播放器body
var playerBody = playerContainer.querySelector('.neetflix-player-body');
if (playerBody) {
resizeObserver.observe(playerBody);
}
resizeObserver.observe(playerContainer);
}
document.addEventListener('fullscreenchange', function() {
var fullscreenEl = document.fullscreenElement;
if (fullscreenEl && canvas) {
if (canvas.parentElement !== fullscreenEl) {
fullscreenEl.appendChild(canvas);
resize();
}
} else if (!fullscreenEl && canvas && canvas.parentElement !== document.body && canvas.parentElement !== playerContainer) {
if (playerMode && playerContainer) {
var playerBody = document.querySelector('#neetflix-player .neetflix-player-body');
if (playerBody) playerBody.appendChild(canvas);
} else {
document.body.appendChild(canvas);
}
resize();
}
});
lastTime = performance.now();
animate();
console.log('[NeetFlix Danmaku] Canvas initialized (playerMode=' + playerMode + ')');
}
function resize() {
if (!canvas || !ctx) return;
if (playerMode && playerContainer) {
var playerBody = playerContainer.querySelector('.neetflix-player-body');
if (playerBody) {
var w = playerBody.clientWidth || 640;
var h = playerBody.clientHeight || 360;
canvas.width = w;
canvas.height = h;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
}
} else {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
}
lineCount = Math.floor(canvas.height / (fontSize * lineHeight));
}
function animate() {
if (!ctx || !canvas) {
requestAnimationFrame(animate);
return;
}
var now = performance.now();
var delta = (now - lastTime) / 1000;
lastTime = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (enabled) {
for (var i = danmakuList.length - 1; i >= 0; i--) {
var d = danmakuList[i];
if (!currentVideoPaused) {
d.x -= d.speed * delta;
}
if (d.x + d.width < 0) {
danmakuList.splice(i, 1);
continue;
}
ctx.globalAlpha = opacity;
ctx.font = 'bold ' + fontSize + 'px Microsoft YaHei, SimHei, sans-serif';
ctx.fillStyle = d.color;
ctx.strokeStyle = 'rgba(0,0,0,0.8)';
ctx.lineWidth = 2;
ctx.strokeText(d.text, d.x, d.y);
ctx.fillText(d.text, d.x, d.y);
}
}
requestAnimationFrame(animate);
}
function findLine() {
var startY = fontSize * lineHeight;
var maxLine = Math.min(lineCount, Math.floor((canvas.height - startY) / (fontSize * lineHeight)));
if (maxLine <= 0) maxLine = 1;
for (var i = 0; i < maxLine; i++) {
var available = true;
for (var j = 0; j < danmakuList.length; j++) {
var d = danmakuList[j];
if (d.line === i && d.x + d.width > canvas.width * 0.8) {
available = false;
break;
}
}
if (available) return i;
}
return Math.floor(Math.random() * maxLine);
}
var danmakuShownCount = 0;
function showDanmaku(text, color, type) {
if (!enabled || !ctx || !canvas) return;
ctx.font = 'bold ' + fontSize + 'px Microsoft YaHei, SimHei, sans-serif';
var metrics = ctx.measureText(text);
var width = metrics.width;
var line = findLine();
var y = fontSize * lineHeight + line * fontSize * lineHeight + fontSize / 2;
var d = {
text: text,
color: color || '#FFFFFF',
x: canvas.width,
y: y,
width: width,
speed: (canvas.width + width) / speed,
line: line,
type: type || 1
};
danmakuList.push(d);
danmakuShownCount++;
if (danmakuShownCount <= 5) {
console.log('[NeetFlix Danmaku] showDanmaku: "' + text + '", total: ' + danmakuShownCount);
}
}
function showDanmakusForSecond(second) {
if (!danmakuData[second]) return;
danmakuData[second].forEach(function(d, idx) {
setTimeout(function() {
showDanmaku(d.message, d.color, d.type);
}, idx * 100);
});
}
function checkDanmakuTime() {
var intTime = Math.floor(currentVideoTime);
if (intTime !== lastVideoSecond) {
if (intTime % 10 === 0) {
console.log('[NeetFlix Danmaku] Time: ' + intTime + 's, paused: ' + currentVideoPaused);
}
if (!currentVideoPaused && danmakuData[intTime]) {
console.log('[NeetFlix Danmaku] Showing danmaku for second ' + intTime);
showDanmakusForSecond(intTime);
}
lastVideoSecond = intTime;
}
}
function mainLoop() {
if (!foundVideo) {
findVideoInTop();
}
checkDanmakuTime();
requestAnimationFrame(mainLoop);
}
function start() {
initCanvas();
mainLoop();
// 监听播放器出现(iframe模式)
// 需要观察整个document,因为播放器是动态创建的
setTimeout(function() {
console.log('[NeetFlix Danmaku] Setting up player observer...');
var playerObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// 检查class属性变化(播放器显示/隐藏)
// 只在播放器从隐藏变为显示时重新初始化,忽略拖动等class变化
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
var target = mutation.target;
if (target.id === 'neetflix-player') {
var oldValue = mutation.oldValue || '';
var wasHidden = !oldValue.includes('show');
var isNowShown = target.classList.contains('show');
// 只有从隐藏变为显示时才重新初始化
if (wasHidden && isNowShown) {
console.log('[NeetFlix Danmaku] Player shown, reinitializing canvas');
if (canvas) {
canvas.remove();
canvas = null;
ctx = null;
danmakuList = [];
}
setTimeout(function() {
initCanvas();
setTimeout(resize, 100);
}, 300);
}
}
}
// 检查新增节点
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
console.log('[NeetFlix Danmaku] DOM changed, added nodes:', mutation.addedNodes.length);
mutation.addedNodes.forEach(function(node) {
if (node.id === 'neetflix-player') {
console.log('[NeetFlix Danmaku] Player detected, reinitializing canvas');
if (canvas) {
canvas.remove();
canvas = null;
ctx = null;
danmakuList = [];
}
setTimeout(function() {
initCanvas();
setTimeout(resize, 100);
}, 300);
}
});
}
});
});
// 观察整个document,需要 attributeOldValue 来判断 class 变化
try {
playerObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], attributeOldValue: true });
console.log('[NeetFlix Danmaku] Player observer started on documentElement');
} catch(e) {
console.error('[NeetFlix Danmaku] Observer error:', e);
}
// 也观察body
if (document.body) {
playerObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
console.log('[NeetFlix Danmaku] Player observer started on body');
}
// 直接检查播放器是否已存在(可能是页面加载时创建的)
var existingPlayer = document.getElementById('neetflix-player');
if (existingPlayer) {
console.log('[NeetFlix Danmaku] Player already exists in DOM');
// 如果播放器有show类,说明用户已经打开过播放器了
if (existingPlayer.classList.contains('show')) {
console.log('[NeetFlix Danmaku] Player already visible');
setTimeout(function() {
initCanvas();
setTimeout(resize, 100);
}, 300);
}
}
// 定时检查播放器是否存在(MutationObserver的备选方案)
var checkCount = 0;
var checkInterval = setInterval(function() {
checkCount++;
var player = document.getElementById('neetflix-player');
if (player) {
console.log('[NeetFlix Danmaku] Periodic check found player after ' + checkCount + ' checks');
clearInterval(checkInterval);
if (canvas) {
canvas.remove();
canvas = null;
ctx = null;
danmakuList = [];
}
setTimeout(function() {
initCanvas();
setTimeout(resize, 100);
}, 300);
}
// 最多检查60次(60秒)
if (checkCount >= 60) {
clearInterval(checkInterval);
}
}, 1000);
}, 100);
}
if (document.body) {
start();
} else {
document.addEventListener('DOMContentLoaded', start);
}
console.log('[NeetFlix Danmaku] Top window script initialized');
})();
`;
if (document.head) {
document.head.appendChild(script);
} else {
document.addEventListener('DOMContentLoaded', () => {
document.head.appendChild(script);
});
}
}
function initVideoSiteMode() {
const playerMode = GM_getValue(PLAYER_MODE_KEY, '');
const videoUrl = (GM_getValue(PLAYER_VIDEO_URL_KEY, '') || '').replace(/,+$/, '');
const title = GM_getValue(PLAYER_TITLE_KEY, '');
const episode = GM_getValue(PLAYER_EPISODE_KEY, '');
console.log(`[NeetFlix VideoSite] mode=${playerMode}, url=${videoUrl}, title="${title}", episode="${episode}", isIframe=${isIframe}, href=${window.location.href}`);
// 如果没有playerMode,无条件注入(支持窗口模式下切换集数)
if (!playerMode && !isIframe) {
const isVideoSite = currentHost.includes('agedm.io') ||
currentHost.includes('7sefun.top') ||
currentHost.includes('dmbus.cc');
if (isVideoSite) {
console.log('[NeetFlix] Window mode (no GM value), injecting...');
window.playerModeActive = false;
injectPlayerModeCSS();
injectWindowModeUI();
return;
}
}
// 如果有playerMode设置,说明需要激活播放器
if (playerMode) {
console.log(`[NeetFlix] Player mode: ${playerMode}, isIframe: ${isIframe}`);
if (playerMode === 'iframe' && isIframe) {
// 7sefun/DM84: 当前页面是被嵌入的播放页面
console.log('[NeetFlix] Iframe mode detected, injecting CSS...');
// 清除之前的标志,允许重新注入(切换集数时需要)
window.playerModeActive = false;
injectPlayerModeCSS();
return;
}
if (playerMode === 'window' && !isIframe) {
// AGE新窗口: 只要在支持的视频网站上就激活
const currentUrl = window.location.href.replace(/,+$/, '');
console.log(`[NeetFlix] Window mode check: currentUrl="${currentUrl}"`);
// 检查是否在支持的视频网站
const isVideoSite = currentHost.includes('agedm.io') ||
currentHost.includes('7sefun.top') ||
currentHost.includes('dmbus.cc');
if (isVideoSite) {
console.log('[NeetFlix] Window mode detected, injecting CSS...');
injectPlayerModeCSS();
injectWindowModeUI();
if (title) {
setTimeout(() => loadDanmakuForTitle(title, episode), 1500);
}
} else {
console.log('[NeetFlix] Not a supported video site, skipping');
}
return;
}
}
// 无论是否有playerMode设置,只要是播放页面,都尝试注入播放器样式
// 这样可以处理嵌套iframe的情况
if (isIframe) {
checkAndInjectPlayerMode();
}
}
function checkAndInjectPlayerMode() {
// 对于支持的视频网站,无条件注入(切换集数后GM值可能还没同步)
if (!isBangumi) {
const isVideoPage = currentHost.includes('agedm.io') ||
currentHost.includes('7sefun.top') ||
currentHost.includes('dmbus.cc');
if (isVideoPage) {
console.log('[NeetFlix] Video page detected, injecting CSS...');
injectPlayerModeCSS();
}
}
}
// 监听iframe加载完成事件
window.addEventListener('load', function() {
const playerMode = GM_getValue(PLAYER_MODE_KEY, '');
if (playerMode) {
console.log('[NeetFlix] Window load, playerMode=' + playerMode + ', isIframe=' + isIframe);
if (playerMode === 'iframe' && isIframe) {
// 清除之前的标志,允许重新注入
window.playerModeActive = false;
injectPlayerModeCSS();
}
if (playerMode === 'window' && !isIframe) {
const isVideoSite = currentHost.includes('agedm.io') ||
currentHost.includes('7sefun.top') ||
currentHost.includes('dmbus.cc');
if (isVideoSite) {
window.playerModeActive = false;
injectPlayerModeCSS();
injectWindowModeUI();
}
}
}
});
function injectPlayerModeCSS() {
if (window.playerModeActive) return;
window.playerModeActive = true;
// 清除playerMode,避免重复注入
GM_setValue(PLAYER_MODE_KEY, '');
const style = document.createElement('style');
style.id = 'playerModeStyle';
style.textContent = `
html, body {
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
background: #000 !important;
}
video, iframe {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
border: none !important;
}
video {
z-index: 999999 !important;
object-fit: contain !important;
}
iframe {
z-index: 999998 !important;
}
`;
if (document.head) {
document.head.appendChild(style);
} else {
document.addEventListener('DOMContentLoaded', () => {
document.head.appendChild(style);
});
}
function findAndResizeVideo() {
const videos = document.querySelectorAll('video');
videos.forEach(v => {
v.style.position = 'fixed';
v.style.top = '0';
v.style.left = '0';
v.style.width = '100%';
v.style.height = '100%';
v.style.objectFit = 'contain';
v.style.zIndex = '999999';
});
const iframes = document.querySelectorAll('iframe');
iframes.forEach(f => {
f.style.position = 'fixed';
f.style.top = '0';
f.style.left = '0';
f.style.width = '100%';
f.style.height = '100%';
f.style.border = 'none';
f.style.zIndex = '999998';
});
}
findAndResizeVideo();
setTimeout(findAndResizeVideo, 500);
setTimeout(findAndResizeVideo, 1000);
setTimeout(findAndResizeVideo, 2000);
const startObserver = () => {
if (document.body) {
const observer = new MutationObserver(findAndResizeVideo);
observer.observe(document.body, { childList: true, subtree: true });
} else {
setTimeout(startObserver, 100);
}
};
startObserver();
console.log('[NeetFlix] Player mode activated');
}
function injectWindowModeUI() {
const style = document.createElement('style');
style.textContent = `
.neetflix-window-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
z-index: 1000000;
}
.neetflix-window-title {
color: white;
font-weight: bold;
font-size: 14px;
}
.neetflix-window-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 8px;
}
.neetflix-window-btn:hover {
background: rgba(255,255,255,0.3);
}
.neetflix-window-btn.active {
background: rgba(255,255,255,0.5);
}
`;
if (document.head) {
document.head.appendChild(style);
}
const addHeader = () => {
if (!document.body) {
setTimeout(addHeader, 50);
return;
}
const header = document.createElement('div');
header.className = 'neetflix-window-header';
header.innerHTML = `
NeetFlix 播放器
`;
document.body.appendChild(header);
document.getElementById('neetflix-back-btn').addEventListener('click', () => {
window.close();
});
// 选集按钮
document.getElementById('neetflix-episode-toggle').addEventListener('click', (e) => {
var roads = GM_getValue('neetflix_roads', null);
var currentRoad = GM_getValue('neetflix_current_road', 0);
var currentEp = GM_getValue('neetflix_current_episode', 0);
if (!roads || roads.length === 0) {
alert('暂无选集信息');
return;
}
var road = roads[currentRoad];
if (!road || !road.episodes) {
alert('暂无选集信息');
return;
}
var html = '';
html += '
' + road.name + '
';
road.episodes.forEach(function(ep, idx) {
var isActive = (idx === currentEp) ? 'background:#667eea;' : '';
html += '
' + ep.name + '
';
});
html += '
';
var popup = document.getElementById('neetflix-episode-popup');
if (popup) popup.remove();
popup = document.createElement('div');
popup.id = 'neetflix-episode-popup';
popup.innerHTML = html;
popup.style.cssText = 'position:fixed;top:50px;right:10px;width:200px;background:#1a1a2e;border:1px solid #333;border-radius:8px;z-index:999999;color:#fff;';
document.body.appendChild(popup);
popup.querySelectorAll('.neetflix-window-episode').forEach(function(el) {
el.addEventListener('click', function() {
var url = this.dataset.url;
var idx = parseInt(this.dataset.idx);
var epName = this.textContent;
GM_setValue('neetflix_current_episode', idx);
GM_setValue(PLAYER_EPISODE_KEY, epName);
window.location.href = url;
});
});
});
// 点击其他地方关闭选集弹窗
document.addEventListener('click', function(e) {
if (e.target.id !== 'neetflix-episode-toggle') {
var popup = document.getElementById('neetflix-episode-popup');
if (popup) popup.remove();
}
});
document.getElementById('neetflix-danmaku-toggle').addEventListener('click', (e) => {
var btn = e.currentTarget;
var isActive = btn.classList.contains('active');
isActive = !isActive;
btn.classList.toggle('active', isActive);
// 保存状态
GM_setValue('neetflix_danmaku_enabled', isActive);
// 发送弹幕开关消息到脚本
window.postMessage({
type: 'neetflixWindowSetDanmaku',
enabled: isActive
}, '*');
});
document.body.style.paddingTop = '40px';
// 读取当前弹幕状态并更新按钮
const danmakuEnabled = GM_getValue('neetflix_danmaku_enabled', true);
const danmakuBtn = document.getElementById('neetflix-danmaku-toggle');
if (danmakuBtn && !danmakuEnabled) {
danmakuBtn.classList.remove('active');
}
};
// 如果有集数信息,触发弹幕加载
const danmakuTitle = GM_getValue(PLAYER_TITLE_KEY, '');
const danmakuEpisode = GM_getValue(PLAYER_EPISODE_KEY, '');
const danmakuBgmId = GM_getValue(PLAYER_BGM_ID_KEY, null);
console.log('[NeetFlix Window] Danmaku title: "' + danmakuTitle + '", episode: "' + danmakuEpisode + '", bgmId: ' + danmakuBgmId);
if (danmakuTitle) {
console.log('[NeetFlix Window] Triggering danmaku load');
setTimeout(function() {
loadDanmakuForTitle(danmakuTitle, danmakuEpisode, danmakuBgmId);
}, 1500);
}
addHeader();
}
async function loadDanmakuForTitle(title, episode, bgmBangumiId) {
console.log(`[NeetFlix] Loading danmaku for: ${title} episode ${episode} bgmId: ${bgmBangumiId}`);
let animeId = null;
try {
if (bgmBangumiId) {
animeId = await danmakuGetByBgmId(bgmBangumiId);
}
if (!animeId && title) {
const searchResults = await danmakuSearch(title);
if (searchResults.length > 0) {
const bestMatch = searchResults[0];
animeId = bestMatch.animeId;
console.log(`[NeetFlix] Best match by title: ${bestMatch.animeTitle} (ID: ${animeId})`);
}
}
if (!animeId) {
console.log('[NeetFlix] No danmaku found');
return;
}
const episodes = await danmakuGetEpisodes(animeId);
if (episodes.length === 0) {
console.log('[NeetFlix] No episodes found');
return;
}
const epNumMatch = episode.match(/(\d+)/);
const epNum = epNumMatch ? parseInt(epNumMatch[1]) : 1;
console.log(`[NeetFlix] Extracted episode number: ${epNum} from "${episode}"`);
let targetEpisode = episodes.find(e =>
e.episodeTitle.includes(`第${epNum}集`) ||
e.episodeTitle.includes(`第${epNum}话`) ||
e.episodeTitle.includes(`${epNum}`) ||
e.episodeTitle === epNum.toString()
);
if (!targetEpisode) {
targetEpisode = episodes[Math.min(epNum - 1, episodes.length - 1)];
}
console.log(`[NeetFlix] Target episode: ${targetEpisode.episodeTitle} (ID: ${targetEpisode.episodeId})`);
const danmakus = await danmakuGetComments(targetEpisode.episodeId);
const data = {};
danmakus.forEach(d => {
const second = Math.floor(d.time);
if (!data[second]) data[second] = [];
data[second].push({
message: d.message,
color: d.color,
type: d.type
});
});
window.postMessage({
type: 'neetflixLoadDanmaku',
data: data
}, '*');
console.log(`[NeetFlix] Sent ${danmakus.length} danmakus to player`);
} catch (e) {
console.error('[NeetFlix] Load danmaku error:', e);
}
}
function generateDanmakuSignature(path, timestamp) {
const appId = 'kvpx7qkqjh';
const appValue = 'rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax';
const data = appId + timestamp.toString() + path + appValue;
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
return crypto.subtle.digest('SHA-256', dataBuffer).then(hashBuffer => {
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashBase64 = btoa(String.fromCharCode.apply(null, hashArray));
return hashBase64;
});
}
async function danmakuFetch(url) {
const appId = 'kvpx7qkqjh';
const uri = new URL(url);
const path = uri.pathname;
const timestamp = Math.floor(Date.now() / 1000);
const signature = await generateDanmakuSignature(path, timestamp);
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json',
'X-Auth': '1',
'X-AppId': appId,
'X-Timestamp': timestamp.toString(),
'X-Signature': signature
},
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
resolve(JSON.parse(response.responseText));
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: reject
});
});
}
async function danmakuSearch(keyword) {
const url = `https://api.dandanplay.net/api/v2/search/anime?keyword=${encodeURIComponent(keyword)}`;
const json = await danmakuFetch(url);
const results = [];
if (json.animes) {
json.animes.forEach(a => {
results.push({
animeId: a.animeId,
animeTitle: a.animeTitle
});
});
}
return results;
}
async function danmakuGetByBgmId(bgmBangumiId) {
const url = `https://api.dandanplay.net/api/v2/bangumi/bgmtv/${bgmBangumiId}`;
try {
const json = await danmakuFetch(url);
console.log('[NeetFlix] danmakuGetByBgmId response:', JSON.stringify(json).substring(0, 500));
if (json.bangumi && json.bangumi.animeId) {
console.log(`[NeetFlix] Found danmaku by BgmId: ${json.bangumi.animeId}`);
return json.bangumi.animeId;
}
} catch (e) {
console.log(`[NeetFlix] danmakuGetByBgmId failed: ${e.message}`);
}
return null;
}
async function danmakuGetEpisodes(animeId) {
const url = `https://api.dandanplay.net/api/v2/bangumi/${animeId}`;
const json = await danmakuFetch(url);
const results = [];
if (json.bangumi && json.bangumi.episodes) {
json.bangumi.episodes.forEach(e => {
results.push({
episodeId: e.episodeId,
episodeTitle: e.episodeTitle
});
});
}
return results;
}
async function danmakuGetComments(episodeId) {
const url = `https://api.dandanplay.net/api/v2/comment/${episodeId}?withRelated=true`;
const json = await danmakuFetch(url);
const results = [];
if (json.comments) {
json.comments.forEach(c => {
if (c.p) {
const parts = c.p.split(',');
if (parts.length >= 4) {
const time = parseFloat(parts[0]) || 0;
const type = parseInt(parts[1]) || 1;
const colorValue = parseInt(parts[2]) || 16777215;
const r = (colorValue >> 16) & 0xFF;
const g = (colorValue >> 8) & 0xFF;
const b = colorValue & 0xFF;
const color = `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
results.push({
message: c.m || '',
time: time,
type: type,
color: color
});
}
}
});
}
return results;
}
function getBangumiName() {
let nameCn = '';
let nameJp = '';
const infobox = document.querySelector('#infobox') || document.querySelector('.infobox');
if (infobox) {
const rows = infobox.querySelectorAll('li');
for (const row of rows) {
const tipEl = row.querySelector('.tip') || row.querySelector('span');
if (tipEl) {
const tipText = tipEl.textContent || '';
if (tipText.includes('中文名')) {
const fullText = row.textContent || '';
nameCn = fullText.replace(tipText, '').trim();
console.log(`[NeetFlix] Found 中文名: "${nameCn}" from "${fullText}"`);
break;
}
}
}
}
const titleEl = document.querySelector('h1.nameSingle a');
if (titleEl) {
nameJp = titleEl.textContent.trim();
}
if (!nameJp) {
const h1El = document.querySelector('h1');
if (h1El) {
const link = h1El.querySelector('a');
nameJp = link ? link.textContent.trim() : h1El.textContent.trim();
}
}
const displayName = nameCn || nameJp || document.title.split(' ')[0];
console.log(`[NeetFlix] NameCn: "${nameCn}", NameJp: "${nameJp}", DisplayName: "${displayName}"`);
return displayName;
}
function getBangumiId() {
const match = window.location.pathname.match(/\/subject\/(\d+)/);
return match ? match[1] : null;
}
function createButton() {
const btn = document.createElement('button');
btn.id = 'neetflix-play-btn';
btn.textContent = '直接看番';
btn.style.cssText = `
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 24px;
border-radius: 25px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
margin-left: 10px;
`;
btn.onmouseover = () => {
btn.style.transform = 'translateY(-2px)';
btn.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
};
btn.onmouseout = () => {
btn.style.transform = 'translateY(0)';
btn.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
};
return btn;
}
function createPanel() {
const panel = document.createElement('div');
panel.id = 'neetflix-panel';
panel.innerHTML = `
`;
return panel;
}
function createPlayer() {
const player = document.createElement('div');
player.id = 'neetflix-player';
player.innerHTML = `
`;
return player;
}
function addStyles() {
GM_addStyle(`
#neetflix-panel {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99999;
}
#neetflix-panel.show {
display: block;
}
.neetflix-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
}
.neetflix-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1a1a2e;
border-radius: 16px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
.neetflix-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.neetflix-header h3 {
margin: 0;
font-size: 18px;
}
.neetflix-close {
background: none;
border: none;
color: white;
font-size: 28px;
cursor: pointer;
line-height: 1;
padding: 0;
width: 30px;
height: 30px;
}
.neetflix-close:hover {
opacity: 0.8;
}
.neetflix-body {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
color: #fff;
}
.neetflix-search-keyword {
background: #252540;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
color: #888;
}
.neetflix-search-keyword strong {
color: #667eea;
font-size: 16px;
}
.neetflix-loading {
text-align: center;
padding: 40px;
color: #888;
}
.neetflix-source {
margin-bottom: 20px;
border: 1px solid #333;
border-radius: 8px;
overflow: hidden;
}
.neetflix-source-header {
background: #252540;
padding: 12px 16px;
font-weight: bold;
color: #667eea;
}
.neetflix-results {
padding: 10px;
}
.neetflix-result-item {
padding: 10px 12px;
margin: 5px 0;
background: #2a2a4a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.neetflix-result-item:hover {
background: #3a3a5a;
transform: translateX(5px);
}
.neetflix-episodes {
display: none;
padding: 10px;
background: #1e1e36;
}
.neetflix-episodes.show {
display: block;
}
.neetflix-episode {
display: inline-block;
padding: 8px 14px;
margin: 4px;
background: #3a3a5a;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.neetflix-episode:hover {
background: #667eea;
}
.neetflix-episode.playing {
background: #667eea;
box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
}
.neetflix-error {
color: #ff6b6b;
text-align: center;
padding: 20px;
}
#neetflix-player {
display: none;
position: fixed;
width: 640px;
height: 400px;
background: #0a0a14;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
z-index: 100000;
overflow: hidden;
min-width: 320px;
min-height: 200px;
}
#neetflix-player.show {
display: flex;
flex-direction: column;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
#neetflix-player.show.dragging {
transform: translate(-50%, -50%);
transition: none;
}
#neetflix-player.fullscreen {
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0;
transform: none;
}
.neetflix-player-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: move;
flex-shrink: 0;
}
.neetflix-player-title {
color: white;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.neetflix-player-controls {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.neetflix-player-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.neetflix-player-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.neetflix-player-btn.active {
background: #667eea;
}
.neetflix-player-close {
font-size: 18px;
padding: 0 8px;
}
.neetflix-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: se-resize;
z-index: 10;
}
.neetflix-resize-handle::after {
content: '';
position: absolute;
bottom: 4px;
right: 4px;
width: 8px;
height: 8px;
border-right: 2px solid rgba(255,255,255,0.4);
border-bottom: 2px solid rgba(255,255,255,0.4);
}
.neetflix-player-body {
flex: 1;
position: relative;
overflow: hidden;
}
#neetflix-drag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 5;
}
#neetflix-player-iframe {
width: 100%;
height: 100%;
border: none;
background: #000;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.neetflix-player-sidebar {
display: none;
position: absolute;
top: 40px;
right: 0;
width: 200px;
height: calc(100% - 40px);
background: rgba(26, 26, 46, 0.95);
border-left: 1px solid #333;
flex-direction: column;
z-index: 2;
}
.neetflix-player-sidebar.show {
display: flex;
}
.neetflix-sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #333;
color: #fff;
font-weight: bold;
}
.neetflix-sidebar-close {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
}
.neetflix-sidebar-body {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.neetflix-road-label {
color: #888;
font-size: 12px;
margin: 10px 0 5px 0;
padding-bottom: 5px;
border-bottom: 1px solid #333;
}
.neetflix-sidebar-episode {
display: inline-block;
padding: 6px 10px;
margin: 3px;
background: #2a2a4a;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
color: #fff;
transition: all 0.2s;
}
.neetflix-sidebar-episode:hover {
background: #3a3a5a;
}
.neetflix-sidebar-episode.playing {
background: #667eea;
}
`);
}
function fetchUrl(url, referer) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': getRandomUA(),
'Referer': referer || url,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
},
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: (error) => {
reject(error);
}
});
});
}
function resolveUrl(url, baseUrl) {
if (!url) return url;
if (url.startsWith('http')) return url;
if (!baseUrl) return url;
if (baseUrl.endsWith('/') && url.startsWith('/')) {
return baseUrl + url.substring(1);
}
if (!baseUrl.endsWith('/') && !url.startsWith('/')) {
return baseUrl + '/' + url;
}
return baseUrl + url;
}
function parseXPath(html, xpath) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const result = doc.evaluate(xpath, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
const nodes = [];
for (let i = 0; i < result.snapshotLength; i++) {
nodes.push(result.snapshotItem(i));
}
return nodes;
}
async function searchPlugin(plugin, keyword) {
const searchUrl = plugin.searchUrl.replace('@keyword', encodeURIComponent(keyword));
console.log(`[${plugin.name}] Searching: ${searchUrl}`);
try {
const html = await fetchUrl(searchUrl, plugin.baseUrl);
const results = [];
const listNodes = parseXPath(html, plugin.searchList);
console.log(`[${plugin.name}] Found ${listNodes.length} items`);
for (const node of listNodes) {
try {
let nameXPath = plugin.searchName;
let resultXPath = plugin.searchResult;
if (nameXPath.startsWith('//') && !nameXPath.startsWith('.//')) {
nameXPath = '.' + nameXPath;
}
if (resultXPath.startsWith('//') && !resultXPath.startsWith('.//')) {
resultXPath = '.' + resultXPath;
}
const nameResult = document.evaluate(nameXPath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const urlResult = document.evaluate(resultXPath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const nameNode = nameResult.singleNodeValue;
const urlNode = urlResult.singleNodeValue;
if (nameNode && urlNode) {
const name = nameNode.textContent?.trim() || '';
const url = urlNode.getAttribute('href') || '';
if (name && url && url !== '/') {
results.push({
name: name,
url: resolveUrl(url, plugin.baseUrl)
});
}
}
} catch (e) {
console.error(`[${plugin.name}] Parse error:`, e);
}
}
return results;
} catch (e) {
console.error(`[${plugin.name}] Search error:`, e);
return [];
}
}
async function getEpisodes(plugin, pageUrl) {
console.log(`[${plugin.name}] Getting episodes from: ${pageUrl}`);
try {
const html = await fetchUrl(pageUrl, plugin.baseUrl);
const roads = [];
if (!plugin.chapterRoads) {
const road = { name: '播放列表1', episodes: [] };
const epNodes = parseXPath(html, plugin.chapterResult);
for (const node of epNodes) {
const name = node.textContent?.trim() || '';
const url = node.getAttribute('href') || '';
if (name && url) {
road.episodes.push({
name: name,
url: resolveUrl(url, plugin.baseUrl)
});
}
}
if (road.episodes.length > 0) {
roads.push(road);
}
} else {
const roadNodes = parseXPath(html, plugin.chapterRoads);
let count = 1;
for (const roadNode of roadNodes) {
const road = { name: `播放列表${count}`, episodes: [] };
let epXPath = plugin.chapterResult;
if (epXPath.startsWith('//') && !epXPath.startsWith('.//')) {
epXPath = '.' + epXPath;
}
const epResult = document.evaluate(epXPath, roadNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < epResult.snapshotLength; i++) {
const node = epResult.snapshotItem(i);
const name = node.textContent?.trim() || '';
const url = node.getAttribute('href') || '';
if (name && url) {
road.episodes.push({
name: name,
url: resolveUrl(url, plugin.baseUrl)
});
}
}
if (road.episodes.length > 0) {
roads.push(road);
count++;
}
}
}
return roads;
} catch (e) {
console.error(`[${plugin.name}] Get episodes error:`, e);
return [];
}
}
function showPlayer(url, title, episode, roads, roadIndex, episodeIndex, pluginName) {
const plugin = Plugins.find(p => p.name === pluginName);
currentPlayerState = {
visible: true,
url: url,
title: title,
episode: episode,
roads: roads,
currentRoad: roadIndex,
currentEpisode: episodeIndex,
pluginName: pluginName,
danmakuEnabled: currentPlayerState.danmakuEnabled,
danmakuOpacity: currentPlayerState.danmakuOpacity,
danmakuFontSize: currentPlayerState.danmakuFontSize,
danmakuSpeed: currentPlayerState.danmakuSpeed
};
const bgmId = getBangumiId();
if (plugin && plugin.useIframe) {
const player = document.getElementById('neetflix-player');
const iframe = document.getElementById('neetflix-player-iframe');
const titleEl = player.querySelector('.neetflix-player-title');
GM_setValue(PLAYER_MODE_KEY, 'iframe');
GM_setValue(PLAYER_VIDEO_URL_KEY, url);
GM_setValue(PLAYER_TITLE_KEY, title);
GM_setValue(PLAYER_EPISODE_KEY, episode);
GM_setValue(PLAYER_BGM_ID_KEY, bgmId);
titleEl.textContent = `${title} - ${episode}`;
player.classList.add('show');
// 显示遮罩层,阻止操作后面的页面
showPlayerOverlay();
// 对于需要代理的网站,使用 GM_xmlhttpRequest 获取内容
if (plugin.needsProxy) {
loadIframeWithProxy(iframe, url, plugin.baseUrl);
} else {
iframe.src = url;
}
updateSidebar();
updateDanmakuButton();
// 加载弹幕(使用 Bangumi ID 精确匹配)
console.log('[NeetFlix] BgmId for danmaku:', bgmId);
setTimeout(() => loadDanmakuForTitle(title, episode, bgmId), 2000);
} else {
GM_setValue(PLAYER_MODE_KEY, 'window');
GM_setValue(PLAYER_VIDEO_URL_KEY, url);
GM_setValue(PLAYER_TITLE_KEY, title);
GM_setValue(PLAYER_EPISODE_KEY, episode);
GM_setValue(PLAYER_BGM_ID_KEY, bgmId);
GM_setValue('neetflix_roads', roads);
GM_setValue('neetflix_current_road', roadIndex);
GM_setValue('neetflix_current_episode', episodeIndex);
window.open(url, '_blank');
}
}
function hidePlayer() {
const player = document.getElementById('neetflix-player');
const iframe = document.getElementById('neetflix-player-iframe');
GM_setValue(PLAYER_MODE_KEY, '');
iframe.src = '';
player.classList.remove('show');
player.classList.remove('fullscreen');
// 隐藏遮罩层
hidePlayerOverlay();
currentPlayerState.visible = false;
}
function showPlayerOverlay() {
let overlay = document.getElementById('neetflix-player-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'neetflix-player-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 99999;
display: none;
`;
document.body.appendChild(overlay);
}
overlay.style.display = 'block';
}
function hidePlayerOverlay() {
const overlay = document.getElementById('neetflix-player-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
function loadIframeWithProxy(iframe, url, baseUrl) {
console.log('[NeetFlix] Loading iframe with proxy:', url);
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': getRandomUA(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
},
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
let html = response.responseText;
// 修改相对路径为绝对路径
html = html.replace(/href="\/([^"]*)"/g, `href="${baseUrl}$1"`);
html = html.replace(/src="\/([^"]*)"/g, `src="${baseUrl}$1"`);
html = html.replace(/href='\/([^']*)'/g, `href='${baseUrl}$1'`);
html = html.replace(/src='\/([^']*)'/g, `src='${baseUrl}$1'`);
// 添加 base 标签
if (!html.includes(']*)>/i, ``);
}
// 注入播放器样式、弹幕脚本
const injectedCode = `
`;
// 在 前插入
html = html.replace(/<\/head>/i, injectedCode + '');
// 创建 blob URL
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const blobUrl = URL.createObjectURL(blob);
iframe.src = blobUrl;
console.log('[NeetFlix] Iframe loaded with proxy');
} else {
console.log('[NeetFlix] Proxy load failed:', response.status);
// 回退到直接加载
iframe.src = url;
}
},
onerror: function(err) {
console.log('[NeetFlix] Proxy request error:', err);
// 回退到直接加载
iframe.src = url;
}
});
}
function updateSidebar() {
const sidebar = document.getElementById('neetflix-player-sidebar');
const body = document.getElementById('neetflix-sidebar-body');
if (!currentPlayerState.roads || currentPlayerState.roads.length === 0) {
body.innerHTML = '无选集信息
';
return;
}
let html = '';
currentPlayerState.roads.forEach((road, roadIdx) => {
html += `${road.name}
`;
road.episodes.forEach((ep, epIdx) => {
const isPlaying = roadIdx === currentPlayerState.currentRoad && epIdx === currentPlayerState.currentEpisode;
html += ``;
});
});
body.innerHTML = html;
body.querySelectorAll('.neetflix-sidebar-episode').forEach(el => {
el.addEventListener('click', function() {
const roadIdx = parseInt(this.dataset.road);
const epIdx = parseInt(this.dataset.episode);
const url = this.dataset.url;
currentPlayerState.currentRoad = roadIdx;
currentPlayerState.currentEpisode = epIdx;
currentPlayerState.url = url;
currentPlayerState.episode = currentPlayerState.roads[roadIdx].episodes[epIdx].name;
const iframe = document.getElementById('neetflix-player-iframe');
const titleEl = document.querySelector('.neetflix-player-title');
// 设置playerMode让iframe页面注入CSS
GM_setValue(PLAYER_MODE_KEY, 'iframe');
GM_setValue(PLAYER_VIDEO_URL_KEY, url);
GM_setValue(PLAYER_EPISODE_KEY, currentPlayerState.episode);
iframe.src = url;
titleEl.textContent = `${currentPlayerState.title} - ${currentPlayerState.episode}`;
// 重新加载弹幕
const bgmId = getBangumiId();
setTimeout(() => loadDanmakuForTitle(currentPlayerState.title, currentPlayerState.episode, bgmId), 2000);
updateSidebar();
});
});
}
function updateDanmakuButton() {
const btn = document.getElementById('neetflix-danmaku-btn');
if (currentPlayerState.danmakuEnabled) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
async function copyScreenshotToClipboard(dataUrl) {
try {
const response = await fetch(dataUrl);
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
console.log('[NeetFlix] Screenshot copied to clipboard');
showScreenshotToast('截图已复制到剪贴板');
} catch(err) {
console.log('[NeetFlix] Failed to copy to clipboard:', err.message);
alert('截图失败:无法复制到剪贴板');
}
}
function showScreenshotToast(message) {
let toast = document.getElementById('neetflix-screenshot-toast');
if (toast) toast.remove();
toast = document.createElement('div');
toast.id = 'neetflix-screenshot-toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(102, 126, 234, 0.95);
color: white;
padding: 16px 32px;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
z-index: 999999;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
animation: neetflixToastFade 2s ease forwards;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes neetflixToastFade {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}
`;
document.head.appendChild(style);
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
style.remove();
}, 2000);
}
function takeScreenshotNewWindow(videoSrc, currentTime) {
if (!videoSrc) {
console.log('[NeetFlix] No video source for new window screenshot');
showScreenshotToast('截图失败:无法获取视频源');
return;
}
console.log('[NeetFlix] Opening new window for screenshot');
const win = window.open('', '_blank', 'width=800,height=600');
if (!win) {
showScreenshotToast('请允许弹出窗口');
return;
}
win.document.write(`
截图预览 - NeetFlix
正在截图...
提示:请使用系统截图工具 (Win+Shift+S) 截取此窗口
`);
win.document.close();
}
function takeScreenshotWithPrivilege(videoSrc, currentTime) {
if (!videoSrc) {
console.log('[NeetFlix] No video source for privileged screenshot');
showScreenshotToast('截图失败:无法获取视频源');
return;
}
console.log('[NeetFlix] Attempting privileged screenshot for:', videoSrc);
showScreenshotToast('正在截图...');
GM_xmlhttpRequest({
method: 'GET',
url: videoSrc,
responseType: 'blob',
headers: {
'Accept': 'video/*, */*'
},
onload: function(response) {
if (response.status >= 200 && response.status < 300 || response.status === 206) {
const blob = response.response;
const videoUrl = URL.createObjectURL(blob);
const video = document.createElement('video');
video.muted = true;
let done = false;
let timeoutId = null;
function cleanup() {
if (timeoutId) clearTimeout(timeoutId);
URL.revokeObjectURL(videoUrl);
}
function doScreenshot() {
if (done) return;
done = true;
try {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 360;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/png');
copyScreenshotToClipboard(dataUrl);
cleanup();
console.log('[NeetFlix] Privileged screenshot success');
} catch(err) {
console.log('[NeetFlix] Privileged screenshot failed:', err.message);
cleanup();
takeScreenshotPreview(videoSrc, currentTime);
}
}
video.addEventListener('loadeddata', function() {
if (currentTime && currentTime > 0) {
video.currentTime = currentTime;
} else {
doScreenshot();
}
});
video.addEventListener('seeked', doScreenshot);
video.addEventListener('canplay', function() {
if (!done && video.readyState >= 2) {
if (!currentTime || currentTime === 0) {
doScreenshot();
}
}
});
video.addEventListener('error', function(err) {
if (done) return;
done = true;
console.log('[NeetFlix] Video load error:', err);
cleanup();
takeScreenshotPreview(videoSrc, currentTime);
});
video.src = videoUrl;
video.load();
timeoutId = setTimeout(function() {
if (!done) {
done = true;
console.log('[NeetFlix] Screenshot timeout');
cleanup();
takeScreenshotPreview(videoSrc, currentTime);
}
}, 8000);
} else {
console.log('[NeetFlix] HTTP error:', response.status);
takeScreenshotPreview(videoSrc, currentTime);
}
},
onerror: function(err) {
console.log('[NeetFlix] Request error:', err);
takeScreenshotPreview(videoSrc, currentTime);
}
});
}
function takeScreenshotPreview(videoSrc, currentTime) {
if (!videoSrc) {
console.log('[NeetFlix] No video source for preview screenshot');
showScreenshotToast('截图失败:无法获取视频源');
return;
}
console.log('[NeetFlix] Creating preview screenshot');
const video = document.createElement('video');
video.src = videoSrc;
video.muted = true;
video.crossOrigin = 'anonymous';
let done = false;
function showPreview() {
if (done) return;
done = true;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 360;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.style = 'max-width:100%';
const win = window.open('', '_blank');
if (win) {
win.document.title = `截图预览 - ${currentPlayerState.title || 'NeetFlix'}`;
win.document.body.style.textAlign = 'center';
win.document.body.style.background = '#000';
win.document.body.appendChild(canvas);
} else {
showScreenshotToast('请允许弹出窗口');
}
}
video.addEventListener('loadeddata', () => {
if (currentTime > 0) {
video.currentTime = currentTime;
} else {
showPreview();
}
});
video.addEventListener('seeked', showPreview);
video.addEventListener('error', (e) => {
if (done) return;
done = true;
showScreenshotToast('视频加载失败');
});
video.load();
setTimeout(() => {
if (!done) {
done = true;
showScreenshotToast('截图超时');
}
}, 10000);
}
function initPlayerEvents() {
const player = document.getElementById('neetflix-player');
const header = player.querySelector('.neetflix-player-header');
const closeBtn = document.getElementById('neetflix-close-btn');
const openBtn = document.getElementById('neetflix-open-btn');
const episodeBtn = document.getElementById('neetflix-episode-btn');
const danmakuBtn = document.getElementById('neetflix-danmaku-btn');
const sidebar = document.getElementById('neetflix-player-sidebar');
const sidebarClose = sidebar.querySelector('.neetflix-sidebar-close');
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
dragOverlay.style.pointerEvents = 'none';
hidePlayer();
});
openBtn.addEventListener('click', (e) => {
e.stopPropagation();
// 关闭dragOverlay
dragOverlay.style.pointerEvents = 'none';
if (currentPlayerState.url) {
const bgmId = getBangumiId();
GM_setValue(PLAYER_MODE_KEY, 'window');
GM_setValue(PLAYER_VIDEO_URL_KEY, currentPlayerState.url);
GM_setValue(PLAYER_TITLE_KEY, currentPlayerState.title);
GM_setValue(PLAYER_EPISODE_KEY, currentPlayerState.episode);
GM_setValue(PLAYER_BGM_ID_KEY, bgmId);
window.open(currentPlayerState.url, '_blank');
}
});
danmakuBtn.addEventListener('click', (e) => {
e.stopPropagation();
// 关闭dragOverlay
dragOverlay.style.pointerEvents = 'none';
currentPlayerState.danmakuEnabled = !currentPlayerState.danmakuEnabled;
updateDanmakuButton();
window.postMessage({
type: 'neetflixSetDanmakuEnabled',
enabled: currentPlayerState.danmakuEnabled
}, '*');
});
const screenshotBtn = document.getElementById('neetflix-screenshot-btn');
screenshotBtn.addEventListener('click', (e) => {
e.stopPropagation();
// 关闭dragOverlay
dragOverlay.style.pointerEvents = 'none';
// 发送截图命令到iframe
console.log('[NeetFlix] Screenshot command received');
var iframe = document.getElementById('neetflix-player-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'neetflixScreenshot'
}, '*');
}
});
// 监听截图结果
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'neetflixScreenshotResult') {
console.log('[NeetFlix] Screenshot result received');
copyScreenshotToClipboard(e.data.dataUrl);
}
if (e.data && e.data.type === 'neetflixScreenshotFailed') {
console.log('[NeetFlix] Screenshot failed, trying new window mode...');
console.log('[NeetFlix] Video src:', e.data.videoSrc, 'Time:', e.data.currentTime);
takeScreenshotNewWindow(e.data.videoSrc, e.data.currentTime);
}
if (e.data && e.data.type === 'neetflixTryPrivileged') {
console.log('[NeetFlix] New window failed, trying privileged mode...');
console.log('[NeetFlix] Video src:', e.data.videoSrc, 'Time:', e.data.currentTime);
takeScreenshotWithPrivilege(e.data.videoSrc, e.data.currentTime);
}
});
episodeBtn.addEventListener('click', (e) => {
e.stopPropagation();
// 检查popup是否已显示,如果已显示则关闭
var existingPopup = document.getElementById('neetflix-episode-popup');
if (existingPopup) {
existingPopup.remove();
return;
}
// 关闭dragOverlay,允许点击iframe
dragOverlay.style.pointerEvents = 'none';
var roads = currentPlayerState.roads;
var currentRoad = currentPlayerState.currentRoad;
var currentEp = currentPlayerState.currentEpisode;
if (!roads || roads.length === 0) {
alert('暂无选集信息');
return;
}
var road = roads[currentRoad];
if (!road || !road.episodes) {
alert('暂无选集信息');
return;
}
var html = '';
html += '
' + road.name + '
';
road.episodes.forEach(function(ep, idx) {
var isActive = (idx === currentEp) ? 'background:#667eea;' : '';
html += '
' + ep.name + '
';
});
html += '
';
var popup = document.getElementById('neetflix-episode-popup');
if (popup) popup.remove();
popup = document.createElement('div');
popup.id = 'neetflix-episode-popup';
popup.innerHTML = html;
popup.style.cssText = 'position:absolute;top:40px;right:50px;width:180px;background:#1a1a2e;border:1px solid #333;border-radius:8px;z-index:100;color:#fff;';
player.appendChild(popup);
popup.querySelectorAll('.neetflix-episode-item').forEach(function(el) {
el.addEventListener('click', function(e) {
e.stopPropagation();
var url = this.dataset.url;
var idx = parseInt(this.dataset.idx);
var name = this.dataset.name;
// 关闭弹窗
popup.remove();
// 更新iframe
var iframe = document.getElementById('neetflix-player-iframe');
GM_setValue(PLAYER_MODE_KEY, 'iframe');
iframe.src = url;
// 更新状态
currentPlayerState.currentEpisode = idx;
currentPlayerState.episode = name;
updateSidebar();
// 重新加载弹幕
const bgmId = getBangumiId();
setTimeout(() => {
loadDanmakuForTitle(currentPlayerState.title, currentPlayerState.episode, bgmId);
}, 2000);
});
});
// 点击其他地方关闭
setTimeout(function() {
document.addEventListener('click', function closePopup(e) {
var popup = document.getElementById('neetflix-episode-popup');
if (popup && !popup.contains(e.target) && e.target !== episodeBtn) {
popup.remove();
document.removeEventListener('click', closePopup);
}
});
}, 10);
});
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let isFirstDrag = true;
window.playerDragging = false;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
e.preventDefault();
e.stopPropagation();
isDragging = true;
isFirstDrag = true;
window.playerDragging = true;
player.classList.add('dragging');
// 从居中位置切换到绝对位置
const rect = player.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
// 移除居中样式,切换到绝对定位
player.style.left = rect.left + 'px';
player.style.top = rect.top + 'px';
player.style.transform = 'none';
});
// 拖动覆盖层
const dragOverlay = document.getElementById('neetflix-drag-overlay');
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
// 防止选择文本
e.preventDefault();
// 计算新位置
let x = e.clientX - dragOffset.x;
let y = e.clientY - dragOffset.y;
// 边界限制(允许拖出屏幕边缘)
const maxX = window.innerWidth - 50;
const maxY = window.innerHeight - 50;
x = Math.max(-player.offsetWidth + 50, Math.min(maxX, x));
y = Math.max(-player.offsetHeight + 50, Math.min(maxY, y));
player.style.left = x + 'px';
player.style.top = y + 'px';
});
const stopDragging = (e) => {
if (isDragging) {
isDragging = false;
window.playerDragging = false;
player.classList.remove('dragging');
player.classList.add('dragged');
// 禁用覆盖层
dragOverlay.style.pointerEvents = 'none';
}
};
// 拖动时启用覆盖层
header.addEventListener('mousedown', () => {
dragOverlay.style.pointerEvents = 'auto';
});
document.addEventListener('mouseup', stopDragging);
document.addEventListener('mouseleave', stopDragging);
// 自定义resize手柄逻辑(固定左上角缩放)
const resizeHandle = document.getElementById('neetflix-resize-handle');
let isResizing = false;
let resizeStartX = 0;
let resizeStartY = 0;
let resizeStartWidth = 0;
let resizeStartHeight = 0;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
isResizing = true;
// 启用覆盖层
dragOverlay.style.pointerEvents = 'auto';
// 如果是居中状态,先切换到绝对定位
if (player.classList.contains('show') && !player.style.left) {
const rect = player.getBoundingClientRect();
player.style.left = rect.left + 'px';
player.style.top = rect.top + 'px';
player.style.transform = 'none';
}
resizeStartX = e.clientX;
resizeStartY = e.clientY;
resizeStartWidth = player.offsetWidth;
resizeStartHeight = player.offsetHeight;
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = e.clientX - resizeStartX;
const deltaY = e.clientY - resizeStartY;
const newWidth = Math.max(320, resizeStartWidth + deltaX);
const newHeight = Math.max(200, resizeStartHeight + deltaY);
player.style.width = newWidth + 'px';
player.style.height = newHeight + 'px';
// 触发resize更新canvas
resize();
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
dragOverlay.style.pointerEvents = 'none';
}
});
}
function renderResults(panel, results, searchKeyword) {
const body = panel.querySelector('.neetflix-body');
let html = `搜索关键词: ${searchKeyword}
`;
if (results.length === 0) {
html += '未找到相关资源
';
body.innerHTML = html;
return;
}
for (const { plugin, items } of results) {
if (items.length === 0) continue;
html += `
`;
for (let i = 0; i < items.length; i++) {
const item = items[i];
html += `
${item.name}
`;
}
html += '
';
}
body.innerHTML = html;
body.querySelectorAll('.neetflix-result-item').forEach(item => {
item.addEventListener('click', async function() {
const url = this.dataset.url;
const pluginName = this.dataset.plugin;
const plugin = Plugins.find(p => p.name === pluginName);
const episodesDiv = this.nextElementSibling;
if (episodesDiv.classList.contains('show')) {
episodesDiv.classList.remove('show');
return;
}
episodesDiv.innerHTML = '加载中...
';
episodesDiv.classList.add('show');
const roads = await getEpisodes(plugin, url);
if (roads.length === 0) {
episodesDiv.innerHTML = '未找到播放链接
';
return;
}
let epHtml = '';
for (const road of roads) {
epHtml += `${road.name}
`;
for (const ep of road.episodes) {
epHtml += `${ep.name}`;
}
}
episodesDiv.innerHTML = epHtml;
const bangumiName = getBangumiName();
episodesDiv.querySelectorAll('.neetflix-episode').forEach(ep => {
ep.addEventListener('click', function() {
const playUrl = this.dataset.url;
const roadIdx = parseInt(this.dataset.road);
const epIdx = parseInt(this.dataset.episode);
const epName = this.textContent;
const plugin = Plugins.find(p => p.name === pluginName);
showPlayer(playUrl, bangumiName, epName, roads, roadIdx, epIdx, pluginName);
});
});
});
});
}
async function searchAll(keyword) {
const results = [];
const promises = Plugins.map(async plugin => {
const items = await searchPlugin(plugin, keyword);
return { plugin, items };
});
const allResults = await Promise.all(promises);
return allResults.filter(r => r.items.length > 0);
}
function init() {
addStyles();
const nameSingle = document.querySelector('.nameSingle');
if (!nameSingle) {
console.log('Not a bangumi subject page');
return;
}
const btn = createButton();
nameSingle.appendChild(btn);
const panel = createPanel();
document.body.appendChild(panel);
const player = createPlayer();
document.body.appendChild(player);
initPlayerEvents();
btn.addEventListener('click', async () => {
panel.classList.add('show');
const body = panel.querySelector('.neetflix-body');
const name = getBangumiName();
body.innerHTML = `搜索关键词: ${name}
搜索中...
`;
console.log('Searching for:', name);
const results = await searchAll(name);
renderResults(panel, results, name);
});
panel.querySelector('.neetflix-close').addEventListener('click', () => {
panel.classList.remove('show');
});
panel.querySelector('.neetflix-overlay').addEventListener('click', () => {
panel.classList.remove('show');
});
console.log('Bangumi Direct Play initialized');
console.log('Bangumi Name:', getBangumiName());
console.log('Bangumi ID:', getBangumiId());
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();