初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import { getQuery, createError } from 'h3'
import axios from 'axios'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const url = query.url as string
if (!url || !url.includes('kugou.com')) {
throw createError({
statusCode: 400,
message: 'Invalid Kugou URL'
})
}
try {
// 1. 获取重定向后的最终URL或HTML内容
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
}
})
// 很多时候链接会跳转,或者HTML里面有 hash 变量
const html = response.data
// 尝试正则匹配 "hash":"xxx" 或者 hash: 'xxx'
const hashMatch = html.match(/"hash"\s*:\s*"([a-zA-Z0-9]+)"/i) || html.match(/hash\s*=\s*"([a-zA-Z0-9]+)"/i) || html.match(/"320hash"\s*:\s*"([a-zA-Z0-9]+)"/i)
const albumMatch = html.match(/"album_id"\s*:\s*(\d+)/i)
if (hashMatch && hashMatch[1]) {
const hash = hashMatch[1]
// 用 hash 再次查询歌曲详情或者直接返回
// 我们可以调用移动端 track info API
const infoUrl = `http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=${hash}`
const infoRes = await axios.get(infoUrl, { validateStatus: () => true })
if (infoRes.data && infoRes.data.songName) {
return {
id: hash,
title: infoRes.data.songName,
artist: infoRes.data.singerName,
cover: infoRes.data.imgUrl ? infoRes.data.imgUrl.replace('{size}', '150') : '',
duration: infoRes.data.timeLength * 1000,
musicPlatform: 'kugou',
musicId: hash,
hasUrl: true,
isVip: infoRes.data.privilege >= 10,
sourceInfo: {
source: 'kugou',
originalId: hash,
fetchedAt: new Date()
}
}
}
// 如果 getSongInfo 失败,我们只返回 hash
return {
id: hash,
title: "解析成功,请点击投稿",
artist: "未知歌手",
musicPlatform: 'kugou',
musicId: hash,
hasUrl: true,
sourceInfo: {
source: 'kugou',
originalId: hash,
fetchedAt: new Date()
}
}
}
throw createError({
statusCode: 404,
message: 'Cannot find song hash in the provided link'
})
} catch (error: any) {
throw createError({
statusCode: 500,
message: error.message || 'Failed to parse link'
})
}
})
+73
View File
@@ -0,0 +1,73 @@
import { getQuery, createError } from 'h3'
import axios from 'axios'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const hash = query.hash as string
if (!hash) {
throw createError({
statusCode: 400,
message: 'Hash is required'
})
}
try {
// 首先尝试通过移动端API获取播放链接
const infoUrl = `http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=${hash}`
const infoRes = await axios.get(infoUrl, { validateStatus: () => true })
if (infoRes.data && infoRes.data.url) {
return {
data: {
play_url: infoRes.data.url
}
}
}
// 如果失败或者需要付费(VIP),通过标题和歌手名从网易云匹配一个试听链接
if (infoRes.data && infoRes.data.songName) {
const keyword = `${infoRes.data.songName} ${infoRes.data.singerName || ''}`.trim()
try {
// 搜索网易云音乐
const searchUrl = `https://music.163.com/api/search/get/web?s=${encodeURIComponent(keyword)}&type=1&offset=0&total=true&limit=1`
const searchRes = await axios.get(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
})
if (searchRes.data?.result?.songs?.length > 0) {
const songId = searchRes.data.result.songs[0].id
// 获取网易云播放链接
const neteaseUrl = `https://music.163.com/song/media/outer/url?id=${songId}.mp3`
return {
data: {
play_url: neteaseUrl
}
}
}
} catch (e) {
console.error('Fallback to Netease failed:', e)
}
}
// 如果都没有,尝试使用原本的 API
const url = `http://www.kugou.com/yy/index.php?r=play/getdata&hash=${hash}`
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Cookie': 'kg_mid=1234567890'
},
validateStatus: () => true
})
return response.data
} catch (error) {
throw createError({
statusCode: 500,
message: 'Music service unavailable'
})
}
})
+54
View File
@@ -0,0 +1,54 @@
import axios from 'axios'
import crypto from 'crypto'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const keyword = query.keyword || query.keywords
if (!keyword) {
throw createError({
statusCode: 400,
message: 'Keyword is required'
})
}
// 我们在这里使用不需要签名的通用接口或移动端旧接口
try {
const url = `http://mobilecdn.kugou.com/api/v3/search/song?format=json&keyword=${encodeURIComponent(keyword as string)}&page=1&pagesize=20&showtype=1`
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
// 允许处理不严谨的 SSL 证书或忽略部分请求错误
validateStatus: () => true
})
const data = response.data
// Process results to map cover images and VIP status correctly
if (data && data.data && data.data.info) {
data.data.info = data.data.info.map((item: any) => {
return {
...item,
isVip: item.pay_type !== 0,
// Extract proper image URL from 'union_cover', '320' size or hash, and use https
pic: item.trans_param?.union_cover
? item.trans_param.union_cover.replace('{size}', '400').replace('http://', 'https://')
: item['320hash']
? `https://imge.kugou.com/stdmusic/400/${item['320hash'].slice(0,2)}/${item['320hash'].slice(2,4)}/${item['320hash']}.jpg`
: item['hash']
? `https://imge.kugou.com/stdmusic/400/${item['hash'].slice(0,2)}/${item['hash'].slice(2,4)}/${item['hash']}.jpg`
: undefined
}
})
}
return data
} catch (error) {
throw createError({
statusCode: 500,
message: 'Music service unavailable'
})
}
})