import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; import { crypto } from "jsr:@std/crypto"; import dayjs from "npm:dayjs"; // 引入支持ECB模式的AES库 import { encode as hexEncode } from "https://deno.land/std@0.208.0/encoding/hex.ts"; const pageSize = 30; // 音质参数 const qualityMap = { "low": "standard", "standard": "exhigh", "high": "lossless", "super": "hires", }; // CORS 头部 const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With", "Access-Control-Max-Age": "86400", }; // MD5 加密函数 async function MD5(data: string): Promise { const encoder = new TextEncoder(); const dataBuffer = encoder.encode(data); const hashBuffer = await crypto.subtle.digest("MD5", dataBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // AES-ECB 加密函数 (用原生Web Crypto API实现ECB模式) async function AES(data: string): Promise { // 由于WebCrypto API不直接支持ECB模式,我们需要手动实现 // 将输入分成16字节的块,单独加密每个块,然后拼接结果 const key = "e82ckenh8dichen8"; const encoder = new TextEncoder(); const keyBuffer = encoder.encode(key); // 为AES-CBC创建密钥(我们会复用它来模拟ECB) const cryptoKey = await crypto.subtle.importKey( "raw", keyBuffer, { name: "AES-CBC" }, false, ["encrypt"] ); // 准备数据 const dataBuffer = encoder.encode(data); // 处理PKCS7填充 const blockSize = 16; const paddingSize = blockSize - (dataBuffer.length % blockSize); const paddedBuffer = new Uint8Array(dataBuffer.length + paddingSize); paddedBuffer.set(dataBuffer); // 填充剩余字节 paddedBuffer.fill(paddingSize, dataBuffer.length); // 将数据分成16字节的块 const blocks = []; for (let i = 0; i < paddedBuffer.length; i += blockSize) { blocks.push(paddedBuffer.slice(i, i + blockSize)); } // 为每个块单独加密(模拟ECB模式) const encryptedBlocks = await Promise.all(blocks.map(async (block) => { // 使用零IV(在ECB模式下IV不相关) const iv = new Uint8Array(16); const encrypted = await crypto.subtle.encrypt( { name: "AES-CBC", iv }, cryptoKey, block ); // 只取加密结果的前16字节(块大小) return new Uint8Array(encrypted).slice(0, blockSize); })); // 合并所有加密块 const encryptedArray = new Uint8Array(encryptedBlocks.reduce((acc, block) => { const combined = new Uint8Array(acc.length + block.length); combined.set(acc); combined.set(block, acc.length); return combined; }, new Uint8Array(0))); // 转换为大写十六进制 return Array.from(encryptedArray) .map(b => b.toString(16).padStart(2, '0')) .join('') .toUpperCase(); } // EAPI 请求函数 async function EAPI(path: string, json: any = {}, music_u?: string): Promise { try { const params = [path, JSON.stringify(json)]; const md5Hash = await MD5("nobody" + params.join("use") + "md5forencrypt"); params.push(md5Hash); const encryptedParams = await AES(params.join("-36cd479b6b5-")); // 处理cookie格式 let cookieValue = "os=pc;"; if (music_u) { // 检查是否已经包含MUSIC_U/MUSIC_A格式 const musicMatch = music_u.match(/MUSIC_[UA]=([^;]+)/i); if (musicMatch) { cookieValue = `os=pc; appver=9.0.25; MUSIC_U=${musicMatch[1]}`; } else { cookieValue = `os=pc; appver=9.0.25; MUSIC_U=${music_u}`; } } const response = await fetch(path.replace("/", "https://interface.music.163.com/e"), { method: "POST", body: "params=" + encryptedParams, headers: { "Content-Type": "application/x-www-form-urlencoded", "Cookie": cookieValue, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } }); if (!response.ok) { throw new Error(`API错误: ${response.status} ${response.statusText}`); } const text = await response.text(); try { return JSON.parse(text); } catch (e) { console.error("JSON解析失败:", text); throw new Error(`JSON解析失败: ${e.message}`); } } catch (error) { console.error("EAPI请求失败:", error); throw error; } } // 格式化歌曲信息 function formatMusicItem(item: any): any { const _ = item.baseInfo || item.song || item; const name = _.name || _.songname; const singer = _.ar && _.ar.map((a: any) => a.name).join('&') || ""; const albumName = _.al && _.al.name; const albumId = (_.al && _.al.id) || ""; const picUrl = (_.al && _.al.picUrl) || ""; const qualities: any = {}; for (const k of ['l', 'h', 'sq', 'hr']) { if (_[k] || (k === 'l' && _['m'])) { const quality = { 'm': "low", 'l': "low", 'h': "standard", 'sq': "high", 'hr': "super" }[k === 'l' && _['m'] ? 'm' : k]; if (quality) { qualities[quality] = { size: _[k === 'l' && _['m'] ? 'm' : k].size }; } } } const content = ((_.fee === 0 || _.fee === 8) && (_.privilege ? (_.privilege.st > -1) : 1)) ? 0 : 1; return { id: _.id, artist: singer, title: name, duration: _.dt, album: albumName, artwork: picUrl, qualities, albumId, content, rawLrc: _.lyrics, }; } // 格式化专辑信息 function formatAlbumItem(item: any): any { return { id: item.id, title: item.name, artist: item.artist.name, artwork: item.picUrl, description: item.description, date: dayjs.unix(item.publishTime / 1000).format("YYYY-MM-DD"), worksNum: item.artist.musicSize, content: 4 }; } // 格式化作者信息 function formatArtistItem(item: any): any { return { id: item.id, name: item.name, avatar: item.img1v1Url, description: item.briefDesc || item.description, worksNum: item.musicSize || item.albumSize, content: 5 }; } // 格式化歌单信息 function formatSheetItem(item: any): any { const _ = item.baseInfo || item; return { id: _.id || _.resourceId, title: _.name || _.title, artist: (_.creator && _.creator.nickname), artwork: _.coverImgUrl || _.picUrl || _.coverImg, description: _.description, worksNum: _.trackCount, playCount: _.playCount, date: _.updateTime, createUserId: _.userId, createTime: _.createTime, content: 2 }; } // 获取排行榜详情 async function getTopListDetail(topListItem: any, music_u?: string): Promise { const res = await getMusicSheetInfo(topListItem, 1, music_u); res.topListItem = res.sheetItem; res.topListItem.content = 3; return res; } // API 路由处理器 async function handleRequest(req: Request): Promise { // 处理 CORS 预检请求 if (req.method === "OPTIONS") { return new Response(null, { status: 200, headers: corsHeaders }); } const url = new URL(req.url); const path = url.pathname; const params = url.searchParams; try { let result: any = {}; // 获取请求体数据(如果有) let body: any = {}; if (req.method === "POST") { try { body = await req.json(); } catch { // 忽略解析错误 } } const music_u = params.get("music_u") || body.music_u; switch (path) { case "/search": const query = params.get("query") || body.query; const page = parseInt(params.get("page") || body.page || "1"); const type = params.get("type") || body.type || "music"; result = await search(query, page, type, music_u); break; case "/music/info": const musicId = params.get("id") || body.id; result = await getMusicInfo({ id: musicId }, music_u); break; case "/music/url": const songId = params.get("id") || body.id; const quality = params.get("quality") || body.quality || "standard"; const musicItem = { id: songId, qualities: { [quality]: {} } }; result = await getMediaSource(musicItem, quality, music_u); break; case "/lyric": const lyricId = params.get("id") || body.id; result = await getLyric({ id: lyricId }, music_u); break; case "/playlist/detail": const playlistId = params.get("id") || body.id; const playlistPage = parseInt(params.get("page") || body.page || "1"); result = await getMusicSheetInfo({ id: playlistId }, playlistPage, music_u); break; case "/album/detail": const albumId = params.get("id") || body.id; result = await getAlbumInfo({ id: albumId }, music_u); break; case "/artist/works": const artistId = params.get("id") || body.id; const artistPage = parseInt(params.get("page") || body.page || "1"); const artistType = params.get("type") || body.type || "music"; result = await getArtistWorks({ id: artistId }, artistPage, artistType, music_u); break; case "/toplists": result = await getTopLists(music_u); break; case "/toplistdetail": result = await getTopListDetail( { id: params.get("id") || body.id }, music_u ); break; case "/recommend/tags": result = await getRecommendSheetTags(music_u); break; case "/recommend/sheets": const tag = params.get("tag") || body.tag; const sheetPage = parseInt(params.get("page") || body.page || "1"); result = await getRecommendSheetsByTag(tag, sheetPage, music_u); break; case "/import/sheet": const urlLike = params.get("url") || body.url; result = await importMusicSheet(urlLike, music_u); break; case "/import/music": const musicUrl = params.get("url") || body.url; result = await importMusicItem(musicUrl, music_u); break; case "/comments": const commentId = params.get("id") || body.id; const commentPage = parseInt(params.get("page") || body.page || "1"); result = await getMusicComments({ id: commentId }, commentPage, music_u); break; // case "/": // case "/health": // result = { // status: "ok", // message: "网易云音乐 API 服务正在运行", // version: "2025.01.22", // endpoints: [ // "/search - 搜索音乐", // "/music/info - 获取歌曲详情", // "/music/url - 获取播放链接", // "/lyric - 获取歌词", // "/playlist/detail - 获取歌单详情", // "/album/detail - 获取专辑详情", // "/artist/works - 获取歌手作品", // "/toplists - 获取排行榜", // "/recommend/tags - 获取推荐标签", // "/recommend/sheets - 获取推荐歌单", // "/import/sheet - 导入歌单", // "/import/music - 导入单曲", // "/comments - 获取评论" // ] // }; // break; case "/": case "/health": result = { status: "ok", message: "云音乐 API 服务正在运行", version: "2025.07.22", description: "基于 Deno 的云音乐 API 服务,支持搜索、播放、歌词等功能", baseUrl: `${url.protocol}//${url.host}`, authentication: { description: "部分接口需要登录态,可通过 music_u 参数传递", parameter: "music_u", usage: "在查询参数或请求体中添加 music_u 字段" }, endpoints: [ { path: "/search", method: "GET/POST", description: "搜索音乐、专辑、歌手、歌单", parameters: { query: "搜索关键词(必需)", page: "页码,默认为1", type: "搜索类型:music(歌曲)、album(专辑)、artist(歌手)、sheet(歌单)、lyric(歌词),默认music", music_u: "登录凭证(可选)" }, example: "/search?query=周杰伦&page=1&type=music" }, { path: "/music/info", method: "GET/POST", description: "获取歌曲详细信息", parameters: { id: "歌曲ID(必需)", music_u: "登录凭证(可选)" }, example: "/music/info?id=123456" }, { path: "/music/url", method: "GET/POST", description: "获取歌曲播放链接", parameters: { id: "歌曲ID(必需)", quality: "音质:low(标准)、standard(较高)、high(无损)、super(Hi-Res),默认standard", music_u: "登录凭证(推荐,提高成功率)" }, example: "/music/url?id=123456&quality=high" }, { path: "/lyric", method: "GET/POST", description: "获取歌曲歌词", parameters: { id: "歌曲ID(必需)", music_u: "登录凭证(可选)" }, example: "/lyric?id=123456" }, { path: "/playlist/detail", method: "GET/POST", description: "获取歌单详情和歌曲列表", parameters: { id: "歌单ID(必需)", page: "页码,默认为1", music_u: "登录凭证(可选)" }, example: "/playlist/detail?id=123456" }, { path: "/album/detail", method: "GET/POST", description: "获取专辑详情和歌曲列表", parameters: { id: "专辑ID(必需)", music_u: "登录凭证(可选)" }, example: "/album/detail?id=123456" }, { path: "/artist/works", method: "GET/POST", description: "获取歌手作品(热门歌曲或专辑)", parameters: { id: "歌手ID(必需)", page: "页码,默认为1", type: "作品类型:music(歌曲)、album(专辑),默认music", music_u: "登录凭证(可选)" }, example: "/artist/works?id=123456&type=music" }, { path: "/toplists", method: "GET/POST", description: "获取所有排行榜列表", parameters: { music_u: "登录凭证(可选)" }, example: "/toplists" }, { path: "/recommend/tags", method: "GET/POST", description: "获取歌单推荐标签", parameters: { music_u: "登录凭证(可选)" }, example: "/recommend/tags" }, { path: "/recommend/sheets", method: "GET/POST", description: "根据标签获取推荐歌单", parameters: { tag: "标签名称(可选,不传则返回推荐歌单)", page: "页码,默认为1", music_u: "登录凭证(可选)" }, example: "/recommend/sheets?tag=流行&page=1" }, { path: "/import/sheet", method: "GET/POST", description: "通过URL或ID导入歌单", parameters: { url: "歌单URL或歌单ID(必需)", music_u: "登录凭证(可选)" }, example: "/import/sheet?url=123456 或 /import/sheet?url=https://music.163.com/playlist?id=123456" }, { path: "/import/music", method: "GET/POST", description: "通过URL或ID导入单曲", parameters: { url: "歌曲URL或歌曲ID(必需)", music_u: "登录凭证(可选)" }, example: "/import/music?url=123456 或 /import/music?url=https://music.163.com/song?id=123456" }, { path: "/comments", method: "GET/POST", description: "获取歌曲评论", parameters: { id: "歌曲ID(必需)", page: "页码,默认为1", music_u: "登录凭证(可选)" }, example: "/comments?id=123456&page=1" } ], responseFormat: { success: { description: "成功响应格式", example: { isEnd: "boolean - 是否为最后一页(分页接口)", data: "array - 数据列表", total: "number - 总数(部分接口)" } }, error: { description: "错误响应格式", example: { error: "string - 错误描述", message: "string - 详细错误信息" } } }, notes: [ "所有接口均支持 GET 和 POST 请求方式", "GET 请求参数通过查询字符串传递", "POST 请求参数通过 JSON 格式的请求体传递", "推荐使用 music_u 参数以获得更好的接口响应", "部分高音质音频需要会员权限", "请遵守相关法律法规,合理使用 API" ] }; break; default: return new Response(JSON.stringify({ error: "API 路径不存在", path: path }), { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } }); } return new Response(JSON.stringify(result), { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }); } catch (error) { // console.error("API Error:", error); // return new Response(JSON.stringify({ // error: "服务器内部错误", // message: error.message // }), { // status: 500, // headers: { // "Content-Type": "application/json", // ...corsHeaders // } // }); console.error("API错误:", error); console.error("错误堆栈:", error.stack); // 尝试获取请求信息以便调试 try { const url = new URL(req.url); console.error("路径:", url.pathname); console.error("查询参数:", Object.fromEntries(url.searchParams.entries())); if (req.method === "POST") { try { const cloned = req.clone(); const body = await cloned.text(); console.error("请求体:", body); } catch (e) { console.error("无法读取请求体:", e); } } } catch (e) { console.error("无法获取请求信息:", e); } return new Response(JSON.stringify({ error: "服务器内部错误", message: error.message, stack: Deno.env.get("DEBUG") === "true" ? error.stack : undefined }), { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }); } } // 实现具体的 API 功能函数 async function search(query: string, page: number, type: string, music_u?: string): Promise { const stype: any = { music: { t: "song", m: formatMusicItem }, album: { t: "album", v: "/v1", m: formatAlbumItem }, artist: { t: "artist", v: "/v1", m: formatArtistItem }, sheet: { t: "playlist", m: formatSheetItem }, lyric: { t: "resource/lyric", m: formatMusicItem } }[type]; const path = "/api" + (stype.v || "") + "/search/" + stype.t + (stype.t.includes("/") ? "" : "/get"); const data = { "filterCode": "-1", "offset": ((page - 1) * pageSize).toString(), "limit": pageSize.toString(), "channel": "typing", "keyword": query, "scene": "normal", "s": query, }; const res = await EAPI(path, data, music_u); const result = res.data || res.result; const list = result.resources || result.albums || result.artists || result.songs || []; const total1 = page * pageSize; const total2 = result.songCount || result.playlistCount || result.albumCount || result.totalCount || (total1 - pageSize + list.length); return { isEnd: total2 <= total1, data: list.map(stype.m) }; } async function getMusicInfo(musicItem: any, music_u?: string): Promise { const res = await EAPI("/api/v3/song/detail", { c: `[{"id":"${musicItem.id}"}]` }, music_u); const song = res.songs[0] || res.privileges[0]; song.privilege = res.privileges[0]; return formatMusicItem(song); } async function getMediaSource(musicItem: any, quality: string, music_u?: string): Promise { if (!musicItem.qualities[quality]) { return false; } const res = await EAPI("/api/song/enhance/player/url/v1", { ids: `["${musicItem.id}"]`, encodeType: "flac", immerseType: "c51", trialMode: "23", level: qualityMap[quality as keyof typeof qualityMap] }, music_u); if (res.data && res.data[0] && res.data[0].url) { return { url: String(res.data[0].url).split("?")[0], size: res.data[0].size, quality, }; } return false; } async function getLyric(musicItem: any, music_u?: string): Promise { const res = await EAPI("/api/song/lyric", { id: musicItem.id, lv: -1, kv: -1, tv: -1 }, music_u); return { rawLrc: res.lrc?.lyric || "", }; } async function getMusicSheetInfo(sheet: any, page: number = 1, music_u?: string): Promise { const res = await EAPI("/api/v6/playlist/detail", { n: 99999, id: sheet.id }, music_u); const playlist = res.playlist; const list = playlist.tracks || []; return { isEnd: 99999 >= playlist.trackCount, sheetItem: formatSheetItem(playlist), musicList: list.map(formatMusicItem) }; } async function getAlbumInfo(albumItem: any, music_u?: string): Promise { const res = await EAPI("/api/v1/album/" + albumItem.id, {}, music_u); return { isEnd: true, albumItem: formatAlbumItem(res.album), musicList: (res.songs || []).map(formatMusicItem), }; } async function getArtistWorks(artistItem: any, page: number, type: string, music_u?: string): Promise { const typeConfig: any = { "music": { "path1": "/api/v1/artist/", "path2": "hotSongs", "mapJs": formatMusicItem }, "album": { "path1": "/api/artist/albums/", "path2": "hotAlbums", "mapJs": formatAlbumItem }, }[type]; const res = await EAPI(typeConfig.path1 + artistItem.id, {}, music_u); return { isEnd: true, artistItem: formatArtistItem(res.artist), data: res[typeConfig.path2].map(typeConfig.mapJs), }; } async function getTopLists(music_u?: string): Promise { const group1: any[] = []; const group2 = await EAPI("/api/toplist/detail/v2", {}, music_u); group2.data.map((item: any) => { if (item.list && item.list.length) { const group3: any[] = []; item.list.map((listItem: any) => { if (listItem.id !== 0) { group3.push({ title: listItem.name, coverImg: listItem.coverUrl, content: 3, id: listItem.id, description: listItem.updateFrequency }); } }); group1.push({ title: item.name, data: group3 }); } }); return group1; } async function getRecommendSheetTags(music_u?: string): Promise { const pinned = [ { title: "推荐", id: "_SPECIAL_CLOUD_VILLAGE_PLAYLIST" }, { title: "官方", id: "官方" }, { title: "雷达", id: "_RADAR" }, { title: "原创", id: "_SPECIAL_ORIGIN_SONG_LOCATION" }, { title: "心情", id: "_FEELING_PLAYLIST_LOCATION" }, { title: "场景", id: "_SCENE_PLAYLIST_LOCATION" }, { title: "专属", id: "_COMBINATION" }, { title: "全部", id: "全部歌单" }, { title: "新热", id: "_NEW_SONG_AND_ALBUM" }, { title: "影视", id: "_FIRM_PLAYLIST" }, { title: "奖项", id: "_AWARDS_PLAYLIST" }, ]; const data = ["语种", "风格", "场景", "情感", "主题"].map(name => ({ title: name, data: [] })); const res = await EAPI("/api/playlist/catalogue/v1", {}, music_u); res.sub.map((item: any) => { data[item.category].data.push({ title: item.name, id: item.name }); }); return { pinned, data }; } async function getRecommendSheetsByTag(tag: string | null, page: number, music_u?: string): Promise { let res: any; const tagId = tag; if (!tagId || tagId === "" || tagId === "true") { res = await EAPI("/api/personalized/playlist", { limit: 30 }, music_u); } else if (/^_[A-Z]+/.test(tagId)) { res = await EAPI("/api/link/page/rcmd/resource/show", { "pageCode": "HOME_RECOMMEND_PAGE", "isFirstScreen": "false", "cursor": "6", "refresh": "true", "blockCodeOrderList": `["PAGE_RECOMMEND${tagId}"]` }, music_u); res = res.data.blocks[0].dslData.blockResource; } else { res = await EAPI("/api/playlist/list", { cat: tagId || "全部", order: "hot", limit: pageSize, offset: (page - 1) * pageSize, total: true, csrf_token: "", }, music_u); } const list = res.result || res.playlists || res.resources || []; const total1 = page * pageSize; const total2 = res.total || (total1 - pageSize + list.length); return { isEnd: (res.more !== true) || (total2 <= total1), data: list.map(formatSheetItem) }; } async function importMusicSheet(urlLike: string, music_u?: string): Promise { let id = (urlLike.match(/^(\d+)$/) || [])[1]; if (!id && !urlLike.match(/music\.163\.com/i)) { return false; } if (!id) { id = (urlLike.match(/playlist(\/|.*?[\?\&]id=)(\d+)/i) || [])[2]; } if (!id) { return false; } const result = await getMusicSheetInfo({ id }, 1, music_u); return result.musicList; } async function importMusicItem(urlLike: string, music_u?: string): Promise { let id = (urlLike.match(/^(\d+)$/) || [])[1]; if (!id && !urlLike.match(/music\.163\.com/i)) { return false; } if (!id) { id = (urlLike.match(/song(.*?[\?\&]id=|\/)(\d+)/i) || [])[2]; } if (!id) { return false; } return await getMusicInfo({ id }, music_u); } async function getMusicComments(musicItem: any, page: number = 1, music_u?: string): Promise { const res = await EAPI("/api/v2/resource/comments", { "threadId": "R_SO_4_" + musicItem.id, "cursor": "20", "sortType": "1", "pageNo": page, "pageSize": pageSize.toString(), "parentCommentld": "0", "showlnner": false }, music_u); const formatComment = (comment: any): any => ({ id: comment.commentId, nickName: comment.user && comment.user.nickname, avatar: comment.user && comment.user.avatarUrl, comment: comment.content, like: comment.likedCount, createAt: comment.time, location: comment.ipLocation && comment.ipLocation.location, replies: (comment.beReplied || []).map(formatComment), content: 6 }); return { isEnd: res.data.hasMore !== true, data: res.data.comments.map(formatComment) }; } let hbCount = 0; setInterval(()=>{ hbCount++; if (hbCount % 6 === 0) { // every 60s if interval is 10s console.log('[diagnostic] heartbeat 60s elapsed, process alive'); } }, 10_000); // 启动服务器 const port = parseInt(Deno.env.get("PORT") || "8080"); console.log(`🎵 Deno 网易云音乐 API 服务启动成功!`); console.log(`🚀 服务运行在: http://localhost:${port}`); console.log(`📖 API 文档: http://localhost:${port}/`); serve(handleRequest, { port });