feat: 文件清单与图片预览的 P2P 部分挪到浏览器端(WebRTC),服务端仅缓存兜底
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { TorrentFileEntry } from '~/types/api'
|
||||
import { readFileBytesClient } from '~/utils/clientTorrent'
|
||||
|
||||
const props = defineProps<{
|
||||
infoHash: string
|
||||
@@ -16,6 +17,20 @@ const errorText = ref('')
|
||||
const src = ref('')
|
||||
let objectUrl = ''
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
bmp: 'image/bmp',
|
||||
avif: 'image/avif'
|
||||
}
|
||||
function extOf(p: string): string {
|
||||
const i = p.lastIndexOf('.')
|
||||
return i >= 0 ? p.slice(i + 1).toLowerCase() : ''
|
||||
}
|
||||
|
||||
const current = computed<TorrentFileEntry | undefined>(() => props.images[props.index])
|
||||
const requestUrl = computed(
|
||||
() =>
|
||||
@@ -37,25 +52,34 @@ async function load() {
|
||||
errorText.value = ''
|
||||
status.value = 'loading'
|
||||
try {
|
||||
const res = await fetch(requestUrl.value)
|
||||
if (!res.ok) {
|
||||
// 服务端用 statusMessage 带上可读原因:没有做种节点 / 图片过大 / 文件不存在
|
||||
const text = await res.text()
|
||||
let message = text
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
message = json.statusMessage || json.message || text
|
||||
} catch {
|
||||
/* 不是 JSON 就用原文 */
|
||||
}
|
||||
throw new Error(message || `请求失败(HTTP ${res.status})`)
|
||||
}
|
||||
objectUrl = URL.createObjectURL(await res.blob())
|
||||
// 优先在用户浏览器里用 WebRTC 从 P2P 网络直接拉取图片字节(不经过 Vercel/Serverless)
|
||||
const bytes = await readFileBytesClient(props.magnet, current.value?.path || '', 25000)
|
||||
const mime = MIME[extOf(current.value?.path || '')] || 'application/octet-stream'
|
||||
objectUrl = URL.createObjectURL(new Blob([bytes], { type: mime }))
|
||||
src.value = objectUrl
|
||||
status.value = 'ok'
|
||||
} catch (err) {
|
||||
errorText.value = errorMessage(err)
|
||||
status.value = 'err'
|
||||
} catch {
|
||||
// 回退到服务端预览(自托管环境可用;Serverless 上也会失败,此时给出可读错误)
|
||||
try {
|
||||
const res = await fetch(requestUrl.value)
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
let message = text
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
message = json.statusMessage || json.message || text
|
||||
} catch {
|
||||
/* 不是 JSON 就用原文 */
|
||||
}
|
||||
throw new Error(message || `请求失败(HTTP ${res.status})`)
|
||||
}
|
||||
objectUrl = URL.createObjectURL(await res.blob())
|
||||
src.value = objectUrl
|
||||
status.value = 'ok'
|
||||
} catch (err) {
|
||||
errorText.value = errorMessage(err)
|
||||
status.value = 'err'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { TorrentFileEntry, TorrentView } from '~/types/api'
|
||||
import { resolveMetadataClient } from '~/utils/clientTorrent'
|
||||
|
||||
const props = defineProps<{
|
||||
torrent: TorrentView
|
||||
@@ -49,24 +50,36 @@ const sourceIcon: Record<string, string> = {
|
||||
database: '数据库'
|
||||
}
|
||||
|
||||
function applyResolved(res: { files: TorrentFileEntry[]; size: number; name: string }) {
|
||||
local.value.files = res.files
|
||||
local.value.size = res.size
|
||||
local.value.fileCount = res.files.length
|
||||
local.value.resolveError = null
|
||||
if (res.name) local.value.name = res.name
|
||||
emit('resolved', res)
|
||||
}
|
||||
|
||||
async function doResolve() {
|
||||
resolving.value = true
|
||||
try {
|
||||
// 服务端先尝试(缓存服务取清单,自托管还会走 P2P),成功即返回并落库
|
||||
const res = await $fetch<{ files: TorrentFileEntry[]; size: number; name: string }>(
|
||||
`/api/torrent/${local.value.infoHash}/resolve`,
|
||||
{ method: 'POST', body: { magnet: local.value.magnet } }
|
||||
)
|
||||
local.value.files = res.files
|
||||
local.value.size = res.size
|
||||
local.value.fileCount = res.files.length
|
||||
local.value.resolveError = null
|
||||
if (res.name) local.value.name = res.name
|
||||
emit('resolved', res)
|
||||
applyResolved(res)
|
||||
pushToast('ok', '文件清单已解析', `共 ${res.files.length} 个文件 / ${formatBytes(res.size)}`)
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err)
|
||||
local.value.resolveError = msg
|
||||
pushToast('err', '解析失败', msg)
|
||||
} catch {
|
||||
// 服务端无法走 P2P(如 Vercel/Serverless)→ 回退到浏览器端 WebRTC 直接解析
|
||||
try {
|
||||
const meta = await resolveMetadataClient(local.value.magnet, 30000)
|
||||
applyResolved(meta)
|
||||
pushToast('ok', '通过浏览器 P2P 解析文件清单', `共 ${meta.files.length} 个文件 / ${formatBytes(meta.size)}`)
|
||||
} catch (err2) {
|
||||
const msg = errorMessage(err2)
|
||||
local.value.resolveError = msg
|
||||
pushToast('err', '解析失败', msg)
|
||||
}
|
||||
} finally {
|
||||
resolving.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user