You've already forked bilibili-subtitle
init
This commit is contained in:
72
src/chrome/background.ts
Normal file
72
src/chrome/background.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {v4} from 'uuid'
|
||||
import {handleTask, initTaskService, tasksMap} from './taskService'
|
||||
|
||||
/**
|
||||
* 消息处理入口
|
||||
* 注意:需要异步sendResponse时返回true
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener((event, sender, sendResponse) => {
|
||||
console.debug('收到请求: ', event)
|
||||
if (event.type === 'p') { // 发出http请求
|
||||
const {url, options} = event
|
||||
// 发出请求
|
||||
fetch('http://localhost:27081/oproxy/p', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
options,
|
||||
}),
|
||||
}).then(async res => await res.json()).then(sendResponse).catch(console.error)
|
||||
return true
|
||||
} else if (event.type === 'syncGet') { // sync.get
|
||||
chrome.storage.sync.get(event.keys, data => {
|
||||
sendResponse(data)
|
||||
})
|
||||
return true
|
||||
} else if (event.type === 'syncSet') { // sync.set
|
||||
chrome.storage.sync.set(event.items).catch(console.error)
|
||||
} else if (event.type === 'syncRemove') { // sync.remove
|
||||
chrome.storage.sync.remove(event.keys).catch(console.error)
|
||||
} else if (event.type === 'addTask') {
|
||||
// 新建任务
|
||||
const task: Task = {
|
||||
id: v4(),
|
||||
startTime: Date.now(),
|
||||
status: 'pending',
|
||||
def: event.taskDef,
|
||||
}
|
||||
tasksMap.set(task.id, task)
|
||||
|
||||
// 立即触发任务
|
||||
handleTask(task).catch(console.error)
|
||||
|
||||
// 返回任务信息
|
||||
sendResponse(task)
|
||||
} else if (event.type === 'getTask') {
|
||||
// 返回任务信息
|
||||
const taskId = event.taskId
|
||||
const task = tasksMap.get(taskId)
|
||||
if (task == null) {
|
||||
sendResponse({
|
||||
code: 'not_found',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检测删除缓存
|
||||
if (task.status === 'done') {
|
||||
tasksMap.delete(taskId)
|
||||
}
|
||||
|
||||
// 返回任务
|
||||
sendResponse({
|
||||
code: 'ok',
|
||||
task,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
initTaskService()
|
238
src/chrome/content-script.cjs
Normal file
238
src/chrome/content-script.cjs
Normal file
@@ -0,0 +1,238 @@
|
||||
const {TOTAL_HEIGHT_DEF, HEADER_HEIGHT, TOTAL_HEIGHT_MIN, TOTAL_HEIGHT_MAX} = require("../const");
|
||||
var totalHeight = TOTAL_HEIGHT_DEF
|
||||
|
||||
const getVideoElement = () => {
|
||||
const videoWrapper = document.getElementById('bilibili-player')
|
||||
return videoWrapper.querySelector('video')
|
||||
}
|
||||
|
||||
var danmukuBoxLoaded = false
|
||||
setInterval(function () {
|
||||
if (danmukuBoxLoaded) return
|
||||
|
||||
var danmukuBox = document.getElementById('danmukuBox')
|
||||
if (danmukuBox) {
|
||||
danmukuBoxLoaded = true
|
||||
setTimeout(function () {
|
||||
var vKey = ''
|
||||
for (const key in danmukuBox?.dataset) {
|
||||
if (key.startsWith('v-')) {
|
||||
vKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.id = 'bilibili-subtitle-iframe'
|
||||
iframe.src = chrome.runtime.getURL('index.html')
|
||||
iframe.style = 'border: none; width: 100%; height: 44px;'
|
||||
iframe.allow = 'clipboard-read; clipboard-write;'
|
||||
if (vKey) {
|
||||
iframe.dataset[vKey] = danmukuBox?.dataset[vKey]
|
||||
}
|
||||
//insert before first child
|
||||
danmukuBox?.insertBefore(iframe, danmukuBox?.firstChild)
|
||||
}, 1500)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
let aid = 0
|
||||
let title = ''
|
||||
let pages = []
|
||||
let pagesMap = {}
|
||||
|
||||
let lastAidOrBvid = null
|
||||
const refreshVideoInfo = async () => {
|
||||
const iframe = document.getElementById('bilibili-subtitle-iframe')
|
||||
if (!iframe) return
|
||||
|
||||
let path = location.pathname
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(0, -1)
|
||||
}
|
||||
const paths = path.split('/')
|
||||
const aidOrBvid = paths[paths.length - 1]
|
||||
|
||||
if (aidOrBvid !== lastAidOrBvid) {
|
||||
// console.debug('refreshVideoInfo')
|
||||
|
||||
lastAidOrBvid = aidOrBvid
|
||||
if (aidOrBvid) {
|
||||
//aid,pages
|
||||
let cid
|
||||
let subtitles
|
||||
if (aidOrBvid.toLowerCase().startsWith('av')) {//avxxx
|
||||
title = ''
|
||||
aid = aidOrBvid.slice(2)
|
||||
cid = 1
|
||||
pages = await fetch(`https://api.bilibili.com/x/player/pagelist?aid=${aid}`, {credentials: 'include'}).then(res => res.json()).then(res => res.data)
|
||||
subtitles = await fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, {credentials: 'include'}).then(res => res.json()).then(res => res.data.subtitle.subtitles)
|
||||
} else {//bvxxx
|
||||
pages = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${aidOrBvid}`, {credentials: 'include'}).then(res => res.json()).then(res => {
|
||||
title = res.data.title
|
||||
aid = res.data.aid
|
||||
cid = res.data.cid
|
||||
subtitles = res.data.subtitle.list
|
||||
return res.data.pages
|
||||
})
|
||||
}
|
||||
|
||||
//pagesMap
|
||||
pagesMap = {}
|
||||
pages.forEach(page => {
|
||||
pagesMap[page.page + ''] = page
|
||||
})
|
||||
|
||||
console.debug('refreshVideoInfo: ', aid, cid, pages, subtitles)
|
||||
|
||||
//send setVideoInfo
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'setVideoInfo',
|
||||
title,
|
||||
aid,
|
||||
pages,
|
||||
infos: subtitles,
|
||||
}, '*')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastAid = null
|
||||
let lastCid = null
|
||||
const refreshSubtitles = () => {
|
||||
const iframe = document.getElementById('bilibili-subtitle-iframe')
|
||||
if (!iframe) return
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const p = urlSearchParams.get('p') || 1
|
||||
const page = pagesMap[p]
|
||||
if (!page) return
|
||||
const cid = page.cid
|
||||
|
||||
if (aid !== lastAid || cid !== lastCid) {
|
||||
console.debug('refreshSubtitles', aid, cid)
|
||||
|
||||
lastAid = aid
|
||||
lastCid = cid
|
||||
if (aid && cid) {
|
||||
fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
// console.log('refreshSubtitles: ', aid, cid, res)
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'setInfos',
|
||||
infos: res.data.subtitle.subtitles
|
||||
}, '*')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息
|
||||
window.addEventListener("message", (event) => {
|
||||
const {data} = event
|
||||
|
||||
if (data.type === 'fold') {
|
||||
const iframe = document.getElementById('bilibili-subtitle-iframe')
|
||||
iframe.style.height = (data.fold ? HEADER_HEIGHT : totalHeight) + 'px'
|
||||
}
|
||||
|
||||
if (data.type === 'move') {
|
||||
const video = getVideoElement()
|
||||
if (video) {
|
||||
video.currentTime = data.time
|
||||
}
|
||||
}
|
||||
|
||||
//刷新视频信息
|
||||
if (data.type === 'refreshVideoInfo') {
|
||||
refreshVideoInfo().catch(console.error)
|
||||
}
|
||||
//刷新字幕
|
||||
if (data.type === 'refreshSubtitles') {
|
||||
refreshSubtitles()
|
||||
}
|
||||
if (data.type === 'getSubtitle') {
|
||||
let url = data.info.subtitle_url
|
||||
if (url.startsWith('http://')) {
|
||||
url = url.replace('http://', 'https://')
|
||||
}
|
||||
fetch(url).then(res => res.json()).then(res => {
|
||||
event.source.postMessage({
|
||||
data: {
|
||||
info: data.info,
|
||||
data: res,
|
||||
}, type: 'setSubtitle'
|
||||
}, '*')
|
||||
})
|
||||
}
|
||||
|
||||
if (data.type === 'getCurrentTime') {
|
||||
const video = getVideoElement()
|
||||
if (video) {
|
||||
event.source.postMessage({
|
||||
data: {
|
||||
currentTime: video.currentTime
|
||||
}, type: 'setCurrentTime'
|
||||
}, '*')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'getSettings') {
|
||||
const videoElement = getVideoElement()
|
||||
totalHeight = videoElement ? Math.min(Math.max(videoElement.offsetHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX) : TOTAL_HEIGHT_DEF
|
||||
event.source.postMessage({
|
||||
data: {
|
||||
noVideo: !videoElement,
|
||||
totalHeight,
|
||||
}, type: 'setSettings'
|
||||
}, '*')
|
||||
}
|
||||
|
||||
if (data.type === 'downloadAudio') {
|
||||
const html = document.getElementsByTagName('html')[0].innerHTML
|
||||
const playInfo = JSON.parse(html.match(/window.__playinfo__=(.+?)<\/script/)[1])
|
||||
const audioUrl = playInfo.data.dash.audio[0].baseUrl
|
||||
|
||||
fetch(audioUrl).then(res => res.blob()).then(blob => {
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `${title}.m4s`
|
||||
a.click()
|
||||
})
|
||||
}
|
||||
|
||||
if (data.type === 'updateTransResult') {
|
||||
const trans = data.result??''
|
||||
let text = document.getElementById('trans-result-text')
|
||||
if (text) {
|
||||
text.innerHTML = trans
|
||||
} else {
|
||||
const container = document.getElementsByClassName('bpx-player-subtitle-panel-wrap')?.[0]
|
||||
if (container) {
|
||||
const div = document.createElement('div')
|
||||
div.style.display = 'flex'
|
||||
div.style.justifyContent = 'center'
|
||||
div.style.margin = '2px'
|
||||
text = document.createElement('text')
|
||||
text.id = 'trans-result-text'
|
||||
text.innerHTML = trans
|
||||
text.style.fontSize = '1rem'
|
||||
text.style.padding = '5px'
|
||||
text.style.color = 'white'
|
||||
text.style.background = 'rgba(0, 0, 0, 0.4)'
|
||||
div.append(text)
|
||||
|
||||
container.append(div)
|
||||
}
|
||||
}
|
||||
text.style.display = trans ? 'block' : 'none'
|
||||
}
|
||||
}, false);
|
||||
|
||||
setInterval(() => {
|
||||
refreshVideoInfo().catch(console.error)
|
||||
refreshSubtitles()
|
||||
}, 1000)
|
20
src/chrome/openaiService.ts
Normal file
20
src/chrome/openaiService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {getServerUrl} from '../util/biz_util'
|
||||
|
||||
export const handleChatCompleteTask = async (task: Task) => {
|
||||
const data = task.def.data
|
||||
const serverUrl = getServerUrl(task.def.serverUrl)
|
||||
const resp = await fetch(`${serverUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + task.def.extra.apiKey,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
task.resp = await resp.json()
|
||||
if (task.resp.usage) {
|
||||
return (task.resp.usage.total_tokens??0) > 0
|
||||
} else {
|
||||
throw new Error(`${task.resp.error.code as string??''} ${task.resp.error.message as string ??''}`)
|
||||
}
|
||||
}
|
51
src/chrome/taskService.ts
Normal file
51
src/chrome/taskService.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {TASK_EXPIRE_TIME} from '../const'
|
||||
import {handleChatCompleteTask} from './openaiService'
|
||||
|
||||
export const tasksMap = new Map<string, Task>()
|
||||
|
||||
export const handleTask = async (task: Task) => {
|
||||
console.debug(`处理任务: ${task.id} (type: ${task.def.type})`)
|
||||
try {
|
||||
task.status = 'running'
|
||||
switch (task.def.type) {
|
||||
case 'chatComplete':
|
||||
await handleChatCompleteTask(task)
|
||||
break
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`任务类型不支持: ${task.def.type}`)
|
||||
}
|
||||
|
||||
console.debug(`处理任务成功: ${task.id} (type: ${task.def.type})`)
|
||||
} catch (e: any) {
|
||||
task.error = e.message
|
||||
console.debug(`处理任务失败: ${task.id} (type: ${task.def.type})`, e.message)
|
||||
}
|
||||
task.status = 'done'
|
||||
task.endTime = Date.now()
|
||||
}
|
||||
|
||||
export const initTaskService = () => {
|
||||
// 处理任务: tasksMap
|
||||
setInterval(() => {
|
||||
for (const [_, task] of tasksMap) {
|
||||
if (task.status === 'pending') {
|
||||
handleTask(task).catch(console.error)
|
||||
break
|
||||
} else if (task.status === 'running') {
|
||||
break
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
// 检测清理tasksMap
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
|
||||
for (const [taskId, task] of tasksMap) {
|
||||
if (task.startTime < now - TASK_EXPIRE_TIME) {
|
||||
tasksMap.delete(taskId)
|
||||
console.debug(`清理任务: ${task.id} (type: ${task.def.type})`)
|
||||
}
|
||||
}
|
||||
}, 10000)
|
||||
}
|
Reference in New Issue
Block a user