背景:看到 青旨佬的音乐解析 API,想着用到博客上,但是大多数博客系统和插件使用的是 Meting 框架,API 互不兼容,所以用 Cloudflare Workers 部署一个适配器。

感谢:青旨大佬的 API,还有详尽的 API 文档,介绍在下面,还有稳定成熟的 Meting 框架

https://linux.do/t/topic/1212285

难点:Gemini 2.5 和 3.0-pro 实现代码然后人工调试,比较难处理的是 QQ 音乐和酷我使用了新的链接,但是 MetingJS 没有适配,导致使用 MetingJS 的插件等认不出链接,无法发送正确的参数,这我没有办法解决了,适配器是以 MetingJS 支持的旧版 QQ 音乐和酷我的链接的,下面是旧版格式

对于 QQ 音乐:
单曲:https://y.qq.com/n/yqq/song/{歌曲ID}.html
歌单:https://y.qq.com/n/yqq/playlist/{歌单ID}.html
专辑:https://y.qq.com/n/yqq/album/{专辑ID}.html

对于酷我音乐:
单曲:https://www.kuwo.cn/yinyue/{ID}
歌单:https://www.kuwo.cn/playlist/index?pid={ID}

源码:

// TuneHub API 的基础地址 (固定)
const TUNEHUB_BASE = "https://music-dl.sayqz.com/api";

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // 策略配置 (优先读取环境变量,否则使用默认值)
    const config = {
      INFO_CACHE_TTL: env.INFO_CACHE_TTL !== undefined ? parseInt(env.INFO_CACHE_TTL) : 600,
      AUDIO_CACHE_TTL: env.AUDIO_CACHE_TTL !== undefined ? parseInt(env.AUDIO_CACHE_TTL) : 3600,
      MAX_RETRIES: env.MAX_RETRIES !== undefined ? parseInt(env.MAX_RETRIES) : 3
    };

    // 路由分发
    if (url.pathname.startsWith('/proxy')) {
      // 只有网易云的音频会走到这里
      return handleProxy(request, config);
    }

    return handleInfoWithCache(request, ctx, config);
  },
};

/**
 * 模块 A: 带缓存的信息获取处理
 */
async function handleInfoWithCache(request, ctx, config) {
  if (config.INFO_CACHE_TTL <= 0) {
    return handleInfo(request, config);
  }
  const cache = caches.default;
  let response = await cache.match(request);
  if (response) {
    const newHeaders = new Headers(response.headers);
    newHeaders.set('X-Worker-Cache', 'HIT');
    return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders });
  }
  response = await handleInfo(request, config);
  if (response.status === 200) {
    const responseToCache = response.clone();
    responseToCache.headers.set('Cache-Control', `public, max-age=${config.INFO_CACHE_TTL}`);
    ctx.waitUntil(cache.put(request, responseToCache));
  }
  return response;
}

/**
 * 模块 B: 核心逻辑 - 获取歌单/歌曲元数据 (智能分流版)
 */
