refactor(api)!: 平台 id 改两字母短码——qian→bd、joox→jx、5sing→5s、bili→bl

对外 id 统一为两字母短码,与既有的 kw/kg/tx/wy/qs/xm/me/qt/lr 对齐;
migu 与 apple 保持原样(Apple Music 没有自然的短码)。

**破坏性变更**:id 出现在所有 URL、/sources 返回、OpenAPI 示例以及
data/auth.json 的凭据键里,这四个平台已存的登录态会失效,需重新登录。

- sdk/index.ts:id 与 supportQuality 映射键
- sdk/types.ts:Source 联合类型
- src/routes.ts 与 routes-login.ts:路由路径与 schema 描述
  (/login/bili/qr → /login/bl/qr 等)、/login/status 的响应键
- test/smoke-test.ts、README.md、docs/API.md 同步

有意未改:sdk 目录与文件名仍用平台名(sdk/qian/、sdk/apis/joox.ts …),
规则定为「目录=平台名,id=对外短码」,映射只在 SUPPORTED_SOURCES 一处声明。
域名与正则(joox.com、5sing.kugou.com、/joox\.com\//)、jooxSign 等标识符、
错误文案里的中文平台名、以及 kg 榜单名(含「joox本地热歌榜」)全部保持原样——
它们只是恰好含平台名,并不是 id。

验证:typecheck ✓、pnpm test 112/112 ✓;另起临时实例实证 /sources 与
/login/status 的新 id 与展示名,新旧 id 均无残留。
This commit is contained in:
snowy-yang committed 2026-10-03 11:03:06 +08:00
1 parent 14f7295fd4
commit 3381e9b906
33 files changed
+155 -155

No files matched your search

+4 -4
View File
@@ -29,7 +29,7 @@ const mapAlbumSummary = (item: QianAlbumSearchItem) => {
genre: String(item?.genre ?? '').trim(),
lang: String(item?.lang ?? '').trim(),
link: id ? albumLink(id) : '',
source: 'qian' as const,
source: 'bd' as const,
}
}
@@ -74,7 +74,7 @@ export const searchAlbumItems = async(keyword: string, page = 1, limit = 10): Pr
/** 搜索专辑(返回歌单/专辑统一摘要形状) */
export const searchAlbum = async(keyword: string, page = 1, limit = 10) => {
const word = String(keyword ?? '').trim()
const empty = { source: 'qian' as const, list: [] as any[], total: 0, page, limit }
const empty = { source: 'bd' as const, list: [] as any[], total: 0, page, limit }
if (!word) return empty
const items = await searchAlbumItems(word, page, limit)
@@ -82,7 +82,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 10) => {
.map(mapAlbumSummary)
.filter((album) => album.id)
.slice(0, limit)
return { source: 'qian' as const, list, total: items.length, page, limit }
return { source: 'bd' as const, list, total: items.length, page, limit }
}
/** 旧数字专辑 id → PSID(P 码);已是 P 码则原样返回 */
@@ -146,7 +146,7 @@ export const getAlbumDetail = async(id: string, page = 1, limit = 30) => {
}
return {
source: 'qian' as const,
source: 'bd' as const,
info: albumInfo,
list: paged,
page,
+1 -1
View File
@@ -6,7 +6,7 @@
// - VIP/版权曲目 tracklink 无 path(需登录态会员 cookie),搜索保留但标记 payPlay;
// - 歌单关键词搜索(/v1/search?type=6)服务端已下线(errno 23001),searchPlaylist 返回空列表;
// - 上游 GetUserPlaylists 明确 Unsupported,本移植不提供 userPlaylist。
// 凭据:上游 New(cookie) 的 cookie 从 sdk/auth/store.ts 的 getCredentials('qian') 读取(可选)。
// 凭据:上游 New(cookie) 的 cookie 从 sdk/auth/store.ts 的 getCredentials('bd') 读取(可选)。
import qianApi from '../apis/qian.ts'
import musicSearch from './musicSearch.ts'
import lyric from './lyric.ts'
+2 -2
View File
@@ -6,7 +6,7 @@ import { APP_ID, apiGet, apiOk, mapTrack } from './util.ts'
export const search = async(keyword: string, page = 1, limit = 20) => {
const word = String(keyword ?? '').trim()
const empty = { list: [] as any[], allPage: 1, total: 0, limit, source: 'qian' as const }
const empty = { list: [] as any[], allPage: 1, total: 0, limit, source: 'bd' as const }
if (!word) return empty
const body = await apiGet('/v1/search', {
@@ -28,7 +28,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
allPage: haveMore ? page + 1 : Math.max(page, Math.ceil(total / Math.max(1, limit)) || 1),
total,
limit,
source: 'qian' as const,
source: 'bd' as const,
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ export const parseSong = async(link: string) => {
}
delete song.lyricUrl
delete song.raw
return { ...song, songmid: firstNonEmpty(song.songmid, tsid), source: 'qian' as const }
return { ...song, songmid: firstNonEmpty(song.songmid, tsid), source: 'bd' as const }
}
export const parseAlbum = async(link: string) => parseAlbumLink(link)
+6 -6
View File
@@ -19,14 +19,14 @@ const mapPlaylistSummary = (item: any) => {
creator: firstNonEmpty(item?.creator, item?.author, item?.userName, item?.nickName, item?.ownerName),
description: rawDesc || tagList.join('、'),
link: id ? playlistLink(id) : '',
source: 'qian' as const,
source: 'bd' as const,
}
}
/** 上游 SearchPlaylist(type=6 歌单搜索;服务端已停用,返回空列表) */
export const searchPlaylist = async(keyword: string, page = 1, limit = 10) => {
const word = String(keyword ?? '').trim()
const empty = { source: 'qian' as const, list: [] as any[], total: 0, page, limit }
const empty = { source: 'bd' as const, list: [] as any[], total: 0, page, limit }
if (!word) return empty
const body = await apiGet('/v1/search', {
@@ -40,7 +40,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 10) => {
const items = Array.isArray(body?.data?.typeSonglist) ? body.data.typeSonglist : []
const list = items.map(mapPlaylistSummary).filter((item: any) => item.id)
return { source: 'qian' as const, list, total: Number(body?.data?.total) || list.length, page, limit }
return { source: 'bd' as const, list, total: Number(body?.data?.total) || list.length, page, limit }
}
/** 歌单详情(含曲目;tracklist/info 支持 pageNo/pageSize) */
@@ -69,7 +69,7 @@ export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => {
const total = Number(data?.trackCount) || list.length
return {
source: 'qian' as const,
source: 'bd' as const,
info: { id: playlistId, name: title, img: cover, desc: description, author: creator, link: playlistLink(playlistId) },
list,
page,
@@ -105,7 +105,7 @@ export const getCategories = async() => {
list.push({ id, name, group: groupName, count: Number(item?.count ?? 0) || 0 })
}
}
return { source: 'qian' as const, list }
return { source: 'bd' as const, list }
}
/** 上游 GetCategoryPlaylists(page >= 1,limit 1..100) */
@@ -125,7 +125,7 @@ export const getCategoryPlaylists = async(categoryId: string | number, page = 1,
.map((item: any) => ({ ...mapPlaylistSummary(item), categoryId: cateId }))
.filter((item: any) => item.id)
return {
source: 'qian' as const,
source: 'bd' as const,
list,
total: Number(body?.data?.total) || list.length,
page: pageNo,
+3 -3
View File
@@ -2,7 +2,7 @@
// 上游 music-lib/qianqian 的全部接口都走 /v1/*,参数 = 业务参数 + appid + timestamp + sign:
// sign = md5(按 key 升序拼接 "k=v&..."(不含 sign)+ Secret)
// 该平台匿名即可搜索 / 查专辑 / 查歌单 / 拿免费曲目直链;cookie 仅影响 VIP、收藏等登录态资源,
// MusicServer 从 sdk/auth/store.ts 的 getCredentials('qian') 读取(可选,未登录留空)。
// MusicServer 从 sdk/auth/store.ts 的 getCredentials('bd') 读取(可选,未登录留空)。
import crypto from 'node:crypto'
import { httpFetch } from '../request.ts'
import { getCredentials } from '../auth/store.ts'
@@ -18,7 +18,7 @@ export const RATE_ORDER = ['3000', '320', '128', '64']
/** 千千平台 cookie(未绑定返回空串;上游所有请求都会带上该 header,此处保持一致) */
export const getQianCookie = (): string => {
const cred = getCredentials('qian') as { cookie?: string } | null
const cred = getCredentials('bd') as { cookie?: string } | null
return cred?.cookie ? String(cred.cookie) : ''
}
@@ -196,7 +196,7 @@ export const mapTrack = (item: any, opts: MapTrackOptions = {}) => {
lrcx: '',
types,
_types,
source: 'qian' as const,
source: 'bd' as const,
}
}