您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
调用jellyfin API,突出显示本地不存在的影片
当前为
// ==UserScript== // @name Jellyfin番号过滤 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 调用jellyfin API,突出显示本地不存在的影片 // @author Squirtle // @license MIT // @match https://www.javbus.com/* // @match https://www.javlibrary.com/* // @match https://javdb.com/* // @match https://jinjier.art/* // @match https://www.youtube.com/* // @match http://localhost:3000/* // @icon data:image/png;base64,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 // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // ==/UserScript== ;(function () { 'use strict' const defalultSettings = { apiKey: '7d7ba7ea977b496c8f113f1a07a03136', serverUrl: 'http://127.0.0.1:8096', // 跳转时${code}会被替换为真正的番号 openSite: 'https://javdb.com/search?q=${code}', // openSite: 'https://www.javbus.com/${code}', isEmby: false, // 若为true,则在页面加载完成后自动触发一次过滤 triggerOnload: false, // 自定义快捷键,可以是任意长度的数字或字母 hotKeys: 'ee', debug: false, linkColor: 'red', linkVisitedColor: '#87CEEB', linkExistColor: '#0000FF', emphasisOutlineStyle: '2px solid red', reverseEmphasis: false } const CONFIG = [ { site: /https:\/\/www\.javbus\.com(?!\/forum\/forum\.php)/, cb: () => findCode('a.movie-box', 'date', '.item-tag') }, { site: /https:\/\/www\.javlibrary\.com\/cn(?!(\/tl_bestreviews.php|\/publicgroups.php|\/publictopic.php))/, cb: () => findCode('.video', '.id', 'a[href]') }, { site: /javdb/, cb: () => findCode('.movie-list .item', '.video-title strong', '.tags.has-addons') }, { site: /https:\/\/jinjier\.art\/sql.*/, cb: () => findCode( 'tbody tr', box => { const td = box.querySelector('td:nth-of-type(3)') return td.textContent.split(' ')[0] }, 'td:nth-of-type(3)' ) } ] let settings = getSettings() if (window.trustedTypes && window.trustedTypes.createPolicy) { window.trustedTypes.createPolicy('default', { createHTML: string => string, createScript: string => string }) } const codeMap = new Map() function getSettings() { return { ...defalultSettings, ...GM_getValue('settings') } } function findCode(boxSelector, codeSelector, iconParentSelector) { if (!settings.apiKey || !settings.serverUrl) { showModal() return } const boxes = document.querySelectorAll(boxSelector) for (const box of boxes) { const code = typeof codeSelector === 'function' ? codeSelector(box) : box.querySelector(codeSelector)?.textContent if (!code) return queue.push(async () => { const item = await getItemByCode(code) if (item) { const iconParent = box.querySelector(iconParentSelector) || box addIcon(iconParent, item) } if ((!settings.reverseEmphasis && !item) || (settings.reverseEmphasis && item)) { setStyle(box, { outline: settings.emphasisOutlineStyle }) } }) } } function registerKeysEvent(eventType, keys, callback, timeout = 500) { if (!keys) { throw new Error('keys不能为空') } const innerKeys = keys.split('') let firstTime = 0 let index = 0 document.addEventListener(eventType, e => { const currentTime = Date.now() const key = innerKeys[index] if (index > innerKeys.length - 1 || e.key.toLowerCase() !== key.toLowerCase()) { firstTime = 0 index = 0 return } if (currentTime - firstTime > timeout) { firstTime = 0 index = 0 } if (index === innerKeys.length - 1) { try { callback() } catch (error) { console.error(error) } firstTime = 0 index = 0 return } if (index === 0) { firstTime = currentTime } index++ }) } class AsyncQueue { constructor(concurrent = 5) { this.concurrent = concurrent this.activeCount = 0 this.queue = [] } push(promiseCreator) { this.queue.push(promiseCreator) this.next() } next() { if (this.activeCount < this.concurrent && this.queue.length) { const promiseCreator = this.queue.shift() this.activeCount++ promiseCreator().finally(() => { this.activeCount-- this.next() }) } } } const queue = new AsyncQueue() function request(url, method = 'GET') { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers: { 'X-Emby-Token': settings.apiKey }, onload(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)) } catch (error) { reject(error) } } else { reject(response) } }, onerror(error) { reject(error) } }) }) } function addQuery(base, obj) { if (!obj) { return base } const query = Object.entries(obj) .filter(([_, value]) => value != null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&') if (!query) { return base } return base.endsWith('?') ? base + query : `${base}?${query}` } async function fetchItems(params) { const finalParams = { startIndex: 0, fields: 'SortName', imageTypeLimit: 1, includeItemTypes: 'Movie', recursive: true, sortBy: 'SortName', sortOrder: 'Ascending', limit: 2, ...params } const url = settings.isEmby ? `${settings.serverUrl}/emby/Items` : `${settings.serverUrl}/Items` try { const response = await request(addQuery(url, finalParams)) return response.Items } catch (error) { log('请检查apiKey与serverUrl是否设置正确') console.error(error) } } async function getItemByCode(code) { if (codeMap.has(code)) { return codeMap.get(code) } const items = await fetchItems({ searchTerm: code }) if (items.length > 0) { codeMap.set(code, items[0]) return items[0] } return null } function setStyle(element, styles) { Object.entries(styles).forEach(([key, value]) => { element.style[key] = value }) } function createLink(text, code) { const link = document.createElement('a') link.append(text) link.className = 'jv-link' setStyle(link, { color: settings.linkColor }) link.setAttribute('data-jv-click', 0) link.setAttribute('data-jv-code', code) link.onclick = e => { e.preventDefault() e.stopPropagation() setStyle(link, { color: settings.linkVisitedColor }) const clickCount = link.getAttribute('data-jv-click') link.setAttribute('data-jv-click', Number(clickCount) + 1) if (settings.openSite) { const url = settings.openSite.replaceAll('${code}', code) GM_openInTab(url, { active: true, insert: true, setParent: true }) } } queue.push(async () => { const item = await getItemByCode(code) if (item) { setStyle(link, { color: settings.linkExistColor }) addIcon(link, item) } }) return link } function getTextNodes() { const nodes = [] const iterator = document.createNodeIterator( document.body, NodeFilter.SHOW_TEXT, node => { const parent = node.parentNode return parent.tagName.toLowerCase() === 'a' && parent.hasAttribute('data-jv-click') ? NodeFilter.FILTER_SKIP : NodeFilter.FILTER_ACCEPT }, false ) let node = iterator.nextNode() while (node) { nodes.push(node) node = iterator.nextNode() } return nodes } function convertTextToElement(text) { const div = document.createElement('div') div.innerHTML = text return div.firstChild } function addIcon(parent, item) { if (!parent || !item) return if (parent.querySelector('.jv-icon-wrapper')) return let text let url if (settings.isEmby) { text = '<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>' url = `${settings.serverUrl}/web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}` } else { text = '<svg class="jv-svg" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <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><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>' url = `${settings.serverUrl}/web/index.html#!/details?id=${item.Id}` } const element = convertTextToElement(text) element.addEventListener('click', e => { e.stopPropagation() e.preventDefault() GM_openInTab(url, { active: true, insert: true, setParent: true }) }) parent.append(element) } function replaceCodeWithLink(text, reg) { let match = reg.exec(text) if (!match) return null const fragment = document.createDocumentFragment() let lastIndex = 0 while (match) { const textBeforeMatch = text.substring(lastIndex, match.index) if (textBeforeMatch.length > 0) { const textNode = document.createTextNode(textBeforeMatch) fragment.append(textNode) } const code = `${match[1]}-${match[2]}` const link = createLink(match[0], code) fragment.append(link) lastIndex = reg.lastIndex match = reg.exec(text) } const remainingText = text.substring(lastIndex) if (remainingText.length > 0) { const textNode = document.createTextNode(remainingText) fragment.append(textNode) } return fragment } function traverse() { const nodes = getTextNodes() for (const node of nodes) { const text = node.nodeValue.trim() if (!text) continue const fc2Reg = /(fc2)(?:\s*[-_]?ppv)?[-_]?(\d+)/gi const reg1 = /(?<![a-z\d]|btih:)([a-z]{2,5})(?:[-_]|\s*)?((?<=[-_0a-z\s])\d{3,4}|(?<=0{2,})[1-9]\d{3,4})(?!\w*p|\d)/gi const reg2 = /(?<![a-z\d]|btih:)([nk])(\d{3,})/gi const regList = [fc2Reg, reg1, reg2] for (const reg of regList) { const fragment = replaceCodeWithLink(text, reg) if (fragment) { node.replaceWith(fragment) break } } } } function log(...args) { if (!settings.debug) return console.log(...args) } function createConfigModal() { function buildFormItems() { return Object.keys(defalultSettings) .map(key => { return ` <div class='jv-form-item'> <label for='${key}'>${key}: </lable> <input type='text' name='${key}' value='${settings[key]}' /> </div> ` }) .join('\n') } function showModal() { modal.style.display = 'block' } function hideModal() { modal.style.display = 'none' } function resetForm() { settings = getSettings() form.innerHTML = buildFormItems() } function submitForm() { const formData = new FormData(form) const data = {} for (const [key, value] of formData) { data[key] = convertFormValue(value.trim()) } GM_setValue('settings', data) settings = getSettings() log('设置成功') hideModal() } function createModal() { const modal = document.createElement('div') modal.id = 'jv-modal' modal.innerHTML = ` <div class='jv-close-icon'>X</div> <div class='jv-section'> <h2>设置参数</h2> <form id='jv-form'> ${buildFormItems()} </form> <div class='jv-btn-group'> <button id='jv-submit'>确定</button> <button id='jv-reset'>重置</button> </div> </div> ` return modal } function convertFormValue(value) { if (value === 'true') return true if (value === 'false') return false return value } const modal = createModal() const form = modal.querySelector('#jv-form') const submitBtn = modal.querySelector('#jv-submit') const resetBtn = modal.querySelector('#jv-reset') const closeIcon = modal.querySelector('.jv-close-icon') submitBtn.addEventListener('click', submitForm) closeIcon.addEventListener('click', hideModal) resetBtn.addEventListener('click', resetForm) GM_registerMenuCommand('打开设置', showModal) document.body.appendChild(modal) } function start() { log('jellyfin过滤插件已启动...') let isMatchConfig = false createConfigModal() const { hotKeys, triggerOnload } = settings for (const config of CONFIG) { const { site, cb } = config if (site.test(location.href)) { log('网址匹配路径正则,会进行特殊处理') isMatchConfig = true registerKeysEvent('keypress', hotKeys, cb, 500) if (triggerOnload) { cb() } break } } if (!isMatchConfig) { log('网址不匹配路径正则,进行一般化处理') registerKeysEvent('keypress', hotKeys, traverse) if (triggerOnload) { traverse() } } } start() const css = ` .jv-link { display: inline-block; white-space: nowrap; cursor: pointer; } .jv-svg { margin-left: 2px; width: 1em; max-width: 16px; vertical-align: middle; cursor: pointer; } #jv-modal { position: fixed; left: 50%; top: 40%; transform: translate(-50%, -50%); background: #fff; z-index: 1100; overflow: auto; display: none; padding: 0 50px; border: 1px solid black; } .jv-section { width: 500px; margin-bottom: 50px; } .jv-form-item { margin-bottom: 20px; } .jv-form-item input { width: 300px; } .jv-btn-group { margin-top: 20px; } .jv-btn-group button { margin-right: 5px; cursor: pointer; } .jv-close-icon { position: absolute; right: 10px; top: 10px; cursor: pointer; font-size: 20px; font-weight: bold; } ` GM_addStyle(css) })()