refactor(api)!: 平台 id 回退——joox/5sing/bili/apple 恢复全名,目录对齐 bd/mg

- 撤销 3381e9b/1b972ec 中 jx/5s/bl/ap 四个短码,恢复 joox/5sing/bili/apple;
  qian→bd、migu→mg 保留
- 目录与文件对齐保留的新 id:sdk/qian/→sdk/bd/、sdk/migu/→sdk/mg/、
  sdk/apis/qian.ts→bd.ts、migu.ts→mg.ts、src/http/migu-stream.ts→mg-stream.ts
  (规则改为「目录=对外 id」,聚合入口的模块变量同步更名;music.migu.cn 等域名、
  上游 music-lib 对照注释、miguGet 等功能标识符不动)
- 对外 id 全表:kw kg tx wy qs xm me qt lr mg bd joox 5sing bili apple

**破坏性变更**:joox/5sing/bili/apple 四平台的 URL、/sources 返回与
data/auth.json 凭据键恢复全名,已存短码键的登录态需重新登录。

验证:typecheck ✓、pnpm test 112/112 ✓
This commit is contained in:
snowy-yang committed 2026-10-03 21:04:55 +08:00
1 parent 8c02fb5c15
commit e1357dd412
50 files changed
+178 -178

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
// 5sing(5sing.kugou.com,酷狗旗下原创/翻唱音乐平台)平台模块
// 上游 music-lib/fivesing 移植(平台 id 统一为 '5s',上游 model.Source 为 "fivesing")。
// 上游 music-lib/fivesing 移植(平台 id 统一为 '5sing',上游 model.Source 为 "fivesing")。
// 能力:搜索(原创 yc / 翻唱 fc / 伴奏 bz,统一种类字段 songType)/ 取流(明文直链,档位 sq→hq→lq)/
// 歌词(newget.dynamicWords)/ 歌单(搜索、详情、链接解析)/ 单曲链接解析。
// 不实现:专辑、封面(上游无 GetPic)、分类歌单/个人歌单(上游显式 not supported,保留抛错语义)、
// 榜单/热搜/歌手/评论(上游均无)。
// 取流入口在 sdk/apis/5sing.ts;songmid 约定 "<songId>|<songType>"(如 "932557|yc",与上游 ID 一致)。
// 登录:上游 New(cookie) 的 cookie 会随请求发送(付费/受限内容可能受益);
// 本移植从 sdk/auth/store.ts 的 getCredentials('5s') 读取 cookie(无则匿名,匿名实测可搜索/取流/歌词/歌单)。
// 本移植从 sdk/auth/store.ts 的 getCredentials('5sing') 读取 cookie(无则匿名,匿名实测可搜索/取流/歌词/歌单)。
import musicSearch from './musicSearch.ts'
import lyric from './lyric.ts'
+1 -1
View File
@@ -106,7 +106,7 @@ export const fetchSongInfo = async(songID: string, songType: string, register?:
link: buildSongLink(songType, songID),
songType,
typeName: kindName(songType),
source: '5s' as const,
source: '5sing' as const,
}
}
+4 -4
View File
@@ -9,7 +9,7 @@
import { formatPlayTime } from '../common/utils.ts'
import { SEARCH_API, fetch5singJson, removeEmTags, unescapeHtml, buildSongLink, estimateDuration, kindName } from './util.ts'
/** 上游 Search 的 list 项 → 统一 Song 形状(source 统一 '5s') */
/** 上游 Search 的 list 项 → 统一 Song 形状(source 统一 '5sing') */
export const mapSearchSong = (item: any) => {
const songType = String(item?.typeEname ?? '')
const duration = estimateDuration(Number(item?.songSize) || 0)
@@ -29,7 +29,7 @@ export const mapSearchSong = (item: any) => {
typeName: String(item?.typeName ?? kindName(songType)),
size: Number(item?.songSize) || 0,
link: buildSongLink(songType, item?.songId),
source: '5s' as const,
source: '5sing' as const,
}
}
@@ -37,7 +37,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
const word = String(keyword ?? '').trim()
const pageNo = Math.max(1, Number(page) || 1)
const pageSize = Math.max(1, Number(limit) || 20)
const empty = { list: [] as any[], allPage: 0, total: 0, limit: pageSize, source: '5s' as const }
const empty = { list: [] as any[], allPage: 0, total: 0, limit: pageSize, source: '5sing' as const }
if (!word) return empty
const url = `${SEARCH_API}?keyword=${encodeURIComponent(word)}&sort=1&page=${pageNo}&filter=0&type=0`
@@ -50,7 +50,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
const total = Number(pageInfo.totalCount) || list.length
// 服务端每页 10 条(固定),allPage 直接用 pageInfo.totalPages
const allPage = Number(pageInfo.totalPages) || (list.length >= pageSize ? pageNo + 1 : pageNo)
return { list, allPage, total, limit: pageSize, source: '5s' as const }
return { list, allPage, total, limit: pageSize, source: '5sing' as const }
}
export default { search }
+7 -7
View File
@@ -32,7 +32,7 @@ const mapPlaylistSong = (songID: string, kind: string, name: string, artist: str
songType: kind,
typeName: kindName(kind),
link: buildSongLink(kind, songID),
source: '5s' as const,
source: '5sing' as const,
})
/**
@@ -103,7 +103,7 @@ const mapPlaylistSummary = (item: any) => {
// 上游怪癖:content 为字符串 "0" 时视为空简介
description: desc === '0' ? '' : desc,
link: buildPlaylistLink(userId, id),
source: '5s' as const,
source: '5sing' as const,
}
}
@@ -112,7 +112,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
const word = String(keyword ?? '').trim()
const pageNo = Math.max(1, Number(page) || 1)
const pageSize = Math.max(1, Number(limit) || 20)
const empty = { list: [] as any[], total: 0, page: pageNo, limit: pageSize, source: '5s' as const }
const empty = { list: [] as any[], total: 0, page: pageNo, limit: pageSize, source: '5sing' as const }
if (!word) return empty
const url = `${SEARCH_API}?keyword=${encodeURIComponent(word)}&sort=1&page=${pageNo}&filter=0&type=1`
@@ -132,7 +132,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
}
const total = Number(body?.pageInfo?.totalCount) || list.length
return { list, total, page: pageNo, limit: pageSize, source: '5s' as const }
return { list, total, page: pageNo, limit: pageSize, source: '5sing' as const }
}
/** 元数据 + 全量曲目(上游 fetchPlaylistDetail 核心;HTML 不分页,一次拿全) */
@@ -165,7 +165,7 @@ export const fetchPlaylistAll = async(playlistIdRaw: string) => {
description: String(data?.C ?? ''),
link: buildPlaylistLink(userId, playlistId),
userId,
source: '5s' as const,
source: '5sing' as const,
}
// 2) 歌单页 HTML(数字 userId 会 302 → 个性域名页,fetch5sing 已跟跳)
@@ -191,7 +191,7 @@ export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => {
const list = songs.slice(start, start + pageSize)
const total = songs.length
return {
source: '5s' as const,
source: '5sing' as const,
info: {
id: playlist.id,
name: playlist.name,
@@ -222,7 +222,7 @@ export const parsePlaylistLink = async(link: string) => {
if (!parsed) throw new Error('invalid 5sing playlist link')
const { playlist, songs } = await fetchPlaylistAll(parsed.playlistId)
return {
source: '5s' as const,
source: '5sing' as const,
info: {
id: playlist.id,
name: playlist.name,
+6 -6
View File
@@ -1,8 +1,8 @@
// 5sing 公共工具(上游 fivesing/fivesing.go 的 getFirstValid / removeEmTags 等移植)
// 平台 id 统一为 '5s'(上游包名 fivesing,Go 源码 model.Source 用 "fivesing";
// 本仓库契约要求 id 为 5s,故所有对外 source 字段均为 '5s'。
// 平台 id 统一为 '5sing'(上游包名 fivesing,Go 源码 model.Source 用 "fivesing";
// 本仓库契约要求 id 为 5sing,故所有对外 source 字段均为 '5sing'。
// 上游 New(cookie) 的 cookie 实际参与请求(搜索/getSongUrl/newget/歌单页),
// 这里按约定从 sdk/auth/store.ts 的 getCredentials('5s') 读取(无则匿名)。
// 这里按约定从 sdk/auth/store.ts 的 getCredentials('5sing') 读取(无则匿名)。
import { httpFetch } from '../common/request.ts'
import { getCredentials } from '../auth/store.ts'
@@ -65,17 +65,17 @@ export const unescapeHtml = (s: any): string =>
})
/** 上游 New(cookie):cookie 仅在有值时随请求发送(匿名可用) */
export const get5singCookie = (): string => String((getCredentials('5s') as { cookie?: string } | null)?.cookie ?? '')
export const get5singCookie = (): string => String((getCredentials('5sing') as { cookie?: string } | null)?.cookie ?? '')
/** 登录态(凭据存在性判断,与 me 平台 status 对齐) */
export const getStatus = () => {
const cred = getCredentials('5s') as { cookie?: string, updatedAt?: string } | null
const cred = getCredentials('5sing') as { cookie?: string, updatedAt?: string } | null
return cred?.cookie ? { logged: true, updatedAt: cred.updatedAt } : { logged: false }
}
/** 是否显式声明了非本平台的 source(上游 GetLyrics/GetDownloadURL 的 source mismatch 检查) */
export const isForeignSource = (songInfo: any): boolean =>
Boolean(songInfo?.source) && songInfo.source !== '5s'
Boolean(songInfo?.source) && songInfo.source !== '5sing'
/**
* 带 Cookie/UA 的请求(上游 utils.Get + http.Client 自动跟跳的等价实现)
+1 -1
View File
@@ -3,7 +3,7 @@
// 完整曲目是 FairPlay DRM 加密流(extendedAssetUrls.enhancedHls),需要 gamdl 等外部工具解密,
// 本 SDK 不提供。因此这里**任何请求档位都返回 type='preview'**,并在注释/响应里明确标注,不假装是无损完整曲。
// 歌曲的 previews[0].url 来自 catalog 歌曲详情(搜索结果缓存里的 previewUrl 也会直接复用,同上游 s.URL 逻辑)。
// 统一接口约定:apis('ap').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// 统一接口约定:apis('apple').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type:'preview', url, ext:'m4a', preview:true, songDuration, isrc }
// (songDuration 是歌曲完整时长,试听片段本身约 30-90 秒)
// type 请求参数不影响结果(只有 preview 一档);supportQualitys 声明为 ['128k'] 以对齐
+1 -1
View File
@@ -3,7 +3,7 @@
// - 免费曲目(搜索 isVip=0,如 SSR Beats 特辑)各档位均返回明文直链 data.path(audio0x.dmhmusic.com,CDN 签名 URL)
// - VIP/版权曲目返回 200/errno 22000 但 data 无 path、无 trail_audio_info,需登录态带会员 cookie 才可能有直链
// 契约:getMusicUrl(songInfo, type) → { promise, cancelHttp },promise resolve { type, url }(type 为实际命中档位)
import { apiOk, firstNonEmpty, trackLink } from '../qian/util.ts'
import { apiOk, firstNonEmpty, trackLink } from '../bd/util.ts'
// 请求档位 → tracklink rate 尝试链(优先请求档,逐级降级;与上游 3000→320→128→64 的探测顺序一致)
const QUALITY_RATES: Record<string, string[]> = {
+1 -1
View File
@@ -120,7 +120,7 @@ const followRedirects = async(url: string, cookie: string, tracker?: { current:
}
/**
* 统一取流接口:apis('bl').getMusicUrl(songInfo, type) → { promise, cancelHttp }
* 统一取流接口:apis('bili').getMusicUrl(songInfo, type) → { promise, cancelHttp }
* promise resolve → { type, url }(type 为实际命中档位,可能高于/低于请求档位)
* songInfo.songmid 约定 `<bvid>|<cid>`;仅给 bvid 时回源 view 取首P cid。
*/
+1 -1
View File
@@ -7,7 +7,7 @@
// wmid/session_key 会作为 uid/usk 参与签名(/login/joox 绑定后 VIP 曲目可取完整流)。
// 实测:游客态返回 128k MP3(免费完整曲目)或 VIP 曲目的 preview/refrain 片段;
// 未登录请求 VIP 曲目时 play_url_list 为试听片段——与上游行为一致(上游同样只能拿游客流)。
// 统一接口约定:apis('jx').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// 统一接口约定:apis('joox').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }(type 为实际命中档位,可能低于请求值)
import { httpFetch } from '../common/request.ts'
+1 -1
View File
@@ -6,7 +6,7 @@ import { httpFetch } from '../common/request.ts'
import {
AndroidChannel, AndroidUA, AndroidVersion, decryptZ3D, firstNonEmpty,
getMiguCookie, isMiguAudioMagic, miguFormatExt, TONE_QUALITY, type MiguSongItem,
} from '../migu/util.ts'
} from '../mg/util.ts'
export const supportQualitys = ['64k', '128k', '320k', 'flac', 'flac24bit']
+2 -2
View File
@@ -35,7 +35,7 @@ export const getAlbumDetail = async(rawId: string, page = 1, limit = 50) => {
if (!summary.trackCount || songs.length > summary.trackCount) summary.trackCount = songs.length
return {
source: 'ap' as const,
source: 'apple' as const,
info: {
id: albumId,
name: summary.name,
@@ -64,7 +64,7 @@ export const getAlbumSongs = async(id: string): Promise<any[]> => {
export const parseAlbumLink = async(link: string) => {
const detail = await getAlbumDetail(link, 1, 100000)
return {
source: 'ap' as const,
source: 'apple' as const,
id: detail.info.id,
info: detail.info,
list: detail.list,
+4 -4
View File
@@ -1,8 +1,8 @@
// Apple Music 平台模块(上游 music-lib/apple/apple.go 的 Node/TS 移植,平台 id: ap)
// Apple Music 平台模块(上游 music-lib/apple/apple.go 的 Node/TS 移植,平台 id: apple)
// 能力:搜索 / 歌词(TTML,匿名 developer token 通常拿不到,见 lyric.ts)/ 封面 /
// 专辑(搜索+详情+链接解析)/ 歌单(搜索+详情+分类+分类歌单+链接解析)/
// 链接解析(song/album/playlist)/ 取流(**仅 preview 试听片段**,见 sdk/apis/apple.ts)。
// 凭据:上游 New(cookie) → getCredentials('ap'),cookie 形如
// 凭据:上游 New(cookie) → getCredentials('apple'),cookie 形如
// "media-user-token=xxx;storefront=cn;token=xxx",也支持 storefront/mediaUserToken/token 直填字段。
// 未实现(上游也没有):userPlaylist、榜单、热搜、歌手、评论、扫码登录。
import appleApi from '../apis/apple.ts'
@@ -72,7 +72,7 @@ const apple = {
playlist,
// 契约别名:Apple 的歌单分类即 genre curators,挂到 songList 键供 /song-list/ap 复用
// 契约别名:Apple 的歌单分类即 genre curators,挂到 songList 键供 /song-list/apple 复用
songList: {
sortList: [{ name: '热门', id: '1526756058' }],
getTags() {
@@ -96,7 +96,7 @@ const apple = {
/** 登录状态(media-user-token 存在性判断;无在线校验接口) */
export const status = () => {
const cred = getCredentials('ap') as any
const cred = getCredentials('apple') as any
const cfg = getAppleConfig()
return (cfg.mediaUserToken || cred?.cookie)
? { logged: true, updatedAt: cred?.updatedAt }
+3 -3
View File
@@ -39,7 +39,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
allPage: hasNext ? p + 1 : p,
total: (p - 1) * l + list.length,
limit: l,
source: 'ap' as const,
source: 'apple' as const,
}
}
@@ -53,7 +53,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => {
total: (p - 1) * l + list.length,
page: p,
limit: l,
source: 'ap' as const,
source: 'apple' as const,
}
}
@@ -67,7 +67,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
total: (p - 1) * l + list.length,
page: p,
limit: l,
source: 'ap' as const,
source: 'apple' as const,
}
}
+5 -5
View File
@@ -53,8 +53,8 @@ export const appleCuratorCategories: Array<{ id: string, name: string }> = [
/** 上游 GetPlaylistCategories:静态返回 Apple Music genre curators */
export const getCategories = async() => ({
source: 'ap' as const,
list: appleCuratorCategories.map(item => ({ id: item.id, name: item.name, group: 'Apple Music 分类', source: 'ap' as const })),
source: 'apple' as const,
list: appleCuratorCategories.map(item => ({ id: item.id, name: item.name, group: 'Apple Music 分类', source: 'apple' as const })),
})
/** 上游 GetCategoryPlaylists:按 curator 分类取歌单(cn storefront,单请求上限 25,循环补齐) */
@@ -88,7 +88,7 @@ export const getCategoryPlaylists = async(categoryID: string, page = 1, limit =
}
return {
source: 'ap' as const,
source: 'apple' as const,
list: all.slice(0, l),
page: p,
limit: l,
@@ -118,7 +118,7 @@ export const getPlaylistDetail = async(rawId: string, page = 1, limit = 100) =>
if (!summary.trackCount || songs.length > summary.trackCount) summary.trackCount = songs.length
return {
source: 'ap' as const,
source: 'apple' as const,
info: {
id: playlistId,
name: summary.name,
@@ -145,7 +145,7 @@ export const getPlaylistSongs = async(id: string): Promise<any[]> => {
export const parsePlaylistLink = async(link: string) => {
const detail = await getPlaylistDetail(link, 1, 100000)
return {
source: 'ap' as const,
source: 'apple' as const,
id: detail.info.id,
info: detail.info,
list: detail.list,
+5 -5
View File
@@ -1,6 +1,6 @@
// Apple Music(music.apple.com / amp-api.music.apple.com)公共工具
// 上游 music-lib/apple/apple.go 的移植基础层:
// - New(cookie) → getCredentials('ap'):cookie 支持 "media-user-token=xxx;storefront=cn;token=xxx"
// - New(cookie) → getCredentials('apple'):cookie 支持 "media-user-token=xxx;storefront=cn;token=xxx"
// - fetchAppleToken():从 music.apple.com 首页 → index-legacy JS 抓 developer token(amp-api 的 Bearer)
// 当前(2026-10)Apple 下发的 JWT 以 eyJ0eXAi({"typ":...})开头而非上游正则写的 eyJh,
// 故这里泛化为任意 JWT(eyJ…)并优先选择 issuer=AMPWebPlay 的 token;带回退与失效重取见 getAppleToken/invalidateAppleToken。
@@ -42,7 +42,7 @@ export const parseCookieParts = (cookie: string): Record<string, string> => {
/** 读取 apple 平台运行时配置(对应上游 New(cookie)) */
export const getAppleConfig = (): AppleRuntimeConfig => {
const cred = (getCredentials('ap') as any) || {}
const cred = (getCredentials('apple') as any) || {}
const parts = parseCookieParts(String(cred.cookie ?? ''))
const storefront = String(cred.storefront || parts.storefront || DEFAULT_STOREFRONT).trim()
return {
@@ -297,7 +297,7 @@ export const appleSongFromCatalog = (res: any, defaultAlbumId = ''): any => {
lrc: null,
types: [],
_types: {},
source: 'ap' as const,
source: 'apple' as const,
link: attr.url || '', // music.apple.com 网页地址
previewUrl: previewURL, // 30-90 秒试听片段直链(Apple 不提供完整曲目明文流)
duration: durationSec,
@@ -316,7 +316,7 @@ export const appleAlbumToPlaylist = (res: any): any => {
creator: attr.artistName || '',
description: attr.editorialNotes?.short || '',
link: attr.url || '',
source: 'ap' as const,
source: 'apple' as const,
extra: {
release_date: attr.releaseDate || '',
record_label: attr.recordLabel || '',
@@ -340,7 +340,7 @@ export const applePlaylistToPlaylist = (res: any): any => {
creator: attr.curatorName || '',
description,
link: attr.url || '',
source: 'ap' as const,
source: 'apple' as const,
}
}
File renamed without changes.
+4 -4
View File
@@ -7,7 +7,7 @@
// - 歌单关键词搜索(/v1/search?type=6)服务端已下线(errno 23001),searchPlaylist 返回空列表;
// - 上游 GetUserPlaylists 明确 Unsupported,本移植不提供 userPlaylist。
// 凭据:上游 New(cookie) 的 cookie 从 sdk/auth/store.ts 的 getCredentials('bd') 读取(可选)。
import qianApi from '../apis/qian.ts'
import bdApi from '../apis/bd.ts'
import musicSearch from './musicSearch.ts'
import lyric from './lyric.ts'
import { getSongInfo } from './musicInfo.ts'
@@ -16,11 +16,11 @@ import playlist from './playlist.ts'
import parse from './parse.ts'
import { getQianCookie } from './util.ts'
const qian = {
const bd = {
musicSearch,
getMusicUrl(songInfo, type) {
return qianApi.getMusicUrl(songInfo, type)
return bdApi.getMusicUrl(songInfo, type)
},
getLyric(songInfo) {
@@ -83,4 +83,4 @@ export const status = () => {
return cookie ? { logged: true } : { logged: false }
}
export default qian
export default bd
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1 -1
View File
@@ -2,7 +2,7 @@
// 歌曲:music.91q.com/song/<TSID>(上游 Parse:取 song/info 后顺带取直链,失败不影响返回)
// 专辑:music.91q.com/album/<P码> / ?albumAssetCode= / ?albumid=
// 歌单:music.91q.com/songlist|tracklist|playlist/<id> / ?*id= / 裸歌单 id
import { getDownloadUrl } from '../apis/qian.ts'
import { getDownloadUrl } from '../apis/bd.ts'
import { getSongInfo } from './musicInfo.ts'
import { parseAlbumLink } from './album.ts'
import { parsePlaylistLink } from './playlist.ts'
File renamed without changes.
View File
File renamed without changes.
+1 -1
View File
@@ -1,4 +1,4 @@
// Bilibili(B站)平台模块 —— 上游 music-lib/bilibili 的移植(平台 id: bl)
// Bilibili(B站)平台模块 —— 上游 music-lib/bilibili 的移植(平台 id: bili)
// 能力:搜索 / 取流(DASH,VIP 可出 FLAC/Hi-Res)/ 歌单(合集 ugc_season + 多分P稿件)/
// 链接解析(BV/av、分P)/ 封面 / 扫码登录(sdk/bili/login.ts,凭据落 sdk/auth/store.ts)
// 不实现:专辑、歌词(上游即空实现)、榜单、热搜、歌手、评论、个人歌单、歌单分类(上游均未实现)。
+3 -3
View File
@@ -5,7 +5,7 @@
// 0 成功 / 86038 过期 / 86090 已扫码待确认 / 86101 未扫码
// 成功时登录态 Cookie 在 poll 响应的 Set-Cookie 与 data.url 的 crossDomain 查询串里(两处都收集)。
// 凭据落盘到 sdk/auth/store.ts 的 `bili` 记录(cookie / refreshToken / 账号资料),与 kg/wy 一致;
// 上游 New(cookie) 的实例化语义由 getCredentials('bl') 承担(见 util.ts getCookie)。
// 上游 New(cookie) 的实例化语义由 getCredentials('bili') 承担(见 util.ts getCookie)。
// 状态码沿用本仓库约定:800 过期 / 801 等待扫码 / 802 已扫码待确认 / 803 登录成功。
import QRCode from 'qrcode'
import { httpFetch } from '../common/request.ts'
@@ -125,7 +125,7 @@ export const checkQRLogin = async(key: string) => {
const cookie = cookieString(jar)
if (!cookie) throw new Error('B站登录成功但未获取到 Cookie,请重试')
const profile = await fetchProfile(cookie)
setCredentials('bl', {
setCredentials('bili', {
cookie,
refreshToken: String(data.refresh_token || ''),
mid: profile.mid,
@@ -148,7 +148,7 @@ export const checkQRLogin = async(key: string) => {
/** 当前登录态(凭据存在性 + 落盘时保存的账号资料) */
export const status = () => {
const cred = getCredentials('bl') as any
const cred = getCredentials('bili') as any
return cred?.cookie
? {
logged: true,
+1 -1
View File
@@ -1,6 +1,6 @@
// Bilibili 歌词(上游 bilibili/lyric.go 即空实现:GetLyrics 对 bilibili 源恒返回空字符串)
// B 站视频没有公开歌词接口(cc_subtitle 需登录且绝大多数视频没有),上游未实现,此处保持一致:
// 返回统一结构 { lyric: '', tlyric: '' },让 /lyric/bl 有响应而不是 404。
// 返回统一结构 { lyric: '', tlyric: '' },让 /lyric/bili 有响应而不是 404。
export const getLyric = (_songInfo?: any, ..._rest: any[]) => {
const requestObj = {
promise: Promise.resolve({ lyric: '', tlyric: '' }),
+1 -1
View File
@@ -50,7 +50,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
allPage: numPages || p + (list.length >= l ? 1 : 0),
total,
limit: l,
source: 'bl' as const,
source: 'bili' as const,
}
}
+6 -6
View File
@@ -57,7 +57,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
creator: view?.owner?.name || item.author || '',
description: season.intro || '',
link: `https://www.bilibili.com/video/${item.bvid}`,
source: 'bl' as const,
source: 'bili' as const,
}
}
const pages: any[] = view?.pages ?? []
@@ -73,14 +73,14 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
creator: view?.owner?.name || item.author || '',
description: '',
link: `https://www.bilibili.com/video/${item.bvid}`,
source: 'bl' as const,
source: 'bili' as const,
}
}
return null
})
const list = result.filter(Boolean)
return { list, page: p, limit: l, total: list.length, source: 'bl' as const }
return { list, page: p, limit: l, total: list.length, source: 'bili' as const }
}
/** 由 bvid 构造歌单详情(上游 GetPlaylistSongs 的 bvid 分支 + ParsePlaylist 的元数据) */
@@ -167,7 +167,7 @@ const pageSlice = (detail: { info: any, songs: any[] }, page: any, limit: any) =
const p = Math.max(1, Number(page) || 1)
const l = Math.min(Math.max(1, Number(limit) || 100), 10000)
return {
source: 'bl' as const,
source: 'bili' as const,
info: detail.info,
list: detail.songs.slice((p - 1) * l, p * l),
page: p,
@@ -216,7 +216,7 @@ const parsePlaylistView = async(bvid: string, view: any) => {
let trackCount = countSeasonEpisodes(season.sections)
if (!trackCount) trackCount = detail.songs.length
return {
source: 'bl' as const,
source: 'bili' as const,
id,
info: { ...detail.info, trackCount, playCount: Number(season?.stat?.view) || undefined },
list: detail.songs,
@@ -227,7 +227,7 @@ const parsePlaylistView = async(bvid: string, view: any) => {
}
const detail = await detailFromBvid(bvid)
return {
source: 'bl' as const,
source: 'bili' as const,
id: bvidKey(bvid),
info: { ...detail.info, trackCount: detail.songs.length },
list: detail.songs,
+2 -2
View File
@@ -25,7 +25,7 @@ export const biliHeaders = (cookie = ''): Record<string, string> => ({
})
/** 已保存的登录 Cookie(未登录返回空串) */
export const getLoginCookie = (): string => String((getCredentials('bl') as any)?.cookie || '')
export const getLoginCookie = (): string => String((getCredentials('bili') as any)?.cookie || '')
const cookieMapFromHeaders = (headers: any): Record<string, string> => {
const raw = headers?.['set-cookie']
@@ -147,7 +147,7 @@ export const makeSong = ({ bvid, cid, name, artist, album, duration, cover, page
lrc: null,
types: [] as any[],
_types: {} as Record<string, any>,
source: 'bl' as const,
source: 'bili' as const,
bvid,
cid: String(cid ?? ''),
page,
+16 -16
View File
@@ -7,8 +7,8 @@ import xm from './xm/index.ts'
import me from './me/index.ts'
import qt from './qt/index.ts'
import lr from './lr/index.ts'
import migu from './migu/index.ts'
import qian from './qian/index.ts'
import mg from './mg/index.ts'
import bd from './bd/index.ts'
import joox from './joox/index.ts'
import bili from './bili/index.ts'
import fivesing from './5sing/index.ts'
@@ -22,8 +22,8 @@ import { supportQualitys as xmQualitys } from './apis/xm.ts'
import { supportQualitys as meQualitys } from './apis/me.ts'
import { supportQualitys as qtQualitys } from './apis/qt.ts'
import { supportQualitys as lrQualitys } from './apis/lr.ts'
import { supportQualitys as miguQualitys } from './apis/migu.ts'
import { supportQualitys as qianQualitys } from './apis/qian.ts'
import { supportQualitys as mgQualitys } from './apis/mg.ts'
import { supportQualitys as bdQualitys } from './apis/bd.ts'
import { supportQualitys as jooxQualitys } from './apis/joox.ts'
import { supportQualitys as biliQualitys } from './apis/bili.ts'
import { supportQualitys as fivesingQualitys } from './apis/5sing.ts'
@@ -53,13 +53,13 @@ const SUPPORTED_SOURCES: Array<{ id: string; name: string; module: PlatformModul
{ id: 'me', name: '猫耳FM', module: me, noAggregate: true },
{ id: 'qt', name: '蜻蜓FM', module: qt, noAggregate: true },
{ id: 'lr', name: '懒人听书', module: lr, noAggregate: true },
{ id: 'mg', name: '咪咕音乐', module: migu },
{ id: 'bd', name: '千千音乐', module: qian },
{ id: 'jx', name: 'JOOX', module: joox },
{ id: 'bl', name: '哔哩哔哩', module: bili },
{ id: '5s', name: '5sing', module: fivesing },
{ id: 'mg', name: '咪咕音乐', module: mg },
{ id: 'bd', name: '千千音乐', module: bd },
{ id: 'joox', name: 'JOOX', module: joox },
{ id: 'bili', name: '哔哩哔哩', module: bili },
{ id: '5sing', name: '5sing', module: fivesing },
// Apple Music 仅提供 preview 试听片段(完整曲为 DRM),不参与跨平台精确匹配
{ id: 'ap', name: 'Apple Music', module: apple, noAggregate: true },
{ id: 'apple', name: 'Apple Music', module: apple, noAggregate: true },
]
const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module]))
@@ -81,12 +81,12 @@ export const supportQuality: Record<string, string[]> = {
me: meQualitys,
qt: qtQualitys,
lr: lrQualitys,
mg: miguQualitys,
bd: qianQualitys,
jx: jooxQualitys,
bl: biliQualitys,
'5s': fivesingQualitys,
ap: appleQualitys,
mg: mgQualitys,
bd: bdQualitys,
joox: jooxQualitys,
bili: biliQualitys,
'5sing': fivesingQualitys,
apple: appleQualitys,
}
const trimStr = (str: unknown): string => (typeof str === 'string' ? str.trim() : str as string)
+5 -5
View File
@@ -40,7 +40,7 @@ const fetchAlbumTracks = async(albumId: string, page: number, limit: number) =>
const total = firstNonZero(body?.tracks?.total_count, body?.tracks?.list_count, list.length)
return {
source: 'jx' as const,
source: 'joox' as const,
info: {
name: String(firstNonEmpty(body?.name, albumId)),
img: pickJooxImage(body?.images) || null,
@@ -95,7 +95,7 @@ export const getAlbumDetailFromPage = async(rawId: string, page = 1, limit = 50)
const total = firstNonZero(albumData?.trackList?.total_count, albumData?.trackList?.list_count, items.length)
return {
source: 'jx' as const,
source: 'joox' as const,
info: {
name: String(firstNonEmpty(albumData?.title, albumId)),
img: cover || null,
@@ -120,7 +120,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => {
const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}`
const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any
if (statusCode != 200 || !body?.section_list) {
return { list: [], total: 0, page, limit, source: 'jx' as const }
return { list: [], total: 0, page, limit, source: 'joox' as const }
}
const list: any[] = []
@@ -142,12 +142,12 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => {
publishDate: String(album.publish_date ?? '').trim(),
trackCount: 0,
link: jooxAlbumLink(id),
source: 'jx' as const,
source: 'joox' as const,
})
}
}
return { list: list.slice(0, limit), total: list.length, page, limit, source: 'jx' as const }
return { list: list.slice(0, limit), total: list.length, page, limit, source: 'joox' as const }
}
/** 专辑链接解析(上游 ParseAlbum) */
+3 -3
View File
@@ -7,7 +7,7 @@
// - 取流/歌曲详情/专辑曲目/歌单曲目:cache.api.joox.com/openjoox2/v1(需 md5 签名,见 util.jooxSign)
// - 歌词:api.joox.com/web-fcgi-bin/web_lyric(旧接口仍可用;失败回退 openjoox2 lrc_content)
// - 分类/歌单/专辑 SSR 兜底:www.joox.com __NEXT_DATA__
// 上游 New(cookie):默认游客 Cookie 来自 joox.go 常量;/login/jx 绑定后从 sdk/auth/store.ts 读取,
// 上游 New(cookie):默认游客 Cookie 来自 joox.go 常量;/login/joox 绑定后从 sdk/auth/store.ts 读取,
// 登录 cookie 的 wmid/session_key 会作为 uid/usk 参与 openjoox2 签名(VIP 完整曲目依赖它)。
// userPlaylist:上游 joox/user_playlist.go 返回 ErrUserPlaylistsUnsupported,未实现(不造)。
import jooxApi from '../apis/joox.ts'
@@ -35,7 +35,7 @@ const joox = {
album,
playlist,
// 契约别名:JOOX 的「分类/列表」能力即 playlist.go 的歌单分类,没有独立的 songList 接口,
// 这里把同一套分类/分类歌单挂到 songList 键,便于 /song-list/jx 路由复用。
// 这里把同一套分类/分类歌单挂到 songList 键,便于 /song-list/joox 路由复用。
songList: {
sortList: [{ name: '全部', id: '' }],
getTags() {
@@ -53,7 +53,7 @@ const joox = {
/** 登录状态(凭据存在性判断;无在线校验接口) */
export const status = () => {
const cred = getCredentials('jx') as { cookie?: string, updatedAt?: string } | null
const cred = getCredentials('joox') as { cookie?: string, updatedAt?: string } | null
return cred?.cookie
? { logged: true, updatedAt: cred.updatedAt }
: { logged: false }
+2 -2
View File
@@ -14,7 +14,7 @@ export const search = async(keyword: string, _page = 1, limit = 20) => {
const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}`
const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any
if (statusCode != 200 || !body?.section_list) {
return { list: [], allPage: 0, total: 0, limit, source: 'jx' as const }
return { list: [], allPage: 0, total: 0, limit, source: 'joox' as const }
}
const list: any[] = []
@@ -35,7 +35,7 @@ export const search = async(keyword: string, _page = 1, limit = 20) => {
allPage: 1,
total: list.length,
limit,
source: 'jx' as const,
source: 'joox' as const,
}
}
+8 -8
View File
@@ -39,7 +39,7 @@ const mapPlaylistSummary = (item: any) => {
creator: '',
description: '',
link: jooxPlaylistLink(id),
source: 'jx' as const,
source: 'joox' as const,
}
}
@@ -60,7 +60,7 @@ const fetchPlaylistTracks = async(playlistId: string, page: number, limit: numbe
const total = firstNonZero(body?.tracks?.total_count, body?.tracks?.list_count, body?.track_count, list.length)
return {
source: 'jx' as const,
source: 'joox' as const,
info: {
name: String(firstNonEmpty(body?.name, playlistId)),
img: pickJooxImage(body?.images) || null,
@@ -136,7 +136,7 @@ export const getPlaylistDetailFromPage = async(rawId: string) => {
.filter(Boolean)
if (!list.length) throw new Error('joox 歌单无歌曲')
return {
source: 'jx' as const,
source: 'joox' as const,
info: {
name: String(parsed.name || pageId),
img: cover || null,
@@ -185,7 +185,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}`
const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any
if (statusCode != 200 || !body?.section_list) {
return { list: [], total: 0, page, limit, source: 'jx' as const }
return { list: [], total: 0, page, limit, source: 'joox' as const }
}
const list: any[] = []
@@ -200,7 +200,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
}
}
return { list: list.slice(0, limit), total: list.length, page, limit, source: 'jx' as const }
return { list: list.slice(0, limit), total: list.length, page, limit, source: 'joox' as const }
}
/** 歌单分类页原始数据(上游 fetchJooxPlaylistCategoriesPage) */
@@ -230,7 +230,7 @@ export const getCategories = async() => {
list.push({ id: String(index), name, group: 'JOOX', hot: false, count: items.length })
})
if (list.length === 1) throw new Error('joox 歌单分类不存在')
return { source: 'jx' as const, list }
return { source: 'joox' as const, list }
}
/** 分类下歌单(上游 GetCategoryPlaylists) */
@@ -282,13 +282,13 @@ export const getCategoryPlaylists = async(categoryId = '', page = 1, limit = 20)
link: jooxPlaylistLink(id),
categoryId: cid,
categoryName,
source: 'jx' as const,
source: 'joox' as const,
}
})
.filter(Boolean)
if (!list.length) throw new Error('joox 分类下无歌单')
return { list, total: items.length, page, limit, source: 'jx' as const }
return { list, total: items.length, page, limit, source: 'joox' as const }
}
/** 歌单链接解析(上游 ParsePlaylist) */
+2 -2
View File
@@ -45,7 +45,7 @@ export const jooxSongFromTrack = (
vip: Number(item?.vip_flag ?? 0) === 1,
types: [],
_types: {},
source: 'jx' as const,
source: 'joox' as const,
}
}
@@ -65,7 +65,7 @@ export const jooxSongFromDetail = (detail: any, playUrl = ''): any => {
lrc: null,
types: [],
_types: {},
source: 'jx' as const,
source: 'joox' as const,
}
if (playUrl) song.url = playUrl
song.vip = Number(detail?.vip_flag ?? 0) === 1
+1 -1
View File
@@ -33,7 +33,7 @@ export const jooxSign = (params: Record<string, any>): string =>
/** 绑定到 sdk/auth/store.ts 的登录 cookie(未登录返回空串,不回退游客 cookie) */
export const jooxLoginCookie = (): string => {
const cred = getCredentials('jx') as { cookie?: string } | null
const cred = getCredentials('joox') as { cookie?: string } | null
return cred?.cookie || ''
}
File renamed without changes.
+4 -4
View File
@@ -2,17 +2,17 @@
// 上游对应:music-lib/migu/(migu.go、song.go、lyric.go、album.go、playlist.go、download.go)
// 匿名面(2026-10-02 实测):搜索、PQ(128k mp3) 取流、歌词、封面、专辑曲目、歌单曲目均可用;
// HQ/SQ/ZQ 各档需登录 Cookie(接口返回「登录后免费试听」)。
import miguApi from '../apis/migu.ts'
import mgApi from '../apis/mg.ts'
import musicSearch from './musicSearch.ts'
import lyric, { contentIdOf } from './lyric.ts'
import album from './album.ts'
import playlist from './playlist.ts'
import { fetchSongDetail, miguSongLink } from './util.ts'
const migu = {
const mg = {
musicSearch,
getMusicUrl(songInfo: any, type: string) {
return miguApi.getMusicUrl(songInfo, type)
return mgApi.getMusicUrl(songInfo, type)
},
getLyric(songInfo: any) {
return lyric.getLyric(songInfo)
@@ -60,4 +60,4 @@ export const parseSong = async(link: string) => {
return song
}
export default migu
export default mg
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
+1 -1
View File
@@ -4,7 +4,7 @@
/** 音乐平台标识 */
export type Source =
| 'kw' | 'kg' | 'tx' | 'wy' | 'qs'
| 'mg' | 'bd' | 'jx' | 'bl' | '5s' | 'ap'
| 'mg' | 'bd' | 'joox' | 'bili' | '5sing' | 'apple'
| 'xm' | 'me' | 'qt' | 'lr'
/** 音质标识(wav 仅部分接口声明使用) */