Files
MusicServer/sdk/qs/musicSearch.ts
T
snowy-yang 0f9cff2a08 feat(qs): 汽水音乐平台接入——匿名搜索/试听、抖音扫码登录、解密流代理
- sdk/qs 平台模块(安卓参数指纹匿名搜索)+ sdk/apis/qs 取流:
  匿名 30s 试听,登录态走签名 track_v2 + spade key AES-CTR 解密
- /login/qs/qr 抖音扫码登录(授权后 sessionid 解锁完整曲目)
- /stream/qs/:id 解密流代理,/url 返回的相对地址按请求 host 补全
- 回归测试覆盖 kw 双通道(VIP 真音频/降级链)与 qs 试听;start.bat 去掉
  SOURCE_ENABLED 直启
2026-10-01 22:43:49 +08:00

132 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 汽水音乐搜索(App 搜索接口直连)
// 接口: https://api.qishui.com/luna/search/track?<安卓设备参数>&q=<关键词>&cursor=&count=
// 实测(2026-10-01)匿名可搜索;关键在安卓 App 的设备参数指纹——
// 缺参数或用 PC 端参数(device_platform=windows)上游会静默风控,返回 200 + 空 body
import { sizeFormate, formatPlayTime } from '../index.ts'
import { httpFetch } from '../request.ts'
import { coverUrl } from './musicInfo.ts'
// 安卓 App 设备参数指纹(社区验证可用的静态设备 id,与 musicdl 一致;被风控时需换新)
const SEARCH_DEVICE_PARAMS = {
device_platform: 'android',
os: 'android',
ssmix: 'a',
cdid: '46556f98-1720-4248-83da-62b74b60b46a',
channel: 'xiaomi_8478_64',
aid: '386088',
app_name: 'luna',
version_code: '100198030',
version_name: '19.8.0',
manifest_version_code: '100198030',
update_version_code: '100198030',
resolution: '1080*1920',
dpi: '480',
device_type: 'ABR-AL80',
device_brand: 'HUAWEI',
language: 'zh',
os_api: '35',
os_version: '15',
ac: 'wifi',
device_model: 'ABR-AL80',
save_power: '0',
font_size: '1.00',
luna_first_launch_apk_type: 'normal_apk',
diversion_channel_name: 'xiaomi_8478_64',
is_car_play: '0',
battery: '0.99',
network_speed: '10156',
hybrid_version_code: '100198030',
tz_name: 'Asia/Shanghai',
tz_offset: '28800',
luna_register_time: '1784311292',
package: 'com.luna.music',
charge: '0',
luna_apk_type: 'normal_apk',
output_device_type: 'Phone',
volume: '1.00',
brightness: '0.08',
need_personal_recommend: '1',
is_teen_mode: '0',
sim_region: 'cn',
android_device_type: 'default',
iid: '2204957404569386',
device_id: '2204957404565290',
}
const SEARCH_UA = 'Luna/19.8.0 (com.luna.music/100198030);Android 15;ABR-AL80'
const buildSearchUrl = (str: string, cursor: number, count: number) => {
const params = new URLSearchParams({
...SEARCH_DEVICE_PARAMS,
_rticket: String(Date.now()),
q: str,
cursor: String(cursor),
count: String(count),
})
return `https://api.qishui.com/luna/search/track?${params.toString()}`
}
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
getSinger(artists) {
return (artists || []).map(singer => singer.name).filter(Boolean).join('、')
},
handleResult(rawList) {
if (!rawList) return []
return rawList.map(item => {
const track = item?.entity?.track
if (!track?.id) return null
// types 反映匿名实际可获取的资源:30 秒试听(preview.bit_rates 为试听码率)
// track.bit_rates 是完整曲目码率元数据(66k~hi_res),但匿名无权取流,不对外声明
const previewSize = track.preview?.bit_rates?.[0]?.size
const size = previewSize ? sizeFormate(previewSize) : null
const types = [{ type: '128k', size }]
const _types = { '128k': { size } }
return {
singer: this.getSinger(track.artists),
name: track.name,
albumName: track.album?.name ?? null,
albumId: track.album?.id ?? null,
source: 'qs',
interval: formatPlayTime(track.duration / 1000),
songmid: String(track.id),
img: coverUrl(track.album),
lrc: null,
types,
_types,
typeUrl: {},
}
}).filter(Boolean)
},
search(str, page = 1, limit, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit == null) limit = this.limit
const cursor = limit * (page - 1)
const searchRequest = httpFetch(buildSearchUrl(str, cursor, limit), {
method: 'get',
headers: { 'User-Agent': SEARCH_UA },
timeout: 15000,
})
return searchRequest.promise.then(({ body }) => {
// 被风控/无结果时上游不返回 result_groups(200 + {"extra":{"empty_search":1}}),重试
if (!body?.result_groups) return this.search(str, page, limit, retryNum)
const group = body.result_groups.find(g => g?.data?.length) || { data: [], has_more: false }
const list = this.handleResult(group.data)
const total = group.has_more ? cursor + list.length + 1 : cursor + list.length
this.total = total
this.page = page
this.allPage = group.has_more ? page + 1 : page
return {
list,
allPage: this.allPage,
limit: this.limit,
total,
source: 'qs',
}
})
},
}