async function handleInfo(request, config) {
  try {
    const url = new URL(request.url);
    const params = url.searchParams;
    const server = params.get("server") || "netease";
    const id = params.get("id");
    const type = params.get("type");
    const bitrate = params.get("bitrate");

    let tuneHubBr = null;
    if (bitrate) {
      if (bitrate === '320000') tuneHubBr = '320k';
      else if (bitrate === '999000') tuneHubBr = 'flac';
      else if (bitrate === '1400000') tuneHubBr = 'flac24bit';
    }

    const tuneHubParams = new URLSearchParams();
    let targetSource = server;
    if (server === 'tencent') {
        targetSource = 'qq';
    }
    tuneHubParams.set("source", targetSource);
    tuneHubParams.set("id", id);
    let tuneHubType = type === "playlist" ? "playlist" : "info";
    tuneHubParams.set("type", tuneHubType);

    if (tuneHubType !== 'playlist' && tuneHubBr) {
        tuneHubParams.set('br', tuneHubBr);
    }

    const targetUrl = `${TUNEHUB_BASE}?${tuneHubParams.toString()}`;
    const response = await fetchWithRetry(targetUrl, {}, config.MAX_RETRIES);
    
    if (!response.ok) {
        throw new Error(`Upstream API Error: ${response.status}`);
    }

    const data = await response.json();

    let finalResult = [];
    if (data.code === 200 && data.data) {
      let songList = type === "playlist" ? (data.data.list || []) : [data.data];
      
      finalResult = songList.map(song => {
        // [关键修改] 智能分流
        if (song.url) {
          const realUrl = new URL(song.url);
          if (tuneHubBr) {
            realUrl.searchParams.set('br', tuneHubBr);
          }

          // 只对网易云(netease)的链接进行反向代理
          if (server === 'netease') {
            const proxyUrl = new URL(url.origin);
            proxyUrl.pathname = '/proxy';
            proxyUrl.searchParams.set('real_url', realUrl.toString());
            song.url = proxyUrl.toString();
          } else {
            // 对于 QQ、酷我等,直接返回 TuneHub 的 API 链接
            // (虽然很可能因为 Referer 等原因播放失败,但这是不代理的唯一选择)
            song.url = realUrl.toString();
          }
        }
        
        if (song.pic) song.pic = song.pic.replace('http://', 'https://');
        if (song.lrc) song.lrc = song.lrc.replace('http://', 'https://');
        return song;
      });
    }

    return new Response(JSON.stringify(finalResult), {
      headers: { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" },
    });

  } catch (error) {
    console.error("handleInfo Error:", error);
    return new Response("[]", {
      headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
    });
  }
}

/**
 * 模块 C: 核心逻辑 - 音频流反向代理 (只为网易云服务)
 */
async function handleProxy(request, config) {
    try {
        const url = new URL(request.url);
        const realUrl = url.searchParams.get('real_url');

        if (!realUrl) {
            return new Response('Missing real_url parameter', { status: 400 });
        }

        const realUrlObj = new URL(realUrl);
        const source = realUrlObj.searchParams.get('source');

        const initialRequest = new Request(realUrl, {
            method: 'GET',
            redirect: 'manual'
        });

        let finalUrl = realUrl;
        let initialResponse = await fetch(initialRequest);

        if (initialResponse.status === 301 || initialResponse.status === 302) {
            finalUrl = initialResponse.headers.get('Location');
            if (!finalUrl) finalUrl = realUrl;
        }

        const newHeaders = new Headers();
        if (request.headers.has('range')) {
            newHeaders.set('Range', request.headers.get('range'));
        }
        if (request.headers.has('user-agent')) {
            newHeaders.set('User-Agent', request.headers.get('user-agent'));
        }

        // Referer 伪装只对网易云生效
        if (source === 'netease') {
            newHeaders.set('Referer', 'https://music.163.com/');
            newHeaders.set('Cookie', 'os=pc');
        }

        const finalRequest = new Request(finalUrl, {
            headers: newHeaders,
            redirect: 'follow',
            cf: {
                cacheTtl: config.AUDIO_CACHE_TTL > 0 ? config.AUDIO_CACHE_TTL : undefined,
                cacheEverything: config.AUDIO_CACHE_TTL > 0
            }
        });

        const realResponse = await fetchWithRetry(finalRequest, {}, config.MAX_RETRIES);

        const isRangeSupported = realResponse.status === 206;
        const responseHeaders = new Headers(realResponse.headers);
        responseHeaders.set('Access-Control-Allow-Origin', '*');
        responseHeaders.set('Access-Control-Allow-Headers', 'Range, User-Agent');

        if (config.AUDIO_CACHE_TTL > 0) {
            responseHeaders.set('Cache-Control', `public, max-age=${config.AUDIO_CACHE_TTL}`);
        } else {
            responseHeaders.set('Cache-Control', 'no-store');
        }

        if (isRangeSupported) {
            responseHeaders.set('Accept-Ranges', 'bytes');
        }

        return new Response(realResponse.body, {
            status: realResponse.status,
            statusText: realResponse.statusText,
            headers: responseHeaders
        });

    } catch (error) {
        return new Response(null, { status: 500, statusText: "Proxy Failed" });
    }
}


/**
 * 工具函数:带重试机制的 fetch
 */
async function fetchWithRetry(input, init, maxRetries = 1) {
  let attempt = 0;
  while (attempt <= maxRetries) {
    try {
      const response = await fetch(input, init);
      if (response.status >= 500) {
        throw new Error(`Server Error: ${response.status}`);
      }
      return response;
    } catch (error) {
      attempt++;
      if (attempt > maxRetries) {
        throw error;
      }
      await new Promise(resolve => setTimeout(resolve, 200));
    }
  }
}

复制粘贴到 Workers 代码部署就可用,国内屏蔽 workers.dev 域名,需要自定义域名,下面介绍代码功能吧 (懒得自己写了


为什么始终使用最高音质呢?因为 TuneHub 解析如果没有对应音质会自动降级,出处:


📌 转载信息
原作者:
1208091109
转载时间:
2025/12/28 10:58:36