// ==UserScript== // @name 迅雷搜索下载字幕 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 下载字幕,依赖于迅雷的公共API // @author qingtian1 // @include * // @icon https://www.google.com/s2/favicons?sz=64&domain=bbs.tampermonkey.net.cn // @require https://code.jquery.com/jquery-3.7.1.min.js // @require https://cdn.jsdelivr.net/npm/jschardet@3.0.0/dist/jschardet.min.js // @grant GM_xmlhttpRequest // @connect api-shoulei-ssl.xunlei.com // @connect subtitle.v.geilijiasu.com // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict' let inputIsFocus = false if (window.self !== window.top) { console.log('当前页面不是主窗口,无法运行脚本') return } const url = window.location.href if (url.startsWith('http://localhost:9528') && !['http://localhost:9528/#/movie/search', 'http://localhost:9528/#/wiki/search'].includes(url)) { return } /** * 将毫秒值转化为总分钟数 * @param {number} milliseconds - 需要转换的毫秒值 * @returns {number} - 转换后的总分钟数 */ function convertMillisecondsToMinutes(milliseconds) { // 检查输入是否为有效数字 if (typeof milliseconds !== 'number' || milliseconds < 0) { return 0 } // 将毫秒转换为分钟 return Math.floor(milliseconds / 1000 / 60) } function loadTable(list) { // 定义表头和数据 const headers = ['文件名', '时长', '操作'] // 创建表格元素 const $table = $('
').attr('border', 1).css({ 'border-collapse': 'collapse', 'width': '30%', 'text-align': 'center', 'background-color': '#fafafa', position: 'fixed', left: '300px', top: '20px', zIndex: 9998 // 保证在最上层 }).attr('id', 'myTableContainer') // 创建表头 const $thead = $('') const $headerRow = $('') headers.forEach(header => { $headerRow.append($('').text(header).css({ 'background-color': '#f2f2f2', 'padding': '8px' })) }) $thead.append($headerRow) $table.append($thead) // 创建表格内容 const $tbody = $('') list.forEach(row => { const $row = $('') $row.append($('').text(`${row.name}`).css({ 'padding': '8px' })) $row.append($('').text(`${convertMillisecondsToMinutes(row.duration)}`).css({ 'padding': '8px' })) const $actionCell = $('').css({ 'padding': '8px' }) // 下载按钮 const $downloadButton = $('').css({ 'margin-right': '5px', // 添加间距 'padding': '5px 10px', 'cursor': 'pointer' }) $downloadButton.on('click', function() { // 使用 GM_xmlhttpRequest 下载文件 GM_xmlhttpRequest({ method: 'GET', url: row.url, responseType: 'blob', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://subtitle.v.geilijiasu.com/' // 模拟来源页面 }, onload: function(response) { if (response.status === 200 && response.response) { // 创建 Blob 对象 const blob = response.response // 创建一个下载链接 const link = document.createElement('a') const url = URL.createObjectURL(blob) // 创建 Blob 的 URL link.href = url link.download = row.name || 'download' // 设置下载文件的名称(可使用 row.name 或其他值) link.style.display = 'none' document.body.appendChild(link) // 将链接添加到页面 link.click() // 模拟点击下载文件 document.body.removeChild(link) // 下载完成后移除链接 URL.revokeObjectURL(url) // 释放 Blob URL } else { console.error('Failed to fetch the file. Status:', response.status) } }, onerror: function(error) { console.error('Failed to download the file:', error) } }) }) // 预览按钮 const $previewButton = $('').css({ 'padding': '5px 10px', 'cursor': 'pointer' }) $previewButton.on('click', async function() { await request2subtitle(row.url) }) // 将按钮添加到操作单元格 $actionCell.append($downloadButton).append($previewButton) // 添加操作单元格到行 $row.append($actionCell) $tbody.append($row) }) $table.append($tbody) // 创建删除按钮 const $deleteButton = $('') $deleteButton.css({ 'position': 'absolute', 'top': '-10px', 'right': '-10px', 'background-color': 'red', 'color': 'white', 'border': 'none', 'border-radius': '50%', 'width': '20px', 'height': '20px', 'text-align': 'center', 'line-height': '18px', 'cursor': 'pointer', 'font-size': '14px', 'box-shadow': '0 2px 5px rgba(0,0,0,0.3)' }) // 按钮的悬停效果 $deleteButton.hover( function() { $(this).css('background-color', 'darkred') // 鼠标移入 }, function() { $(this).css('background-color', 'red') // 鼠标移出 } ) $deleteButton.on('click', function() { $table.remove() // 删除表格 }) // 将按钮添加到表格上 $table.append($deleteButton) // === // 将表格添加到页面容器中 $('#table-container').append($table) // 实现拖动功能 let isDragging = false // 标记是否正在拖动 let startX, startY // 记录鼠标初始位置 let tableStartX, tableStartY // 记录表格的初始位置 $table.on('mousedown', function(event) { isDragging = true // 获取鼠标初始位置 startX = event.clientX startY = event.clientY // 获取表格初始位置 tableStartX = parseInt($table.css('left'), 10) tableStartY = parseInt($table.css('top'), 10) $table.css('cursor', 'grabbing') // 拖动时鼠标样式改变 event.preventDefault() // 防止默认行为(如文本选择) }) $(document).on('mousemove', function(event) { if (isDragging) { // 计算表格的新位置 const newLeft = tableStartX + (event.clientX - startX) const newTop = tableStartY + (event.clientY - startY) // 更新表格位置 $table.css({ left: `${newLeft}px`, top: `${newTop}px` }) } }) $(document).on('mouseup', function() { if (isDragging) { isDragging = false // 停止拖动 $table.css('cursor', 'move') // 恢复鼠标样式 } }) // 将表格添加到页面容器中 $('body').append($table) } async function request2xunlei(keyword) { return new Promise((resolve, reject) => { keyword = keyword.trim() const arr = [] // 使用 GM_xmlhttpRequest 发送跨域请求 GM_xmlhttpRequest({ method: 'GET', url: `https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${keyword}`, // responseType: 'blob', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://api-shoulei-ssl.xunlei.com/' // 模拟来源页面 }, onload: function(response) { // console.log('response', response) // console.log('response.status', response.status) // console.log('response.response', response.response) // console.log('responseText', response.responseText) const res = JSON.parse(response.responseText) // console.log('res', res) if (res.code === 0 && res.data && res.data instanceof Array && res.data.length > 0) { arr.push(...res.data) } resolve(arr) }, onerror: function(error) { resolve(arr) console.error('Failed to fetch image:', error) } }) }) } async function request2subtitle(url) { return new Promise((resolve, reject) => { // console.log('url', url) // 使用 GM_xmlhttpRequest 发送跨域请求 GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://subtitle.v.geilijiasu.com/' // 模拟来源页面 }, onload: function(response) { // console.log('response', response) // console.log('response.status', response.status) // console.log('response.response', response.response) if (response.status === 200 && response.response) { const arrayBuffer = response.response // 获取 ArrayBuffer 数据 // console.log('arrayBuffer', arrayBuffer) // 将文件内容转化为 Base64 编码 const base64String = arrayBufferToBase64(arrayBuffer) // console.log('Base64 String:', base64String) // 解码 Base64 后进行编码检测 const detected = jschardet.detect(atob(base64String.split(';base64,')[1])) // 从 base64 数据中提取并解码 console.log('Detected encoding:', detected) const encoding = detected.encoding.toLowerCase() const blob = new Blob([arrayBuffer]) // 读取文件内容 const reader = new FileReader() reader.onload = function(event) { const textContent = event.target.result // console.log('Text content:', textContent) const oldMyTextContainer = $('#myTextContainer') if (oldMyTextContainer && oldMyTextContainer.length && oldMyTextContainer.length > 0) { // console.log('remove oldMyTextContainer', oldMyTextContainer) oldMyTextContainer.remove() } // 创建文本显示容器 const $textContainer = $('
').css({ position: 'fixed', top: '50%', // 使其垂直居中 left: '50%', // 使其水平居中 width: '80%', // 宽度为页面的 80% height: '80%', // 高度为页面的 80% transform: 'translate(-50%, -50%)', // 使用 translate 来精确居中 background: 'white', border: '1px solid #ccc', borderRadius: '5px', zIndex: 9999, // 保证在最上层 overflow: 'auto', // 如果文本过长,添加滚动条 padding: '10px', boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)' }).attr('id', 'myTextContainer') // 创建关闭按钮 const $closeButton = $('').css({ position: 'absolute', top: '5px', right: '5px', background: 'red', color: 'white', border: 'none', borderRadius: '50%', width: '20px', height: '20px', lineHeight: '18px', textAlign: 'center', cursor: 'pointer', fontSize: '14px', boxShadow: '0 2px 5px rgba(0, 0, 0, 0.3)' }) // 关闭按钮的悬停效果 $closeButton.hover( function() { $(this).css('background-color', 'darkred') // 鼠标移入 }, function() { $(this).css('background-color', 'red') // 鼠标移出 } ) // 按钮点击事件:销毁文本容器 $closeButton.on('click', function() { $textContainer.remove() // 移除文本容器 }) // 创建文本区域 const $textArea = $('').css({ width: '100%', // 占满容器的宽度 height: 'calc(100% - 30px)', // 减去顶部和按钮的高度 border: 'none', resize: 'none', outline: 'none', boxSizing: 'border-box', fontFamily: 'monospace', fontSize: '14px' }).val(textContent) // 设置内容为文件文本 // 将关闭按钮和文本区域添加到文本容器 $textContainer.append($closeButton).append($textArea) // 将文本容器添加到页面 $('body').append($textContainer) } reader.readAsText(blob, encoding) // 使用检测到的编码读取文件 } else { console.error('Failed to fetch the file. Status:', response.status) } resolve() }, onerror: function(error) { resolve() console.error('Failed to fetch image:', error) } }) }) } function createButton() { // 创建右侧窗口元素 const rightSubWindow = $('
') // 设置样式 rightSubWindow.css({ zIndex: 9997, position: 'fixed', width: '230px', // height: 'auto', height: '30px', lineHeight: '30px', left: '60px', // right: '0', // bottom: '350px', top: '60px', textAlign: 'center', background: 'cadetblue' }) // 设置内容 rightSubWindow.html( ` 关键字: ` ) const body = $('body') // 追加到页面 rightSubWindow.appendTo(body) // 给输入框添加样式 const iSubTitleInput = $('.iSubTitleInput') iSubTitleInput.css({ width: '120px', padding: '0' }) // 监听 input 是否聚焦 iSubTitleInput.on('focus', function() { inputIsFocus = true // console.log('Input 聚焦了') }) // 监听 input 失去焦点 iSubTitleInput.on('blur', function() { inputIsFocus = false // console.log('Input 失去焦点') }) // 给按钮添加样式和移入效果 const iSubTitleButton = $('.iSubTitleButton') iSubTitleButton.css({ background: 'rgb(255, 255, 255)', // fontSize: '20px', cursor: 'pointer' }) iSubTitleButton.on('mouseenter', function() { $(this).css({ background: 'red' }) }) iSubTitleButton.on('mouseleave', function() { $(this).css({ background: 'rgb(255, 255, 255)' }) }) $('#searchButton').on('click', async function() { const oldMyTableContainer = $('#myTableContainer') if (oldMyTableContainer && oldMyTableContainer.length && oldMyTableContainer.length > 0) { // console.log('remove oldMyTextContainer', oldMyTableContainer) oldMyTableContainer.remove() } const keyword = $('#keywordInput').val() if (keyword.trim() !== '') { GM_setValue('download_subtitle_default_input', keyword) // console.log('keyword', keyword) const list = await request2xunlei(keyword) console.log('list', list) if (list.length === 0) { console.log('没有找到相关字幕') return } list.sort((pre, next) => { const preExt = pre['ext'] if (isBlank(preExt)) { return 1 } const nextExt = next['ext'] if (isBlank(nextExt)) { return -1 } const extDiff = nextExt.localeCompare(preExt) if (extDiff !== 0) { return extDiff } const preDuration = pre['duration'] if (isBlank(preDuration)) { return 1 } const nextDuration = next['duration'] if (isBlank(nextDuration)) { return -1 } return nextDuration - preDuration }) if (list.length > 0) { // GM_setValue('download_subtitle_default_list', list) loadTable(list) } } }) } function isBlank(value) { if (value === undefined || value === '' || value === null || (value instanceof Array && value.length === 0) || (value instanceof Object && Object.keys(value).length === 0) ) { return true } return false } // 辅助函数:将 ArrayBuffer 转换为 Base64 function arrayBufferToBase64(buffer) { let binary = '' const bytes = new Uint8Array(buffer) const length = bytes.byteLength for (let i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]) } return 'data:application/octet-stream;base64,' + window.btoa(binary) } $(() => { createButton() document.addEventListener('selectionchange', () => { const selectedText = window.getSelection().toString() // 实时获取选中的文本 if (selectedText && selectedText.trim() !== '' && !inputIsFocus) { // console.log('当前选中的文本:', selectedText) $('#keywordInput').val(selectedText) } }) const download_subtitle_default_input = GM_getValue('download_subtitle_default_input', null) if (download_subtitle_default_input) { $('#keywordInput').val(download_subtitle_default_input) } // const download_subtitle_default_list = GM_getValue('download_subtitle_default_list', []) // if (download_subtitle_default_list && download_subtitle_default_list.length > 0) { // console.log('download_subtitle_default_list', download_subtitle_default_list) // loadTable(download_subtitle_default_list) // } }) })()