// ==UserScript==
// @name Jav跳转到Emby播放,支持 JavBus/Javdb/library/javmunu/XXXClub
// @namespace http://tampermonkey.net/
// @version 2025.3.7
// @description 在JavBus/Javdb/library/javmunu图书馆高亮emby存在的视频,并在详情页提供一键跳转功能
// @include /^.*(jav|bus|dmm|see|cdn|fan){2}\..*$/
// @match *://www.javbus.com/*
// @include *://javdb*.com/v/*
// @match *://javmenu.com/*
// @match *://xxxclub.to/*
// @include *://javdb*.com/search?q=*
// @match *://www.javdb.com/*
// @match *://javdb.com/*
// @include *://*.javlib.com/*
// @include *://*.javmenu.com/*
// @include *://*.javlibrary.com/*
// @include *://*/cn/*v=jav*
// @include *://*/en/*v=jav*
// @include *://*/tw/*v=jav*
// @include *://*/ja/*v=jav*
// @include /^.*(avmoo|avsox)\..*$/
// @include *://avmoo.*/*/movie/*
// @include *://avsox.*/*/movie/*
// @match https://www.sehuatang.net/thread-*
// @match https://www.sehuatang.net/forum.php?mod=viewthread&tid=*
// @match https://.*/thread-*
// @match https://.*/forum.php?mod=viewthread&tid=*
// @match https://www.tanhuazu.com/threads/*
// @match *://javbooks.com/content*censored/*.htm
// @match *://jmvbt.com/content*censored/*.htm
// @match *://*.com/content*censored/*.htm
// @include *://*.cc/content_censored/*.htm
// @include /^https:\/\/jbk008\.com\/serchinfo\_(censored|uncensored)\/topicsbt/
// @match *://db.msin.jp/jp.page/movie?id=*
// @match *://db.msin.jp/page/movie?id=*
// @include *://*/works/detail/*
// @match *://xslist.org/search?query=*
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
const embyAPI = "279b8961e1fd44b1bf8ea4c025f67075";
const embyBaseUrl = "http://192.168.5.8:35405/";
const defaultColor = "#52b54b"; // HotPink
(function () {
'use strict';
// 新增 Cloudflare 检测函数
function checkCloudflareChallenge() {
const cloudflareSelectors = [
'#challenge-form', // Cloudflare验证表单
'.cf-browser-verification',// 浏览器验证容器
'div.ray-id', // Ray ID 元素
'div.cf-spinner-rotator', // 加载动画
'trk-page[data-title^="Just a moment"]' // 特定页面标题
];
return cloudflareSelectors.some(selector => document.querySelector(selector)) ||
document.title.includes('Just a moment') ||
document.body.textContent.includes('Cloudflare');
}
// 新增等待函数
function waitForCloudflare(callback, maxAttempts = 30, interval = 1000) {
let attempts = 0;
const checkInterval = setInterval(() => {
if (!checkCloudflareChallenge()) {
clearInterval(checkInterval);
callback();
} else if (attempts++ >= maxAttempts) {
clearInterval(checkInterval);
console.log('Cloudflare验证等待超时');
callback();
}
}, interval);
}
class Base {
fetchEmbyData(code, callback) {
if (!code) {
console.warn("番号为空,跳过请求");
return; // 番号为空时直接返回
}
console.log('Fetching data for code:', code);
GM_xmlhttpRequest({
method: "GET",
url: `${embyBaseUrl}emby/Users/${embyAPI}/Items?api_key=${embyAPI}&Recursive=true&IncludeItemTypes=Movie&SearchTerm=${code}`,
headers: { accept: "application/json" },
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
callback(data);
} catch (error) {
console.error("Failed to parse response:", error);
}
},
onerror: (e) => {
console.error("Error fetching Emby data:", e);
},
ontimeout: () => {
console.error("Request to Emby timed out");
}
});
}
insertEmbyLink(targetElement, data) {
const maxLinks = 5; // 限制最多插入 5 个链接
let insertedLinks = 0;
data.Items.forEach(item => {
if (insertedLinks >= maxLinks) {
console.log("已达到最大链接插入数量,跳过剩余链接");
return;
}
const embyUrl = `${embyBaseUrl}web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`;
console.log(`生成的 embyUrl: ${embyUrl}`);
// 确保 targetElement 是 DOM 元素
const domElement = targetElement instanceof jQuery ? targetElement[0] : targetElement;
// 检查是否已经存在 "跳转到Emby" 链接
console.log('正在检查是否存在跳转到Emby链接...');
// 使用 class 判断是否已经插入
const parentElement = domElement.parentElement || domElement; // 检查父级范围
if (parentElement.querySelector(`a[href="${embyUrl}"]`)) {
console.log('跳转到Emby链接已存在,跳过插入');
return;
}
const embyUrlSpanStyle = `background: ${defaultColor}; border-radius: 3px; padding: 3px 6px;`;
const embyUrlAStyle = `color: white; text-decoration: none;`;
const embyLink = `<div style="${embyUrlSpanStyle}">
<a href="${embyUrl}" style="${embyUrlAStyle}" target="_blank">
<b>跳转到emby👉</b>
</a>
</div>`;
// 使用 jQuery 插入链接
console.log('正在插入跳转到Emby链接...');
$(domElement).after(embyLink);
insertedLinks++; // 记录已插入的链接数量
});
}
highlightAndInsertEmbyLink(videos, extractFanhaoFunction, insertAfterSelector) {
// console.log('Highlighting videos...', videos);
const videoArray = Array.from(videos);
videoArray.forEach(videoElement => {
const fanhaos = extractFanhaoFunction(videoElement);
// console.log('Fanhaos:', fanhaos);
if (!fanhaos || fanhaos.length === 0) {
console.warn("未提取到番号,跳过该视频");
return; // 番号为空时跳过
}
const searchNextFanhao = (fanhaoIndex) => {
if (fanhaoIndex >= fanhaos.length) return;
let fanhao = fanhaos[fanhaoIndex];
this.fetchEmbyData(fanhao, (data) => {
if (data.Items.length > 0) {
const targetElement = insertAfterSelector
? videoElement.querySelector(insertAfterSelector)
: videoElement;
const domElement = targetElement instanceof jQuery ? targetElement[0] : targetElement;
this.insertEmbyLink(domElement, data);
// 高亮
videoElement.style.borderWidth = "3px";
videoElement.style.borderStyle = "solid";
videoElement.style.borderColor = defaultColor;
videoElement.style.backgroundColor = defaultColor;
} else {
searchNextFanhao(fanhaoIndex + 1);
}
});
};
searchNextFanhao(0);
});
}
}
// 定义各站点处理类(保持空类结构)
class JavBus extends Base { }
class JavLibrary extends Base { }
class Javdb extends Base { }
class Javbooks extends Base { }
class Avmoo extends Base { }
class Sehuatang extends Base { }
class Msin extends Base { }
class Javmenu extends Base { }
class XXXClub extends Base { }
class Main {
constructor() {
console.log('Jav跳转Emby启动...');
this.sites = {
'javBus': {
// 选择器,用于判断当前页面是否属于 JavBus 网站。
// 这里通过选择页脚 `<footer>` 标签中包含文本 'JavBus' 来判断。
selector: "footer:contains('JavBus')",
// JavBus 的站点处理类。这是一个自定义的逻辑类,用于处理该站点的具体操作。
class: JavBus,
// 选择器,用于判断是否是 JavBus 网站的列表页(瀑布流页面)。
// 这里通过选择 `#waterfall`(id)下的 `.item.masonry-brick`(class)元素来进行判断。
listPageSelector: "#waterfall .item.masonry-brick",
// 定义在列表页中插入新内容的位置。
// 这里选择 `.item` 元素下的 `date` 标签作为插入位置的参考点。
listPageInsertAfter: ".item date",
// 在列表页中提取番号的逻辑。
// `el` 是当前列表项的 DOM 元素。
// 通过查询 `.item date` 元素来获取番号,如果找到对应的元素,则返回其文本内容(去除前后空格)。
// 如果找不到则返回空数组。
listPageExtract: (el) => {
const fanhaoElement = el.querySelector('.item date'); // 查找 `.item date` 元素
return fanhaoElement ? [fanhaoElement.textContent.trim()] : []; // 提取文本并返回
},
// 选择器,用于判断是否是 JavBus 网站的详情页。
// 这里通过选择 `.col-md-3.info p span:nth-child(2)` 来判断,这个选择器通常用于获取影片的识别码/番号。
detailPageSelector: '.col-md-3.info p span:nth-child(2)',
// 定义在详情页中插入新内容的容器。
// 这里选择 `.col-md-3.info p span:nth-child(2):first`,表示在符合条件的第一个 `span` 元素中插入内容。
detailPageContainer: ".col-md-3.info p span:nth-child(2):first",
// 在详情页中提取番号的逻辑。
// 通过 jQuery 查询 `.col-md-3.info p` 的第一个段落,并查找其第二个 `span` 元素。
// 如果找到有效的内容,则返回其文本内容。
// 如果找不到则返回空数组。
detailPageExtract: () => {
const code = $('.col-md-3.info p').eq(0).find('span').eq(1).html(); // 查找番号
return code ? [code] : []; // 提取文本并返回
}
},
'javmenu': {
selector: "footer:contains('JAVMENU V3')",
class: Javmenu,
listPageSelector: ".page-content .category-page.video-list-item",
listPageInsertAfter: ".card-title.text-dark",
listPageExtract: (el) => {
console.log(el);
const fanhaoElement = el.querySelector('.card-title.text-dark');
console.log(fanhaoElement);
return fanhaoElement ? [fanhaoElement.textContent.trim()] : [];
},
detailPageSelector: '.page-content .container-fluid .tab-content h1 strong',
detailPageContainer: ".page-content",
detailPageExtract: () => {
const code = $('.page-content .container-fluid .tab-content h1 strong').text().trim().split(' ')[0];
return code ? [code] : [];
}
},
'xxxclub': {
selector: ".page-footer:contains('XXXClub')",
// selector: "div.page-footer:contains('XXXClub 2020 - 2025')", // 通过页脚标识站点
class: XXXClub,
listPageSelector: ".main-content ul li", // 列表项选择器
listPageInsertAfter: "span:nth-of-type(2) > a", // 在标题链接后插入
listPageExtract: (el) => {
const aElement = el.querySelector('span:nth-of-type(2) > a');
if (!aElement) return [];
const title = aElement.textContent.trim();
// console.log(title);
// 从标题文本提取番号
// 情况一:Brand后接三组两位数字: (格式示例: Brand YY MM DD)
const case1Match = title.match(/^(\S+)\s+(\d{2})\s+(\d{2})\s+(\d{2})/);
// return match ? [`${match[1]}.${match[2]}.${match[3]}.${match[4]}`] : [];
// console.log(match);
if (case1Match) {
// 组合成标准番号格式: Brand.YY.MM.DD
return [`${case1Match[1]}.${case1Match[2]}.${case1Match[3]}.${case1Match[4]}`];
}
// 情况二:匹配括号内的日期(DD.MM.YYYY)
const case2Match = title.match(/^(\S+?) - .*?\((\d{2})\.(\d{2})\.(\d{4})\)$/);
if (case2Match) {
const [, brand, dd, mm, yyyy] = case2Match;
const yy = yyyy.slice(-2); // 提取年份后两位
return [`${brand}.${yy}.${mm}.${dd}`];
}
// 情况三:匹配品牌 - 演员 - 标题的结构
const case3Match = title.match(/^(\S+) - .+? - (?!.*-)(.+)$/);
if (case3Match) {
return [`${case3Match[1]} ${case3Match[2]}`];
}
// 情况四:提取前五个空格前的部分
let currentIndex = -1;
let found = true;
for (let i = 0; i < 5; i++) {
currentIndex = title.indexOf(' ', currentIndex + 1);
if (currentIndex === -1) {
found = false;
break;
}
};
return found ? [title.substring(0, currentIndex)] : [];
},
},
'javLibrary': {
selector: "#bottomcopyright:contains('JAVLibrary')",
class: JavLibrary,
listPageSelector: ".video",
listPageInsertAfter: "a",
detailPageSelector: '#content #video_title #video_jacket_info #video_info .item .text',
detailPageContainer: "#video_info",
commentPageSelector: "#video_comments .comment",
commentPageInsertAfter: "strong",
listPageExtract: (el) => {
const fanhao = el.children[0]?.title.split(" ")[0] || el.children[1]?.title.split(" ")[0];
return fanhao ? [fanhao] : [];
},
detailPageExtract: () => {
const code = $('#video_info .item').eq(0).find('.text').html();
return code ? [code] : [];
},
commentPageExtract: (el) => {
const anchorElement = el.querySelector('a[href^="videoreviews.php?v="]');
return anchorElement ? [anchorElement.textContent.split(" ")[0]] : [];
}
},
'javdb': {
selector: "#footer:contains('javdb')",
class: Javdb,
listPageSelector: ".movie-list .item",
listPageInsertAfter: ".video-title strong", // 新增插入位置控制
detailPageSelector: 'body > section > div > div.video-detail > h2 > strong',
detailPageContainer: ".panel.movie-panel-info .value:first",
listPageExtract: (el) => {
const result = [];
const videoTitleElement = el.querySelector('.video-title strong');
if (videoTitleElement) {
const strongText = videoTitleElement.textContent.trim();
// 检查 strong 文本中是否包含至少 3 位数字
const hasThreeDigits = (strongText.match(/\d/g) || []).length >= 3;
if (hasThreeDigits) {
// 仅移除空格,保留其他字符
const processed = strongText.replace(/ /g, '');
// 仅移除第一个出现的空格
// const processed = strongText.replace(/ /, '');
result.push(processed);
} else {
// 克隆节点以避免修改原始 DOM
const videoTitle = el.querySelector('.video-title');
const clonedTitle = videoTitle.cloneNode(true);
// 处理克隆节点中的 strong 标签第一个空格
const clonedStrong = clonedTitle.querySelector('strong');
if (clonedStrong) {
clonedStrong.textContent = clonedStrong.textContent
.trim()
.replace(/ /g, ''); // 仅替换第一个空格
}
// 统一清理所有非字母数字字符
const fullTitle = clonedTitle.textContent
.trim()
.replace(/[^a-zA-Z0-9]+/g, ' ') // 合并非字母数字字符为空格
.trim();
result.push(fullTitle);
}
}
return result;
},
detailPageExtract: () => {
const code = $('body > section > div > div.video-detail > h2 > strong').text().trim().split(' ')[0];
return code ? [code] : [];
}
},
'javbooks': {
selector: "#Declare_box:contains('javbooks')",
class: Javbooks,
detailPageSelector: '#info > div:nth-child(2) > font',
detailPageContainer: "#info",
detailPageExtract: () => {
const code = $('#info > div:nth-child(2) > font').text().trim().split(' ')[0];
return code ? [code] : [];
}
},
'avmoo': {
selector: "footer:contains('AVMOO')",
class: Avmoo,
listPageSelector: "#waterfall .item", // 根据id和class判断瀑布/列表页
listPageInsertAfter: ".item date", // 新增插入位置控制
listPageExtract: (el) => { // 列表页提取番号
const fanhaoElement = el.querySelector('.item date');
return fanhaoElement ? [fanhaoElement.textContent.trim()] : [];
},
detailPageSelector: '.col-md-3.info p span:nth-child(2)',
detailPageContainer: ".col-md-3.info",
detailPageExtract: () => {
const code = $('.col-md-3.info p').eq(0).find('span').eq(1).html();
return code ? [code] : [];
}
},
'sehuatang': {
selector: "#flk:contains('色花堂')",
class: Sehuatang,
detailPageCodeRegex: /([a-zA-Z]{2,15}[-\s]?\d{2,15}|FC2PPV-[^\d]{0,5}\d{6,7})/i,
detailPageContainer: "#pgt",
detailPageExtract: () => {
const str = document.title.split(" ")[0];
return str.match(this.detailPageCodeRegex) || [];
}
},
'msin': {
selector: "#footer:contains('db.msin.jp')",
class: Msin,
detailPageSelector: 'div.mv_pn',
detailPageContainer: "#top_content",
detailPageExtract: () => {
const code = $('div.mv_pn').text().trim().split(' ')[0];
return code ? [code] : [];
}
}
};
this.site = Object.keys(this.sites).find(key => $(this.sites[key].selector).length) || null;
console.log('Matched site:', this.site);
this.siteClass = this.site ? this.sites[this.site].class : null;
// console.log('Site class:', this.siteClass);
}
make() {
if (!this.siteClass) return;
const siteConfig = this.sites[this.site];
// console.log('Site Config:', this.siteConfig);
const instance = new siteConfig.class();
console.log('判断是否是列表页 $(siteConfig.listPageSelector).length:', $(siteConfig.listPageSelector).length);
console.log('判断是否是评论页 $(siteConfig.commentPageSelector).length:', $(siteConfig.commentPageSelector).length);
// 处理列表页
if ($(siteConfig.listPageSelector).length > 0) {
console.log('处理列表页', $(siteConfig.listPageSelector).length);
instance.highlightAndInsertEmbyLink(
$(siteConfig.listPageSelector),
(el) => siteConfig.listPageExtract ? siteConfig.listPageExtract(el) : [],
siteConfig.listPageInsertAfter ? siteConfig.listPageInsertAfter : null // 传递插入位置选择器
);
}
// 处理详情页
else if ($(siteConfig.detailPageSelector).length > 0) {
console.log('处理详情页', $(siteConfig.detailPageSelector).length);
const codes = siteConfig.detailPageExtract ? siteConfig.detailPageExtract() : [];
codes.forEach(code => {
instance.fetchEmbyData(code, (data) => {
if (data.Items.length > 0) {
// instance.insertEmbyLink($(siteConfig.detailPageContainer), data);
const detailContainer = $(siteConfig.detailPageContainer);
if (detailContainer.length > 0) {
instance.insertEmbyLink(detailContainer[0], data);
}
}
});
});
}
// 处理评论页
else if ($(siteConfig.commentPageSelector).length > 0) {
console.log('处理评论页');
// 添加对应的评论页选择器和处理逻辑
instance.highlightAndInsertEmbyLink(
$(siteConfig.commentPageSelector),
(el) => siteConfig.commentPageExtract ? siteConfig.commentPageExtract(el) : [],
siteConfig.commentPageInsertAfter
);
}
}
}
// 添加浮动按钮
function addFloatingButton() {
const button = document.createElement('div');
button.textContent = '运行脚本';
button.style.position = 'fixed';
button.style.left = '10px';
button.style.top = '50%';
button.style.transform = 'translateY(-50%)';
button.style.backgroundColor = '#52b54b';
button.style.color = 'white';
button.style.padding = '10px 15px';
button.style.borderRadius = '5px';
button.style.cursor = 'pointer';
button.style.zIndex = 9999;
button.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
button.addEventListener('click', () => {
console.log('运行脚本按钮被点击');
new Main().make();
});
document.body.appendChild(button);
}
// 修改初始化逻辑
setTimeout(() => {
// 先添加浮动按钮(用户可手动触发)
addFloatingButton();
// 检测并等待 Cloudflare
waitForCloudflare(() => {
console.log('Cloudflare验证完成,启动主逻辑');
new Main().make();
});
}, 1000);
})();