feat: DSL 磁力聚合 — 搜索/解析/预览/投递 + cilisousuo 源 + 中英双语 + Docker 部署

This commit is contained in:
ice-a
2026-10-16 18:53:45 +08:00
commit d6999104bf
64 changed files with 16524 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import { getDb } from './mongo'
export type BtClientType = 'qbittorrent' | 'transmission' | 'aria2'
export interface BtClientConfig {
id: string
type: BtClientType
name: string
host: string
port: number
/** 自定义 RPC / API 路径,留空使用各客户端的默认路径 */
path?: string
username?: string
password?: string
downloadDir?: string
useHttps?: boolean
enabled?: boolean
}
export interface AppSettings {
/** 访问搜索源时使用的 HTTP 代理 */
searchProxy: string
searchTimeout: number
metadataTimeout: number
enabledProviders: string[]
btClients: BtClientConfig[]
}
/** 本机常见 BT 客户端的默认端点,用户没配置时自动探测这些 */
export const DEFAULT_CLIENTS: BtClientConfig[] = [
{ id: 'qbittorrent-local', type: 'qbittorrent', name: 'qBittorrent(本机)', host: '127.0.0.1', port: 8080, username: 'admin', password: '', enabled: true },
{ id: 'transmission-local', type: 'transmission', name: 'Transmission(本机)', host: '127.0.0.1', port: 9091, path: '/transmission/rpc', username: '', password: '', enabled: true },
{ id: 'aria2-local', type: 'aria2', name: 'aria2(本机)', host: '127.0.0.1', port: 6800, path: '/jsonrpc', password: '', enabled: true }
]
const SETTINGS_ID = 'app'
function envDefaults(): AppSettings {
return {
searchProxy: process.env.SEARCH_PROXY || '',
searchTimeout: Number(process.env.SEARCH_TIMEOUT || 12000),
metadataTimeout: Number(process.env.METADATA_TIMEOUT || 30000),
enabledProviders: ['apibay', 'nyaa', 'cilisousuo'],
btClients: DEFAULT_CLIENTS.map((c) => ({ ...c }))
}
}
let cache: { value: AppSettings; at: number; ttl: number } | null = null
export function invalidateSettingsCache() {
cache = null
}
export async function loadSettings(): Promise<AppSettings> {
if (cache && Date.now() - cache.at < cache.ttl) return cache.value
const base = envDefaults()
try {
const db = await getDb()
const doc = await db.collection('settings').findOne({ _id: SETTINGS_ID as never })
const stored = (doc?.value || {}) as Partial<AppSettings>
const merged: AppSettings = { ...base, ...stored }
if (!Array.isArray(merged.btClients) || merged.btClients.length === 0) merged.btClients = base.btClients
if (!Array.isArray(merged.enabledProviders) || merged.enabledProviders.length === 0) {
merged.enabledProviders = base.enabledProviders
}
cache = { value: merged, at: Date.now(), ttl: 5000 }
return merged
} catch {
// MongoDB 不可用:短时缓存默认值,避免每个请求都卡 3 秒去连库
cache = { value: base, at: Date.now(), ttl: 3000 }
return base
}
}
export async function saveSettings(patch: Partial<AppSettings>): Promise<AppSettings> {
const db = await getDb()
const current = await loadSettings()
const next: AppSettings = { ...current, ...patch }
await db
.collection('settings')
.updateOne(
{ _id: SETTINGS_ID as never },
{ $set: { value: next, updatedAt: new Date() } },
{ upsert: true }
)
cache = { value: next, at: Date.now(), ttl: 5000 }
return next
}
export function normalizeClient(raw: Partial<BtClientConfig>, index = 0): BtClientConfig {
const type = (raw.type || 'qbittorrent') as BtClientType
return {
id: raw.id || `${type}-${Date.now().toString(36)}-${index}`,
type,
name: raw.name || type,
host: raw.host || '127.0.0.1',
port: Number(raw.port) || defaultPort(type),
path: raw.path || undefined,
username: raw.username || '',
password: raw.password || '',
downloadDir: raw.downloadDir || '',
useHttps: !!raw.useHttps,
enabled: raw.enabled !== false
}
}
export function defaultPort(type: BtClientType): number {
if (type === 'transmission') return 9091
if (type === 'aria2') return 6800
return 8080
}
export function baseUrlOf(cfg: BtClientConfig): string {
const scheme = cfg.useHttps ? 'https' : 'http'
return `${scheme}://${cfg.host}:${cfg.port}`
}