Jellyfin番号过滤

标记所有番号,并添加跳转链接。支持jellyfin、emby、115、手动输入的番号查重。

  1. // ==UserScript==
  2. // @name Jellyfin番号过滤
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2.27
  5. // @description 标记所有番号,并添加跳转链接。支持jellyfin、emby、115、手动输入的番号查重。
  6. // @author Squirtle
  7. // @license MIT
  8. // @match https://www.javbus.com/*
  9. // @match https://www.javlibrary.com/*
  10. // @match https://javdb.com/*
  11. // @match https://jinjier.art/*
  12. // @match https://fc2ppvdb.com/*
  13. // @match https://www.youtube.com/*
  14. // @match https://missav123.com/*
  15. // @match https://missav.ws/*
  16. // @match https://sukebei.nyaa.si/*
  17. // @match https://115.com/*
  18. // @match https://115vod.com/*
  19. // @match https://dl.115cdn.net/*
  20. // @match https://sehuatang.net/*
  21. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsEAAA7BAbiRa+0AAAX+SURBVFhHzVdpbFVFFD4z994WBcQSQNQALhEwBWQt8CiBhmisSiSsSrqAYbEEDKuoQF6KSGkhLhFFoFWJLNJKFEzUxLjWBy00UUDhh8FAAIGIIEGxfXcZv5k7975X2tKHmuhJJvfO+c6c891zzsybR/+1MP28bnm7oDrT42ytw5lwLWNJ0cZhRzR0XfK3CGwt+CbHZWKPY/B2NmcU53TZNmjsog2Rr7RJysL1M2XZVlg9SBDt8Thv54K+P1h7l/iHJfNiA7RZynJdBLY/tu8W8mi3x0kHZ/6AF4z2cWHsjj4Z66LNU5KUCUSjgnPL2Ya63x4GliRkcD0H1s1ON9+JRqMp+03Z8N5j1Ys9xsYk0i6DJxFRA+8Gf+Dy5Ufm62WtCpa1Lu/mfdFHMLMOHZ8eR1DbwGB03OaU9ydw1+LbHIP1kA2pmpJRvc2dgRvKhh/1PbQsrWagclKlQWRU4OvT1RfKlJM4wywxeuHGSGwZhmuI0S7RmSAbKEUbl1vlsmzaTYvSqoFIv7VIMJalG40cItvzxIS5r484oU2o+JXhx5GVidiatioLBkhETjjfzdQmLco1CciuFyRWJddaML5i3pvZ+7RJKGUvZu11iEeDfnBgb3Ox+vFoXSdt0qxck4BhxEuw3zsETl1iNZ0vnVyn4Sby89BBZbDbL209ac9Zxzg3V2m4WWmRQFV+bABSX+gHZyr1DqdZk6smo9y+rJh/YMSyhfsjekpVk5lrMzYTto6fNZkJmpG78mBfbdJEWiTgCW+tyzn3CWBOtH7e5shhDdOKuTUjPfI+s4k+X7z4wAitpoqSQYewFV8LiKN5DWGwUg03kWYJ7Mz78n6kUO95fD2j8w1ufKWG6Zmi6gzibIfcGXLEGdsxa2ldBw2TZxvFWPOrWo8IGLmjSw+N0nAjaT4DzHgejlU3SwcoxQsLtuT8plFKt9LQG8GJqAJ0w6LVGqbta/pdtEmUJDcv6tZsLzQhsGNqdS7SPVQx9xeeOmc3vKFhWj635j5kZ4afHRlcB+E0O+/Z/X20Gd0sMmQZTvt2wA2WPWTdD2M0HEoTAoyL55KZe0yUFW/JqdcwMk8rgRl+UN9GEzE8wwzLtKX4znqclut8zCfhcLFMw6E0IlCVF4tg22WHTonOm7xNhYZp+ZxaeQkZqxwGGdJE5cCv5Ljx0bre2pxwWm6G/kL4MZzlZL58eIiGlTQi4DBvQcIpk7Uvn71p8BUNE5lIPWcswBOB8e4P5nFrhramT5f0/8PmvDz0CRvb4As0rCQksLNgL5qKxgVOHSYEs8zNGlaC2j+cHFgHlbtE6+RcPKTNlXgekwREmDHOJ/RY/31XDScI2J47HZ1vBk7xXjtnQ9ZPGlaCLz2I814EXxMQUTtGDSFAJjwrpNQuzfwRujofV2vSHNMs1HCCgOAsP9kpxicaCmXNS1mTLEt0xok4Er+IOBJxUhIVolEnYWRbrtXp4+X9p2jzUPAz/lHo14+RryH/PrC9YF8/m7sHg99zNZgYv2hj5H1l9Q+l16tHJ6B87+Hiigus79917MwLRX2OqAw4zH1Qp0ezhM7gd6jV/4IIJroHWzGIY6dZuRJTGXhreuwD1OlR9eXytoNnnAsXz63IRKV9gxUrLR18SdqmKn1LDmWItmYEP0xTcHOaihuT4fsNYohdV6b1mqgIVEyPyfT3kylSJPSI6zmuWALb5xTInICDs7bJL+KQ+R1bLB43YcN4GvqiPf4fZMCuK7A74Os2vDMEb+QzLANj3zZMu2egIrDpiVg1WGUnDLEILIN6hU5CXZLTMGNX6xL19nU+Huiwzb+2C3uOgpnsUmMJ6nIq0QOJWvkD70qXNFe6YB7o8Ax12ibU+XN1UWHiJC65T8vYisCc8mE1dlvqCeN8bKtd6IdfGjlJcurvebwnOfUxqbtqHurwkURn8dWVDuNT457V0ym4q1bGBtS8LH2qrrubJno3EN2N9HZHE3VBzTsivTdhfiPS2AbvFuqN9KIXOK7iBrsC/SWk/wJSfc4h7ySex+qZefRCUa/T2vX/SYj+Av6m66t3we/RAAAAAElFTkSuQmCC
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_addStyle
  24. // @grant GM_registerMenuCommand
  25. // @grant GM_unregisterMenuCommand
  26. // @grant GM_getValue
  27. // @grant GM_setValue
  28. // @grant GM_setClipboard
  29. // @grant GM_info
  30. // @grant GM.cookie
  31. // @grant unsafeWindow
  32. // @grant GM_openInTab
  33. // ==/UserScript==
  34.  
  35. ;(function () {
  36. 'use strict'
  37. if (unsafeWindow.top.jellyfinFilterScript) {
  38. console.log('该脚本已经在运行了, 本次直接返回')
  39. return
  40. }
  41. unsafeWindow.top.jellyfinFilterScript = true
  42.  
  43. // 默认设置项
  44. const defaultSettings = {
  45. // 是否开启jellyfin/emby功能
  46. enable: true,
  47. // 是否开启本地番号填写功能
  48. localCodeEnable: false,
  49. // 在其他图标已存在时,是否依然显示本地图标
  50. forceShowLocalBtn: false,
  51. // 从jellyfin/emby 控制台获取
  52. apiKey: '',
  53. // 服务器地址
  54. serverUrl: 'http://127.0.0.1:8096',
  55. // jellyfin用户保持为false,emby用户需设置为true
  56. isEmby: false,
  57. // 额外的搜索参数,格式为:parentId=xxx;isFavorite=true
  58. extraSearchParams: '',
  59. // 若为true,则在页面加载完成后自动触发一次过滤
  60. triggerOnload: false,
  61. // 是否显示收藏按钮
  62. showFavoriteBtn: false,
  63. // 如果要显示收藏按钮,则必须填写userId
  64. userId: '',
  65. // 自定义快捷键,可以是任意长度的字母或数字
  66. hotKeys: 'ee',
  67. // 脚本会改变页面的原有结构,此处定义可使页面恢复原状的快捷键
  68. recoverHotKeys: 'ss',
  69. // 复制所有磁力链接快捷键
  70. copyMagnetHotKeys: 'aa',
  71. // 复制所有ed2k链接快捷键
  72. copyEd2kHotKeys: 'qq',
  73. // 复制所有番号快捷键
  74. copyCodeHotKeys: 'ww',
  75. // 复制与用户自定义正则匹配的字符串
  76. copyUserRegHotKeys: 'cc',
  77. // 点击番号时的默认跳转链接,${code}会被替换为真正的番号
  78. openSite: 'https://www.javbus.com/${code}',
  79. // 点击番号时按住shift键时的跳转链接
  80. secondarySite: 'https://javdb.com/search?q=${code}',
  81. // 若番号被识别为fc2,默认会跳转到的链接
  82. fc2Site: 'https://sukebei.nyaa.si/user/offkab?q=${code}',
  83. // 设为true时浏览器控制台会输出log
  84. debug: false,
  85. // 定义生成链接的默认颜色
  86. linkColor: '#236ED0FF',
  87. // 定义被访问过的链接颜色
  88. linkVisitedColor: 'brown',
  89. // 番号在jellyfin/115/本地中存在时显示的链接颜色
  90. linkExistColor: '#2A7B5FFF',
  91. // 定义磁力和ed2k链接的颜色
  92. magnetColor: 'indianred',
  93. // 高亮卡片边框样式
  94. emphasisOutlineStyle: '2px solid red',
  95. // 默认会高亮不存在的番号,设置为true则反之
  96. reverseEmphasis: false,
  97. // 是否尽量复用窗口,可以加快打开速度
  98. openLinkInSameTab: false,
  99. // 自定义正则,匹配优先级最低
  100. // \d*[a-z]\d*[-_]\d{2,}
  101. userRegexp: '',
  102. // 自定义正则匹配的高亮颜色
  103. userRegColor: 'orange',
  104. // 在jellyfin的movies页面是否自动触发过滤
  105. jellyfinAutoTrigger: false
  106. }
  107.  
  108. // 默认115设置项
  109. const defaultOOFSettings = {
  110. // 是否开启115相关功能
  111. enable: false,
  112. // 115的cookie,可自行输入或点击自动获取,任选其一
  113. cookie: '',
  114. // 自定义cookie过期时间,单位为天
  115. expiresIn: '30',
  116. // 一个番号如果在jellyfin和115中都存在,默认只显示一个jellyfin图标,若设置为true,则也会显示115图标
  117. forceShowOOFBtn: false,
  118. // 115在线观看链接
  119. openSite: 'https://115vod.com/?pickcode=${pickcode}',
  120. // 搭配115Master插件使用时,可使用以下链接,按住shift键再点击
  121. secondarySite: 'https://dl.115cdn.net/master/video/?pick_code=${pickcode}&avNumber=${code}&title=${text}',
  122. // 首次匹配115网盘文件时,需要批次获取全量数据
  123. // limit定义每次获取条数,根据实际情况谨慎填写,过大可能导致服务器返回缓慢,过小请求次数过多可能触发115风控
  124. limit: '500',
  125. // 离线目录id, 留空则115会保存在云下载目录
  126. offlineCid: '',
  127. // 获取该目录下的文件来更新缓存缓存,包括点击刷新按钮时
  128. fetchCid: '0',
  129. // 是否开启alist功能
  130. alistEnable: false,
  131. // alist地址: http://127.0.0.1:5244/d/115/${dir}/${file}
  132. alistUrl: '',
  133. // cid与目录名的对应关系: 1111:目录1; 2222:目录2; *:目录3 用来匹配alistUrl
  134. // 可以使用*通配符,表示匹配所有,比如 *:目录3
  135. cidPair: '',
  136. // 将115中某个目录中所有匹配番号规则的视频文件移动到另一个目录
  137. // 格式为cid对,比如 1111:2222 会将目录1中的文件移动到目录2
  138. move: ''
  139. }
  140.  
  141. const CONFIG = [
  142. {
  143. site: /^https:\/\/(www\.)?javbus\.com(?=\/?$|\/page\/\d+|\/search|\/genre|\/uncensored|\/star|\/label|\/director|\/studio|\/series)/i,
  144. cb: () => findCode('a.movie-box', 'date')
  145. },
  146. {
  147. site: /^https:\/\/www\.javlibrary\.com\/cn(?!(\/tl_bestreviews.php|\/publicgroups.php|\/publictopic.php))/i,
  148. cb: () => findCode('.video', '.id')
  149. },
  150. {
  151. site: /^https:\/\/(www\.)?javdb\.com(?!\/v)/i,
  152. cb: () => findCode('.movie-list .item', '.video-title strong')
  153. },
  154. {
  155. site: /^https:\/\/jinjier\.art\/sql.*/i,
  156. cb: () => {
  157. return findCode('tbody tr', box => {
  158. const td = box.querySelector('td:nth-of-type(3)')
  159. return td.textContent.split(' ')[0]
  160. })
  161. }
  162. },
  163. {
  164. site: /https:\/\/fc2ppvdb\.com/i,
  165. cb: () => {
  166. return findCode('.flex section .container .relative', box => {
  167. const span = box.querySelector('a + span')
  168. const code = span.textContent
  169. if (!code.startsWith('fc2')) {
  170. span.textContent = `fc2-${code}`
  171. }
  172. return code
  173. })
  174. }
  175. }
  176. ]
  177.  
  178. const REG = {
  179. magnet: /magnet:\?xt=urn:btih:[\da-f]{40}/,
  180. ed2k: /ed2k:\/\/(?:\|.+)+\|\//,
  181. fc2: /(?:fc2?)\s*[-_]?\s*(?:ppv)?\s*[-_]?\s*(\d{6,8})/,
  182. num2: /(?<![a-z\d])(\d{4,8})[-_](\d{3,4})/,
  183. censored: /(?<=[\W_]\d{3}|^\d{3}|[\W_]|^)([a-z]{3,9})(?:[-_]|\s*|0*)?(\d{3,6})(?![a-z\d]|\.com)/,
  184. uncensored: /(?<![a-z\d])([nk])(\d{3,6})(?![a-z\d]|\.com)/
  185. }
  186.  
  187. function isTypeMagnetLike(type) {
  188. return type === 'magnet' || type === 'ed2k'
  189. }
  190.  
  191. function getCodeByRegType(match, type, fc2Prefix = false) {
  192. if (!match) return
  193. if (isTypeMagnetLike(type)) return match[0]
  194. if (type === 'fc2') return fc2Prefix ? `fc2-${match[1]}` : match[1]
  195. if (type === 'uncensored') return match[1] + match[2]
  196. if (type === 'userReg') return match[0]
  197. return `${match[1]}-${match[2]}`
  198. }
  199.  
  200. function getRegEntries() {
  201. const { userRegexp } = settings
  202. const entries = Object.entries(REG)
  203. if (userRegexp) {
  204. entries.push(['userReg', userRegexp])
  205. }
  206. return entries
  207. }
  208.  
  209. function getTextRegList() {
  210. return getRegEntries().map(([type, reg]) => ({ type, reg: new RegExp(reg, 'ig') }))
  211. }
  212.  
  213. function getFileRegList() {
  214. return getRegEntries()
  215. .filter(([type]) => !isTypeMagnetLike(type))
  216. .map(([type, reg]) => ({ type, reg: new RegExp(reg, 'i') }))
  217. }
  218.  
  219. function getLinkRegList() {
  220. return getRegEntries()
  221. .filter(([type]) => isTypeMagnetLike(type))
  222. .map(([type, reg]) => ({ type, reg: new RegExp(reg, 'i') }))
  223. }
  224.  
  225. function getSettings() {
  226. return {
  227. ...defaultSettings,
  228. ...GM_getValue('settings')
  229. }
  230. }
  231.  
  232. function getOOFSettings() {
  233. return {
  234. ...defaultOOFSettings,
  235. ...GM_getValue('oofSettings')
  236. }
  237. }
  238.  
  239. function getLocalCodeList() {
  240. return GM_getValue('localCodeList', [])
  241. }
  242.  
  243. function getLocalNameList() {
  244. return GM_getValue('localNameList', [])
  245. }
  246.  
  247. let settings = getSettings()
  248. let oofSettings = getOOFSettings()
  249. const codeMap = new Map()
  250. const oofCodeMap = new Map()
  251. let localCodeList = getLocalCodeList()
  252. let localNameList = getLocalNameList()
  253.  
  254. function noop() {}
  255. let log = noop
  256.  
  257. let myPolicy
  258. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  259. myPolicy = window.trustedTypes.createPolicy('jvJellyfinPolicy', {
  260. createHTML: string => string
  261. })
  262. }
  263.  
  264. function setInnerHTML(element, html) {
  265. const escapeHtml = myPolicy ? myPolicy.createHTML(html) : html
  266. element.innerHTML = escapeHtml
  267. }
  268.  
  269. function openTab(url, ctrlKey = false) {
  270. log(url)
  271. const { serverUrl, openLinkInSameTab } = settings
  272. const urlObj = new URL(url)
  273. if (ctrlKey || !openLinkInSameTab || new URL(serverUrl).origin === urlObj.origin) {
  274. GM_openInTab(url, { active: !ctrlKey, insert: true, setParent: true })
  275. } else {
  276. window.open(url, urlObj.origin)
  277. }
  278. }
  279.  
  280. const copy = ({ text, desc }) => {
  281. if (!text) return
  282. GM_setClipboard(text)
  283. notify(desc || '复制成功', text)
  284. }
  285.  
  286. function addClickEvent({ element, copyText, withCtrl = false, handler = noop }) {
  287. const prevent = e => {
  288. e.stopPropagation()
  289. e.preventDefault()
  290. }
  291. const handleMousedown = e => {
  292. e.stopPropagation()
  293. e.preventDefault()
  294. if (e.button === 0) {
  295. if (withCtrl && !e.ctrlKey) {
  296. notify('请按住ctrl键再点击', '此举是为了防止不小心点错')
  297. return
  298. }
  299. handler(e)
  300. } else if (e.button === 2) {
  301. if (typeof copyText === 'string') {
  302. copy({ text: copyText })
  303. } else if (copyText === null) {
  304. // text为null则执行handler,交给handler自己去处理鼠标左右键的情况
  305. handler(e)
  306. }
  307. }
  308. }
  309. element.addEventListener('contextmenu', prevent)
  310. element.addEventListener('click', prevent)
  311. element.addEventListener('mousedown', handleMousedown)
  312. return () => {
  313. document.removeEventListener('contextmenu', prevent)
  314. document.removeEventListener('click', prevent)
  315. document.removeEventListener('mousedown', handleMousedown)
  316. }
  317. }
  318.  
  319. const ICONS = {
  320. close: '<svg fill-rule="evenodd" viewBox="64 64 896 896" focusable="false" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"></path></svg>',
  321. jellyfin:
  322. '<svg class="jv-svg" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="30%" style="stop-color:#AA5CC3;stop-opacity:1" /><stop offset="100%" style="stop-color:#00A4DC;stop-opacity:1" /></linearGradient></defs><path style="fill:url(#grad3)" d="M12 .002C8.826.002-1.398 18.537.16 21.666c1.56 3.129 22.14 3.094 23.682 0C25.384 18.573 15.177 0 12 0zm7.76 18.949c-1.008 2.028-14.493 2.05-15.514 0C3.224 16.9 9.92 4.755 12.003 4.755c2.081 0 8.77 12.166 7.759 14.196zM12 9.198c-1.054 0-4.446 6.15-3.93 7.189.518 1.04 7.348 1.027 7.86 0 .511-1.027-2.874-7.19-3.93-7.19z"/></svg>',
  323. emby: '<svg class="jv-svg" role="img" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" ><path d="M469.333333 85.333333L256 298.666667l42.666667 42.666666-213.333334 213.333334 213.333334 213.333333 42.666666-42.666667 213.333334 213.333334 213.333333-213.333334-42.666667-42.666666 213.333334-213.333334-213.333334-213.333333-42.666666 42.666667-213.333334-213.333334m-42.666666 277.333334l256 149.333333-256 149.333333v-298.666666z" fill="#05b010" p-id="1934"></path></svg>',
  324. oof: '<svg class="jv-oof-svg" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><path fill="#224888" d="M16,0 C24.836556,-1.623249e-15 32,7.163444 32,16 C32,24.836556 24.836556,32 16,32 C7.163444,32 1.082166e-15,24.836556 0,16 C-1.082166e-15,7.163444 7.163444,1.623249e-15 16,0 Z"/><path fill="#FFF" d="M21.7114092,7.38461538 C21.6913564,7.38461538 21.6724177,7.38794634 21.654036,7.39294276 C21.6406676,7.39793919 21.6272991,7.40349078 21.6133736,7.4084872 C21.217889,7.55838001 20.644715,7.7204863 20.0097117,7.7204863 C20.0097117,7.7204863 15.0121242,7.7204863 13.9315046,7.7204863 C13.5427043,7.7204863 13.2006937,7.90924021 12.9973813,8.19625717 C12.9957102,8.19958812 10.0769231,13.9971088 10.0769231,13.9971088 C10.0769231,13.9971088 11.5886766,14.1725389 11.6298961,14.1786456 C12.9762145,14.3851646 14.5737491,14.7771065 15.6822198,15.4444071 C17.2752983,16.4037211 18.2706939,17.7977241 18.1698732,19.2944316 C17.974359,22.195135 15.2421737,23.5003127 13.0937458,23.8139773 C11.5853344,24.0227169 10.5838117,23.8139773 10.5838117,23.8139773 C10.5838117,23.8139773 11.8148269,24.5562242 13.588937,24.6106298 C16.6180142,24.7038964 18.9736537,23.4159286 20.2197084,21.5539268 C22.1871046,18.6160278 21.0875463,13.9399274 15.9300939,12.5475898 C14.9408256,12.2805586 13.5672132,12.0279614 13.5672132,12.0279614 L14.3019232,10.5867699 C14.4651302,10.1792835 14.8750972,9.89060104 15.3558059,9.89060104 C15.3780867,9.89060104 15.4003675,9.89171136 15.4226483,9.89282168 L17.8746523,9.89282168 C17.8752093,9.89282168 17.8763234,9.89282168 17.8768804,9.89282168 C17.8774374,9.89282168 17.8785515,9.89282168 17.8802225,9.89282168 L20.0587295,9.89282168 C20.4854071,9.89282168 20.8563828,9.66465151 21.0485548,9.32822544 L21.0691646,9.28825402 L21.8801863,7.70993829 C21.8957829,7.69050774 21.9069233,7.6683014 21.9141646,7.64387442 C21.9197348,7.62610935 21.9230769,7.60778912 21.9230769,7.58835857 C21.9230769,7.47621654 21.8283834,7.38461538 21.7114092,7.38461538 Z"/></g></svg>',
  325. alist: '<svg class="jv-alist-svg" width="22" height="22" viewBox="0 0 1252 1252" xmlns="http://www.w3.org/2000/svg" version="1.1"><g><g id="#70c6beff"><path id="svg_2" d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z" fill="#70c6be"/></g><g id="#1ba0d8ff"><path id="svg_3" d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z" fill="#1ba0d8"/></g></g></svg>'
  326. }
  327.  
  328. function getUniqueId() {
  329. return Math.random().toString(36).slice(2)
  330. }
  331.  
  332. function getIcon(name) {
  333. if (name === 'jellyfin') {
  334. const id = getUniqueId()
  335. return ICONS.jellyfin.replaceAll('grad3', id)
  336. }
  337. return ICONS[name]
  338. }
  339.  
  340. function throttle(fn, threshhold = 500, scope) {
  341. let previous = 0
  342. return (...args) => {
  343. const context = scope || this
  344. const now = Date.now()
  345. if (now - previous > threshhold) {
  346. previous = now
  347. return fn.apply(context, args)
  348. }
  349. }
  350. }
  351.  
  352. function notify(title, content, timeout = 3000) {
  353. log(title, content)
  354. const element = document.createElement('div')
  355. element.className = 'jv-notification'
  356. const texts = [
  357. timeout && `<span class='jv-close-icon'>${ICONS.close}</span>`,
  358. title && `<div class='jv-title'>${title}</div>`,
  359. content && `<div class='jv-content'>${content}</div>`
  360. ]
  361. setInnerHTML(element, texts.filter(Boolean).join('\n'))
  362. const closeIcon = element.querySelector('.jv-close-icon')
  363. const close = () => {
  364. element?.remove()
  365. }
  366. closeIcon?.addEventListener('click', close)
  367. document.body.append(element)
  368. if (timeout > 0) {
  369. setTimeout(close, timeout)
  370. }
  371. return {
  372. close,
  373. element
  374. }
  375. }
  376.  
  377. function notifyWithConfirm(title, content) {
  378. return new Promise(resolve => {
  379. const { close, element } = notify(title, content, 0)
  380. const confirm = result => () => {
  381. close()
  382. resolve(result)
  383. }
  384. const footer = document.createElement('div')
  385. footer.className = 'jv-btn-group'
  386. const confirmBtn = document.createElement('button')
  387. const cancelBtn = document.createElement('button')
  388. confirmBtn.textContent = '确定'
  389. cancelBtn.textContent = '取消'
  390. confirmBtn.addEventListener('click', confirm(true))
  391. cancelBtn.addEventListener('click', confirm(false))
  392. footer.append(confirmBtn, cancelBtn)
  393. element.append(footer)
  394. })
  395. }
  396.  
  397. const throttleNotify = throttle(notify)
  398.  
  399. class KeysEvent {
  400. constructor(eventType = 'keypress', interval = 500) {
  401. this.interval = interval
  402. this.inputs = []
  403. this.eventMap = new Map()
  404.  
  405. document.addEventListener(eventType, this.handler)
  406. }
  407.  
  408. handler = e => {
  409. const now = Date.now()
  410. const key = e.key.toLowerCase()
  411. log(key)
  412. const index = this.inputs.findLastIndex(({ time }) => now - time > this.interval)
  413. if (index > -1) {
  414. this.inputs.splice(0, index + 1)
  415. }
  416. this.inputs.push({ key, time: now })
  417. this.trigger()
  418. }
  419.  
  420. trigger = () => {
  421. const inputKeys = this.inputs.map(input => input.key).join('')
  422. for (const [keys, listeners] of this.eventMap) {
  423. const startIndex = inputKeys.indexOf(keys)
  424. if (startIndex > -1) {
  425. try {
  426. listeners.forEach(listener => listener())
  427. this.inputs.splice(startIndex, keys.length)
  428. } catch (error) {
  429. console.error(error)
  430. }
  431. }
  432. }
  433. }
  434.  
  435. on = (keys, listener) => {
  436. if (!keys) return noop
  437. const listeners = this.eventMap.get(keys)
  438. if (listeners) {
  439. // 不允许注册相同的处理函数
  440. if (!listeners.includes(listener)) {
  441. listeners.push(listener)
  442. }
  443. } else {
  444. this.eventMap.set(keys, [listener])
  445. }
  446. return () => this.off(keys, listener)
  447. }
  448.  
  449. off = (keys, listener) => {
  450. const listeners = this.eventMap.get(keys)
  451. if (!listeners) return
  452. const index = listeners.findIndex(l => l === listener)
  453. listeners.splice(index, 1)
  454. }
  455. }
  456.  
  457. const keysEvent = new KeysEvent()
  458.  
  459. class EventEmitter {
  460. constructor() {
  461. this.events = {}
  462. }
  463.  
  464. getListeners(eventName) {
  465. return this.events[eventName] || (this.events[eventName] = [])
  466. }
  467.  
  468. on(eventName, listener) {
  469. if (typeof listener === 'function') {
  470. this.getListeners(eventName).push(listener)
  471. }
  472. }
  473.  
  474. off(eventName, listener) {
  475. this.events[eventName] = this.getListeners(eventName).filter(item => item !== listener)
  476. }
  477.  
  478. emit(eventName, ...args) {
  479. this.getListeners(eventName).forEach(cb => {
  480. try {
  481. cb.apply(this, args)
  482. } catch (error) {
  483. log(error)
  484. }
  485. })
  486. }
  487. }
  488.  
  489. const eventEmitter = new EventEmitter()
  490. const EVENT_Type = {
  491. clearDom: Symbol(),
  492. clearEvent: Symbol(),
  493. startMatch: Symbol()
  494. }
  495.  
  496. class CopySet {
  497. constructor() {
  498. this.sets = {}
  499. }
  500.  
  501. getSetByType = type => {
  502. let innerType = 'code'
  503. const types = ['magnet', 'ed2k', 'userReg']
  504. if (types.includes(type)) {
  505. innerType = type
  506. }
  507. return this.sets[innerType] || (this.sets[innerType] = new Set())
  508. }
  509.  
  510. registerCopyEvent = () => {
  511. const { copyMagnetHotKeys, copyEd2kHotKeys, copyCodeHotKeys, copyUserRegHotKeys } = settings
  512. const copyEvents = [
  513. [copyMagnetHotKeys, 'magnet'],
  514. [copyEd2kHotKeys, 'ed2k'],
  515. [copyCodeHotKeys, 'code'],
  516. [copyUserRegHotKeys, 'userReg']
  517. ]
  518. const clearEventList = copyEvents.map(([keys, type]) => {
  519. const set = this.getSetByType(type)
  520. return keysEvent.on(keys, () => {
  521. if (set.size > 0) {
  522. const content = Array.from(set.values()).join('\n')
  523. GM_setClipboard(content)
  524. notify(`成功复制${set.size}条`, content)
  525. } else {
  526. notify('无内容')
  527. }
  528. })
  529. })
  530. eventEmitter.on(EVENT_Type.clearDom, () => {
  531. Object.values(this.sets).forEach(set => set.clear())
  532. })
  533. eventEmitter.on(EVENT_Type.clearEvent, () => {
  534. clearEventList.forEach(clear => clear())
  535. })
  536. }
  537.  
  538. setCopySetByType = (type, code) => {
  539. const set = this.getSetByType(type)
  540. set.add(code)
  541. }
  542. }
  543.  
  544. const copySet = new CopySet()
  545.  
  546. function setStyle(element, styles, isImportant = false) {
  547. Object.entries(styles).forEach(([key, value]) => {
  548. element.style.setProperty(key, value, isImportant ? 'important' : '')
  549. })
  550. }
  551.  
  552. class AsyncQueue {
  553. constructor(concurrent = 5) {
  554. this.concurrent = concurrent
  555. this.activeCount = 0
  556. this.queue = []
  557. }
  558.  
  559. push(promiseCreator) {
  560. this.queue.push(promiseCreator)
  561. this.next()
  562. }
  563.  
  564. next() {
  565. if (this.activeCount < this.concurrent && this.queue.length) {
  566. const promiseCreator = this.queue.shift()
  567. this.activeCount++
  568.  
  569. promiseCreator()
  570. .then(() => {
  571. this.activeCount--
  572. this.next()
  573. })
  574. .catch(error => {
  575. console.error(error)
  576. this.activeCount = 0
  577. this.queue.length = 0
  578. })
  579. }
  580. }
  581. }
  582.  
  583. const queue = new AsyncQueue()
  584.  
  585. function request(url, { method = 'GET', data, headers = {} } = {}) {
  586. log(`请求: ${url}`)
  587. if (new URL(url).origin === settings.serverUrl) {
  588. headers['X-Emby-Token'] = settings.apiKey
  589. }
  590. return new Promise((resolve, reject) => {
  591. GM_xmlhttpRequest({
  592. method,
  593. url,
  594. data,
  595. headers: {
  596. 'Content-Type': 'application/json',
  597. ...headers
  598. },
  599. onload(response) {
  600. if (response.status === 200) {
  601. try {
  602. resolve(JSON.parse(response.responseText))
  603. } catch (error) {
  604. reject(error)
  605. }
  606. } else {
  607. reject(response)
  608. }
  609. },
  610. onerror: reject
  611. })
  612. })
  613. }
  614.  
  615. function requestWithCookie(url, config = {}) {
  616. config.headers = { ...config.headers, cookie: oofSettings.cookie }
  617. return request(url, config)
  618. }
  619.  
  620. function getQuery(params) {
  621. return Object.entries(params)
  622. .filter(([_, value]) => value != null && value !== '')
  623. .map(([key, value]) => {
  624. if (value && typeof value === 'object') {
  625. return Object.entries(value)
  626. .map(([k, v]) => `${encodeURIComponent(`${key}[${k}]`)}=${encodeURIComponent(v)}`)
  627. .join('&')
  628. } else {
  629. return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  630. }
  631. })
  632. .join('&')
  633. }
  634.  
  635. function addQuery(base, params) {
  636. if (!params) return base
  637. const query = getQuery(params)
  638. if (!query) return base
  639. return base.endsWith('?') ? base + query : `${base}?${query}`
  640. }
  641.  
  642. function getExtraSearchParams() {
  643. const { extraSearchParams } = settings
  644. if (!extraSearchParams) return
  645. const params = parseUserPair(extraSearchParams)
  646. return params.reduce((acc, [key, value]) => {
  647. acc[key] = value
  648. return acc
  649. }, {})
  650. }
  651.  
  652. async function fetchItems(params) {
  653. const { isEmby, serverUrl, userId } = settings
  654. const extraParams = getExtraSearchParams()
  655. log('extraParams', extraParams)
  656. const finalParams = {
  657. startIndex: 0,
  658. fields: 'SortName',
  659. imageTypeLimit: 1,
  660. includeItemTypes: 'Movie',
  661. recursive: true,
  662. sortBy: 'SortName',
  663. sortOrder: 'Ascending',
  664. limit: 2,
  665. enableUserData: true,
  666. enableImages: false,
  667. userId,
  668. ...extraParams,
  669. ...params
  670. }
  671. const url = isEmby ? `${serverUrl}/emby/Items` : `${serverUrl}/Items`
  672. try {
  673. const response = await request(addQuery(url, finalParams))
  674. return response.Items
  675. } catch (error) {
  676. throttleNotify('请求jellyfin/emby报错', '请检查apiKey与serverUrl是否设置正确', 0)
  677. console.error(error)
  678. throw error
  679. }
  680. }
  681.  
  682. async function getItemByCode({ code, type }) {
  683. if (type === 'num2') {
  684. const specialNumCode = parseSpecialNumCode(code)
  685. if (specialNumCode) {
  686. code = specialNumCode
  687. }
  688. }
  689. if (codeMap.has(code)) {
  690. return codeMap.get(code)
  691. }
  692. const items = await fetchItems({ searchTerm: code })
  693. const item = items?.[0] || null
  694. codeMap.set(code, item)
  695. return item
  696. }
  697.  
  698. function requireCookie(cb) {
  699. return (...args) => {
  700. const { enable, cookie } = oofSettings
  701. if (enable && !cookie) {
  702. notify('缺失cookie', '打开115设置, 手动设置cookie; 或者去115登录, 再点击获取cookie')
  703. return
  704. }
  705. return cb.apply(this, args)
  706. }
  707. }
  708.  
  709. const requiredCookieNames = ['UID', 'CID', 'SEID', 'KID']
  710.  
  711. function checkRequiredCookies(cookies) {
  712. if (!Array.isArray(cookies) || cookies.length === 0) {
  713. notify('Cookie不存在, 请先登录', '若已登录, 可点击获取Cookie按钮')
  714. return false
  715. }
  716. const lackOfCookieNames = requiredCookieNames.filter(name => !cookies.some(item => item.name === name))
  717. if (lackOfCookieNames.length > 0) {
  718. notify(`Cookie缺少: ${lackOfCookieNames.join('、')}`)
  719. return false
  720. }
  721. return true
  722. }
  723.  
  724. function getRequiredCookies(cookies) {
  725. return cookies.filter(item => requiredCookieNames.includes(item.name))
  726. }
  727.  
  728. function parseCookiesToText(array) {
  729. return array.map(({ name, value }) => `${name}=${value}`).join(';')
  730. }
  731.  
  732. function parseTextToCookies(text) {
  733. const cookies = []
  734. if (!text) return cookies
  735. text.split(/;\s*/).forEach(item => {
  736. if (!item) return
  737. const [name, value] = item.split('=')
  738. cookies.push({ name, value })
  739. })
  740. return cookies
  741. }
  742.  
  743. function getParsedCode({ text, fc2Prefix = false }) {
  744. const fileRegList = getFileRegList()
  745. for (const { type, reg } of fileRegList) {
  746. const match = text.match(reg)
  747. const code = getCodeByRegType(match, type, fc2Prefix)
  748. if (code) {
  749. return code.toLowerCase()
  750. }
  751. }
  752. return null
  753. }
  754.  
  755. function setOOFFiles() {
  756. const oofFiles = [...oofCodeMap.values()]
  757. GM_setValue('oofFiles', oofFiles)
  758. }
  759.  
  760. async function initOOFCodeMap(cid, update = false) {
  761. let oofFiles = GM_getValue('oofFiles', [])
  762. const force = oofFiles.length === 0 || update
  763. if (force) {
  764. const files = await getAllOFFFiles({ cid })
  765. oofFiles = filterFiles(files)
  766. }
  767. oofFiles.forEach(file => {
  768. oofCodeMap.set(file.code, file)
  769. })
  770. if (force) {
  771. setOOFFiles()
  772. }
  773. }
  774.  
  775. function clearOOFCodeMap() {
  776. oofCodeMap.clear()
  777. GM_setValue('oofFiles', [])
  778. }
  779.  
  780. async function sleep(seconds = 2) {
  781. return new Promise(resolve => setTimeout(resolve, seconds * 1000))
  782. }
  783.  
  784. const getAllOFFFiles = requireCookie(async ({ cid = 0 } = {}) => {
  785. const limit = parseInt(oofSettings.limit) || parseInt(defaultOOFSettings.limit)
  786. const baseUrl = 'https://webapi.115.com/files'
  787. const params = {
  788. aid: 1,
  789. cid,
  790. o: 'user_ptime',
  791. asc: 0,
  792. show_dir: 0,
  793. type: 4,
  794. format: 'json'
  795. }
  796. let files = []
  797. let count = 1
  798. let offset = 0
  799. let pageIndex = 0
  800. while (offset < count) {
  801. const url = addQuery(baseUrl, { ...params, limit, offset })
  802. const response = await requestWithCookie(url)
  803. files = files.concat(response.data)
  804. count = response.count
  805. offset += limit
  806. pageIndex++
  807. if (offset < count) {
  808. await sleep()
  809. }
  810. }
  811. log(`视频总数量为${count}条, 每次获取${limit}条, 共分了${pageIndex}次请求`)
  812. log(files)
  813. return files
  814. })
  815.  
  816. // 20210710-001 -> 071021-001
  817. function parseSpecialNumCode(code) {
  818. const match = code.match(/20(\d{2})(\d{4})-(\d+)/)
  819. if (!match) return null
  820. return `${match[2]}${match[1]}-${match[3]}`
  821. }
  822.  
  823. function getKeyword({ code, text, type }) {
  824. const words = [code]
  825. if (type === 'num2') {
  826. const parsedCode = parseSpecialNumCode(code)
  827. if (parsedCode) {
  828. words.push(parsedCode)
  829. }
  830. }
  831. if (text.toLowerCase() !== code.toLowerCase()) {
  832. words.push(text)
  833. }
  834. return words.join(' ')
  835. }
  836.  
  837. const getAndUpdateFileByCode = requireCookie(async ({ code, text = '', forceCheck = false, type }) => {
  838. if (!forceCheck) return oofCodeMap.get(code)
  839. const keyword = getKeyword({ code, text, type })
  840. log(`搜索关键字(${type}): ${keyword}`)
  841. const params = { search_value: keyword, format: 'json', limit: 100 }
  842. const url = addQuery('https://webapi.115.com/files/search', params)
  843. const response = await requestWithCookie(url)
  844. log(response)
  845. const filteredFiles = filterFiles(response.data)
  846. log(filteredFiles)
  847. let item = null
  848. let shouldUpdate = false
  849. if (filteredFiles.length > 0) {
  850. filteredFiles.forEach(file => {
  851. if (file.code === code) {
  852. item = file
  853. }
  854. oofCodeMap.set(file.code, file)
  855. })
  856. if (!item && type === 'num2') {
  857. const parsedCode = parseSpecialNumCode(code)
  858. if (parsedCode) {
  859. const file = filteredFiles.find(f => f.code === parsedCode)
  860. if (file) {
  861. item = file
  862. oofCodeMap.set(code, { ...file, code })
  863. }
  864. }
  865. }
  866. shouldUpdate = true
  867. }
  868.  
  869. if (!item && oofCodeMap.has(code)) {
  870. oofCodeMap.delete(code)
  871. shouldUpdate = true
  872. }
  873. if (shouldUpdate) {
  874. setOOFFiles()
  875. }
  876. return item
  877. })
  878.  
  879. function filterFiles(files) {
  880. const newFiles = []
  881. files.forEach(file => {
  882. if (!file.play_long) return
  883. const localName = localNameList.find(name => file.n.startsWith(name))
  884. if (localName) {
  885. file.code = localName
  886. } else {
  887. const code = getParsedCode({ text: file.n })
  888. if (!code) return
  889. file.code = code
  890. }
  891. newFiles.push(file)
  892. })
  893. return newFiles
  894. }
  895.  
  896. const moveFiles = requireCookie(async (srcId, destId) => {
  897. if (!srcId || !destId) {
  898. notify('移动失败', '请设置移动源和目标目录')
  899. return
  900. }
  901. const srcFiles = await getAllOFFFiles({ cid: srcId })
  902. const filteredFiles = filterFiles(srcFiles)
  903. log(filteredFiles)
  904. if (filteredFiles.length === 0) {
  905. notify('源目录为空')
  906. return
  907. }
  908. const config = {
  909. method: 'POST',
  910. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  911. data: getQuery({ pid: destId, fid: filteredFiles.map(file => file.fid) })
  912. }
  913. try {
  914. const response = await requestWithCookie('https://webapi.115.com/files/move', config)
  915. log(response)
  916. notify('移动成功', `共移动了${filteredFiles.length}/${srcFiles.length}个文件`)
  917. filteredFiles.forEach(file => {
  918. file.cid = destId
  919. oofCodeMap.set(file.code, file)
  920. })
  921. setOOFFiles()
  922. } catch (error) {
  923. notify('移动失败')
  924. console.error(error)
  925. }
  926. })
  927.  
  928. const deleteFile = requireCookie(async file => {
  929. const confirm = await notifyWithConfirm(`确定从115中删除 ${file.n} 吗?`)
  930. if (!confirm) return
  931. const url = 'https://webapi.115.com/rb/delete'
  932. const config = {
  933. method: 'POST',
  934. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  935. data: getQuery({ fid: [file.fid], ignore_warn: 1 })
  936. }
  937. try {
  938. await requestWithCookie(url, config)
  939. if (oofCodeMap.has(file.code)) {
  940. oofCodeMap.delete(file.code)
  941. setOOFFiles()
  942. }
  943. notify('删除成功', file.n)
  944. return true
  945. } catch (error) {
  946. notify('删除失败')
  947. console.error(error)
  948. return false
  949. }
  950. })
  951.  
  952. function convertFormValue(value) {
  953. const switchMap = new Map([
  954. ['true', true],
  955. ['false', false],
  956. ['undefined', undefined],
  957. ['null', null],
  958. ['', undefined]
  959. ])
  960. if (switchMap.has(value)) {
  961. return switchMap.get(value)
  962. }
  963. return value
  964. }
  965.  
  966. class ConfigModal {
  967. constructor({ title = '设置', getFormData, onSubmit, onShow, extraButtons, defaultData, buildItems }) {
  968. this.title = title
  969. this.getFormData = getFormData
  970. this.onSubmit = onSubmit
  971. this.onShow = onShow
  972. this.extraButtons = extraButtons
  973. this.defaultData = defaultData
  974. this.buildItems = buildItems
  975. this.initialized = false
  976. }
  977.  
  978. init = () => {
  979. this.formData = this.getFormData()
  980.  
  981. const modal = this.create()
  982.  
  983. const form = modal.querySelector('.jv-form')
  984. const submitBtn = modal.querySelector('.jv-submit')
  985. const cancelBtn = modal.querySelector('.jv-cancel')
  986. const closeIcon = modal.querySelector('.jv-close-icon')
  987.  
  988. submitBtn.addEventListener('click', this.submit)
  989. closeIcon.addEventListener('click', this.hide)
  990. cancelBtn.addEventListener('click', this.hide)
  991. this.modal = modal
  992. this.form = form
  993.  
  994. this.buildExtraButtons()
  995. this.makeDraggable()
  996.  
  997. document.body.append(this.modal)
  998. this.initialized = true
  999. }
  1000.  
  1001. makeDraggable = () => {
  1002. let offsetX = 0
  1003. let offsetY = 0
  1004. const titleBar = this.modal.querySelector('.jv-title')
  1005. titleBar.draggable = true
  1006. this.modal.addEventListener('mousedown', () => {
  1007. const modals = document.querySelectorAll('.jv-modal')
  1008. modals.forEach(modal => {
  1009. if (modal !== this.modal) {
  1010. modal.style.zIndex = 1100
  1011. }
  1012. })
  1013. this.modal.style.zIndex = 1101
  1014. })
  1015. titleBar.addEventListener('dragstart', e => {
  1016. offsetX = e.clientX - this.modal.offsetLeft
  1017. offsetY = e.clientY - this.modal.offsetTop
  1018. e.dataTransfer.setData('text/plain', '')
  1019. e.dataTransfer.setDragImage(new Image(), 0, 0)
  1020. e.dataTransfer.dropEffect = 'move'
  1021. e.dataTransfer.effectAllowed = 'move'
  1022. })
  1023.  
  1024. document.addEventListener('dragover', e => {
  1025. if (e.target === titleBar) {
  1026. e.preventDefault()
  1027. const newX = e.clientX - offsetX
  1028. const newY = e.clientY - offsetY
  1029. this.modal.style.left = `${newX}px`
  1030. this.modal.style.top = `${newY}px`
  1031. }
  1032. })
  1033. }
  1034.  
  1035. buildExtraButtons = () => {
  1036. if (!this.extraButtons) return
  1037. const btnGroup = document.createElement('div')
  1038. btnGroup.className = 'jv-btn-group'
  1039. const btnFragment = document.createDocumentFragment()
  1040. this.extraButtons.forEach(({ withCtrl = true, onclick, ...others }) => {
  1041. const btn = document.createElement('button')
  1042. if (onclick) {
  1043. addClickEvent({
  1044. element: btn,
  1045. withCtrl,
  1046. handler: onclick.bind(this)
  1047. })
  1048. }
  1049. Object.entries(others).forEach(([key, value]) => {
  1050. btn[key] = value
  1051. })
  1052. btnFragment.append(btn)
  1053. })
  1054. btnGroup.append(btnFragment)
  1055. this.form.after(btnGroup)
  1056. }
  1057.  
  1058. buildFormItems = () => {
  1059. if (this.buildItems) return this.buildItems(this.formData)
  1060.  
  1061. return Object.keys(this.defaultData)
  1062. .map(key => {
  1063. const id = `jv-label-id-${key}`
  1064. return `
  1065. <div class='jv-form-item'>
  1066. <label for='${id}'>${key}: </label>
  1067. <input type='text' name='${key}' value='${this.formData[key]}' id='${id}'/>
  1068. </div>
  1069. `
  1070. })
  1071. .join('\n')
  1072. }
  1073.  
  1074. create = () => {
  1075. const modal = document.createElement('div')
  1076. modal.className = 'jv-modal'
  1077. const html = `
  1078. <div class='jv-close-icon'>${ICONS.close}</div>
  1079. <div class='jv-section'>
  1080. <div class='jv-title'>${this.title}</div>
  1081. <form class='jv-form'></form>
  1082. <div class='jv-btn-group'>
  1083. <button class='jv-submit'>确定</button>
  1084. <button class='jv-cancel'>取消</button>
  1085. </div>
  1086. </div>`
  1087. setInnerHTML(modal, html)
  1088. return modal
  1089. }
  1090.  
  1091. show = title => {
  1092. if (title) {
  1093. this.title = title
  1094. }
  1095. if (!this.initialized) {
  1096. this.init()
  1097. }
  1098. this.modal.style.display = 'block'
  1099. this.refresh(this.formData)
  1100. this.onShow?.()
  1101. }
  1102.  
  1103. hide = () => {
  1104. this.modal.style.display = 'none'
  1105. }
  1106.  
  1107. refresh = (formData, isUpdate = false) => {
  1108. if (isUpdate) {
  1109. this.formData = {
  1110. ...this.getRealTimeFormData(),
  1111. ...formData
  1112. }
  1113. } else {
  1114. this.formData = formData
  1115. }
  1116.  
  1117. const formItems = this.buildFormItems()
  1118. if (typeof formItems === 'string') {
  1119. setInnerHTML(this.form, formItems)
  1120. } else {
  1121. this.form.replaceChildren(formItems)
  1122. }
  1123. }
  1124.  
  1125. getRealTimeFormData = () => {
  1126. const formData = new FormData(this.form)
  1127. const data = {}
  1128. for (const [key, value] of formData) {
  1129. data[key] = convertFormValue(value.trim())
  1130. }
  1131. return data
  1132. }
  1133.  
  1134. submit = async () => {
  1135. const data = this.getRealTimeFormData()
  1136. this.formData = data
  1137. const shouldHide = await this.onSubmit?.(data)
  1138. if (shouldHide !== false) {
  1139. this.hide()
  1140. }
  1141. }
  1142. }
  1143.  
  1144. const configModal = new ConfigModal({
  1145. defaultData: defaultSettings,
  1146. getFormData: getSettings,
  1147. onSubmit(formData) {
  1148. settings = formData
  1149. GM_setValue('settings', formData)
  1150. notify('设置成功')
  1151. restart()
  1152. }
  1153. })
  1154.  
  1155. const refreshClearBtn = () => {
  1156. const clearBtn = document.querySelector('#jv-refresh-clear-btn')
  1157. if (clearBtn) {
  1158. clearBtn.textContent = `清除所有缓存(${oofCodeMap.size})`
  1159. }
  1160. }
  1161.  
  1162. const oofModal = new ConfigModal({
  1163. defaultData: defaultOOFSettings,
  1164. getFormData: getOOFSettings,
  1165. async onSubmit(formData) {
  1166. const { cookie, expiresIn, enable } = formData
  1167. if (enable && location.hostname === '115.com') {
  1168. const cookies = parseTextToCookies(cookie)
  1169. const checked = checkRequiredCookies(cookies)
  1170. if (!checked) return false
  1171. const requiredCookies = getRequiredCookies(cookies)
  1172. const expires = parseInt(expiresIn) || 30
  1173. for (const { name, value } of requiredCookies) {
  1174. try {
  1175. await GM.cookie.set({
  1176. name,
  1177. value,
  1178. domain: '.115.com',
  1179. path: '/',
  1180. secure: false,
  1181. httpOnly: true,
  1182. expirationDate: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * expires
  1183. })
  1184. } catch (error) {
  1185. console.error(error)
  1186. notify('设置Cookie失败')
  1187. return
  1188. }
  1189. }
  1190. formData.cookie = parseCookiesToText(requiredCookies)
  1191. }
  1192. oofSettings = formData
  1193. GM_setValue('oofSettings', formData)
  1194. notify('设置成功')
  1195. restart()
  1196. },
  1197. onShow: refreshClearBtn,
  1198. extraButtons: [
  1199. {
  1200. textContent: '获取115Cookie',
  1201. onclick: async () => {
  1202. if (location.hostname !== '115.com') {
  1203. notify('请在115页面上获取Cookie')
  1204. return
  1205. }
  1206. try {
  1207. const cookies = await GM.cookie.list({ domain: '.115.com' })
  1208. log(cookies)
  1209. const checked = checkRequiredCookies(cookies)
  1210. if (!checked) return
  1211. const requiredCookies = getRequiredCookies(cookies)
  1212. const cookieText = parseCookiesToText(requiredCookies)
  1213. notify('获取Cookie成功')
  1214. oofModal.refresh({ cookie: cookieText }, true)
  1215. } catch (error) {
  1216. console.error(error)
  1217. }
  1218. }
  1219. },
  1220. {
  1221. textContent: `刷新缓存`,
  1222. async onclick() {
  1223. const formData = this.getRealTimeFormData()
  1224. await initOOFCodeMap(formData.fetchCid, true)
  1225. refreshClearBtn()
  1226. }
  1227. },
  1228. {
  1229. id: 'jv-refresh-clear-btn',
  1230. textContent: `清空缓存(${oofCodeMap.size})`,
  1231. onclick: () => {
  1232. clearOOFCodeMap()
  1233. refreshClearBtn()
  1234. }
  1235. },
  1236. {
  1237. textContent: '批量移动',
  1238. onclick() {
  1239. const formData = this.getRealTimeFormData()
  1240. const [srcId, destId] = formData.move.split(/\s*:\s*/).map(item => item.trim())
  1241. moveFiles(srcId, destId)
  1242. }
  1243. }
  1244. ]
  1245. })
  1246.  
  1247. const localCodeModal = new ConfigModal({
  1248. buildItems(formData) {
  1249. return `
  1250. <textarea name='localCodeList' style='width: 1100px; height: 500px;' id='jv-local-code-textarea'>${formData.localCodeList.join(', ')}</textarea>
  1251. `
  1252. },
  1253. defaultData: { localCodeList: [] },
  1254. getFormData: () => ({ localCodeList: getLocalCodeList() }),
  1255. onSubmit(formData) {
  1256. const newLocalCodeList = formData.localCodeList
  1257. .split(/[,;\s、]\s*/)
  1258. .map(code => getParsedCode({ text: code, fc2Prefix: true }))
  1259. .filter(Boolean)
  1260.  
  1261. localCodeList = newLocalCodeList
  1262. GM_setValue('localCodeList', newLocalCodeList)
  1263. this.refresh({ localCodeList: newLocalCodeList })
  1264. notify('设置成功', `共设置番号${newLocalCodeList.length}个`)
  1265. restart()
  1266. }
  1267. })
  1268.  
  1269. const localNameModal = new ConfigModal({
  1270. buildItems(formData) {
  1271. return `
  1272. <textarea name='localNameList' style='width: 1100px; height: 500px;' id='jv-local-name-textarea'>${formData.localNameList.join('\n')}</textarea>
  1273. `
  1274. },
  1275. defaultData: { localNameList: [] },
  1276. getFormData: () => ({ localNameList: getLocalNameList() }),
  1277. onSubmit(formData) {
  1278. const newLocalNameList = formData.localNameList
  1279. .split('\n')
  1280. .map(name => name.toLowerCase().trim())
  1281. .filter(Boolean)
  1282. localNameList = newLocalNameList
  1283. GM_setValue('localNameList', newLocalNameList)
  1284. this.refresh({ localNameList: newLocalNameList })
  1285. notify('设置成功', `共设置自定义名称${newLocalNameList.length}个`)
  1286. restart()
  1287. }
  1288. })
  1289.  
  1290. function addLocalIcon(link) {
  1291. if (!link) return
  1292. if (link.querySelector('.jv-local-icon')) return
  1293. const icon = document.createElement('span')
  1294. icon.className = 'jv-local-icon'
  1295. icon.textContent = 'L'
  1296. addClickEvent({ element: icon })
  1297. link.append(icon)
  1298. }
  1299.  
  1300. function convertTextToElement(text) {
  1301. const div = document.createElement('div')
  1302. setInnerHTML(div, text)
  1303. return div.firstChild
  1304. }
  1305.  
  1306. function createFavoriteBtn(item) {
  1307. const favoriteBtn = document.createElement('span')
  1308. favoriteBtn.className = 'jv-favorite-btn'
  1309. if (item.UserData.IsFavorite) {
  1310. favoriteBtn.classList.add('jv-is-favorite')
  1311. }
  1312. const { isEmby, serverUrl, userId } = settings
  1313. addClickEvent({
  1314. element: favoriteBtn,
  1315. handler: async () => {
  1316. let url = `${serverUrl}${isEmby ? '/emby' : ''}/Users/${userId}/FavoriteItems/${item.Id}`
  1317. if (item.UserData.IsFavorite) {
  1318. await request(url, { method: 'DELETE' })
  1319. favoriteBtn.classList.remove('jv-is-favorite')
  1320. item.UserData.IsFavorite = false
  1321. } else {
  1322. await request(url, { method: 'POST' })
  1323. favoriteBtn.classList.add('jv-is-favorite')
  1324. item.UserData.IsFavorite = true
  1325. }
  1326. }
  1327. })
  1328. return favoriteBtn
  1329. }
  1330.  
  1331. function addIcon(link, item) {
  1332. if (!link || !item) return
  1333. if (link.querySelector('.jv-svg')) return
  1334. let icon, url
  1335. const { isEmby, serverUrl, showFavoriteBtn, userId } = settings
  1336. if (isEmby) {
  1337. icon = ICONS.emby
  1338. url = `${serverUrl}/web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`
  1339. } else {
  1340. icon = getIcon('jellyfin')
  1341. url = `${serverUrl}/web/index.html#!/details?id=${item.Id}`
  1342. }
  1343. const element = convertTextToElement(icon)
  1344. addClickEvent({
  1345. element,
  1346. copyText: url,
  1347. handler: e => openTab(url, e.ctrlKey)
  1348. })
  1349. link.append(element)
  1350. if (showFavoriteBtn && userId) {
  1351. const favoriteBtn = createFavoriteBtn(item, userId)
  1352. link.append(favoriteBtn)
  1353. }
  1354. }
  1355.  
  1356. async function checkCodeExist({ link, code, text, updateIcon, type }) {
  1357. const { enable: jellyfinEnable, localCodeEnable, forceShowLocalBtn } = settings
  1358. const { enable: oofEnable, forceShowOOFBtn } = oofSettings
  1359. const conditions = [
  1360. {
  1361. andCondition: jellyfinEnable,
  1362. orCondition: false,
  1363. cb: async () => {
  1364. const item = await getItemByCode({ code, type })
  1365. if (updateIcon && item) {
  1366. addIcon(link, item)
  1367. }
  1368. return item
  1369. }
  1370. },
  1371. {
  1372. andCondition: oofEnable,
  1373. orCondition: oofEnable && forceShowOOFBtn,
  1374. cb: () => {
  1375. return checkWatchIcon({ link, code, text, type, forceCheck: false, update: updateIcon })
  1376. }
  1377. },
  1378. {
  1379. andCondition: localCodeEnable,
  1380. orCondition: localCodeEnable && forceShowLocalBtn,
  1381. cb: () => {
  1382. let exist
  1383. if (type) {
  1384. if (type === 'fc2') {
  1385. code = `fc2-${code}`
  1386. }
  1387. exist = localCodeList.includes(code)
  1388. } else {
  1389. exist = localCodeList.some(localCode => localCode.includes(code))
  1390. }
  1391. if (updateIcon && exist) {
  1392. addLocalIcon(link)
  1393. }
  1394. return exist
  1395. }
  1396. }
  1397. ]
  1398. let isExist = false
  1399. for (const { andCondition, orCondition, cb } of conditions) {
  1400. if ((!isExist && andCondition) || orCondition) {
  1401. const result = await cb()
  1402. isExist = isExist || result
  1403. }
  1404. }
  1405. return isExist
  1406. }
  1407.  
  1408. function findCode(boxSelector, codeSelector) {
  1409. const { enable: jellyfinEnable, apiKey, serverUrl, reverseEmphasis, emphasisOutlineStyle } = settings
  1410. if (jellyfinEnable && (!apiKey || !serverUrl)) {
  1411. notify('缺少必填项', '填写apiKey和serverUrl或者关闭jellyfin/emby功能')
  1412. configModal.show()
  1413. return
  1414. }
  1415. const run = () => {
  1416. const boxes = document.querySelectorAll(boxSelector)
  1417. for (const box of boxes) {
  1418. if (box.hasAttribute('data-jv-outline')) continue
  1419. let code = typeof codeSelector === 'function' ? codeSelector(box) : box.querySelector(codeSelector)?.textContent
  1420. if (!code) return
  1421. box.setAttribute('data-jv-outline', box.style.outline)
  1422. box.setAttribute('data-jv-outline-priority', box.style.getPropertyPriority('outline'))
  1423. code = code.toLowerCase()
  1424. queue.push(async () => {
  1425. const isExist = await checkCodeExist({ link: null, code, text: code, updateIcon: false, type: null })
  1426. if ((!reverseEmphasis && !isExist) || (reverseEmphasis && isExist)) {
  1427. setStyle(box, { outline: emphasisOutlineStyle }, true)
  1428. }
  1429. })
  1430. }
  1431. }
  1432. const clear = () => {
  1433. const boxes = document.querySelectorAll(boxSelector)
  1434. boxes.forEach(box => {
  1435. const outline = box.getAttribute('data-jv-outline')
  1436. const priority = box.getAttribute('data-jv-outline-priority')
  1437. box.removeAttribute('data-jv-outline')
  1438. box.removeAttribute('data-jv-outline-priority')
  1439. setStyle(box, { outline }, priority)
  1440. })
  1441. }
  1442.  
  1443. return { run, clear }
  1444. }
  1445.  
  1446. function openSiteByParams(site, params, e) {
  1447. if (!site) return
  1448. const specialNumCode = parseSpecialNumCode(params.code)
  1449. if (specialNumCode) {
  1450. params.code = specialNumCode
  1451. }
  1452. const entries = Object.entries(params)
  1453. const url = entries.reduce((acc, [key, value]) => acc.replaceAll(`\${${key}}`, value), site)
  1454. if (e.button === 0) {
  1455. openTab(url, e.ctrlKey)
  1456. } else if (e.button === 2) {
  1457. copy({ text: url })
  1458. }
  1459. }
  1460.  
  1461. function parseUserPair(pair) {
  1462. return pair
  1463. .split(/\s*;\s*/)
  1464. .filter(Boolean)
  1465. .map(item => item.split(/\s*:\s*/))
  1466. }
  1467.  
  1468. function getCidMap(cidPair) {
  1469. return new Map(parseUserPair(cidPair))
  1470. }
  1471.  
  1472. function handleOOFIconClick(file, link) {
  1473. return async e => {
  1474. if (e.altKey) {
  1475. const success = await deleteFile(file)
  1476. if (success) {
  1477. link.querySelector('.jv-alist-svg')?.remove()
  1478. link.querySelector('.jv-oof-svg')?.remove()
  1479. }
  1480. } else {
  1481. const { openSite, secondarySite } = oofSettings
  1482. const { pc: pickcode, n: text, code, cid } = file
  1483. openSiteByParams(e.shiftKey ? secondarySite : openSite, { pickcode, text, code, cid }, e)
  1484. }
  1485. }
  1486. }
  1487.  
  1488. function alistIconClick(file) {
  1489. return e => {
  1490. const { alistUrl, cidPair } = oofSettings
  1491. if (!alistUrl || !cidPair) {
  1492. notify('缺少参数', '请先设置alistUrl和cidPair')
  1493. return
  1494. }
  1495. const cidMap = getCidMap(cidPair)
  1496. const defaultDir = cidMap.get('*')
  1497. const dir = cidMap.get(file.cid) || defaultDir
  1498. if (!dir) {
  1499. notify('没找到对应的alist目录, 请检查设置')
  1500. return
  1501. }
  1502. const url = new URL(alistUrl.replaceAll('${dir}', dir).replaceAll('${file}', file.n).replace('/d', ''))
  1503. const downloadUrl = `${url.origin}/d${url.pathname}`
  1504. if (e.button === 0) {
  1505. if (e.altKey) {
  1506. window.open(downloadUrl, '_self')
  1507. } else {
  1508. openTab(url.href, e.ctrlKey)
  1509. }
  1510. } else if (e.button === 2) {
  1511. copy({ text: downloadUrl, desc: '下载链接已复制' })
  1512. }
  1513. }
  1514. }
  1515.  
  1516. let removeWatchIconEvent
  1517. async function checkWatchIcon({ link, code, text, type, forceCheck, update }) {
  1518. const { alistEnable } = oofSettings
  1519. const oofIcon = link?.querySelector('.jv-oof-svg')
  1520. const alistIcon = link?.querySelector('.jv-alist-svg')
  1521. if (!forceCheck && oofIcon) return true
  1522. const file = await getAndUpdateFileByCode({ code, text, type, forceCheck })
  1523. if (!update || !link) return file
  1524. if (file) {
  1525. const bindEvent = (innerOOFIcon, innerAlistIcon) => {
  1526. const removeOOFEvent = addClickEvent({
  1527. element: innerOOFIcon,
  1528. handler: handleOOFIconClick(file, link),
  1529. copyText: null
  1530. })
  1531. let removeAlistEvent
  1532. if (innerAlistIcon) {
  1533. removeAlistEvent = addClickEvent({
  1534. element: innerAlistIcon,
  1535. handler: alistIconClick(file),
  1536. copyText: null
  1537. })
  1538. }
  1539. removeWatchIconEvent = () => {
  1540. removeOOFEvent()
  1541. removeAlistEvent?.()
  1542. }
  1543. }
  1544. if (oofIcon) {
  1545. removeWatchIconEvent?.()
  1546. bindEvent(oofIcon, alistIcon)
  1547. } else {
  1548. const newOOFIcon = convertTextToElement(ICONS.oof)
  1549. const newAlistIcon = alistEnable ? convertTextToElement(ICONS.alist) : null
  1550. bindEvent(newOOFIcon, newAlistIcon)
  1551. link.append(newOOFIcon)
  1552. if (newAlistIcon) {
  1553. link.append(newAlistIcon)
  1554. }
  1555. }
  1556. } else {
  1557. oofIcon?.remove()
  1558. alistIcon?.remove()
  1559. }
  1560. return file
  1561. }
  1562.  
  1563. const getUserId = requireCookie(() => {
  1564. const match = oofSettings.cookie.match(/UID=(\d+)_/)
  1565. const userId = match?.[1]
  1566. log(`userId为: ${userId}`)
  1567. return userId
  1568. })
  1569.  
  1570. const addOfflineTask = requireCookie(async magnet => {
  1571. const userId = getUserId()
  1572. if (!userId) {
  1573. notify('未获取到UID, 请检查Cookie')
  1574. return
  1575. }
  1576. const { close } = notify('正在添加离线任务', magnet, 0)
  1577. try {
  1578. const now = Date.now()
  1579. const signUrl = `https://115.com/?ct=offline&ac=space&_=${now}`
  1580. const { sign, time } = await requestWithCookie(signUrl)
  1581. log(`sign: ${sign}, time: ${time}`)
  1582. const { offlineCid } = oofSettings
  1583. const data = { url: magnet, uid: userId, sign, time, wp_path_id: offlineCid || null }
  1584. const response = await requestWithCookie('https://115.com/web/lixian/?ct=lixian&ac=add_task_url', {
  1585. method: 'POST',
  1586. data: getQuery(data),
  1587. headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  1588. })
  1589. log(response)
  1590. if (response.state) {
  1591. notify('离线任务添加成功', '按住alt并单击对应的番号链接, 即可看到在线观看按钮')
  1592. } else {
  1593. notify('离线任务添加失败', response.error_msg)
  1594. }
  1595. } catch (error) {
  1596. notify('离线失败')
  1597. console.error(error)
  1598. } finally {
  1599. close()
  1600. }
  1601. })
  1602.  
  1603. function addOfflineBtn(parent, magnet) {
  1604. const { enable } = oofSettings
  1605. if (!enable) return
  1606. if (parent.querySelector('.jv-oof-btn')) return
  1607. const button = document.createElement('button')
  1608. button.className = 'jv-oof-btn'
  1609. button.textContent = '115离线'
  1610. addClickEvent({
  1611. element: button,
  1612. handler: () => addOfflineTask(magnet),
  1613. url: magnet,
  1614. withCtrl: true,
  1615. copyText: magnet
  1616. })
  1617. parent.append(button)
  1618. }
  1619.  
  1620. function getLinkColorByType(type) {
  1621. const { linkColor, magnetColor, userRegColor } = settings
  1622. if (type === 'userReg') return userRegColor
  1623. if (isTypeMagnetLike(type)) return magnetColor
  1624. return linkColor
  1625. }
  1626.  
  1627. function handleLinkClick(text, code, type) {
  1628. return async e => {
  1629. const link = e.target
  1630. const { linkVisitedColor, linkExistColor, openSite, secondarySite, fc2Site } = settings
  1631. const { enable: oofEnable } = oofSettings
  1632. if (isTypeMagnetLike(type)) {
  1633. if (type === 'magnet') {
  1634. window.open(code, '_self')
  1635. }
  1636. } else {
  1637. if (oofEnable && e.altKey) {
  1638. const isExsit = await checkWatchIcon({ link, code, text, type, forceCheck: true, update: true })
  1639. if (isExsit) {
  1640. setStyle(link, { color: linkExistColor })
  1641. } else {
  1642. notify(`${code}在你的115中不存在`)
  1643. }
  1644. } else {
  1645. setStyle(link, { color: linkVisitedColor })
  1646. if (e.shiftKey) {
  1647. openSiteByParams(secondarySite, { code }, e)
  1648. } else if (type === 'fc2') {
  1649. openSiteByParams(fc2Site || openSite, { code }, e)
  1650. } else {
  1651. openSiteByParams(openSite, { code }, e)
  1652. }
  1653. }
  1654. }
  1655. }
  1656. }
  1657.  
  1658. function createLink(text, code, type) {
  1659. const link = document.createElement('a')
  1660. link.append(text)
  1661. link.className = 'jv-link'
  1662. link.setAttribute('data-jv-code', code)
  1663. const { linkExistColor } = settings
  1664. const { enable: oofEnable } = oofSettings
  1665. const isMagnetLike = isTypeMagnetLike(type)
  1666. const linkColor = getLinkColorByType(type)
  1667. setStyle(link, { color: linkColor })
  1668. copySet.setCopySetByType(type, code)
  1669. if (oofEnable && isMagnetLike) {
  1670. addOfflineBtn(link, code)
  1671. }
  1672. addClickEvent({
  1673. element: link,
  1674. handler: handleLinkClick(text, code, type),
  1675. copyText: code
  1676. })
  1677. if (!isMagnetLike) {
  1678. queue.push(async () => {
  1679. const isExist = await checkCodeExist({ link, code, text, updateIcon: true, type })
  1680. if (isExist) {
  1681. setStyle(link, { color: linkExistColor })
  1682. }
  1683. })
  1684. }
  1685. return link
  1686. }
  1687.  
  1688. function replaceCodeWithLink(text, regItem) {
  1689. const { type, reg } = regItem
  1690. let match = reg.exec(text)
  1691. if (!match) return null
  1692. const fragment = document.createDocumentFragment()
  1693. let lastIndex = 0
  1694. while (match) {
  1695. const textBeforeMatch = text.slice(lastIndex, match.index)
  1696. if (textBeforeMatch.length > 0) {
  1697. fragment.append(textBeforeMatch)
  1698. }
  1699. let code = getCodeByRegType(match, type)
  1700. code = code.toLowerCase()
  1701. const link = createLink(match[0], code, type)
  1702. fragment.append(link)
  1703. lastIndex = reg.lastIndex
  1704. match = reg.exec(text)
  1705. }
  1706. const remainingText = text.slice(lastIndex)
  1707. if (remainingText.length > 0) {
  1708. fragment.append(remainingText)
  1709. }
  1710. return fragment
  1711. }
  1712.  
  1713. function replaceNameWithLink(text) {
  1714. text = text.toLowerCase()
  1715. const fragment = document.createDocumentFragment()
  1716. let isNull = true
  1717. let lastIndex = 0
  1718. const textLength = text.length
  1719. for (const localName of localNameList) {
  1720. if (lastIndex >= textLength) {
  1721. break
  1722. }
  1723. const matchIndex = text.indexOf(localName, lastIndex)
  1724. if (matchIndex > -1) {
  1725. isNull = false
  1726. const textBeforeMatch = text.slice(lastIndex, matchIndex)
  1727. if (textBeforeMatch.length > 0) {
  1728. fragment.append(textBeforeMatch)
  1729. }
  1730. const link = createLink(localName, localName, 'localName')
  1731. fragment.append(link)
  1732. lastIndex = matchIndex + localName.length
  1733. }
  1734. }
  1735. const remainingText = text.slice(lastIndex)
  1736. if (remainingText.length > 0) {
  1737. fragment.append(remainingText)
  1738. }
  1739. return isNull ? null : fragment
  1740. }
  1741.  
  1742. function handleTextNode(node) {
  1743. const text = node.nodeValue
  1744. if (!text) return
  1745. const textRegList = getTextRegList()
  1746. for (const regItem of textRegList) {
  1747. const codeFragment = replaceCodeWithLink(text, regItem)
  1748. if (codeFragment) {
  1749. node.replaceWith(codeFragment)
  1750. return
  1751. }
  1752. }
  1753. const nameFragment = replaceNameWithLink(text)
  1754. if (nameFragment) {
  1755. node.replaceWith(nameFragment)
  1756. }
  1757. }
  1758.  
  1759. function traverseTextNodes(handler) {
  1760. log('开始遍历所有文本')
  1761. const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT, node => {
  1762. const parent = node.parentNode
  1763. if (
  1764. (parent.tagName.toLowerCase() === 'a' && parent.hasAttribute('data-jv-code')) ||
  1765. parent.id === 'jv-local-code-textarea' ||
  1766. parent.id === 'jv-local-name-textarea'
  1767. ) {
  1768. return NodeFilter.FILTER_SKIP
  1769. }
  1770. return NodeFilter.FILTER_ACCEPT
  1771. })
  1772. let node
  1773. while ((node = iterator.nextNode()) !== null) {
  1774. handler(node)
  1775. }
  1776. }
  1777.  
  1778. function handleLink(node) {
  1779. const href = node.getAttribute('href')
  1780. if (!href) return
  1781. const LinkRegList = getLinkRegList()
  1782. for (const { type, reg } of LinkRegList) {
  1783. const match = href.match(reg)
  1784. if (match) {
  1785. copySet.setCopySetByType(type, match[0])
  1786. addOfflineBtn(node, match[0])
  1787. return
  1788. }
  1789. }
  1790. }
  1791.  
  1792. function traverseLinks(handler) {
  1793. log('开始遍历所有链接')
  1794. const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ELEMENT, node => {
  1795. if (node.tagName.toLowerCase() !== 'a' || node.hasAttribute('data-jv-code')) return NodeFilter.FILTER_SKIP
  1796. return NodeFilter.FILTER_ACCEPT
  1797. })
  1798. let node
  1799. while ((node = iterator.nextNode()) !== null) {
  1800. handler(node)
  1801. }
  1802. }
  1803.  
  1804. function traverse() {
  1805. log('traverse...')
  1806. traverseTextNodes(handleTextNode)
  1807. if (oofSettings.enable) {
  1808. traverseLinks(handleLink)
  1809. }
  1810. }
  1811.  
  1812. function startSpecialMatch() {
  1813. const { hotKeys, triggerOnload } = settings
  1814. for (const config of CONFIG) {
  1815. const { site, cb } = config
  1816. if (!site.test(location.href)) continue
  1817. log('网址匹配路径正则, 将额外显示一个边框')
  1818. const { run, clear } = cb()
  1819. if (triggerOnload) {
  1820. run()
  1821. }
  1822. eventEmitter.on(EVENT_Type.clearDom, clear)
  1823. eventEmitter.on(EVENT_Type.clearEvent, keysEvent.on(hotKeys, run))
  1824. }
  1825. }
  1826.  
  1827. function clearOOFBtns() {
  1828. const btns = document.querySelectorAll('.jv-oof-btn')
  1829. btns.forEach(btn => btn.remove())
  1830. }
  1831.  
  1832. function clearCommonMatchDoms() {
  1833. const links = document.querySelectorAll('.jv-link')
  1834. links.forEach(link => {
  1835. const parent = link.parentNode
  1836. link.replaceWith(link.firstChild)
  1837. parent.normalize()
  1838. })
  1839. clearOOFBtns()
  1840. }
  1841.  
  1842. function startCommonMatch() {
  1843. const { hotKeys, triggerOnload } = settings
  1844. if (triggerOnload) {
  1845. traverse()
  1846. }
  1847. eventEmitter.on(EVENT_Type.clearEvent, keysEvent.on(hotKeys, traverse))
  1848. eventEmitter.on(EVENT_Type.clearDom, clearCommonMatchDoms)
  1849. }
  1850.  
  1851. function registerKeysEvent() {
  1852. const { recoverHotKeys } = settings
  1853. const events = [
  1854. [
  1855. recoverHotKeys,
  1856. () => {
  1857. log('清除页面改动')
  1858. eventEmitter.emit(EVENT_Type.clearDom)
  1859. }
  1860. ]
  1861. ]
  1862. events.forEach(([keys, cb]) => {
  1863. eventEmitter.on(EVENT_Type.clearEvent, keysEvent.on(keys, cb))
  1864. })
  1865. }
  1866.  
  1867. function autoTrigger(cb, immediate = false) {
  1868. if (immediate) {
  1869. setTimeout(cb, 500)
  1870. }
  1871. const buttons = document.querySelectorAll('button.btnPreviousPage, button.btnNextPage')
  1872. buttons.forEach(button => {
  1873. const handler = () => {
  1874. setTimeout(() => {
  1875. cb()
  1876. autoTrigger(cb, false)
  1877. }, 500)
  1878. }
  1879. button.addEventListener('click', handler)
  1880. eventEmitter.on(EVENT_Type.clearEvent, () => button.removeEventListener('click', handler))
  1881. })
  1882. }
  1883.  
  1884. function registerAutoTriggerEvent() {
  1885. const { jellyfinAutoTrigger, serverUrl } = settings
  1886. const handler = throttle(() => {
  1887. log('viewshow: ', location.hash)
  1888. if (location.hash.includes('/movies.html')) {
  1889. setTimeout(() => autoTrigger(traverse, true), 1000)
  1890. }
  1891. })
  1892. if (location.origin === serverUrl && jellyfinAutoTrigger) {
  1893. handler()
  1894. document.addEventListener('viewshow', handler)
  1895. }
  1896. eventEmitter.on(EVENT_Type.clearEvent, () => document.removeEventListener('viewshow', handler))
  1897. }
  1898.  
  1899. async function restart() {
  1900. const { debug, serverUrl, extraSearchParams } = settings
  1901. const { enable: oofEnable, fetchCid } = oofSettings
  1902. if (debug) {
  1903. log = console.log.bind(console)
  1904. unsafeWindow.top.oofCodeMap = oofCodeMap
  1905. } else {
  1906. log = noop
  1907. }
  1908. if (location.origin === serverUrl && !extraSearchParams) {
  1909. settings.enable = false
  1910. }
  1911.  
  1912. log('jellyfin过滤插件已启动...')
  1913. log(settings)
  1914. log(oofSettings)
  1915.  
  1916. if (oofEnable) {
  1917. await initOOFCodeMap(fetchCid, false)
  1918. }
  1919.  
  1920. eventEmitter.emit(EVENT_Type.clearDom)
  1921. eventEmitter.emit(EVENT_Type.clearEvent)
  1922. eventEmitter.emit(EVENT_Type.startMatch)
  1923. }
  1924.  
  1925. function removeAD() {
  1926. document.querySelector('#js_common_mini-dialog')?.remove()
  1927. document.querySelector('.vt-headline > div:last-child')?.remove()
  1928. }
  1929.  
  1930. function registerMenuCommand() {
  1931. const events = [
  1932. ['设置', configModal.show],
  1933. ['115设置', oofModal.show],
  1934. ['补充本地番号', localCodeModal.show],
  1935. ['新增匹配名称', localNameModal.show]
  1936. ]
  1937. events.forEach(([title, cb]) => GM_registerMenuCommand(title, () => cb(title)))
  1938. }
  1939.  
  1940. function registerEvents() {
  1941. const startFuncList = [startSpecialMatch, startCommonMatch, registerKeysEvent, copySet.registerCopyEvent, registerAutoTriggerEvent]
  1942. startFuncList.forEach(cb => eventEmitter.on(EVENT_Type.startMatch, cb))
  1943. }
  1944.  
  1945. function addStyle() {
  1946. const baseColor = () => `
  1947. color: rgba(0, 0, 0, 0.88);
  1948. background: #fff;
  1949. border: 1px solid #d9d9d9;
  1950. `
  1951. const baseContainerStyle = () => `
  1952. ${baseColor()}
  1953. font-size: 14px;
  1954. line-height: 1.5;
  1955. padding: 20px 24px;
  1956. border-radius: 8px;
  1957. box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.08), -2px -2px 2px 1px rgba(0, 0, 0, 0.08);
  1958. `
  1959.  
  1960. const baseBtnStyle = () => `
  1961. ${baseColor()}
  1962. height: 2.2em;
  1963. padding: 0 15px;
  1964. outline: none;
  1965. border-radius: 4px;
  1966. font-size: 13px;
  1967. box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02);
  1968. cursor: pointer;
  1969. `
  1970.  
  1971. const focusBtnStyle = () => `
  1972. border: 1px solid #1677ff;
  1973. box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1);
  1974. `
  1975.  
  1976. const css = `
  1977. .jv-link {
  1978. display: inline-flex;
  1979. align-items: center;
  1980. flex-wrap: nowrap;
  1981. cursor: pointer;
  1982. max-width: 100%;
  1983. overflow: auto;
  1984. }
  1985. .jv-svg, .jv-oof-svg, .jv-alist-svg {
  1986. margin: 0 2px;
  1987. width: 1.2em;
  1988. max-width: 32px;
  1989. vertical-align: middle;
  1990. cursor: pointer;
  1991. flex-shrink: 0;
  1992. min-width: 12px;
  1993. }
  1994. .jv-local-icon {
  1995. display: inline-flex;
  1996. justify-content: center;
  1997. align-items: center;
  1998. width: 1.4em;
  1999. height: 1.4em;
  2000. min-width: 12px;
  2001. max-width: 32px;
  2002. flex-shrink: 0;
  2003. margin: 0 2px;
  2004. border-radius: 50%;
  2005. background: #243fbf;
  2006. color: #fff;
  2007. font-size: 14px;
  2008. font-weight: 700;
  2009. cursor: default;
  2010. }
  2011. .jv-oof-btn {
  2012. ${baseBtnStyle()}
  2013. background: #1677ff;
  2014. color: #fff;
  2015. height: 1.7em;
  2016. line-height: 1.7;
  2017. margin: 0 2px;
  2018. padding: 0 6px;
  2019. font-size: 12px;
  2020. border: none;
  2021. }
  2022. .jv-modal {
  2023. ${baseContainerStyle()}
  2024. padding-top: 0;
  2025. position: fixed;
  2026. left: 50%;
  2027. top: 50%;
  2028. transform: translate(-50%, -50%);
  2029. z-index: 1100;
  2030. overflow: auto;
  2031. display: none;
  2032. }
  2033. .jv-section {
  2034. width: 1100px;
  2035. }
  2036. .jv-section .jv-title {
  2037. font-weight: bold;
  2038. font-size: 22px;
  2039. padding: 20px 0;
  2040. text-align: center;
  2041. cursor: grab;
  2042. user-select: none;
  2043. }
  2044. .jv-form {
  2045. display: flex;
  2046. flex-wrap: wrap;
  2047. justify-content: space-between;
  2048. max-width: unset;
  2049. }
  2050. .jv-form-item {
  2051. display: flex;
  2052. justify-content: space-between;
  2053. align-items: center;
  2054. margin: 12px 0;
  2055. width: 500px;
  2056. }
  2057. .jv-form input, .jv-form textarea {
  2058. ${baseBtnStyle()}
  2059. font-size: 14px;
  2060. box-sizing: border-box;
  2061. width: 300px;
  2062. padding: 0 11px;
  2063. cursor: text;
  2064. height: 2.2em;
  2065. line-height: 2.2;
  2066. }
  2067. .jv-form input:focus, .jv-form textarea:focus {
  2068. ${focusBtnStyle()}
  2069. }
  2070. .jv-btn-group {
  2071. margin-top: 20px;
  2072. text-align: center;
  2073. }
  2074. .jv-btn-group button {
  2075. ${baseBtnStyle()}
  2076. margin-right: 5px;
  2077. }
  2078. .jv-btn-group button:hover {
  2079. ${focusBtnStyle()}
  2080. color: #1677ff;
  2081. }
  2082. .jv-close-icon {
  2083. position: absolute;
  2084. right: 0;
  2085. top: 0;
  2086. cursor: pointer;
  2087. padding: 10px;
  2088. }
  2089. .jv-close-icon:hover {
  2090. color: red;
  2091. }
  2092. .jv-notification {
  2093. ${baseContainerStyle()}
  2094. position: fixed;
  2095. z-index: 100001;
  2096. top: 24px;
  2097. right: 24px;
  2098. }
  2099.  
  2100. .jv-notification .jv-title {
  2101. margin-bottom: 8px;
  2102. font-size: 16px;
  2103. padding-right: 24px;
  2104. }
  2105.  
  2106. .jv-notification .jv-content {
  2107. max-height: 500px;
  2108. overflow: auto;
  2109. white-space: pre;
  2110. }
  2111.  
  2112. .jv-favorite-btn {
  2113. position: relative;
  2114. width: 2em;
  2115. height: 1.6em;
  2116. display: inline-block;
  2117. overflow: hidden;
  2118. transform: scale(0.8);
  2119. }
  2120.  
  2121. .jv-favorite-btn::before,
  2122. .jv-favorite-btn::after {
  2123. content: '';
  2124. position: absolute;
  2125. top: 0.1em;
  2126. left: 0;
  2127. width: 1em;
  2128. height: 1.5em;
  2129. background: #ccc;
  2130. cursor: pointer;
  2131. }
  2132. .jv-favorite-btn.jv-is-favorite::before,
  2133. .jv-favorite-btn.jv-is-favorite::after {
  2134. background: #c33;
  2135. }
  2136.  
  2137. .jv-favorite-btn::before {
  2138. left: 1em;
  2139. transform: rotate(-45deg);
  2140. transform-origin: 0 100%;
  2141. border-radius: 50% 50% 50% 0;
  2142. }
  2143.  
  2144. .jv-favorite-btn::after {
  2145. transform: rotate(45deg);
  2146. transform-origin: 100% 100%;
  2147. border-radius: 50% 50% 0 50%;
  2148. }
  2149. `
  2150. const cssWithoutComment = css
  2151. .split('\n')
  2152. .filter(line => !line.trim().startsWith('//'))
  2153. .join('\n')
  2154. GM_addStyle(cssWithoutComment)
  2155. }
  2156.  
  2157. function start() {
  2158. addStyle()
  2159. registerMenuCommand()
  2160. registerEvents()
  2161. removeAD()
  2162. restart()
  2163. }
  2164.  
  2165. start()
  2166. })()