Create web.ts
Browse files
web.ts
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
|
| 2 |
+
import { crypto } from "jsr:@std/crypto";
|
| 3 |
+
import dayjs from "npm:dayjs";
|
| 4 |
+
// 引入支持ECB模式的AES库
|
| 5 |
+
import { encode as hexEncode } from "https://deno.land/std@0.208.0/encoding/hex.ts";
|
| 6 |
+
|
| 7 |
+
const pageSize = 30;
|
| 8 |
+
|
| 9 |
+
// 音质参数
|
| 10 |
+
const qualityMap = {
|
| 11 |
+
"low": "standard",
|
| 12 |
+
"standard": "exhigh",
|
| 13 |
+
"high": "lossless",
|
| 14 |
+
"super": "hires",
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// CORS 头部
|
| 18 |
+
const corsHeaders = {
|
| 19 |
+
"Access-Control-Allow-Origin": "*",
|
| 20 |
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
| 21 |
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
|
| 22 |
+
"Access-Control-Max-Age": "86400",
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
// MD5 加密函数
|
| 26 |
+
async function MD5(data: string): Promise<string> {
|
| 27 |
+
const encoder = new TextEncoder();
|
| 28 |
+
const dataBuffer = encoder.encode(data);
|
| 29 |
+
const hashBuffer = await crypto.subtle.digest("MD5", dataBuffer);
|
| 30 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 31 |
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// AES-ECB 加密函数 (用原生Web Crypto API实现ECB模式)
|
| 35 |
+
async function AES(data: string): Promise<string> {
|
| 36 |
+
// 由于WebCrypto API不直接支持ECB模式,我们需要手动实现
|
| 37 |
+
// 将输入分成16字节的块,单独加密每个块,然后拼接结果
|
| 38 |
+
|
| 39 |
+
const key = "e82ckenh8dichen8";
|
| 40 |
+
const encoder = new TextEncoder();
|
| 41 |
+
const keyBuffer = encoder.encode(key);
|
| 42 |
+
|
| 43 |
+
// 为AES-CBC创建密钥(我们会复用它来模拟ECB)
|
| 44 |
+
const cryptoKey = await crypto.subtle.importKey(
|
| 45 |
+
"raw",
|
| 46 |
+
keyBuffer,
|
| 47 |
+
{ name: "AES-CBC" },
|
| 48 |
+
false,
|
| 49 |
+
["encrypt"]
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
// 准备数据
|
| 53 |
+
const dataBuffer = encoder.encode(data);
|
| 54 |
+
|
| 55 |
+
// 处理PKCS7填充
|
| 56 |
+
const blockSize = 16;
|
| 57 |
+
const paddingSize = blockSize - (dataBuffer.length % blockSize);
|
| 58 |
+
const paddedBuffer = new Uint8Array(dataBuffer.length + paddingSize);
|
| 59 |
+
paddedBuffer.set(dataBuffer);
|
| 60 |
+
// 填充剩余字节
|
| 61 |
+
paddedBuffer.fill(paddingSize, dataBuffer.length);
|
| 62 |
+
|
| 63 |
+
// 将数据分成16字节的块
|
| 64 |
+
const blocks = [];
|
| 65 |
+
for (let i = 0; i < paddedBuffer.length; i += blockSize) {
|
| 66 |
+
blocks.push(paddedBuffer.slice(i, i + blockSize));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// 为每个块单独加密(模拟ECB模式)
|
| 70 |
+
const encryptedBlocks = await Promise.all(blocks.map(async (block) => {
|
| 71 |
+
// 使用零IV(在ECB模式下IV不相关)
|
| 72 |
+
const iv = new Uint8Array(16);
|
| 73 |
+
const encrypted = await crypto.subtle.encrypt(
|
| 74 |
+
{ name: "AES-CBC", iv },
|
| 75 |
+
cryptoKey,
|
| 76 |
+
block
|
| 77 |
+
);
|
| 78 |
+
// 只取加密结果的前16字节(块大小)
|
| 79 |
+
return new Uint8Array(encrypted).slice(0, blockSize);
|
| 80 |
+
}));
|
| 81 |
+
|
| 82 |
+
// 合并所有加密块
|
| 83 |
+
const encryptedArray = new Uint8Array(encryptedBlocks.reduce((acc, block) => {
|
| 84 |
+
const combined = new Uint8Array(acc.length + block.length);
|
| 85 |
+
combined.set(acc);
|
| 86 |
+
combined.set(block, acc.length);
|
| 87 |
+
return combined;
|
| 88 |
+
}, new Uint8Array(0)));
|
| 89 |
+
|
| 90 |
+
// 转换为大写十六进制
|
| 91 |
+
return Array.from(encryptedArray)
|
| 92 |
+
.map(b => b.toString(16).padStart(2, '0'))
|
| 93 |
+
.join('')
|
| 94 |
+
.toUpperCase();
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// EAPI 请求函数
|
| 98 |
+
async function EAPI(path: string, json: any = {}, music_u?: string): Promise<any> {
|
| 99 |
+
try {
|
| 100 |
+
const params = [path, JSON.stringify(json)];
|
| 101 |
+
const md5Hash = await MD5("nobody" + params.join("use") + "md5forencrypt");
|
| 102 |
+
params.push(md5Hash);
|
| 103 |
+
|
| 104 |
+
const encryptedParams = await AES(params.join("-36cd479b6b5-"));
|
| 105 |
+
|
| 106 |
+
// 处理cookie格式
|
| 107 |
+
let cookieValue = "os=pc;";
|
| 108 |
+
if (music_u) {
|
| 109 |
+
// 检查是否已经包含MUSIC_U/MUSIC_A格式
|
| 110 |
+
const musicMatch = music_u.match(/MUSIC_[UA]=([^;]+)/i);
|
| 111 |
+
if (musicMatch) {
|
| 112 |
+
cookieValue = `os=pc; appver=9.0.25; MUSIC_U=${musicMatch[1]}`;
|
| 113 |
+
} else {
|
| 114 |
+
cookieValue = `os=pc; appver=9.0.25; MUSIC_U=${music_u}`;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const response = await fetch(path.replace("/", "https://interface.music.163.com/e"), {
|
| 119 |
+
method: "POST",
|
| 120 |
+
body: "params=" + encryptedParams,
|
| 121 |
+
headers: {
|
| 122 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 123 |
+
"Cookie": cookieValue,
|
| 124 |
+
"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"
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
if (!response.ok) {
|
| 129 |
+
throw new Error(`API错误: ${response.status} ${response.statusText}`);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const text = await response.text();
|
| 133 |
+
try {
|
| 134 |
+
return JSON.parse(text);
|
| 135 |
+
} catch (e) {
|
| 136 |
+
console.error("JSON解析失败:", text);
|
| 137 |
+
throw new Error(`JSON解析失败: ${e.message}`);
|
| 138 |
+
}
|
| 139 |
+
} catch (error) {
|
| 140 |
+
console.error("EAPI请求失败:", error);
|
| 141 |
+
throw error;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// 格式化歌曲信息
|
| 146 |
+
function formatMusicItem(item: any): any {
|
| 147 |
+
const _ = item.baseInfo || item.song || item;
|
| 148 |
+
const name = _.name || _.songname;
|
| 149 |
+
const singer = _.ar && _.ar.map((a: any) => a.name).join('&') || "";
|
| 150 |
+
const albumName = _.al && _.al.name;
|
| 151 |
+
const albumId = (_.al && _.al.id) || "";
|
| 152 |
+
const picUrl = (_.al && _.al.picUrl) || "";
|
| 153 |
+
|
| 154 |
+
const qualities: any = {};
|
| 155 |
+
for (const k of ['l', 'h', 'sq', 'hr']) {
|
| 156 |
+
if (_[k] || (k === 'l' && _['m'])) {
|
| 157 |
+
const quality = {
|
| 158 |
+
'm': "low",
|
| 159 |
+
'l': "low",
|
| 160 |
+
'h': "standard",
|
| 161 |
+
'sq': "high",
|
| 162 |
+
'hr': "super"
|
| 163 |
+
}[k === 'l' && _['m'] ? 'm' : k];
|
| 164 |
+
|
| 165 |
+
if (quality) {
|
| 166 |
+
qualities[quality] = {
|
| 167 |
+
size: _[k === 'l' && _['m'] ? 'm' : k].size
|
| 168 |
+
};
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const content = ((_.fee === 0 || _.fee === 8) && (_.privilege ? (_.privilege.st > -1) : 1)) ? 0 : 1;
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
id: _.id,
|
| 177 |
+
artist: singer,
|
| 178 |
+
title: name,
|
| 179 |
+
duration: _.dt,
|
| 180 |
+
album: albumName,
|
| 181 |
+
artwork: picUrl,
|
| 182 |
+
qualities,
|
| 183 |
+
albumId,
|
| 184 |
+
content,
|
| 185 |
+
rawLrc: _.lyrics,
|
| 186 |
+
};
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// 格式化专辑信息
|
| 190 |
+
function formatAlbumItem(item: any): any {
|
| 191 |
+
return {
|
| 192 |
+
id: item.id,
|
| 193 |
+
title: item.name,
|
| 194 |
+
artist: item.artist.name,
|
| 195 |
+
artwork: item.picUrl,
|
| 196 |
+
description: item.description,
|
| 197 |
+
date: dayjs.unix(item.publishTime / 1000).format("YYYY-MM-DD"),
|
| 198 |
+
worksNum: item.artist.musicSize,
|
| 199 |
+
content: 4
|
| 200 |
+
};
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// 格式化作者信息
|
| 204 |
+
function formatArtistItem(item: any): any {
|
| 205 |
+
return {
|
| 206 |
+
id: item.id,
|
| 207 |
+
name: item.name,
|
| 208 |
+
avatar: item.img1v1Url,
|
| 209 |
+
description: item.briefDesc || item.description,
|
| 210 |
+
worksNum: item.musicSize || item.albumSize,
|
| 211 |
+
content: 5
|
| 212 |
+
};
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// 格式化歌单信息
|
| 216 |
+
function formatSheetItem(item: any): any {
|
| 217 |
+
const _ = item.baseInfo || item;
|
| 218 |
+
return {
|
| 219 |
+
id: _.id || _.resourceId,
|
| 220 |
+
title: _.name || _.title,
|
| 221 |
+
artist: (_.creator && _.creator.nickname),
|
| 222 |
+
artwork: _.coverImgUrl || _.picUrl || _.coverImg,
|
| 223 |
+
description: _.description,
|
| 224 |
+
worksNum: _.trackCount,
|
| 225 |
+
playCount: _.playCount,
|
| 226 |
+
date: _.updateTime,
|
| 227 |
+
createUserId: _.userId,
|
| 228 |
+
createTime: _.createTime,
|
| 229 |
+
content: 2
|
| 230 |
+
};
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
// 获取排行榜详情
|
| 234 |
+
async function getTopListDetail(topListItem: any, music_u?: string): Promise<any> {
|
| 235 |
+
const res = await getMusicSheetInfo(topListItem, 1, music_u);
|
| 236 |
+
res.topListItem = res.sheetItem;
|
| 237 |
+
res.topListItem.content = 3;
|
| 238 |
+
return res;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
// API 路由处理器
|
| 242 |
+
async function handleRequest(req: Request): Promise<Response> {
|
| 243 |
+
// 处理 CORS 预检请求
|
| 244 |
+
if (req.method === "OPTIONS") {
|
| 245 |
+
return new Response(null, {
|
| 246 |
+
status: 200,
|
| 247 |
+
headers: corsHeaders
|
| 248 |
+
});
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
const url = new URL(req.url);
|
| 252 |
+
const path = url.pathname;
|
| 253 |
+
const params = url.searchParams;
|
| 254 |
+
|
| 255 |
+
try {
|
| 256 |
+
let result: any = {};
|
| 257 |
+
|
| 258 |
+
// 获取请求体数据(如果有)
|
| 259 |
+
let body: any = {};
|
| 260 |
+
if (req.method === "POST") {
|
| 261 |
+
try {
|
| 262 |
+
body = await req.json();
|
| 263 |
+
} catch {
|
| 264 |
+
// 忽略解析错误
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const music_u = params.get("music_u") || body.music_u;
|
| 269 |
+
|
| 270 |
+
switch (path) {
|
| 271 |
+
case "/search":
|
| 272 |
+
const query = params.get("query") || body.query;
|
| 273 |
+
const page = parseInt(params.get("page") || body.page || "1");
|
| 274 |
+
const type = params.get("type") || body.type || "music";
|
| 275 |
+
result = await search(query, page, type, music_u);
|
| 276 |
+
break;
|
| 277 |
+
|
| 278 |
+
case "/music/info":
|
| 279 |
+
const musicId = params.get("id") || body.id;
|
| 280 |
+
result = await getMusicInfo({ id: musicId }, music_u);
|
| 281 |
+
break;
|
| 282 |
+
|
| 283 |
+
case "/music/url":
|
| 284 |
+
const songId = params.get("id") || body.id;
|
| 285 |
+
const quality = params.get("quality") || body.quality || "standard";
|
| 286 |
+
const musicItem = { id: songId, qualities: { [quality]: {} } };
|
| 287 |
+
result = await getMediaSource(musicItem, quality, music_u);
|
| 288 |
+
break;
|
| 289 |
+
|
| 290 |
+
case "/lyric":
|
| 291 |
+
const lyricId = params.get("id") || body.id;
|
| 292 |
+
result = await getLyric({ id: lyricId }, music_u);
|
| 293 |
+
break;
|
| 294 |
+
|
| 295 |
+
case "/playlist/detail":
|
| 296 |
+
const playlistId = params.get("id") || body.id;
|
| 297 |
+
const playlistPage = parseInt(params.get("page") || body.page || "1");
|
| 298 |
+
result = await getMusicSheetInfo({ id: playlistId }, playlistPage, music_u);
|
| 299 |
+
break;
|
| 300 |
+
|
| 301 |
+
case "/album/detail":
|
| 302 |
+
const albumId = params.get("id") || body.id;
|
| 303 |
+
result = await getAlbumInfo({ id: albumId }, music_u);
|
| 304 |
+
break;
|
| 305 |
+
|
| 306 |
+
case "/artist/works":
|
| 307 |
+
const artistId = params.get("id") || body.id;
|
| 308 |
+
const artistPage = parseInt(params.get("page") || body.page || "1");
|
| 309 |
+
const artistType = params.get("type") || body.type || "music";
|
| 310 |
+
result = await getArtistWorks({ id: artistId }, artistPage, artistType, music_u);
|
| 311 |
+
break;
|
| 312 |
+
|
| 313 |
+
case "/toplists":
|
| 314 |
+
result = await getTopLists(music_u);
|
| 315 |
+
break;
|
| 316 |
+
|
| 317 |
+
case "/toplistdetail":
|
| 318 |
+
result = await getTopListDetail(
|
| 319 |
+
{ id: params.get("id") || body.id },
|
| 320 |
+
music_u
|
| 321 |
+
);
|
| 322 |
+
break;
|
| 323 |
+
|
| 324 |
+
case "/recommend/tags":
|
| 325 |
+
result = await getRecommendSheetTags(music_u);
|
| 326 |
+
break;
|
| 327 |
+
|
| 328 |
+
case "/recommend/sheets":
|
| 329 |
+
const tag = params.get("tag") || body.tag;
|
| 330 |
+
const sheetPage = parseInt(params.get("page") || body.page || "1");
|
| 331 |
+
result = await getRecommendSheetsByTag(tag, sheetPage, music_u);
|
| 332 |
+
break;
|
| 333 |
+
|
| 334 |
+
case "/import/sheet":
|
| 335 |
+
const urlLike = params.get("url") || body.url;
|
| 336 |
+
result = await importMusicSheet(urlLike, music_u);
|
| 337 |
+
break;
|
| 338 |
+
|
| 339 |
+
case "/import/music":
|
| 340 |
+
const musicUrl = params.get("url") || body.url;
|
| 341 |
+
result = await importMusicItem(musicUrl, music_u);
|
| 342 |
+
break;
|
| 343 |
+
|
| 344 |
+
case "/comments":
|
| 345 |
+
const commentId = params.get("id") || body.id;
|
| 346 |
+
const commentPage = parseInt(params.get("page") || body.page || "1");
|
| 347 |
+
result = await getMusicComments({ id: commentId }, commentPage, music_u);
|
| 348 |
+
break;
|
| 349 |
+
|
| 350 |
+
// case "/":
|
| 351 |
+
// case "/health":
|
| 352 |
+
// result = {
|
| 353 |
+
// status: "ok",
|
| 354 |
+
// message: "网易云音乐 API 服务正在运行",
|
| 355 |
+
// version: "2025.01.22",
|
| 356 |
+
// endpoints: [
|
| 357 |
+
// "/search - 搜索音乐",
|
| 358 |
+
// "/music/info - 获取歌曲详情",
|
| 359 |
+
// "/music/url - 获取播放链接",
|
| 360 |
+
// "/lyric - 获取歌词",
|
| 361 |
+
// "/playlist/detail - 获取歌单详情",
|
| 362 |
+
// "/album/detail - 获取专辑详情",
|
| 363 |
+
// "/artist/works - 获取歌手作品",
|
| 364 |
+
// "/toplists - 获取排行榜",
|
| 365 |
+
// "/recommend/tags - 获取推荐标签",
|
| 366 |
+
// "/recommend/sheets - 获取推荐歌单",
|
| 367 |
+
// "/import/sheet - 导入歌单",
|
| 368 |
+
// "/import/music - 导入单曲",
|
| 369 |
+
// "/comments - 获取评论"
|
| 370 |
+
// ]
|
| 371 |
+
// };
|
| 372 |
+
// break;
|
| 373 |
+
case "/":
|
| 374 |
+
case "/health":
|
| 375 |
+
result = {
|
| 376 |
+
status: "ok",
|
| 377 |
+
message: "网易云音乐 API 服务正在运行",
|
| 378 |
+
version: "2025.07.22",
|
| 379 |
+
description: "基于 Deno 的网易云音乐 API 服务,支持搜索、播放、歌词等功能",
|
| 380 |
+
baseUrl: `${url.protocol}//${url.host}`,
|
| 381 |
+
authentication: {
|
| 382 |
+
description: "部分接口需要登录态,可通过 music_u 参数传递",
|
| 383 |
+
parameter: "music_u",
|
| 384 |
+
usage: "在查询参数或请求体中添加 music_u 字段"
|
| 385 |
+
},
|
| 386 |
+
endpoints: [
|
| 387 |
+
{
|
| 388 |
+
path: "/search",
|
| 389 |
+
method: "GET/POST",
|
| 390 |
+
description: "搜索音乐、专辑、歌手、歌单",
|
| 391 |
+
parameters: {
|
| 392 |
+
query: "搜索关键词(必需)",
|
| 393 |
+
page: "页码,默认为1",
|
| 394 |
+
type: "搜索类型:music(歌曲)、album(专辑)、artist(歌手)、sheet(歌单)、lyric(歌词),默认music",
|
| 395 |
+
music_u: "登录凭证(可选)"
|
| 396 |
+
},
|
| 397 |
+
example: "/search?query=周杰伦&page=1&type=music"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
path: "/music/info",
|
| 401 |
+
method: "GET/POST",
|
| 402 |
+
description: "获取歌曲详细信息",
|
| 403 |
+
parameters: {
|
| 404 |
+
id: "歌曲ID(必需)",
|
| 405 |
+
music_u: "登录凭证(可选)"
|
| 406 |
+
},
|
| 407 |
+
example: "/music/info?id=123456"
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
path: "/music/url",
|
| 411 |
+
method: "GET/POST",
|
| 412 |
+
description: "获取歌曲播放链接",
|
| 413 |
+
parameters: {
|
| 414 |
+
id: "歌曲ID(必需)",
|
| 415 |
+
quality: "音质:low(标准)、standard(较高)、high(无损)、super(Hi-Res),默认standard",
|
| 416 |
+
music_u: "登录凭证(推荐,提高成功率)"
|
| 417 |
+
},
|
| 418 |
+
example: "/music/url?id=123456&quality=high"
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
path: "/lyric",
|
| 422 |
+
method: "GET/POST",
|
| 423 |
+
description: "获取歌曲歌词",
|
| 424 |
+
parameters: {
|
| 425 |
+
id: "歌曲ID(必需)",
|
| 426 |
+
music_u: "登录凭证(可选)"
|
| 427 |
+
},
|
| 428 |
+
example: "/lyric?id=123456"
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
path: "/playlist/detail",
|
| 432 |
+
method: "GET/POST",
|
| 433 |
+
description: "获取歌单详情和歌曲列表",
|
| 434 |
+
parameters: {
|
| 435 |
+
id: "歌单ID(必需)",
|
| 436 |
+
page: "页码,默认为1",
|
| 437 |
+
music_u: "登录凭证(可选)"
|
| 438 |
+
},
|
| 439 |
+
example: "/playlist/detail?id=123456"
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
path: "/album/detail",
|
| 443 |
+
method: "GET/POST",
|
| 444 |
+
description: "获取专辑详情和歌曲列表",
|
| 445 |
+
parameters: {
|
| 446 |
+
id: "专辑ID(必需)",
|
| 447 |
+
music_u: "登录凭证(可选)"
|
| 448 |
+
},
|
| 449 |
+
example: "/album/detail?id=123456"
|
| 450 |
+
},
|
| 451 |
+
{
|
| 452 |
+
path: "/artist/works",
|
| 453 |
+
method: "GET/POST",
|
| 454 |
+
description: "获取歌手作品(热门歌曲或专辑)",
|
| 455 |
+
parameters: {
|
| 456 |
+
id: "歌手ID(必需)",
|
| 457 |
+
page: "页码,默认为1",
|
| 458 |
+
type: "作品类型:music(歌曲)、album(专辑),默认music",
|
| 459 |
+
music_u: "登录凭证(可选)"
|
| 460 |
+
},
|
| 461 |
+
example: "/artist/works?id=123456&type=music"
|
| 462 |
+
},
|
| 463 |
+
{
|
| 464 |
+
path: "/toplists",
|
| 465 |
+
method: "GET/POST",
|
| 466 |
+
description: "获取所有排行榜列表",
|
| 467 |
+
parameters: {
|
| 468 |
+
music_u: "登录凭证(可选)"
|
| 469 |
+
},
|
| 470 |
+
example: "/toplists"
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
path: "/recommend/tags",
|
| 474 |
+
method: "GET/POST",
|
| 475 |
+
description: "获取歌单推荐标签",
|
| 476 |
+
parameters: {
|
| 477 |
+
music_u: "登录凭证(可选)"
|
| 478 |
+
},
|
| 479 |
+
example: "/recommend/tags"
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
path: "/recommend/sheets",
|
| 483 |
+
method: "GET/POST",
|
| 484 |
+
description: "根据标签获取推荐歌单",
|
| 485 |
+
parameters: {
|
| 486 |
+
tag: "标签名称(可选,不传则返回推荐歌单)",
|
| 487 |
+
page: "页码,默认为1",
|
| 488 |
+
music_u: "登录凭证(可选)"
|
| 489 |
+
},
|
| 490 |
+
example: "/recommend/sheets?tag=流行&page=1"
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
path: "/import/sheet",
|
| 494 |
+
method: "GET/POST",
|
| 495 |
+
description: "通过URL或ID导入歌单",
|
| 496 |
+
parameters: {
|
| 497 |
+
url: "歌单URL或歌单ID(必需)",
|
| 498 |
+
music_u: "登录凭证(可选)"
|
| 499 |
+
},
|
| 500 |
+
example: "/import/sheet?url=123456 或 /import/sheet?url=https://music.163.com/playlist?id=123456"
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
path: "/import/music",
|
| 504 |
+
method: "GET/POST",
|
| 505 |
+
description: "通过URL或ID导入单曲",
|
| 506 |
+
parameters: {
|
| 507 |
+
url: "歌曲URL或歌曲ID(必需)",
|
| 508 |
+
music_u: "登录凭证(可选)"
|
| 509 |
+
},
|
| 510 |
+
example: "/import/music?url=123456 或 /import/music?url=https://music.163.com/song?id=123456"
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
path: "/comments",
|
| 514 |
+
method: "GET/POST",
|
| 515 |
+
description: "获取歌曲评论",
|
| 516 |
+
parameters: {
|
| 517 |
+
id: "歌曲ID(必需)",
|
| 518 |
+
page: "页码,默认为1",
|
| 519 |
+
music_u: "登录凭证(可选)"
|
| 520 |
+
},
|
| 521 |
+
example: "/comments?id=123456&page=1"
|
| 522 |
+
}
|
| 523 |
+
],
|
| 524 |
+
responseFormat: {
|
| 525 |
+
success: {
|
| 526 |
+
description: "成功响应格式",
|
| 527 |
+
example: {
|
| 528 |
+
isEnd: "boolean - 是否为最后一页(分页接口)",
|
| 529 |
+
data: "array - 数据列表",
|
| 530 |
+
total: "number - 总数(部分接口)"
|
| 531 |
+
}
|
| 532 |
+
},
|
| 533 |
+
error: {
|
| 534 |
+
description: "错误响应格式",
|
| 535 |
+
example: {
|
| 536 |
+
error: "string - 错误描述",
|
| 537 |
+
message: "string - 详细错误信息"
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
notes: [
|
| 542 |
+
"所有接口均支持 GET 和 POST 请求方式",
|
| 543 |
+
"GET 请求参数通过查询字符串传递",
|
| 544 |
+
"POST 请求参数通过 JSON 格式的请求体传递",
|
| 545 |
+
"推荐使用 music_u 参数以获得更好的接口响应",
|
| 546 |
+
"部分高音质音频需要会员权限",
|
| 547 |
+
"请遵守相关法律法规,合理使用 API"
|
| 548 |
+
]
|
| 549 |
+
};
|
| 550 |
+
break;
|
| 551 |
+
|
| 552 |
+
default:
|
| 553 |
+
return new Response(JSON.stringify({
|
| 554 |
+
error: "API 路径不存在",
|
| 555 |
+
path: path
|
| 556 |
+
}), {
|
| 557 |
+
status: 404,
|
| 558 |
+
headers: {
|
| 559 |
+
"Content-Type": "application/json",
|
| 560 |
+
...corsHeaders
|
| 561 |
+
}
|
| 562 |
+
});
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
return new Response(JSON.stringify(result), {
|
| 566 |
+
status: 200,
|
| 567 |
+
headers: {
|
| 568 |
+
"Content-Type": "application/json",
|
| 569 |
+
...corsHeaders
|
| 570 |
+
}
|
| 571 |
+
});
|
| 572 |
+
|
| 573 |
+
} catch (error) {
|
| 574 |
+
// console.error("API Error:", error);
|
| 575 |
+
// return new Response(JSON.stringify({
|
| 576 |
+
// error: "服务器内部错误",
|
| 577 |
+
// message: error.message
|
| 578 |
+
// }), {
|
| 579 |
+
// status: 500,
|
| 580 |
+
// headers: {
|
| 581 |
+
// "Content-Type": "application/json",
|
| 582 |
+
// ...corsHeaders
|
| 583 |
+
// }
|
| 584 |
+
// });
|
| 585 |
+
console.error("API错误:", error);
|
| 586 |
+
console.error("错误堆栈:", error.stack);
|
| 587 |
+
|
| 588 |
+
// 尝试获取请求信息以便调试
|
| 589 |
+
try {
|
| 590 |
+
const url = new URL(req.url);
|
| 591 |
+
console.error("路径:", url.pathname);
|
| 592 |
+
console.error("查询参数:", Object.fromEntries(url.searchParams.entries()));
|
| 593 |
+
|
| 594 |
+
if (req.method === "POST") {
|
| 595 |
+
try {
|
| 596 |
+
const cloned = req.clone();
|
| 597 |
+
const body = await cloned.text();
|
| 598 |
+
console.error("请求体:", body);
|
| 599 |
+
} catch (e) {
|
| 600 |
+
console.error("无法读取请求体:", e);
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
} catch (e) {
|
| 604 |
+
console.error("无法获取请求信息:", e);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
return new Response(JSON.stringify({
|
| 608 |
+
error: "服务器内部错误",
|
| 609 |
+
message: error.message,
|
| 610 |
+
stack: Deno.env.get("DEBUG") === "true" ? error.stack : undefined
|
| 611 |
+
}), {
|
| 612 |
+
status: 500,
|
| 613 |
+
headers: {
|
| 614 |
+
"Content-Type": "application/json",
|
| 615 |
+
...corsHeaders
|
| 616 |
+
}
|
| 617 |
+
});
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
// 实现具体的 API 功能函数
|
| 622 |
+
async function search(query: string, page: number, type: string, music_u?: string): Promise<any> {
|
| 623 |
+
const stype: any = {
|
| 624 |
+
music: { t: "song", m: formatMusicItem },
|
| 625 |
+
album: { t: "album", v: "/v1", m: formatAlbumItem },
|
| 626 |
+
artist: { t: "artist", v: "/v1", m: formatArtistItem },
|
| 627 |
+
sheet: { t: "playlist", m: formatSheetItem },
|
| 628 |
+
lyric: { t: "resource/lyric", m: formatMusicItem }
|
| 629 |
+
}[type];
|
| 630 |
+
|
| 631 |
+
const path = "/api" + (stype.v || "") + "/search/" + stype.t + (stype.t.includes("/") ? "" : "/get");
|
| 632 |
+
const data = {
|
| 633 |
+
"filterCode": "-1",
|
| 634 |
+
"offset": ((page - 1) * pageSize).toString(),
|
| 635 |
+
"limit": pageSize.toString(),
|
| 636 |
+
"channel": "typing",
|
| 637 |
+
"keyword": query,
|
| 638 |
+
"scene": "normal",
|
| 639 |
+
"s": query,
|
| 640 |
+
};
|
| 641 |
+
|
| 642 |
+
const res = await EAPI(path, data, music_u);
|
| 643 |
+
const result = res.data || res.result;
|
| 644 |
+
const list = result.resources || result.albums || result.artists || result.songs || [];
|
| 645 |
+
const total1 = page * pageSize;
|
| 646 |
+
const total2 = result.songCount || result.playlistCount || result.albumCount || result.totalCount || (total1 - pageSize + list.length);
|
| 647 |
+
|
| 648 |
+
return {
|
| 649 |
+
isEnd: total2 <= total1,
|
| 650 |
+
data: list.map(stype.m)
|
| 651 |
+
};
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
async function getMusicInfo(musicItem: any, music_u?: string): Promise<any> {
|
| 655 |
+
const res = await EAPI("/api/v3/song/detail", {
|
| 656 |
+
c: `[{"id":"${musicItem.id}"}]`
|
| 657 |
+
}, music_u);
|
| 658 |
+
const song = res.songs[0] || res.privileges[0];
|
| 659 |
+
song.privilege = res.privileges[0];
|
| 660 |
+
return formatMusicItem(song);
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
async function getMediaSource(musicItem: any, quality: string, music_u?: string): Promise<any> {
|
| 664 |
+
if (!musicItem.qualities[quality]) {
|
| 665 |
+
return false;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
const res = await EAPI("/api/song/enhance/player/url/v1", {
|
| 669 |
+
ids: `["${musicItem.id}"]`,
|
| 670 |
+
encodeType: "flac",
|
| 671 |
+
immerseType: "c51",
|
| 672 |
+
trialMode: "23",
|
| 673 |
+
level: qualityMap[quality as keyof typeof qualityMap]
|
| 674 |
+
}, music_u);
|
| 675 |
+
|
| 676 |
+
if (res.data && res.data[0] && res.data[0].url) {
|
| 677 |
+
return {
|
| 678 |
+
url: String(res.data[0].url).split("?")[0],
|
| 679 |
+
size: res.data[0].size,
|
| 680 |
+
quality,
|
| 681 |
+
};
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
return false;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
async function getLyric(musicItem: any, music_u?: string): Promise<any> {
|
| 688 |
+
const res = await EAPI("/api/song/lyric", {
|
| 689 |
+
id: musicItem.id,
|
| 690 |
+
lv: -1,
|
| 691 |
+
kv: -1,
|
| 692 |
+
tv: -1
|
| 693 |
+
}, music_u);
|
| 694 |
+
|
| 695 |
+
return {
|
| 696 |
+
rawLrc: res.lrc?.lyric || "",
|
| 697 |
+
};
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
async function getMusicSheetInfo(sheet: any, page: number = 1, music_u?: string): Promise<any> {
|
| 701 |
+
const res = await EAPI("/api/v6/playlist/detail", {
|
| 702 |
+
n: 99999,
|
| 703 |
+
id: sheet.id
|
| 704 |
+
}, music_u);
|
| 705 |
+
|
| 706 |
+
const playlist = res.playlist;
|
| 707 |
+
const list = playlist.tracks || [];
|
| 708 |
+
|
| 709 |
+
return {
|
| 710 |
+
isEnd: 99999 >= playlist.trackCount,
|
| 711 |
+
sheetItem: formatSheetItem(playlist),
|
| 712 |
+
musicList: list.map(formatMusicItem)
|
| 713 |
+
};
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
async function getAlbumInfo(albumItem: any, music_u?: string): Promise<any> {
|
| 717 |
+
const res = await EAPI("/api/v1/album/" + albumItem.id, {}, music_u);
|
| 718 |
+
return {
|
| 719 |
+
isEnd: true,
|
| 720 |
+
albumItem: formatAlbumItem(res.album),
|
| 721 |
+
musicList: (res.songs || []).map(formatMusicItem),
|
| 722 |
+
};
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
async function getArtistWorks(artistItem: any, page: number, type: string, music_u?: string): Promise<any> {
|
| 726 |
+
const typeConfig: any = {
|
| 727 |
+
"music": {
|
| 728 |
+
"path1": "/api/v1/artist/",
|
| 729 |
+
"path2": "hotSongs",
|
| 730 |
+
"mapJs": formatMusicItem
|
| 731 |
+
},
|
| 732 |
+
"album": {
|
| 733 |
+
"path1": "/api/artist/albums/",
|
| 734 |
+
"path2": "hotAlbums",
|
| 735 |
+
"mapJs": formatAlbumItem
|
| 736 |
+
},
|
| 737 |
+
}[type];
|
| 738 |
+
|
| 739 |
+
const res = await EAPI(typeConfig.path1 + artistItem.id, {}, music_u);
|
| 740 |
+
return {
|
| 741 |
+
isEnd: true,
|
| 742 |
+
artistItem: formatArtistItem(res.artist),
|
| 743 |
+
data: res[typeConfig.path2].map(typeConfig.mapJs),
|
| 744 |
+
};
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
async function getTopLists(music_u?: string): Promise<any> {
|
| 748 |
+
const group1: any[] = [];
|
| 749 |
+
const group2 = await EAPI("/api/toplist/detail/v2", {}, music_u);
|
| 750 |
+
|
| 751 |
+
group2.data.map((item: any) => {
|
| 752 |
+
if (item.list && item.list.length) {
|
| 753 |
+
const group3: any[] = [];
|
| 754 |
+
item.list.map((listItem: any) => {
|
| 755 |
+
if (listItem.id !== 0) {
|
| 756 |
+
group3.push({
|
| 757 |
+
title: listItem.name,
|
| 758 |
+
coverImg: listItem.coverUrl,
|
| 759 |
+
content: 3,
|
| 760 |
+
id: listItem.id,
|
| 761 |
+
description: listItem.updateFrequency
|
| 762 |
+
});
|
| 763 |
+
}
|
| 764 |
+
});
|
| 765 |
+
group1.push({
|
| 766 |
+
title: item.name,
|
| 767 |
+
data: group3
|
| 768 |
+
});
|
| 769 |
+
}
|
| 770 |
+
});
|
| 771 |
+
|
| 772 |
+
return group1;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
async function getRecommendSheetTags(music_u?: string): Promise<any> {
|
| 776 |
+
const pinned = [
|
| 777 |
+
{ title: "推荐", id: "_SPECIAL_CLOUD_VILLAGE_PLAYLIST" },
|
| 778 |
+
{ title: "官方", id: "官方" },
|
| 779 |
+
{ title: "雷达", id: "_RADAR" },
|
| 780 |
+
{ title: "原创", id: "_SPECIAL_ORIGIN_SONG_LOCATION" },
|
| 781 |
+
{ title: "心情", id: "_FEELING_PLAYLIST_LOCATION" },
|
| 782 |
+
{ title: "场景", id: "_SCENE_PLAYLIST_LOCATION" },
|
| 783 |
+
{ title: "专属", id: "_COMBINATION" },
|
| 784 |
+
{ title: "全部", id: "全部歌单" },
|
| 785 |
+
{ title: "新热", id: "_NEW_SONG_AND_ALBUM" },
|
| 786 |
+
{ title: "影���", id: "_FIRM_PLAYLIST" },
|
| 787 |
+
{ title: "奖项", id: "_AWARDS_PLAYLIST" },
|
| 788 |
+
];
|
| 789 |
+
|
| 790 |
+
const data = ["语种", "风格", "场景", "情感", "主题"].map(name => ({
|
| 791 |
+
title: name,
|
| 792 |
+
data: []
|
| 793 |
+
}));
|
| 794 |
+
|
| 795 |
+
const res = await EAPI("/api/playlist/catalogue/v1", {}, music_u);
|
| 796 |
+
res.sub.map((item: any) => {
|
| 797 |
+
data[item.category].data.push({
|
| 798 |
+
title: item.name,
|
| 799 |
+
id: item.name
|
| 800 |
+
});
|
| 801 |
+
});
|
| 802 |
+
|
| 803 |
+
return { pinned, data };
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
async function getRecommendSheetsByTag(tag: string | null, page: number, music_u?: string): Promise<any> {
|
| 807 |
+
let res: any;
|
| 808 |
+
const tagId = tag;
|
| 809 |
+
|
| 810 |
+
if (!tagId || tagId === "" || tagId === "true") {
|
| 811 |
+
res = await EAPI("/api/personalized/playlist", { limit: 30 }, music_u);
|
| 812 |
+
} else if (/^_[A-Z]+/.test(tagId)) {
|
| 813 |
+
res = await EAPI("/api/link/page/rcmd/resource/show", {
|
| 814 |
+
"pageCode": "HOME_RECOMMEND_PAGE",
|
| 815 |
+
"isFirstScreen": "false",
|
| 816 |
+
"cursor": "6",
|
| 817 |
+
"refresh": "true",
|
| 818 |
+
"blockCodeOrderList": `["PAGE_RECOMMEND${tagId}"]`
|
| 819 |
+
}, music_u);
|
| 820 |
+
res = res.data.blocks[0].dslData.blockResource;
|
| 821 |
+
} else {
|
| 822 |
+
res = await EAPI("/api/playlist/list", {
|
| 823 |
+
cat: tagId || "全部",
|
| 824 |
+
order: "hot",
|
| 825 |
+
limit: pageSize,
|
| 826 |
+
offset: (page - 1) * pageSize,
|
| 827 |
+
total: true,
|
| 828 |
+
csrf_token: "",
|
| 829 |
+
}, music_u);
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
const list = res.result || res.playlists || res.resources || [];
|
| 833 |
+
const total1 = page * pageSize;
|
| 834 |
+
const total2 = res.total || (total1 - pageSize + list.length);
|
| 835 |
+
|
| 836 |
+
return {
|
| 837 |
+
isEnd: (res.more !== true) || (total2 <= total1),
|
| 838 |
+
data: list.map(formatSheetItem)
|
| 839 |
+
};
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
async function importMusicSheet(urlLike: string, music_u?: string): Promise<any> {
|
| 843 |
+
let id = (urlLike.match(/^(\d+)$/) || [])[1];
|
| 844 |
+
if (!id && !urlLike.match(/music\.163\.com/i)) {
|
| 845 |
+
return false;
|
| 846 |
+
}
|
| 847 |
+
if (!id) {
|
| 848 |
+
id = (urlLike.match(/playlist(\/|.*?[\?\&]id=)(\d+)/i) || [])[2];
|
| 849 |
+
}
|
| 850 |
+
if (!id) {
|
| 851 |
+
return false;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
const result = await getMusicSheetInfo({ id }, 1, music_u);
|
| 855 |
+
return result.musicList;
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
async function importMusicItem(urlLike: string, music_u?: string): Promise<any> {
|
| 859 |
+
let id = (urlLike.match(/^(\d+)$/) || [])[1];
|
| 860 |
+
if (!id && !urlLike.match(/music\.163\.com/i)) {
|
| 861 |
+
return false;
|
| 862 |
+
}
|
| 863 |
+
if (!id) {
|
| 864 |
+
id = (urlLike.match(/song(.*?[\?\&]id=|\/)(\d+)/i) || [])[2];
|
| 865 |
+
}
|
| 866 |
+
if (!id) {
|
| 867 |
+
return false;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
return await getMusicInfo({ id }, music_u);
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
async function getMusicComments(musicItem: any, page: number = 1, music_u?: string): Promise<any> {
|
| 874 |
+
const res = await EAPI("/api/v2/resource/comments", {
|
| 875 |
+
"threadId": "R_SO_4_" + musicItem.id,
|
| 876 |
+
"cursor": "20",
|
| 877 |
+
"sortType": "1",
|
| 878 |
+
"pageNo": page,
|
| 879 |
+
"pageSize": pageSize.toString(),
|
| 880 |
+
"parentCommentld": "0",
|
| 881 |
+
"showlnner": false
|
| 882 |
+
}, music_u);
|
| 883 |
+
|
| 884 |
+
const formatComment = (comment: any): any => ({
|
| 885 |
+
id: comment.commentId,
|
| 886 |
+
nickName: comment.user && comment.user.nickname,
|
| 887 |
+
avatar: comment.user && comment.user.avatarUrl,
|
| 888 |
+
comment: comment.content,
|
| 889 |
+
like: comment.likedCount,
|
| 890 |
+
createAt: comment.time,
|
| 891 |
+
location: comment.ipLocation && comment.ipLocation.location,
|
| 892 |
+
replies: (comment.beReplied || []).map(formatComment),
|
| 893 |
+
content: 6
|
| 894 |
+
});
|
| 895 |
+
|
| 896 |
+
return {
|
| 897 |
+
isEnd: res.data.hasMore !== true,
|
| 898 |
+
data: res.data.comments.map(formatComment)
|
| 899 |
+
};
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// 启动服务器
|
| 903 |
+
const port = parseInt(Deno.env.get("PORT") || "8080");
|
| 904 |
+
|
| 905 |
+
console.log(`🎵 Deno 网易云音乐 API 服务启动成功!`);
|
| 906 |
+
console.log(`🚀 服务运行在: http://localhost:${port}`);
|
| 907 |
+
console.log(`📖 API 文档: http://localhost:${port}/`);
|
| 908 |
+
|
| 909 |
+
serve(handleRequest, { port });
|