fix: 移除冲突的 node-polyfills(改用 CDN 加载 WebTorrent)+ 加 P2P 进度翻译与顶栏活动指示灯
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TorrentFileEntry } from '~/types/api'
|
import type { TorrentFileEntry } from '~/types/api'
|
||||||
import { formatBytes } from '~/utils/format'
|
import { formatBytes } from '~/utils/format'
|
||||||
|
import { translateText } from '~/utils/i18n'
|
||||||
import { readFileBytesClient, type FileProgress } from '~/utils/clientTorrent'
|
import { readFileBytesClient, type FileProgress } from '~/utils/clientTorrent'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -62,7 +63,9 @@ async function load() {
|
|||||||
timeoutMs: 25000,
|
timeoutMs: 25000,
|
||||||
onProgress: (p: FileProgress) => {
|
onProgress: (p: FileProgress) => {
|
||||||
progress.value = p.progress
|
progress.value = p.progress
|
||||||
progressText.value = p.message
|
progressText.value = p.peers > 0
|
||||||
|
? `${translateText('下载中')} ${Math.round(p.progress * 100)}% · ${p.peers} ${translateText('个节点')}`
|
||||||
|
: translateText('正在连接 P2P 节点…')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const mime = MIME[extOf(current.value?.path || '')] || 'application/octet-stream'
|
const mime = MIME[extOf(current.value?.path || '')] || 'application/octet-stream'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TorrentFileEntry, TorrentView } from '~/types/api'
|
import type { TorrentFileEntry, TorrentView } from '~/types/api'
|
||||||
|
import { translateText } from '~/utils/i18n'
|
||||||
import { resolveMetadataClient, type ResolveProgress } from '~/utils/clientTorrent'
|
import { resolveMetadataClient, type ResolveProgress } from '~/utils/clientTorrent'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -60,6 +61,16 @@ function applyResolved(res: { files: TorrentFileEntry[]; size: number; name: str
|
|||||||
emit('resolved', res)
|
emit('resolved', res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStatusText(p: ResolveProgress): string {
|
||||||
|
if (p.phase === 'connecting') {
|
||||||
|
return p.peers > 0
|
||||||
|
? `${translateText('已连接')} ${p.peers} ${translateText('个节点,正在获取元数据…')}`
|
||||||
|
: translateText('正在通过 WebRTC 寻找 P2P 节点…')
|
||||||
|
}
|
||||||
|
if (p.phase === 'metadata') return translateText('已拿到元数据,正在整理文件清单…')
|
||||||
|
return `${translateText('共')} ${p.peers} ${translateText('个节点')}`
|
||||||
|
}
|
||||||
|
|
||||||
async function doResolve() {
|
async function doResolve() {
|
||||||
resolving.value = true
|
resolving.value = true
|
||||||
resolveStatus.value = ''
|
resolveStatus.value = ''
|
||||||
@@ -77,7 +88,7 @@ async function doResolve() {
|
|||||||
const meta = await resolveMetadataClient(local.value.magnet, {
|
const meta = await resolveMetadataClient(local.value.magnet, {
|
||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
onProgress: (p: ResolveProgress) => {
|
onProgress: (p: ResolveProgress) => {
|
||||||
resolveStatus.value = p.message
|
resolveStatus.value = resolveStatusText(p)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
applyResolved(meta)
|
applyResolved(meta)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { locale, setLocale, type Locale } from '~/utils/i18n'
|
import { locale, setLocale, translateText, type Locale } from '~/utils/i18n'
|
||||||
|
import { p2pActive } from '~/utils/clientTorrent'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const health = useHealth()
|
const health = useHealth()
|
||||||
@@ -50,6 +51,10 @@ async function logout() {
|
|||||||
<i class="dot" />
|
<i class="dot" />
|
||||||
{{ health?.webTorrent ? '磁力解析就绪' : '磁力解析(缓存模式)' }}
|
{{ health?.webTorrent ? '磁力解析就绪' : '磁力解析(缓存模式)' }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="pill hide-sm" :class="p2pActive ? 'on' : 'dim'">
|
||||||
|
<i class="dot" />
|
||||||
|
{{ p2pActive ? translateText('P2P 活动中') : translateText('P2P 空闲') }}
|
||||||
|
</span>
|
||||||
<NuxtLink to="/settings" class="pill" :class="mongoPill.cls">
|
<NuxtLink to="/settings" class="pill" :class="mongoPill.cls">
|
||||||
<i class="dot" />
|
<i class="dot" />
|
||||||
{{ mongoPill.text }}
|
{{ mongoPill.text }}
|
||||||
|
|||||||
@@ -28,15 +28,61 @@ function withWsTrackers(magnet: string): string {
|
|||||||
return magnet + sep + add.map((t) => `tr=${encodeURIComponent(t)}`).join('&')
|
return magnet + sep + add.map((t) => `tr=${encodeURIComponent(t)}`).join('&')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 浏览器端 WebTorrent 以 CDN 的 UMD 包形式按需加载(避免把 webtorrent 打进 Vite 客户端,
|
||||||
|
// 同时绕开 Vite 8 与 node-polyfills 的 peer 依赖冲突)。优先 jsDelivr,失败再试 unpkg。
|
||||||
|
const WT_CDN_URLS = [
|
||||||
|
'https://cdn.jsdelivr.net/npm/webtorrent@3.0.21/dist/webtorrent.min.js',
|
||||||
|
'https://unpkg.com/webtorrent@3.0.21/dist/webtorrent.min.js'
|
||||||
|
]
|
||||||
|
|
||||||
let wtPromise: Promise<any> | null = null
|
let wtPromise: Promise<any> | null = null
|
||||||
function loadWebTorrent(): Promise<any> {
|
function loadWebTorrent(): Promise<any> {
|
||||||
if (!wtPromise) {
|
if (!wtPromise) {
|
||||||
// 仅在浏览器运行时按需加载,Vite 会将其单独拆包;服务端不会执行到此处
|
wtPromise = new Promise((resolve, reject) => {
|
||||||
wtPromise = import('webtorrent').then((m) => (m as any).default || m)
|
if (typeof window === 'undefined') {
|
||||||
|
reject(new Error('WebTorrent 只能在浏览器中运行'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const existing = (window as any).WebTorrent
|
||||||
|
if (existing) {
|
||||||
|
resolve(existing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tryLoad = (i: number) => {
|
||||||
|
if (i >= WT_CDN_URLS.length) {
|
||||||
|
reject(new Error('无法从 CDN 加载 WebTorrent(可能被网络或 CSP 拦截)'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = WT_CDN_URLS[i]
|
||||||
|
script.async = true
|
||||||
|
script.onload = () => {
|
||||||
|
const WT = (window as any).WebTorrent
|
||||||
|
if (WT) resolve(WT)
|
||||||
|
else tryLoad(i + 1)
|
||||||
|
}
|
||||||
|
script.onerror = () => tryLoad(i + 1)
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
|
tryLoad(0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return wtPromise
|
return wtPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当前活跃的浏览器 P2P 会话数,用于顶栏指示
|
||||||
|
import { ref } from 'vue'
|
||||||
|
export const p2pActive = ref(false)
|
||||||
|
let activeSessions = 0
|
||||||
|
function enterSession() {
|
||||||
|
activeSessions++
|
||||||
|
p2pActive.value = activeSessions > 0
|
||||||
|
}
|
||||||
|
function leaveSession() {
|
||||||
|
activeSessions = Math.max(0, activeSessions - 1)
|
||||||
|
p2pActive.value = activeSessions > 0
|
||||||
|
}
|
||||||
|
|
||||||
function waitForMetadata(torrent: any, timeoutMs: number): Promise<void> {
|
function waitForMetadata(torrent: any, timeoutMs: number): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (torrent.metadata || torrent.ready) return resolve()
|
if (torrent.metadata || torrent.ready) return resolve()
|
||||||
@@ -79,6 +125,7 @@ export async function resolveMetadataClient(
|
|||||||
const timeoutMs = opts.timeoutMs ?? 30000
|
const timeoutMs = opts.timeoutMs ?? 30000
|
||||||
const WebTorrent = await loadWebTorrent()
|
const WebTorrent = await loadWebTorrent()
|
||||||
const client = new WebTorrent()
|
const client = new WebTorrent()
|
||||||
|
enterSession()
|
||||||
try {
|
try {
|
||||||
const torrent = client.add(withWsTrackers(magnet))
|
const torrent = client.add(withWsTrackers(magnet))
|
||||||
const report = (phase: ResolveProgress['phase'], message?: string) => {
|
const report = (phase: ResolveProgress['phase'], message?: string) => {
|
||||||
@@ -103,6 +150,7 @@ export async function resolveMetadataClient(
|
|||||||
onProgress?.({ phase: 'done', peers: torrent.numPeers ?? 0, message: `共 ${files.length} 个文件` })
|
onProgress?.({ phase: 'done', peers: torrent.numPeers ?? 0, message: `共 ${files.length} 个文件` })
|
||||||
return { name: String(torrent.name || ''), files, size: files.reduce((s, f) => s + f.length, 0) }
|
return { name: String(torrent.name || ''), files, size: files.reduce((s, f) => s + f.length, 0) }
|
||||||
} finally {
|
} finally {
|
||||||
|
leaveSession()
|
||||||
try {
|
try {
|
||||||
client.destroy()
|
client.destroy()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -120,6 +168,7 @@ export async function readFileBytesClient(
|
|||||||
const timeoutMs = opts.timeoutMs ?? 25000
|
const timeoutMs = opts.timeoutMs ?? 25000
|
||||||
const WebTorrent = await loadWebTorrent()
|
const WebTorrent = await loadWebTorrent()
|
||||||
const client = new WebTorrent()
|
const client = new WebTorrent()
|
||||||
|
enterSession()
|
||||||
try {
|
try {
|
||||||
const torrent = client.add(withWsTrackers(magnet))
|
const torrent = client.add(withWsTrackers(magnet))
|
||||||
await waitForMetadata(torrent, timeoutMs)
|
await waitForMetadata(torrent, timeoutMs)
|
||||||
@@ -153,6 +202,7 @@ export async function readFileBytesClient(
|
|||||||
})
|
})
|
||||||
return ab
|
return ab
|
||||||
} finally {
|
} finally {
|
||||||
|
leaveSession()
|
||||||
try {
|
try {
|
||||||
client.destroy()
|
client.destroy()
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ export const zh2en: Record<string, string> = {
|
|||||||
'MONGO 未连接': 'MONGO disconnected',
|
'MONGO 未连接': 'MONGO disconnected',
|
||||||
'磁力解析就绪': 'Magnet parsing ready',
|
'磁力解析就绪': 'Magnet parsing ready',
|
||||||
'磁力解析(缓存模式)': 'Magnet parsing (cache mode)',
|
'磁力解析(缓存模式)': 'Magnet parsing (cache mode)',
|
||||||
|
|
||||||
|
// ===== 浏览器端 P2P 进度文案 =====
|
||||||
|
'P2P 活动中': 'P2P active',
|
||||||
|
'P2P 空闲': 'P2P idle',
|
||||||
|
'已连接': 'Connected',
|
||||||
|
'个节点,正在获取元数据…': ' peers, fetching metadata…',
|
||||||
|
'正在通过 WebRTC 寻找 P2P 节点…': 'Looking for P2P nodes via WebRTC…',
|
||||||
|
'已拿到元数据,正在整理文件清单…': 'Metadata received, listing files…',
|
||||||
|
'共': 'Total',
|
||||||
|
'个节点': ' peers',
|
||||||
|
'个文件': ' files',
|
||||||
|
'下载中': 'Downloading',
|
||||||
|
'正在连接 P2P 节点…': 'Connecting to P2P nodes…',
|
||||||
|
'正在通过 WebRTC 从做种节点拉取图片…': 'Fetching image from seeders via WebRTC…',
|
||||||
|
'完成': 'Done',
|
||||||
'本地自托管 · 单进程 Nuxt 单体应用': 'Self-hosted · single-process Nuxt app',
|
'本地自托管 · 单进程 Nuxt 单体应用': 'Self-hosted · single-process Nuxt app',
|
||||||
'种子只做解析与预览,文件内容由你自己的下载器拉取': 'Torrents are only parsed and previewed; file contents are fetched by your own download client',
|
'种子只做解析与预览,文件内容由你自己的下载器拉取': 'Torrents are only parsed and previewed; file contents are fetched by your own download client',
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import nodePolyfills from 'vite-plugin-node-polyfills'
|
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
@@ -29,12 +27,6 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [
|
|
||||||
nodePolyfills({
|
|
||||||
globals: { Buffer: true, global: true, process: true },
|
|
||||||
protocolImports: true
|
|
||||||
})
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
fs: { strict: false }
|
fs: { strict: false }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.13.0",
|
"@types/node": "^22.13.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3"
|
||||||
"vite-plugin-node-polyfills": "^0.23.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user