// ==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)
// }
})
})()