Javdb_Emby助手

Javdb_Emby助手_weiShao

  1. // ==UserScript==
  2. // @name Javdb_Emby助手
  3. // @version 1.0.1
  4. // @author weiShao
  5. // @description Javdb_Emby助手_weiShao
  6. // @license MIT
  7. // @icon https://www.javdb.com/favicon.ico
  8. // @match https://*.javdb.com/*
  9. // @match *://*.javdb.com/*
  10. // @connect jable.tv
  11. // @connect missav.com
  12. // @connect javhhh.com
  13. // @connect netflav.com
  14. // @connect avgle.com
  15. // @connect bestjavporn.com
  16. // @connect jav.guru
  17. // @connect javmost.cx
  18. // @connect hpjav.tv
  19. // @connect av01.tv
  20. // @connect javbus.com
  21. // @connect javmenu.com
  22. // @connect javfc2.net
  23. // @connect paipancon.com
  24. // @connect ggjav.com
  25. // @grant GM_addStyle
  26. // @grant GM_xmlhttpRequest
  27. // @namespace https://greasyfork.org/users/434740
  28. // ==/UserScript==
  29.  
  30. ;(function () {
  31. 'use strict'
  32. /* globals jQuery, $, waitForKeyElements */
  33.  
  34.  
  35. /**
  36. * 加载动画
  37. */
  38. const LoadingGif = {
  39. /**
  40. * 加载动画的元素
  41. */
  42. element: null,
  43.  
  44. /**
  45. * 启动加载动画
  46. */
  47. start: function () {
  48. if (!this.element) {
  49. this.element = document.createElement('img')
  50. this.element.src =
  51. 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Gray_circles_rotate.gif'
  52. this.element.alt = '读取文件夹中...'
  53. Object.assign(this.element.style, {
  54. position: 'fixed',
  55. bottom: '0',
  56. left: '50px',
  57. zIndex: '1000',
  58. width: '40px',
  59. height: '40px',
  60. padding: '5px'
  61. })
  62. document.body.appendChild(this.element)
  63. }
  64. },
  65.  
  66. /**
  67. * 停止加载动画
  68. */
  69. stop: function () {
  70. if (this.element) {
  71. document.body.removeChild(this.element)
  72. this.element = null
  73. }
  74. }
  75. }
  76.  
  77. /**
  78. * 本地文件夹处理函数
  79. */
  80. const LocalFolderHandler = (function () {
  81. class LocalFolderHandlerClass {
  82. constructor() {
  83. this.nfoFileNamesSet = new Set()
  84. this.initButton()
  85. }
  86.  
  87. /**
  88. * 创建一个按钮元素并添加到页面中
  89. */
  90. initButton() {
  91. const button = this.createButtonElement()
  92. button.addEventListener('click', this.handleButtonClick.bind(this))
  93. document.body.appendChild(button)
  94. }
  95.  
  96. /**
  97. * 创建一个按钮元素
  98. * @returns {HTMLButtonElement}
  99. */
  100. createButtonElement() {
  101. const button = document.createElement('button')
  102. button.innerHTML = '仓'
  103.  
  104. Object.assign(button.style, {
  105. color: '#fff',
  106. backgroundColor: '#FF8400',
  107. borderColor: '#FF8400',
  108. borderRadius: '5px',
  109. position: 'fixed',
  110. bottom: '2px',
  111. left: '2px',
  112. zIndex: '1000',
  113. padding: '5px 10px',
  114. cursor: 'pointer',
  115. fontSize: '16px',
  116. fontWeight: 'bold'
  117. })
  118.  
  119. return button
  120. }
  121.  
  122. /**
  123. * 按钮点击事件处理函数
  124. */
  125. async handleButtonClick() {
  126. this.nfoFileNamesSet.clear()
  127.  
  128. const directoryHandle = await window.showDirectoryPicker()
  129. console.log(
  130. '%c Line:90 🍖 directoryHandle',
  131. 'color:#42b983',
  132. directoryHandle.name
  133. )
  134.  
  135. if (!directoryHandle) {
  136. alert('获取本地信息失败')
  137. return
  138. }
  139.  
  140. const startTime = Date.now()
  141. LoadingGif.start()
  142.  
  143. for await (const fileData of this.getFiles(directoryHandle, [
  144. directoryHandle.name
  145. ])) {
  146. const file = await fileData.fileHandle.getFile()
  147. const videoFullName = await this.findVideoFileName(
  148. fileData.parentDirectoryHandle
  149. )
  150.  
  151. const item = {
  152. originalFileName: file.name.substring(
  153. 0,
  154. file.name.length - '.nfo'.length
  155. ),
  156. transformedName: this.processFileName(file.name),
  157. videoFullName: videoFullName,
  158. hierarchicalStructure: [...fileData.folderNames, videoFullName]
  159. }
  160.  
  161. this.nfoFileNamesSet.add(item)
  162. }
  163.  
  164. const str = JSON.stringify(Array.from(this.nfoFileNamesSet))
  165. localStorage.setItem('nfoFiles', str)
  166.  
  167. LoadingGif.stop()
  168.  
  169. const endTime = Date.now()
  170. const time = ((endTime - startTime) / 1000).toFixed(2)
  171.  
  172. alert(
  173. `读取文件夹: '${directoryHandle.name}' 成功,耗时 ${time} 秒, 共读取 ${this.nfoFileNamesSet.size} 个视频。`
  174. )
  175.  
  176. onBeforeMount()
  177. }
  178.  
  179. /**
  180. * 递归获取目录下的所有文件
  181. * @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
  182. * @param {string[]} folderNames - 目录名数组
  183. * @returns {AsyncGenerator}
  184. */
  185. async *getFiles(directoryHandle, folderNames = []) {
  186. for await (const entry of directoryHandle.values()) {
  187. try {
  188. if (entry.kind === 'file' && entry.name.endsWith('.nfo')) {
  189. yield {
  190. fileHandle: entry,
  191. folderNames: [...folderNames],
  192. parentDirectoryHandle: directoryHandle
  193. }
  194. } else if (entry.kind === 'directory') {
  195. yield* this.getFiles(entry, [...folderNames, entry.name])
  196. }
  197. } catch (e) {
  198. console.error(e)
  199. }
  200. }
  201. }
  202.  
  203. /**
  204. * 查找视频文件名
  205. * @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
  206. * @returns {Promise<string>} 找到的视频文件名或空字符串
  207. */
  208. async findVideoFileName(directoryHandle) {
  209. for await (const entry of directoryHandle.values()) {
  210. if (
  211. entry.kind === 'file' &&
  212. (entry.name.endsWith('.mp4') ||
  213. entry.name.endsWith('.mkv') ||
  214. entry.name.endsWith('.avi') ||
  215. entry.name.endsWith('.flv') ||
  216. entry.name.endsWith('.wmv') ||
  217. entry.name.endsWith('.mov') ||
  218. entry.name.endsWith('.rmvb'))
  219. ) {
  220. return entry.name
  221. }
  222. }
  223. return ''
  224. }
  225.  
  226. /**
  227. * 处理文件名
  228. * 去掉 '.nfo'、'-c'、'-C' 和 '-破解' 后缀,并转换为小写
  229. * @param {string} fileName - 原始文件名
  230. * @returns {string} 处理后的文件名
  231. */
  232. processFileName(fileName) {
  233. let processedName = fileName.substring(
  234. 0,
  235. fileName.length - '.nfo'.length
  236. )
  237. processedName = processedName.replace(/-c$/i, '')
  238. processedName = processedName.replace(/-破解$/i, '')
  239. return processedName.toLowerCase()
  240. }
  241. }
  242.  
  243. return function () {
  244. new LocalFolderHandlerClass()
  245. }
  246. })()
  247.  
  248. /**
  249. * 列表页处理函数
  250. */
  251. const ListPageHandler = (function () {
  252. /**
  253. * @type {string} btsow 搜索 URL 基础路径
  254. */
  255. const btsowUrl = 'https://btsow.com/search/'
  256.  
  257. /**
  258. * 获取本地存储的 nfo 文件名的 JSON 字符串
  259. * @returns {string[]|null} nfo 文件名数组或 null
  260. */
  261. function getNfoFiles() {
  262. const nfoFilesJson = localStorage.getItem('nfoFiles')
  263. return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
  264. }
  265.  
  266. /**
  267. * 创建本地打开视频所在文件夹按钮
  268. * @param {HTMLElement} ele 要添加的所在的元素
  269. */
  270. function createOpenLocalFolderBtn(ele) {
  271. if (ele.querySelector('.open_local_folder')) {
  272. return
  273. }
  274.  
  275. const openLocalFolderBtnElement = document.createElement('div')
  276. openLocalFolderBtnElement.className = 'tag open_local_folder'
  277. openLocalFolderBtnElement.textContent = '本地打开'
  278.  
  279. Object.assign(openLocalFolderBtnElement.style, {
  280. marginLeft: '10px',
  281. color: '#fff',
  282. backgroundColor: '#F8D714'
  283. })
  284.  
  285. openLocalFolderBtnElement.addEventListener('click', function (event) {
  286. event.preventDefault()
  287. const localFolderPath = 'Z:\\日本'
  288. // 打开本地文件夹逻辑
  289. })
  290.  
  291. ele.querySelector('.tags').appendChild(openLocalFolderBtnElement)
  292. }
  293.  
  294. /**
  295. * 创建 btsow 搜索视频按钮
  296. * @param {HTMLElement} ele 要添加的所在的元素
  297. * @param {string} videoTitle 视频标题
  298. */
  299. function createBtsowBtn(ele, videoTitle) {
  300. if (ele.querySelector('.btsow')) {
  301. return
  302. }
  303.  
  304. const btsowBtnElement = document.createElement('div')
  305. btsowBtnElement.className = 'tag btsow'
  306. btsowBtnElement.textContent = 'Btsow'
  307.  
  308. Object.assign(btsowBtnElement.style, {
  309. marginLeft: '10px',
  310. color: '#fff',
  311. backgroundColor: '#FF8400'
  312. })
  313.  
  314. btsowBtnElement.addEventListener('click', function (event) {
  315. event.preventDefault()
  316. window.open(`${btsowUrl}${videoTitle}`, '_blank')
  317. })
  318.  
  319. ele.querySelector('.tags').appendChild(btsowBtnElement)
  320. }
  321.  
  322. /**
  323. * 显示本地下载的文件名并改写样式
  324. * @param {HTMLElement} ele 元素
  325. * @param {Object} item 影片项
  326. */
  327. function displayOperationOfTheItemInQuestion(ele, item) {
  328. const imgElement = ele.querySelector('.cover img')
  329. imgElement.style.padding = '10px'
  330. imgElement.style.backgroundColor = '#FF0000'
  331.  
  332. const videoTitleElement = document.createElement('div')
  333. videoTitleElement.textContent = item.originalFileName
  334.  
  335. Object.assign(videoTitleElement.style, {
  336. margin: '1rem',
  337. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  338. color: '#fff',
  339. fontSize: '.75rem',
  340. height: '2rem',
  341. display: 'flex',
  342. alignItems: 'center',
  343. justifyContent: 'center',
  344. borderRadius: '5px'
  345. })
  346.  
  347. ele.querySelector('.box').appendChild(videoTitleElement)
  348.  
  349. videoTitleElement.addEventListener('click', function () {
  350. navigator.clipboard.writeText(item.originalFileName)
  351. videoTitleElement.textContent = item.originalFileName + ' 复制成功'
  352. })
  353. }
  354.  
  355. /**
  356. * 处理列表页逻辑
  357. */
  358. function handler() {
  359. const nfoFilesArray = getNfoFiles()
  360. if (!nfoFilesArray) {
  361. return
  362. }
  363.  
  364. LoadingGif.start()
  365.  
  366. $('.movie-list .item').each(function (index, ele) {
  367. const videoTitle = ele.querySelector('strong').innerText.toLowerCase()
  368. createBtsowBtn(ele, videoTitle)
  369.  
  370. nfoFilesArray.forEach(function (item) {
  371. if (item.transformedName.includes(videoTitle)) {
  372. createOpenLocalFolderBtn(ele)
  373. displayOperationOfTheItemInQuestion(ele, item)
  374. }
  375. })
  376. })
  377.  
  378. LoadingGif.stop()
  379. }
  380.  
  381. return handler
  382. })()
  383.  
  384. /**
  385. * 详情页处理函数
  386. */
  387. const DetailPageHandler = (function () {
  388. /**
  389. * 获取页面视频标题
  390. * @returns {string} 视频标题文本
  391. */
  392. function getVideoTitle() {
  393. return $('.video-detail strong').first().text().trim().toLowerCase()
  394. }
  395.  
  396. /**
  397. * 从 localStorage 获取 nfoFiles
  398. * @returns {Array} nfoFiles 数组
  399. */
  400. function getNfoFiles() {
  401. const nfoFilesJson = localStorage.getItem('nfoFiles')
  402. return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
  403. }
  404.  
  405. /**
  406. * 设置 .video-meta-panel 背景色
  407. */
  408. function highlightVideoPanel() {
  409. $('.video-meta-panel').css({ backgroundColor: '#FFC0CB' })
  410. }
  411.  
  412. /**
  413. * 创建或获取影片存在提示元素
  414. * @returns {HTMLElement} localFolderTitleListElement 元素
  415. */
  416. function createOrGetLocalFolderTitleListElement() {
  417. let localFolderTitleListElement = document.querySelector(
  418. '.localFolderTitleListElement'
  419. )
  420. if (!localFolderTitleListElement) {
  421. localFolderTitleListElement = document.createElement('div')
  422. localFolderTitleListElement.className = 'localFolderTitleListElement'
  423. localFolderTitleListElement.textContent = 'Emby已存在影片'
  424.  
  425. Object.assign(localFolderTitleListElement.style, {
  426. color: '#fff',
  427. backgroundColor: '#FF8400',
  428. padding: '5px 10px',
  429. borderRadius: '5px',
  430. fontSize: '16px',
  431. fontWeight: 'bold',
  432. position: 'fixed',
  433. left: '20px',
  434. top: '200px',
  435. width: '240px'
  436. })
  437.  
  438. document.body.appendChild(localFolderTitleListElement)
  439. }
  440. return localFolderTitleListElement
  441. }
  442.  
  443. /**
  444. * 添加影片列表项
  445. * @param {Object} item 影片项
  446. */
  447. function addLocalFolderTitleListItem(item) {
  448. const localFolderTitleListElement =
  449. createOrGetLocalFolderTitleListElement()
  450.  
  451. const localFolderTitleListItem = document.createElement('div')
  452. localFolderTitleListItem.className = 'localFolderTitleListItem'
  453. localFolderTitleListItem.textContent = item.originalFileName
  454.  
  455. Object.assign(localFolderTitleListItem.style, {
  456. color: '#fff',
  457. padding: '5px 10px',
  458. borderRadius: '5px',
  459. fontSize: '16px',
  460. fontWeight: 'bold',
  461. marginTop: '10px'
  462. })
  463.  
  464. localFolderTitleListElement.appendChild(localFolderTitleListItem)
  465.  
  466. localFolderTitleListItem.addEventListener('click', function () {
  467. navigator.clipboard.writeText(item.transformedName)
  468. localFolderTitleListItem.textContent =
  469. item.originalFileName + ' 复制成功'
  470. })
  471. }
  472.  
  473. /**
  474. * 排序种子列表
  475. */
  476. function sortBtList() {
  477. const magnetsContent = document.getElementById('magnets-content')
  478. if (!magnetsContent?.children.length) return
  479.  
  480. const items = Array.from(magnetsContent.querySelectorAll('.item'))
  481.  
  482. items.forEach(function (item) {
  483. const metaSpan = item.querySelector('.meta')
  484. if (metaSpan) {
  485. const metaText = metaSpan.textContent.trim()
  486. const match = metaText.match(/(\d+(\.\d+)?)GB/)
  487. const size = match ? parseFloat(match[1]) : 0
  488. item.dataset.size = size
  489. }
  490. })
  491.  
  492. items.sort(function (a, b) {
  493. return b.dataset.size - a.dataset.size
  494. })
  495.  
  496. const priority = {
  497. high: [],
  498. medium: [],
  499. low: []
  500. }
  501.  
  502. items.forEach(function (item) {
  503. const nameSpan = item.querySelector('.name')
  504. if (nameSpan) {
  505. const nameText = nameSpan.textContent.trim()
  506.  
  507. if (/(-c| -C)/i.test(nameText)) {
  508. priority.high.push(item)
  509. item.style.backgroundColor = '#FFCCFF'
  510. } else if (!/[A-Z]/.test(nameText)) {
  511. priority.medium.push(item)
  512. item.style.backgroundColor = '#FFFFCC'
  513. } else {
  514. priority.low.push(item)
  515. }
  516. }
  517. })
  518.  
  519. magnetsContent.innerHTML = ''
  520.  
  521. priority.high.forEach(function (item) {
  522. magnetsContent.appendChild(item)
  523. })
  524. priority.medium.forEach(function (item) {
  525. magnetsContent.appendChild(item)
  526. })
  527. priority.low.forEach(function (item) {
  528. magnetsContent.appendChild(item)
  529. })
  530. }
  531.  
  532. /**
  533. * 主函数,处理详情页逻辑
  534. */
  535. function handler() {
  536. const videoTitle = getVideoTitle()
  537. if (!videoTitle) return
  538.  
  539. const nfoFiles = getNfoFiles()
  540. if (!nfoFiles) return
  541.  
  542. LoadingGif.start()
  543.  
  544. nfoFiles.forEach(function (item) {
  545. if (item.transformedName.includes(videoTitle)) {
  546. highlightVideoPanel()
  547. addLocalFolderTitleListItem(item)
  548. }
  549. })
  550.  
  551. sortBtList()
  552.  
  553. LoadingGif.stop()
  554. }
  555.  
  556. return handler
  557. })()
  558.  
  559. /**
  560. * 页面加载前执行
  561. */
  562. async function onBeforeMount() {
  563. // 立即调用以初始化按钮和事件处理程序
  564. LocalFolderHandler()
  565.  
  566. // 调用列表页处理函数
  567. ListPageHandler()
  568.  
  569. // 调用详情页处理函数
  570. DetailPageHandler()
  571. }
  572.  
  573. onBeforeMount()
  574. })()