feat: 声匣 —— 网易云 / QQ 音乐自托管下载服务
由 163-music-dl CLI 重构为 Web 服务: - 下载流水线、歌词与标签逻辑沿用旧版(node-id3 / ffmpeg) - 网易云(chksz 接口 + music.163.com 元数据)与 QQ 音乐(VIP Cookie)provider - QQ 扫码登录与 musickey 自动续期 - SQLite 持久化的下载队列,SSE 实时进度 - 曲库工具(M3U / ZIP / 回收站 / 补全标签)、历史统计与导出、访问密码 - Vite 8 + Vue 3 前端,浅色 / 深色 - Docker 部署配置(飞牛 NAS) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// 试听代理:解析出 320K 音源后由服务端转发(支持 Range),绕过防盗链与混合内容
|
||||
import { Readable } from 'node:stream';
|
||||
import type { Context } from 'koa';
|
||||
import { UA } from './core/net/http.ts';
|
||||
import { getProvider } from './core/providers/index.ts';
|
||||
import { TtlCache } from './core/util.ts';
|
||||
|
||||
const urlCache = new TtlCache<{ url: string; ext: string }>(3 * 60 * 1000, 200);
|
||||
const MIME: Record<string, string> = { mp3: 'audio/mpeg', flac: 'audio/flac', m4a: 'audio/mp4', ogg: 'audio/ogg' };
|
||||
|
||||
export async function proxyStream(ctx: Context, source: string, id: string) {
|
||||
const provider = getProvider(source);
|
||||
const audio = await urlCache.getOrSet(`${source}:${id}`, async () => {
|
||||
const r = await provider.resolveAudio(id, 'high');
|
||||
return { url: r.url, ext: r.ext };
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
ctx.req.on('close', () => controller.abort());
|
||||
const headers: Record<string, string> = { 'User-Agent': UA };
|
||||
if (ctx.get('range')) headers.Range = ctx.get('range');
|
||||
const upstream = await fetch(audio.url, { headers, signal: controller.signal });
|
||||
if (!upstream.ok || !upstream.body) ctx.throw(502, `音源请求失败 (HTTP ${upstream.status})`);
|
||||
|
||||
ctx.status = upstream.status;
|
||||
ctx.set('Content-Type', MIME[audio.ext] || upstream.headers.get('content-type') || 'application/octet-stream');
|
||||
ctx.set('Accept-Ranges', 'bytes');
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
for (const h of ['content-length', 'content-range']) {
|
||||
const v = upstream.headers.get(h);
|
||||
if (v) ctx.set(h, v);
|
||||
}
|
||||
ctx.body = Readable.fromWeb(upstream.body as any);
|
||||
}
|
||||
Reference in New Issue
Block a user