stnh70 commited on
Commit
e918333
·
verified ·
1 Parent(s): e4a1a54

Create web.ts

Browse files
Files changed (1) hide show
  1. web.ts +909 -0
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 });