Skip to content

Commit

Permalink
feat: add music card to LinkCard (netease/tencent) (#470)
Browse files Browse the repository at this point in the history
* feat: add QQMusicSong to LinkCard

* feat: add NeteaseMusicSong to LinkCard

* fix: music card style

* chore: remove unnecessary code
  • Loading branch information
FoskyM authored Oct 9, 2024
1 parent 1cff613 commit 2165b6a
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/app/api/music/netease/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
constants,
createCipheriv,
createHash,
publicEncrypt,
randomBytes,
} from 'node:crypto'

const iv = Buffer.from('0102030405060708')
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud')
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const publicKey =
'-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'
const eapiKey = 'e82ckenh8dichen8'

const aesEncrypt = (
buffer: Buffer,
mode: string,
key: Uint8Array | Buffer | string,
iv: Buffer | string,
) => {
const cipher = createCipheriv(`aes-128-${mode}`, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
}

const rsaEncrypt = (buffer: Uint8Array) =>
publicEncrypt(
{ key: publicKey, padding: constants.RSA_NO_PADDING },
Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]),
)

export const weapi = (
object: Record<string, number | string | boolean>,
): { params: string; encSecKey: string } => {
const text = JSON.stringify(object)
const secretKey = randomBytes(16).map((n) =>
base62.charAt(n % 62).charCodeAt(0),
)
return {
params: aesEncrypt(
Buffer.from(
aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64'),
),
'cbc',
secretKey,
iv,
).toString('base64'),
encSecKey: rsaEncrypt(secretKey.reverse()).toString('hex'),
}
}

export const eapi = (
url: string,
object: Record<string, unknown>,
): { params: string } => {
const text = JSON.stringify(object)
const message = `nobody${url}use${text}md5forencrypt`
const digest = createHash('md5').update(message).digest('hex')
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
return {
params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '')
.toString('hex')
.toUpperCase(),
}
}
24 changes: 24 additions & 0 deletions src/app/api/music/netease/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

import { weapi } from './crypto'

export const POST = async (req: NextRequest) => {
const requestBody = await req.json()
const { songId } = requestBody
const data = {
c: JSON.stringify([{ id: songId, v: 0 }]),
}
const body: any = weapi(data)
const bodyString = `params=${encodeURIComponent(body.params)}&encSecKey=${encodeURIComponent(body.encSecKey)}`

const response = await fetch('http://music.163.com/weapi/v3/song/detail', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: bodyString,
})

return NextResponse.json(await response.json())
}
19 changes: 19 additions & 0 deletions src/app/api/music/tencent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

export const POST = async (req: NextRequest) => {
const requestBody = await req.json()
const { songId } = requestBody

const response = await fetch(
`https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg?songmid=${songId}&platform=yqq&format=json`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
)

return NextResponse.json(await response.json())
}
113 changes: 113 additions & 0 deletions src/components/ui/link-card/LinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ const LinkCardImpl: FC<LinkCardProps> = (props) => {
[LinkCardSource.GHPr]: fetchGitHubPRData,
[LinkCardSource.Self]: fetchMxSpaceData,
[LinkCardSource.LEETCODE]: fetchLeetCodeQuestionData,
[LinkCardSource.QQMusicSong]: fetchQQMusicSongData,
[LinkCardSource.NeteaseMusicSong]: fetchNeteaseMusicSongData,
} as Record<LinkCardSource, FetchObject>
if (tmdbEnabled)
fetchDataFunctions[LinkCardSource.TMDB] = fetchTheMovieDBData
Expand Down Expand Up @@ -615,3 +617,114 @@ const fetchLeetCodeQuestionData: FetchObject = {
}
},
}

