Jav跳转到Emby播放,支持 JavBus/Javdb/library/javmunu/XXXClub

在JavBus/Javdb/library/javmunu图书馆高亮emby存在的视频,并在详情页提供一键跳转功能

  1. // ==UserScript==
  2. // @name Jav跳转到Emby播放,支持 JavBus/Javdb/library/javmunu/XXXClub
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025.3.7
  5. // @description 在JavBus/Javdb/library/javmunu图书馆高亮emby存在的视频,并在详情页提供一键跳转功能
  6. // @include /^.*(jav|bus|dmm|see|cdn|fan){2}\..*$/
  7. // @match *://www.javbus.com/*
  8. // @include *://javdb*.com/v/*
  9. // @match *://javmenu.com/*
  10. // @match *://xxxclub.to/*
  11. // @include *://javdb*.com/search?q=*
  12. // @match *://www.javdb.com/*
  13. // @match *://javdb.com/*
  14. // @include *://*.javlib.com/*
  15. // @include *://*.javmenu.com/*
  16. // @include *://*.javlibrary.com/*
  17. // @include *://*/cn/*v=jav*
  18. // @include *://*/en/*v=jav*
  19. // @include *://*/tw/*v=jav*
  20. // @include *://*/ja/*v=jav*
  21. // @include /^.*(avmoo|avsox)\..*$/
  22. // @include *://avmoo.*/*/movie/*
  23. // @include *://avsox.*/*/movie/*
  24. // @match https://www.sehuatang.net/thread-*
  25. // @match https://www.sehuatang.net/forum.php?mod=viewthread&tid=*
  26. // @match https://.*/thread-*
  27. // @match https://.*/forum.php?mod=viewthread&tid=*
  28. // @match https://www.tanhuazu.com/threads/*
  29. // @match *://javbooks.com/content*censored/*.htm
  30. // @match *://jmvbt.com/content*censored/*.htm
  31. // @match *://*.com/content*censored/*.htm
  32. // @include *://*.cc/content_censored/*.htm
  33. // @include /^https:\/\/jbk008\.com\/serchinfo\_(censored|uncensored)\/topicsbt/
  34. // @match *://db.msin.jp/jp.page/movie?id=*
  35. // @match *://db.msin.jp/page/movie?id=*
  36. // @include *://*/works/detail/*
  37. // @match *://xslist.org/search?query=*
  38. // @grant GM_xmlhttpRequest
  39. // @license MIT
  40.  
  41. // ==/UserScript==
  42.  
  43. const embyAPI = "279b8961e1fd44b1bf8ea4c025f67075";
  44. const embyBaseUrl = "http://192.168.5.8:35405/";
  45. const defaultColor = "#52b54b"; // HotPink
  46.  
  47. (function () {
  48. 'use strict';
  49.  
  50. // 新增 Cloudflare 检测函数
  51. function checkCloudflareChallenge() {
  52. const cloudflareSelectors = [
  53. '#challenge-form', // Cloudflare验证表单
  54. '.cf-browser-verification',// 浏览器验证容器
  55. 'div.ray-id', // Ray ID 元素
  56. 'div.cf-spinner-rotator', // 加载动画
  57. 'trk-page[data-title^="Just a moment"]' // 特定页面标题
  58. ];
  59. return cloudflareSelectors.some(selector => document.querySelector(selector)) ||
  60. document.title.includes('Just a moment') ||
  61. document.body.textContent.includes('Cloudflare');
  62. }
  63. // 新增等待函数
  64. function waitForCloudflare(callback, maxAttempts = 30, interval = 1000) {
  65. let attempts = 0;
  66. const checkInterval = setInterval(() => {
  67. if (!checkCloudflareChallenge()) {
  68. clearInterval(checkInterval);
  69. callback();
  70. } else if (attempts++ >= maxAttempts) {
  71. clearInterval(checkInterval);
  72. console.log('Cloudflare验证等待超时');
  73. callback();
  74. }
  75. }, interval);
  76. }
  77.  
  78.  
  79. class Base {
  80. fetchEmbyData(code, callback) {
  81. if (!code) {
  82. console.warn("番号为空,跳过请求");
  83. return; // 番号为空时直接返回
  84. }
  85.  
  86. console.log('Fetching data for code:', code);
  87.  
  88. GM_xmlhttpRequest({
  89. method: "GET",
  90. url: `${embyBaseUrl}emby/Users/${embyAPI}/Items?api_key=${embyAPI}&Recursive=true&IncludeItemTypes=Movie&SearchTerm=${code}`,
  91. headers: { accept: "application/json" },
  92. onload: (res) => {
  93. try {
  94. const data = JSON.parse(res.responseText);
  95. callback(data);
  96. } catch (error) {
  97. console.error("Failed to parse response:", error);
  98. }
  99. },
  100. onerror: (e) => {
  101. console.error("Error fetching Emby data:", e);
  102. },
  103. ontimeout: () => {
  104. console.error("Request to Emby timed out");
  105. }
  106. });
  107. }
  108.  
  109. insertEmbyLink(targetElement, data) {
  110. const maxLinks = 5; // 限制最多插入 5 个链接
  111. let insertedLinks = 0;
  112.  
  113. data.Items.forEach(item => {
  114. if (insertedLinks >= maxLinks) {
  115. console.log("已达到最大链接插入数量,跳过剩余链接");
  116. return;
  117. }
  118.  
  119. const embyUrl = `${embyBaseUrl}web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`;
  120. console.log(`生成的 embyUrl: ${embyUrl}`);
  121.  
  122. // 确保 targetElement 是 DOM 元素
  123. const domElement = targetElement instanceof jQuery ? targetElement[0] : targetElement;
  124.  
  125. // 检查是否已经存在 "跳转到Emby" 链接
  126. console.log('正在检查是否存在跳转到Emby链接...');
  127. // 使用 class 判断是否已经插入
  128. const parentElement = domElement.parentElement || domElement; // 检查父级范围
  129. if (parentElement.querySelector(`a[href="${embyUrl}"]`)) {
  130. console.log('跳转到Emby链接已存在,跳过插入');
  131. return;
  132. }
  133.  
  134. const embyUrlSpanStyle = `background: ${defaultColor}; border-radius: 3px; padding: 3px 6px;`;
  135. const embyUrlAStyle = `color: white; text-decoration: none;`;
  136. const embyLink = `<div style="${embyUrlSpanStyle}">
  137. <a href="${embyUrl}" style="${embyUrlAStyle}" target="_blank">
  138. <b>跳转到emby👉</b>
  139. </a>
  140. </div>`;
  141.  
  142. // 使用 jQuery 插入链接
  143. console.log('正在插入跳转到Emby链接...');
  144. $(domElement).after(embyLink);
  145. insertedLinks++; // 记录已插入的链接数量
  146. });
  147. }
  148.  
  149. highlightAndInsertEmbyLink(videos, extractFanhaoFunction, insertAfterSelector) {
  150. // console.log('Highlighting videos...', videos);
  151. const videoArray = Array.from(videos);
  152.  
  153. videoArray.forEach(videoElement => {
  154. const fanhaos = extractFanhaoFunction(videoElement);
  155. // console.log('Fanhaos:', fanhaos);
  156.  
  157. if (!fanhaos || fanhaos.length === 0) {
  158. console.warn("未提取到番号,跳过该视频");
  159. return; // 番号为空时跳过
  160. }
  161.  
  162. const searchNextFanhao = (fanhaoIndex) => {
  163. if (fanhaoIndex >= fanhaos.length) return;
  164.  
  165. let fanhao = fanhaos[fanhaoIndex];
  166. this.fetchEmbyData(fanhao, (data) => {
  167. if (data.Items.length > 0) {
  168. const targetElement = insertAfterSelector
  169. ? videoElement.querySelector(insertAfterSelector)
  170. : videoElement;
  171. const domElement = targetElement instanceof jQuery ? targetElement[0] : targetElement;
  172. this.insertEmbyLink(domElement, data);
  173.  
  174. // 高亮
  175. videoElement.style.borderWidth = "3px";
  176. videoElement.style.borderStyle = "solid";
  177. videoElement.style.borderColor = defaultColor;
  178. videoElement.style.backgroundColor = defaultColor;
  179. } else {
  180. searchNextFanhao(fanhaoIndex + 1);
  181. }
  182. });
  183. };
  184.  
  185. searchNextFanhao(0);
  186. });
  187. }
  188. }
  189.  
  190. // 定义各站点处理类(保持空类结构)
  191. class JavBus extends Base { }
  192. class JavLibrary extends Base { }
  193. class Javdb extends Base { }
  194. class Javbooks extends Base { }
  195. class Avmoo extends Base { }
  196. class Sehuatang extends Base { }
  197. class Msin extends Base { }
  198. class Javmenu extends Base { }
  199. class XXXClub extends Base { }
  200.  
  201. class Main {
  202. constructor() {
  203. console.log('Jav跳转Emby启动...');
  204.  
  205. this.sites = {
  206. 'javBus': {
  207. // 选择器,用于判断当前页面是否属于 JavBus 网站。
  208. // 这里通过选择页脚 `<footer>` 标签中包含文本 'JavBus' 来判断。
  209. selector: "footer:contains('JavBus')",
  210.  
  211. // JavBus 的站点处理类。这是一个自定义的逻辑类,用于处理该站点的具体操作。
  212. class: JavBus,
  213.  
  214. // 选择器,用于判断是否是 JavBus 网站的列表页(瀑布流页面)。
  215. // 这里通过选择 `#waterfall`(id)下的 `.item.masonry-brick`(class)元素来进行判断。
  216. listPageSelector: "#waterfall .item.masonry-brick",
  217.  
  218. // 定义在列表页中插入新内容的位置。
  219. // 这里选择 `.item` 元素下的 `date` 标签作为插入位置的参考点。
  220. listPageInsertAfter: ".item date",
  221.  
  222. // 在列表页中提取番号的逻辑。
  223. // `el` 是当前列表项的 DOM 元素。
  224. // 通过查询 `.item date` 元素来获取番号,如果找到对应的元素,则返回其文本内容(去除前后空格)。
  225. // 如果找不到则返回空数组。
  226. listPageExtract: (el) => {
  227. const fanhaoElement = el.querySelector('.item date'); // 查找 `.item date` 元素
  228. return fanhaoElement ? [fanhaoElement.textContent.trim()] : []; // 提取文本并返回
  229. },
  230.  
  231. // 选择器,用于判断是否是 JavBus 网站的详情页。
  232. // 这里通过选择 `.col-md-3.info p span:nth-child(2)` 来判断,这个选择器通常用于获取影片的识别码/番号。
  233. detailPageSelector: '.col-md-3.info p span:nth-child(2)',
  234.  
  235. // 定义在详情页中插入新内容的容器。
  236. // 这里选择 `.col-md-3.info p span:nth-child(2):first`,表示在符合条件的第一个 `span` 元素中插入内容。
  237. detailPageContainer: ".col-md-3.info p span:nth-child(2):first",
  238.  
  239. // 在详情页中提取番号的逻辑。
  240. // 通过 jQuery 查询 `.col-md-3.info p` 的第一个段落,并查找其第二个 `span` 元素。
  241. // 如果找到有效的内容,则返回其文本内容。
  242. // 如果找不到则返回空数组。
  243. detailPageExtract: () => {
  244. const code = $('.col-md-3.info p').eq(0).find('span').eq(1).html(); // 查找番号
  245. return code ? [code] : []; // 提取文本并返回
  246. }
  247. },
  248.  
  249. 'javmenu': {
  250. selector: "footer:contains('JAVMENU V3')",
  251. class: Javmenu,
  252. listPageSelector: ".page-content .category-page.video-list-item",
  253. listPageInsertAfter: ".card-title.text-dark",
  254. listPageExtract: (el) => {
  255. console.log(el);
  256. const fanhaoElement = el.querySelector('.card-title.text-dark');
  257. console.log(fanhaoElement);
  258. return fanhaoElement ? [fanhaoElement.textContent.trim()] : [];
  259. },
  260. detailPageSelector: '.page-content .container-fluid .tab-content h1 strong',
  261. detailPageContainer: ".page-content",
  262. detailPageExtract: () => {
  263. const code = $('.page-content .container-fluid .tab-content h1 strong').text().trim().split(' ')[0];
  264. return code ? [code] : [];
  265. }
  266. },
  267. 'xxxclub': {
  268. selector: ".page-footer:contains('XXXClub')",
  269. // selector: "div.page-footer:contains('XXXClub 2020 - 2025')", // 通过页脚标识站点
  270. class: XXXClub,
  271. listPageSelector: ".main-content ul li", // 列表项选择器
  272. listPageInsertAfter: "span:nth-of-type(2) > a", // 在标题链接后插入
  273. listPageExtract: (el) => {
  274. const aElement = el.querySelector('span:nth-of-type(2) > a');
  275. if (!aElement) return [];
  276. const title = aElement.textContent.trim();
  277. // console.log(title);
  278.  
  279. // 从标题文本提取番号
  280. // 情况一:Brand后接三组两位数字: (格式示例: Brand YY MM DD)
  281. const case1Match = title.match(/^(\S+)\s+(\d{2})\s+(\d{2})\s+(\d{2})/);
  282. // return match ? [`${match[1]}.${match[2]}.${match[3]}.${match[4]}`] : [];
  283. // console.log(match);
  284. if (case1Match) {
  285. // 组合成标准番号格式: Brand.YY.MM.DD
  286. return [`${case1Match[1]}.${case1Match[2]}.${case1Match[3]}.${case1Match[4]}`];
  287. }
  288.  
  289.  
  290. // 情况二:匹配括号内的日期(DD.MM.YYYY)
  291. const case2Match = title.match(/^(\S+?) - .*?\((\d{2})\.(\d{2})\.(\d{4})\)$/);
  292. if (case2Match) {
  293. const [, brand, dd, mm, yyyy] = case2Match;
  294. const yy = yyyy.slice(-2); // 提取年份后两位
  295. return [`${brand}.${yy}.${mm}.${dd}`];
  296. }
  297.  
  298.  
  299. // 情况三:匹配品牌 - 演员 - 标题的结构
  300. const case3Match = title.match(/^(\S+) - .+? - (?!.*-)(.+)$/);
  301. if (case3Match) {
  302. return [`${case3Match[1]} ${case3Match[2]}`];
  303. }
  304.  
  305. // 情况四:提取前五个空格前的部分
  306. let currentIndex = -1;
  307. let found = true;
  308. for (let i = 0; i < 5; i++) {
  309. currentIndex = title.indexOf(' ', currentIndex + 1);
  310. if (currentIndex === -1) {
  311. found = false;
  312. break;
  313. }
  314. };
  315. return found ? [title.substring(0, currentIndex)] : [];
  316. },
  317. },
  318. 'javLibrary': {
  319. selector: "#bottomcopyright:contains('JAVLibrary')",
  320. class: JavLibrary,
  321. listPageSelector: ".video",
  322. listPageInsertAfter: "a",
  323. detailPageSelector: '#content #video_title #video_jacket_info #video_info .item .text',
  324. detailPageContainer: "#video_info",
  325. commentPageSelector: "#video_comments .comment",
  326. commentPageInsertAfter: "strong",
  327. listPageExtract: (el) => {
  328. const fanhao = el.children[0]?.title.split(" ")[0] || el.children[1]?.title.split(" ")[0];
  329. return fanhao ? [fanhao] : [];
  330. },
  331. detailPageExtract: () => {
  332. const code = $('#video_info .item').eq(0).find('.text').html();
  333. return code ? [code] : [];
  334. },
  335. commentPageExtract: (el) => {
  336. const anchorElement = el.querySelector('a[href^="videoreviews.php?v="]');
  337. return anchorElement ? [anchorElement.textContent.split(" ")[0]] : [];
  338. }
  339. },
  340. 'javdb': {
  341. selector: "#footer:contains('javdb')",
  342. class: Javdb,
  343. listPageSelector: ".movie-list .item",
  344. listPageInsertAfter: ".video-title strong", // 新增插入位置控制
  345. detailPageSelector: 'body > section > div > div.video-detail > h2 > strong',
  346. detailPageContainer: ".panel.movie-panel-info .value:first",
  347. listPageExtract: (el) => {
  348. const result = [];
  349. const videoTitleElement = el.querySelector('.video-title strong');
  350. if (videoTitleElement) {
  351. const strongText = videoTitleElement.textContent.trim();
  352. // 检查 strong 文本中是否包含至少 3 位数字
  353. const hasThreeDigits = (strongText.match(/\d/g) || []).length >= 3;
  354. if (hasThreeDigits) {
  355. // 仅移除空格,保留其他字符
  356. const processed = strongText.replace(/ /g, '');
  357. // 仅移除第一个出现的空格
  358. // const processed = strongText.replace(/ /, '');
  359. result.push(processed);
  360. } else {
  361. // 克隆节点以避免修改原始 DOM
  362. const videoTitle = el.querySelector('.video-title');
  363. const clonedTitle = videoTitle.cloneNode(true);
  364. // 处理克隆节点中的 strong 标签第一个空格
  365. const clonedStrong = clonedTitle.querySelector('strong');
  366. if (clonedStrong) {
  367. clonedStrong.textContent = clonedStrong.textContent
  368. .trim()
  369. .replace(/ /g, ''); // 仅替换第一个空格
  370. }
  371. // 统一清理所有非字母数字字符
  372. const fullTitle = clonedTitle.textContent
  373. .trim()
  374. .replace(/[^a-zA-Z0-9]+/g, ' ') // 合并非字母数字字符为空格
  375. .trim();
  376. result.push(fullTitle);
  377. }
  378. }
  379. return result;
  380. },
  381.  
  382. detailPageExtract: () => {
  383. const code = $('body > section > div > div.video-detail > h2 > strong').text().trim().split(' ')[0];
  384. return code ? [code] : [];
  385. }
  386. },
  387. 'javbooks': {
  388. selector: "#Declare_box:contains('javbooks')",
  389. class: Javbooks,
  390. detailPageSelector: '#info > div:nth-child(2) > font',
  391. detailPageContainer: "#info",
  392. detailPageExtract: () => {
  393. const code = $('#info > div:nth-child(2) > font').text().trim().split(' ')[0];
  394. return code ? [code] : [];
  395. }
  396. },
  397. 'avmoo': {
  398. selector: "footer:contains('AVMOO')",
  399. class: Avmoo,
  400. listPageSelector: "#waterfall .item", // 根据id和class判断瀑布/列表页
  401. listPageInsertAfter: ".item date", // 新增插入位置控制
  402. listPageExtract: (el) => { // 列表页提取番号
  403. const fanhaoElement = el.querySelector('.item date');
  404. return fanhaoElement ? [fanhaoElement.textContent.trim()] : [];
  405. },
  406. detailPageSelector: '.col-md-3.info p span:nth-child(2)',
  407. detailPageContainer: ".col-md-3.info",
  408. detailPageExtract: () => {
  409. const code = $('.col-md-3.info p').eq(0).find('span').eq(1).html();
  410. return code ? [code] : [];
  411. }
  412. },
  413. 'sehuatang': {
  414. selector: "#flk:contains('色花堂')",
  415. class: Sehuatang,
  416. detailPageCodeRegex: /([a-zA-Z]{2,15}[-\s]?\d{2,15}|FC2PPV-[^\d]{0,5}\d{6,7})/i,
  417. detailPageContainer: "#pgt",
  418. detailPageExtract: () => {
  419. const str = document.title.split(" ")[0];
  420. return str.match(this.detailPageCodeRegex) || [];
  421. }
  422. },
  423. 'msin': {
  424. selector: "#footer:contains('db.msin.jp')",
  425. class: Msin,
  426. detailPageSelector: 'div.mv_pn',
  427. detailPageContainer: "#top_content",
  428. detailPageExtract: () => {
  429. const code = $('div.mv_pn').text().trim().split(' ')[0];
  430. return code ? [code] : [];
  431. }
  432. }
  433. };
  434.  
  435. this.site = Object.keys(this.sites).find(key => $(this.sites[key].selector).length) || null;
  436. console.log('Matched site:', this.site);
  437.  
  438. this.siteClass = this.site ? this.sites[this.site].class : null;
  439. // console.log('Site class:', this.siteClass);
  440. }
  441.  
  442. make() {
  443. if (!this.siteClass) return;
  444.  
  445. const siteConfig = this.sites[this.site];
  446. // console.log('Site Config:', this.siteConfig);
  447. const instance = new siteConfig.class();
  448.  
  449. console.log('判断是否是列表页 $(siteConfig.listPageSelector).length:', $(siteConfig.listPageSelector).length);
  450. console.log('判断是否是评论页 $(siteConfig.commentPageSelector).length:', $(siteConfig.commentPageSelector).length);
  451.  
  452. // 处理列表页
  453. if ($(siteConfig.listPageSelector).length > 0) {
  454. console.log('处理列表页', $(siteConfig.listPageSelector).length);
  455. instance.highlightAndInsertEmbyLink(
  456. $(siteConfig.listPageSelector),
  457. (el) => siteConfig.listPageExtract ? siteConfig.listPageExtract(el) : [],
  458. siteConfig.listPageInsertAfter ? siteConfig.listPageInsertAfter : null // 传递插入位置选择器
  459. );
  460.  
  461. }
  462. // 处理详情页
  463. else if ($(siteConfig.detailPageSelector).length > 0) {
  464. console.log('处理详情页', $(siteConfig.detailPageSelector).length);
  465. const codes = siteConfig.detailPageExtract ? siteConfig.detailPageExtract() : [];
  466. codes.forEach(code => {
  467. instance.fetchEmbyData(code, (data) => {
  468. if (data.Items.length > 0) {
  469. // instance.insertEmbyLink($(siteConfig.detailPageContainer), data);
  470. const detailContainer = $(siteConfig.detailPageContainer);
  471. if (detailContainer.length > 0) {
  472. instance.insertEmbyLink(detailContainer[0], data);
  473. }
  474. }
  475. });
  476. });
  477. }
  478. // 处理评论页
  479. else if ($(siteConfig.commentPageSelector).length > 0) {
  480. console.log('处理评论页');
  481. // 添加对应的评论页选择器和处理逻辑
  482. instance.highlightAndInsertEmbyLink(
  483. $(siteConfig.commentPageSelector),
  484. (el) => siteConfig.commentPageExtract ? siteConfig.commentPageExtract(el) : [],
  485. siteConfig.commentPageInsertAfter
  486. );
  487. }
  488. }
  489. }
  490.  
  491.  
  492.  
  493. // 添加浮动按钮
  494. function addFloatingButton() {
  495. const button = document.createElement('div');
  496. button.textContent = '运行脚本';
  497. button.style.position = 'fixed';
  498. button.style.left = '10px';
  499. button.style.top = '50%';
  500. button.style.transform = 'translateY(-50%)';
  501. button.style.backgroundColor = '#52b54b';
  502. button.style.color = 'white';
  503. button.style.padding = '10px 15px';
  504. button.style.borderRadius = '5px';
  505. button.style.cursor = 'pointer';
  506. button.style.zIndex = 9999;
  507. button.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
  508.  
  509. button.addEventListener('click', () => {
  510. console.log('运行脚本按钮被点击');
  511. new Main().make();
  512. });
  513.  
  514. document.body.appendChild(button);
  515. }
  516.  
  517. // 修改初始化逻辑
  518. setTimeout(() => {
  519. // 先添加浮动按钮(用户可手动触发)
  520. addFloatingButton();
  521.  
  522. // 检测并等待 Cloudflare
  523. waitForCloudflare(() => {
  524. console.log('Cloudflare验证完成,启动主逻辑');
  525. new Main().make();
  526. });
  527. }, 1000);
  528.  
  529. })();