feat: add netease resolver internally.
This commit is contained in:
parent
2bf505f10e
commit
bef662d5c0
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -126,6 +126,7 @@
|
|||||||
"taza",
|
"taza",
|
||||||
"teruteru",
|
"teruteru",
|
||||||
"timestamptz",
|
"timestamptz",
|
||||||
|
"tlyric",
|
||||||
"toolsmp",
|
"toolsmp",
|
||||||
"tsconfigs",
|
"tsconfigs",
|
||||||
"tview",
|
"tview",
|
||||||
|
830
package-lock.json
generated
830
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -51,17 +51,17 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"marked": "^14.1.3",
|
"marked": "^14.1.3",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.1",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"ultrahtml": "^1.5.3"
|
"ultrahtml": "^1.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@napi-rs/canvas": "^0.1.58",
|
"@napi-rs/canvas": "^0.1.59",
|
||||||
"@types/lodash": "^4.17.12",
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.7.9",
|
"@types/node": "^22.8.4",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"@types/qrcode-svg": "^1.1.5",
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"@types/unist": "^3.0.3",
|
"@types/unist": "^3.0.3",
|
||||||
@ -83,6 +83,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "npm@10.9.0",
|
"packageManager": "npm@10.9.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.10.0"
|
"node": "23.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ for (const p of ps) {
|
|||||||
artist: p.dataset.artist,
|
artist: p.dataset.artist,
|
||||||
url: p.dataset.url,
|
url: p.dataset.url,
|
||||||
cover: p.dataset.cover,
|
cover: p.dataset.cover,
|
||||||
|
lrc: p.dataset.lrc,
|
||||||
theme: '#ebd0c2',
|
theme: '#ebd0c2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -3,14 +3,14 @@ import { resolveSong, type MusicPlayerProps } from '@/components/player/resolver
|
|||||||
|
|
||||||
interface Props extends MusicPlayerProps {}
|
interface Props extends MusicPlayerProps {}
|
||||||
|
|
||||||
const { name, artist, url, pic } = await resolveSong(Astro.props);
|
const { name, artist, url, pic, lrc } = await resolveSong(Astro.props);
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
url === '' ? (
|
url === '' ? (
|
||||||
<p>歌曲加载失败</p>
|
<p>歌曲加载失败</p>
|
||||||
) : (
|
) : (
|
||||||
<div class="aplayer" data-name={name} data-artist={artist} data-url={url} data-cover={pic}>
|
<div class="aplayer" data-name={name} data-artist={artist} data-url={url} data-cover={pic} data-lrc={lrc}>
|
||||||
音乐正在加载中 ...
|
音乐正在加载中 ...
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
4
src/components/player/netease/config.js
Normal file
4
src/components/player/netease/config.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const net_ease_anonymous_token =
|
||||||
|
'de91e1f8119d32e01cc73efcb82c0a30c9137e8d4f88dbf5e3d7bf3f28998f21add2bc8204eeee5e56c0bbb8743574b46ca2c10c35dc172199bef9bf4d60ecdeab066bb4dc737d1c3324751bcc9aaf44c3061cd18d77b7a0';
|
||||||
|
|
||||||
|
export { net_ease_anonymous_token };
|
60
src/components/player/netease/crypto.js
Normal file
60
src/components/player/netease/crypto.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
const iv = Buffer.from('0102030405060708');
|
||||||
|
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud');
|
||||||
|
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q');
|
||||||
|
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
const publicKey =
|
||||||
|
'-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----';
|
||||||
|
const eapiKey = 'e82ckenh8dichen8';
|
||||||
|
|
||||||
|
const aesEncrypt = (buffer, mode, key, iv) => {
|
||||||
|
const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv);
|
||||||
|
return Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rsaEncrypt = (buffer, key) => {
|
||||||
|
return crypto.publicEncrypt(
|
||||||
|
{ key: key, padding: crypto.constants.RSA_NO_PADDING },
|
||||||
|
Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const weapi = (object) => {
|
||||||
|
const text = JSON.stringify(object);
|
||||||
|
const secretKey = crypto.randomBytes(16).map((n) => base62.charAt(n % 62).charCodeAt());
|
||||||
|
return {
|
||||||
|
params: aesEncrypt(
|
||||||
|
Buffer.from(aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64')),
|
||||||
|
'cbc',
|
||||||
|
secretKey,
|
||||||
|
iv,
|
||||||
|
).toString('base64'),
|
||||||
|
encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const linuxapi = (object) => {
|
||||||
|
const text = JSON.stringify(object);
|
||||||
|
return {
|
||||||
|
eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '').toString('hex').toUpperCase(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const eapi = (url, object) => {
|
||||||
|
const text = typeof object === 'object' ? JSON.stringify(object) : object;
|
||||||
|
const message = `nobody${url}use${text}md5forencrypt`;
|
||||||
|
const digest = crypto.createHash('md5').update(message).digest('hex');
|
||||||
|
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
|
||||||
|
return {
|
||||||
|
params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '').toString('hex').toUpperCase(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrypt = (cipherBuffer) => {
|
||||||
|
const decipher = crypto.createDecipheriv('aes-128-ecb', eapiKey, '');
|
||||||
|
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { weapi, linuxapi, eapi, decrypt };
|
21
src/components/player/netease/lyric.js
Normal file
21
src/components/player/netease/lyric.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { request } from './util.js';
|
||||||
|
|
||||||
|
export const get_lyric = async (id, cookie) => {
|
||||||
|
// query.cookie.os = 'ios'
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: id,
|
||||||
|
tv: -1,
|
||||||
|
lv: -1,
|
||||||
|
rv: -1,
|
||||||
|
kv: -1,
|
||||||
|
};
|
||||||
|
const res = await request('POST', 'https://music.163.com/api/song/lyric?_nmclfl=1', data, {
|
||||||
|
crypto: 'api',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lyric: res.lrc?.lyric || '',
|
||||||
|
tlyric: res.tlyric?.lyric || '',
|
||||||
|
};
|
||||||
|
};
|
35
src/components/player/netease/song.js
Normal file
35
src/components/player/netease/song.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { map_song_list, request } from './util.js';
|
||||||
|
|
||||||
|
export const get_song_url = async (id, cookie = '') => {
|
||||||
|
const data = {
|
||||||
|
ids: `[${id}]`,
|
||||||
|
level: 'standard',
|
||||||
|
encodeType: 'flac',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request('POST', 'https://interface.music.163.com/eapi/song/enhance/player/url/v1', data, {
|
||||||
|
crypto: 'eapi',
|
||||||
|
url: '/api/song/enhance/player/url/v1',
|
||||||
|
cookie: {},
|
||||||
|
});
|
||||||
|
const url = res.data[0]?.url?.replace('http://', 'https://');
|
||||||
|
|
||||||
|
return url || `https://music.163.com/song/media/outer/url?id=${id}.mp3`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const get_song_info = async (id, cookie = '') => {
|
||||||
|
const ids = [id];
|
||||||
|
const data = {
|
||||||
|
c: `[${ids.map((id) => `{"id":${id}}`).join(',')}]`,
|
||||||
|
};
|
||||||
|
let res = await request('POST', 'https://music.163.com/api/v3/song/detail', data, {
|
||||||
|
crypto: 'weapi',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.songs) {
|
||||||
|
throw res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = map_song_list(res);
|
||||||
|
return res;
|
||||||
|
};
|
181
src/components/player/netease/util.js
Normal file
181
src/components/player/netease/util.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,8 +1,12 @@
|
|||||||
|
import { get_lyric } from './netease/lyric';
|
||||||
|
import { get_song_info, get_song_url } from './netease/song';
|
||||||
|
|
||||||
export type Song = {
|
export type Song = {
|
||||||
name: string;
|
name: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
url: string;
|
url: string;
|
||||||
pic: string;
|
pic: string;
|
||||||
|
lrc: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The props for music player. We support both netease music and direct linked music.
|
// The props for music player. We support both netease music and direct linked music.
|
||||||
@ -11,25 +15,18 @@ export interface MusicPlayerProps {
|
|||||||
song?: Song;
|
song?: Song;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptySong = { name: '', artist: '', url: '', pic: '' };
|
const emptySong = { name: '', artist: '', url: '', pic: '', lrc: '' };
|
||||||
|
|
||||||
const song = async (props: MusicPlayerProps): Promise<Song> => {
|
const song = async (props: MusicPlayerProps): Promise<Song> => {
|
||||||
const { netease, song } = props;
|
const { netease, song } = props;
|
||||||
|
|
||||||
if (netease) {
|
if (netease) {
|
||||||
// Fix the UNABLE_TO_GET_ISSUER_CERT_LOCALLY issue for the mirror site.
|
const info = await get_song_info(netease);
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
const url = await get_song_url(netease);
|
||||||
|
const lrc = await get_lyric(netease);
|
||||||
// https://github.com/injahow/meting-api
|
|
||||||
const data = await fetch(`https://api.injahow.cn/meting/?type=song&id=${netease}`)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
return [emptySong];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check the return result.
|
// Check the return result.
|
||||||
return { name: data[0].name, artist: data[0].artist, url: data[0].url, pic: data[0].pic };
|
return { name: info[0].title, artist: info[0].author, url: url, pic: info[0].pic, lrc: lrc.lyric };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (song) {
|
if (song) {
|
||||||
|
Loading…
Reference in New Issue
Block a user