const fetchQQMusicSongData: FetchObject = {
isValid: (id) => {
return typeof id === 'string' && id.length > 0
},
fetch: async (id, setCardInfo, _setFullUrl) => {
try {
const songData = await fetch(`/api/music/tencent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ songId: id }),
}).then(async (res) => {
if (!res.ok) {
throw new Error('Failed to fetch QQMusic song title')
}
return res.json()
})
const songInfo = songData.data[0]
const albumId = songInfo.album.mid
setCardInfo({
title: (
<>
<span>{songInfo.title}</span>
{songInfo.subtitle && (
<span className="ml-2 text-sm text-gray-400">
{songInfo.subtitle}
</span>
)}
</>
),
desc: (
<>
<span className="block">
<span className="font-bold">歌手:</span>
<span>
{songInfo.singer.map((person: any) => person.name).join(' / ')}
</span>
</span>
<span className="block">
<span className="font-bold">专辑:</span>
<span>{songInfo.album.name}</span>
</span>
</>
),
image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumId}.jpg?max_age=2592000`,
color: '#31c27c',
})
} catch (err) {
console.error('Error fetching QQMusic song data:', err)
throw err
}
},
}

const fetchNeteaseMusicSongData: FetchObject = {
isValid: (id) => {
return id.length > 0
},
fetch: async (id, setCardInfo, _setFullUrl) => {
try {
const songData = await fetch(`/api/music/netease`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ songId: id }),
}).then(async (res) => {
if (!res.ok) {
throw new Error('Failed to fetch NeteaseMusic song title')
}
return res.json()
})
const songInfo = songData.songs[0]
const albumInfo = songInfo.al
const singerInfo = songInfo.ar
setCardInfo({
title: (
<>
<span>{songInfo.name}</span>
{songInfo.tns && (
<span className="ml-2 text-sm text-gray-400">
{songInfo.tns[0]}
</span>
)}
</>
),
desc: (
<>
<span className="block">
<span className="font-bold">歌手:</span>
<span>
{singerInfo.map((person: any) => person.name).join(' / ')}
</span>
</span>
<span className="block">
<span className="font-bold">专辑:</span>
<span>{albumInfo.name}</span>
</span>
</>
),
image: albumInfo.picUrl,
color: '#e72d2c',
})
} catch (err) {
console.error('Error fetching NeteaseMusic song data:', err)
throw err
}
},
}
2 changes: 2 additions & 0 deletions src/components/ui/link-card/enums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export enum LinkCardSource {
GHPr = 'gh-pr',
TMDB = 'tmdb',
LEETCODE = 'leetcode',
QQMusicSong = 'qq-music-song',
NeteaseMusicSong = 'netease-music-song',
}
25 changes: 25 additions & 0 deletions src/components/ui/markdown/renderers/LinkRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
isGithubRepoUrl,
isGithubUrl,
isLeetCodeUrl,
isNeteaseMusicSongUrl,
isQQMusicSongUrl,
isSelfArticleUrl,
isTMDBUrl,
isTweetUrl,
Expand Down Expand Up @@ -152,6 +154,29 @@ export const BlockLinkRenderer = ({
)
}

case isNeteaseMusicSongUrl(url): {
const urlString = url.toString().replaceAll('/#/', '/')
const _url = new URL(urlString)
const id = _url.searchParams.get('id') ?? ''
return (
<LinkCard
fallbackUrl={url.toString()}
source={LinkCardSource.NeteaseMusicSong}
id={id}
/>
)
}

case isQQMusicSongUrl(url): {
return (
<LinkCard
fallbackUrl={url.toString()}
source={LinkCardSource.QQMusicSong}
id={url.pathname.split('/')[4]}
/>
)
}

case isBilibiliVideoUrl(url): {
const { id } = parseBilibiliVideoUrl(url)

Expand Down
11 changes: 11 additions & 0 deletions src/lib/link-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ export const isLeetCodeUrl = (url: URL) => {
return url.hostname === 'leetcode.cn' || url.hostname === 'leetcode.com'
}

export const isQQMusicSongUrl = (url: URL) => {
return url.hostname === 'y.qq.com' && url.pathname.includes('/songDetail/')
}

export const isNeteaseMusicSongUrl = (url: URL) => {
return (
url.hostname === 'music.163.com' &&
(url.pathname.includes('/song') || url.hash.includes('/song'))
)
}

export const isGithubRepoUrl = (url: URL) => {
return (
url.hostname === GITHUB_HOST &&
Expand Down

0 comments on commit 2165b6a

Please sign in to comment.