feat: DSL 磁力聚合 — 搜索/解析/预览/投递 + cilisousuo 源 + 中英双语 + Docker 部署
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { loadSettings, type BtClientConfig } from '../../utils/appconfig'
|
||||
import { ADAPTERS, listClientStatuses } from '../../utils/clients'
|
||||
|
||||
/** 客户端配置 + 实时在线状态 */
|
||||
export default defineEventHandler(async () => {
|
||||
const settings = await loadSettings()
|
||||
const statuses = await listClientStatuses()
|
||||
|
||||
return {
|
||||
adapters: ADAPTERS.map((a) => ({
|
||||
type: a.type,
|
||||
label: a.label,
|
||||
defaultPort: a.defaultPort,
|
||||
defaultPath: a.defaultPath || null
|
||||
})),
|
||||
configs: settings.btClients as BtClientConfig[],
|
||||
statuses,
|
||||
online: statuses.filter((s) => s.online).length,
|
||||
total: statuses.length,
|
||||
settings: {
|
||||
searchProxy: settings.searchProxy,
|
||||
searchTimeout: settings.searchTimeout,
|
||||
metadataTimeout: settings.metadataTimeout,
|
||||
enabledProviders: settings.enabledProviders
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { normalizeClient, saveSettings, type AppSettings, type BtClientConfig } from '../../utils/appconfig'
|
||||
|
||||
/** 保存客户端配置 / 搜索源配置 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event).catch(() => ({}))
|
||||
const patch: Partial<AppSettings> = {}
|
||||
|
||||
if (Array.isArray(body?.btClients)) {
|
||||
patch.btClients = (body.btClients as Partial<BtClientConfig>[]).map((raw, i) => normalizeClient(raw, i))
|
||||
}
|
||||
if (typeof body?.searchProxy === 'string') patch.searchProxy = body.searchProxy.trim()
|
||||
if (body?.searchTimeout != null) patch.searchTimeout = Math.min(Math.max(Number(body.searchTimeout) || 12000, 1000), 60000)
|
||||
if (body?.metadataTimeout != null) patch.metadataTimeout = Math.min(Math.max(Number(body.metadataTimeout) || 30000, 3000), 180000)
|
||||
if (Array.isArray(body?.enabledProviders) && body.enabledProviders.length) {
|
||||
patch.enabledProviders = body.enabledProviders.map((p: unknown) => String(p))
|
||||
}
|
||||
|
||||
if (!Object.keys(patch).length) {
|
||||
throw createError({ statusCode: 400, statusMessage: '没有可保存的字段' })
|
||||
}
|
||||
|
||||
let settings: AppSettings
|
||||
try {
|
||||
settings = await saveSettings(patch)
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
statusCode: 503,
|
||||
statusMessage: `MongoDB 不可用,配置无法保存:${err instanceof Error ? err.message : err}`
|
||||
})
|
||||
}
|
||||
return { ok: true, settings }
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { listAllTasks } from '../../utils/clients'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const groups = await listAllTasks()
|
||||
return {
|
||||
groups,
|
||||
totalCount: groups.reduce((sum, g) => sum + g.tasks.length, 0),
|
||||
onlineCount: groups.filter((g) => g.online).length
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { normalizeClient, type BtClientConfig } from '../../utils/appconfig'
|
||||
import { endpointOf, friendlyClientError, getAdapter } from '../../utils/clients'
|
||||
|
||||
/** 保存前先试连一次,确认地址 / 账号可用 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event).catch(() => ({}))
|
||||
const cfg: BtClientConfig = normalizeClient((body?.client || body) as Partial<BtClientConfig>)
|
||||
const adapter = getAdapter(cfg.type)
|
||||
if (!adapter) throw createError({ statusCode: 400, statusMessage: `不支持的客户端类型:${cfg.type}` })
|
||||
|
||||
try {
|
||||
const result = await adapter.probe(cfg)
|
||||
return { ok: true, online: result.online, version: result.version || null, endpoint: endpointOf(cfg) }
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
online: false,
|
||||
endpoint: endpointOf(cfg),
|
||||
error: friendlyClientError(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { downloadsCol, tryMongo } from '../utils/mongo'
|
||||
|
||||
/** 投递下载记录 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const limit = Math.min(Number(getQuery(event).limit) || 30, 200)
|
||||
|
||||
const data = await tryMongo(async () => {
|
||||
const col = await downloadsCol()
|
||||
const items = await col.find({}).sort({ createdAt: -1 }).limit(limit).toArray()
|
||||
return items.map((it) => ({ ...it, _id: String(it._id) }))
|
||||
})
|
||||
|
||||
return { items: data || [], offline: data === null }
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { mongoStatus } from '../utils/mongo'
|
||||
import { listProviders } from '../utils/search'
|
||||
import { checkWebTorrent } from '../utils/torrent'
|
||||
import { loadSettings } from '../utils/appconfig'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const [mongo, webTorrent, settings] = await Promise.all([mongoStatus(), checkWebTorrent(), loadSettings()])
|
||||
return {
|
||||
ok: true,
|
||||
time: new Date().toISOString(),
|
||||
mongo,
|
||||
webTorrent,
|
||||
providers: listProviders(),
|
||||
enabledProviders: settings.enabledProviders,
|
||||
searchProxy: settings.searchProxy ? '已配置' : '未配置',
|
||||
clients: settings.btClients.map((c) => ({ id: c.id, type: c.type, name: c.name, endpoint: `${c.host}:${c.port}`, enabled: c.enabled !== false }))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { searchesCol, tryMongo } from '../utils/mongo'
|
||||
|
||||
/** 删除单条历史(?id=)或清空全部历史 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = String(getQuery(event).id || '').trim()
|
||||
if (id && !ObjectId.isValid(id)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'id 不是合法的 ObjectId' })
|
||||
}
|
||||
|
||||
const result = await tryMongo(async () => {
|
||||
const col = await searchesCol()
|
||||
if (id) {
|
||||
const res = await col.deleteOne({ _id: new ObjectId(id) })
|
||||
return { deleted: res.deletedCount }
|
||||
}
|
||||
const res = await col.deleteMany({})
|
||||
return { deleted: res.deletedCount }
|
||||
})
|
||||
|
||||
if (!result) throw createError({ statusCode: 503, statusMessage: 'MongoDB 不可用,无法操作历史记录' })
|
||||
return { ok: true, ...result }
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { searchesCol, torrentsCol, tryMongo } from '../utils/mongo'
|
||||
|
||||
/** 搜索历史 + 高频关键词 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const limit = Math.min(Number(getQuery(event).limit) || 50, 200)
|
||||
|
||||
const data = await tryMongo(async () => {
|
||||
const col = await searchesCol()
|
||||
const items = await col.find({}).sort({ createdAt: -1 }).limit(limit).toArray()
|
||||
const hot = await col
|
||||
.aggregate<{ _id: string; count: number; last: Date }>([
|
||||
{ $group: { _id: '$keyword', count: { $sum: 1 }, last: { $max: '$createdAt' } } },
|
||||
{ $sort: { count: -1, last: -1 } },
|
||||
{ $limit: 12 }
|
||||
])
|
||||
.toArray()
|
||||
const total = await col.estimatedDocumentCount()
|
||||
const torrentTotal = await (await torrentsCol()).estimatedDocumentCount()
|
||||
return {
|
||||
items: items.map((it) => ({ ...it, _id: String(it._id) })),
|
||||
hot: hot.map((h) => ({ keyword: h._id, count: h.count, last: h.last })),
|
||||
total,
|
||||
torrentTotal
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
return { items: [], hot: [], total: 0, torrentTotal: 0, offline: true }
|
||||
}
|
||||
return { ...data, offline: false }
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { parseMagnet, parseTorrentBuffer } from '../utils/torrent'
|
||||
import { tryMongo } from '../utils/mongo'
|
||||
import { upsertParsedTorrent } from '../utils/store'
|
||||
|
||||
const MAX_UPLOAD = 4 * 1024 * 1024
|
||||
const MAX_INLINE_STORE = 1024 * 1024
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const contentType = getHeader(event, 'content-type') || ''
|
||||
|
||||
let info
|
||||
let fileBase64: string | null = null
|
||||
let originalName: string | null = null
|
||||
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
const parts = await readMultipartFormData(event)
|
||||
const filePart = parts?.find((p) => p.name === 'file' && p.data?.length)
|
||||
if (!filePart) throw createError({ statusCode: 400, statusMessage: '没有收到种子文件(字段名应为 file)' })
|
||||
if (filePart.data.length > MAX_UPLOAD) {
|
||||
throw createError({ statusCode: 413, statusMessage: `种子文件过大(上限 ${MAX_UPLOAD / 1024 / 1024}MB)` })
|
||||
}
|
||||
try {
|
||||
info = await parseTorrentBuffer(filePart.data)
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 400, statusMessage: err instanceof Error ? err.message : '种子文件解析失败' })
|
||||
}
|
||||
originalName = filePart.filename || null
|
||||
if (filePart.data.length <= MAX_INLINE_STORE) fileBase64 = filePart.data.toString('base64')
|
||||
} else {
|
||||
const body = await readBody(event).catch(() => ({}))
|
||||
const magnet = String(body?.magnet || '').trim()
|
||||
if (!magnet) throw createError({ statusCode: 400, statusMessage: '请上传 .torrent 文件,或提供 magnet 字段' })
|
||||
try {
|
||||
info = await parseMagnet(magnet)
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 400, statusMessage: err instanceof Error ? err.message : '磁力链接解析失败' })
|
||||
}
|
||||
}
|
||||
|
||||
const stored = await tryMongo(async () => {
|
||||
await upsertParsedTorrent(info!, { fileBase64, originalName, source: fileBase64 ? 'upload' : 'magnet' })
|
||||
return true
|
||||
})
|
||||
|
||||
return { torrent: { ...info, hasFile: !!fileBase64 }, stored: stored === true, fromUpload: !!fileBase64 }
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { runSearch } from '../utils/search'
|
||||
import { tryMongo } from '../utils/mongo'
|
||||
import { recordSearch, upsertSearchResults } from '../utils/store'
|
||||
import type { ProviderReport } from '../utils/search/types'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const keyword = String(query.q || '').trim()
|
||||
if (!keyword) throw createError({ statusCode: 400, statusMessage: '缺少查询参数 q' })
|
||||
|
||||
const providers = String(query.providers || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const limit = Number(query.limit) || 0
|
||||
|
||||
const started = Date.now()
|
||||
const { results, reports } = await runSearch(keyword, {
|
||||
providers: providers.length ? providers : undefined,
|
||||
limit: limit || 300
|
||||
})
|
||||
const durationMs = Date.now() - started
|
||||
|
||||
const stored = await tryMongo(async () => {
|
||||
await recordSearch({
|
||||
keyword,
|
||||
providers: reports.filter((r) => r.ok).map((r) => r.id),
|
||||
resultCount: results.length,
|
||||
durationMs,
|
||||
ok: reports.some((r) => r.ok),
|
||||
error: reports.filter((r) => !r.ok).map((r: ProviderReport) => `${r.label}: ${r.error}`).join(' | ') || null
|
||||
})
|
||||
await upsertSearchResults(results)
|
||||
return true
|
||||
})
|
||||
|
||||
return {
|
||||
query: keyword,
|
||||
durationMs,
|
||||
results,
|
||||
reports,
|
||||
stored: stored === true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { tryMongo } from '../utils/mongo'
|
||||
import { getTorrent, recordDownload } from '../utils/store'
|
||||
import { friendlyClientError, sendTask } from '../utils/clients'
|
||||
|
||||
/** 把种子投递到本机 BT 下载工具 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event).catch(() => ({}))
|
||||
const clientId = String(body?.clientId || '').trim()
|
||||
if (!clientId) throw createError({ statusCode: 400, statusMessage: '缺少 clientId' })
|
||||
|
||||
const infoHash = String(body?.infoHash || '').toLowerCase() || null
|
||||
let magnet = String(body?.magnet || '').trim() || undefined
|
||||
let torrentBase64 = String(body?.torrentBase64 || '').trim() || undefined
|
||||
let name = String(body?.name || '').trim()
|
||||
const downloadDir = String(body?.downloadDir || '').trim() || undefined
|
||||
|
||||
if (!magnet && !torrentBase64) {
|
||||
if (!infoHash) {
|
||||
throw createError({ statusCode: 400, statusMessage: '需要提供 infoHash、magnet 或 torrentBase64 之一' })
|
||||
}
|
||||
const doc = await tryMongo(() => getTorrent(infoHash))
|
||||
if (!doc) throw createError({ statusCode: 404, statusMessage: '库中找不到该种子,请把 magnet 一起传上来' })
|
||||
magnet = doc.magnet || undefined
|
||||
torrentBase64 = doc.fileBase64 || undefined
|
||||
name = name || doc.name
|
||||
}
|
||||
|
||||
let result: { ok: boolean; message: string; client: { id: string; type: string; name: string } | null }
|
||||
try {
|
||||
result = await sendTask(clientId, { magnet, torrentBase64, downloadDir, name })
|
||||
} catch (err) {
|
||||
const message = friendlyClientError(err)
|
||||
await tryMongo(() =>
|
||||
recordDownload({
|
||||
infoHash,
|
||||
name: name || magnet?.slice(0, 80) || '未知任务',
|
||||
clientId,
|
||||
clientType: '',
|
||||
clientName: '',
|
||||
downloadDir: downloadDir || null,
|
||||
ok: false,
|
||||
message
|
||||
})
|
||||
)
|
||||
throw createError({ statusCode: 502, statusMessage: message })
|
||||
}
|
||||
|
||||
await tryMongo(() =>
|
||||
recordDownload({
|
||||
infoHash,
|
||||
name: name || magnet?.slice(0, 80) || '未知任务',
|
||||
clientId,
|
||||
clientType: result.client?.type || '',
|
||||
clientName: result.client?.name || '',
|
||||
downloadDir: downloadDir || null,
|
||||
ok: result.ok,
|
||||
message: result.message
|
||||
})
|
||||
)
|
||||
|
||||
return { ok: result.ok, message: result.message, client: result.client }
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import { tryMongo } from '../../utils/mongo'
|
||||
import { getTorrent } from '../../utils/store'
|
||||
import { parseMagnet } from '../../utils/torrent'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const infoHash = String(getRouterParam(event, 'infohash') || '').toLowerCase()
|
||||
if (!/^[a-f0-9]{40}$/.test(infoHash)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'infoHash 格式不正确(应为 40 位 hex)' })
|
||||
}
|
||||
|
||||
const doc = await tryMongo(() => getTorrent(infoHash))
|
||||
if (doc) {
|
||||
const { fileBase64, ...rest } = doc
|
||||
return { torrent: { ...rest, hasFile: !!fileBase64 }, source: 'database' as const }
|
||||
}
|
||||
|
||||
const magnet = String(getQuery(event).magnet || '').trim()
|
||||
if (magnet) {
|
||||
try {
|
||||
return { torrent: await parseMagnet(magnet), source: 'query' as const }
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 400, statusMessage: err instanceof Error ? err.message : '磁力链解析失败' })
|
||||
}
|
||||
}
|
||||
|
||||
throw createError({ statusCode: 404, statusMessage: '该种子不在库中(MongoDB 未连接或未收录),请在链接上带上 magnet 参数' })
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { tryMongo } from '../../../utils/mongo'
|
||||
import { getTorrent } from '../../../utils/store'
|
||||
|
||||
/** 下载用户上传时的原始 .torrent 文件 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const infoHash = String(getRouterParam(event, 'infohash') || '').toLowerCase()
|
||||
if (!/^[a-f0-9]{40}$/.test(infoHash)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'infoHash 格式不正确' })
|
||||
}
|
||||
|
||||
const doc = await tryMongo(() => getTorrent(infoHash))
|
||||
if (!doc?.fileBase64) {
|
||||
throw createError({ statusCode: 404, statusMessage: '没有保存该种子的原始文件(只有手动上传的种子会保存)' })
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(doc.fileBase64, 'base64')
|
||||
setHeader(event, 'content-type', 'application/x-bittorrent')
|
||||
setHeader(event, 'content-length', String(bytes.length))
|
||||
setHeader(
|
||||
event,
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent((doc.name || infoHash).slice(0, 120))}.torrent"`
|
||||
)
|
||||
return bytes
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { loadSettings } from '../../../utils/appconfig'
|
||||
import { withSwarm } from '../../../utils/swarm'
|
||||
|
||||
const IMAGE_TYPES: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
bmp: 'image/bmp',
|
||||
avif: 'image/avif'
|
||||
}
|
||||
|
||||
/** 超过这个大小就不做在线预览了,从 swarm 拉一张大图的代价太高 */
|
||||
const MAX_PREVIEW_BYTES = 12 * 1024 * 1024
|
||||
/** 单张图片的下载时间上限 */
|
||||
const DOWNLOAD_TIMEOUT_MS = 25_000
|
||||
|
||||
function normPath(value: string): string {
|
||||
return value.replace(/\\/g, '/').replace(/^\.\//, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 swarm 里只把某一张图片的字节拉下来用于预览。
|
||||
* 只下载该文件覆盖到的分片,不落库、不长期占用磁盘。
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const infoHash = String(getRouterParam(event, 'infohash') || '').toLowerCase()
|
||||
if (!/^[a-f0-9]{40}$/.test(infoHash)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'infoHash 格式不正确' })
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const magnet = String(query.magnet || '')
|
||||
const filePath = String(query.path || '')
|
||||
if (!/^magnet:\?/i.test(magnet)) {
|
||||
throw createError({ statusCode: 400, statusMessage: '缺少可用的磁力链,无法定位做种节点' })
|
||||
}
|
||||
if (!filePath) {
|
||||
throw createError({ statusCode: 400, statusMessage: '缺少 path 参数' })
|
||||
}
|
||||
|
||||
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
|
||||
const mime = IMAGE_TYPES[ext]
|
||||
if (!mime) {
|
||||
throw createError({ statusCode: 400, statusMessage: '只支持预览图片文件(jpg / png / gif / webp / bmp / avif)' })
|
||||
}
|
||||
|
||||
const settings = await loadSettings()
|
||||
const bytes = await withSwarm(infoHash, magnet, settings.metadataTimeout, async (torrent) => {
|
||||
const target = (torrent.files || []).find(
|
||||
(f: any) => normPath(String(f.path || f.name || '')) === normPath(filePath)
|
||||
)
|
||||
if (!target) {
|
||||
throw createError({ statusCode: 404, statusMessage: '这个种子里没有该文件,文件清单可能已过期' })
|
||||
}
|
||||
if (target.length > MAX_PREVIEW_BYTES) {
|
||||
throw createError({
|
||||
statusCode: 413,
|
||||
statusMessage: `这张图 ${Math.round(target.length / 1024 / 1024)} MB,超过在线预览上限 ${MAX_PREVIEW_BYTES / 1024 / 1024} MB`
|
||||
})
|
||||
}
|
||||
return Promise.race<ArrayBuffer>([
|
||||
target.arrayBuffer(),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error('下载图片超时:暂时没有可用的做种节点,稍后再试')),
|
||||
DOWNLOAD_TIMEOUT_MS
|
||||
)
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
setHeader(event, 'content-type', mime)
|
||||
setHeader(event, 'content-length', String(bytes.byteLength))
|
||||
setHeader(
|
||||
event,
|
||||
'content-disposition',
|
||||
`inline; filename="${encodeURIComponent(filePath.slice(filePath.lastIndexOf('/') + 1))}"`
|
||||
)
|
||||
setHeader(event, 'cache-control', 'private, max-age=3600')
|
||||
return Buffer.from(bytes)
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { loadSettings } from '../../../utils/appconfig'
|
||||
import { tryMongo } from '../../../utils/mongo'
|
||||
import { getTorrent, markResolved } from '../../../utils/store'
|
||||
import { fetchMetadataFromMagnet } from '../../../utils/torrent'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const infoHash = String(getRouterParam(event, 'infohash') || '').toLowerCase()
|
||||
if (!/^[a-f0-9]{40}$/.test(infoHash)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'infoHash 格式不正确(应为 40 位 hex)' })
|
||||
}
|
||||
|
||||
const body = await readBody(event).catch(() => ({}))
|
||||
const doc = await tryMongo(() => getTorrent(infoHash))
|
||||
const magnet = String(body?.magnet || doc?.magnet || '').trim()
|
||||
if (!magnet) {
|
||||
throw createError({ statusCode: 400, statusMessage: '缺少磁力链,无法在线获取文件清单' })
|
||||
}
|
||||
|
||||
const settings = await loadSettings()
|
||||
try {
|
||||
const meta = await fetchMetadataFromMagnet(magnet, settings.metadataTimeout)
|
||||
if (!meta.files.length) throw new Error('已连上节点,但对方没有返回任何文件信息')
|
||||
await tryMongo(() => markResolved(infoHash, { files: meta.files, size: meta.size, name: meta.name || doc?.name }, null))
|
||||
return { infoHash, name: meta.name, files: meta.files, size: meta.size }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '获取种子元数据失败'
|
||||
await tryMongo(() => markResolved(infoHash, null, message))
|
||||
throw createError({ statusCode: 502, statusMessage: message })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { torrentsCol, tryMongo } from '../utils/mongo'
|
||||
|
||||
/** 最近入库 / 已解析的种子列表 */
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const limit = Math.min(Number(query.limit) || 30, 200)
|
||||
const keyword = String(query.q || '').trim()
|
||||
const onlyResolved = String(query.resolved || '') === '1'
|
||||
|
||||
const filter: Record<string, unknown> = {}
|
||||
if (onlyResolved) filter.resolvedAt = { $ne: null }
|
||||
if (keyword) filter.name = { $regex: keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' }
|
||||
|
||||
const data = await tryMongo(async () => {
|
||||
const col = await torrentsCol()
|
||||
const items = await col
|
||||
.find(filter, { projection: { fileBase64: 0 } })
|
||||
.sort({ updatedAt: -1 })
|
||||
.limit(limit)
|
||||
.toArray()
|
||||
return items.map((it) => ({ ...it, _id: String(it._id) }))
|
||||
})
|
||||
|
||||
return { items: data || [], offline: data === null }
|
||||
})
|
||||
Reference in New Issue
Block a user