- // ==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);
-
- })();