feat: 更换解灰音源

This commit is contained in:
imsyy
2025-11-04 17:49:38 +08:00
parent fc7fc08a6e
commit a1be1e16b2
5 changed files with 185 additions and 28 deletions

View File

@@ -0,0 +1,161 @@
import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import { createHash } from "crypto";
import axios from "axios";
/**
* 生成随机设备 ID
* @returns 随机设备 ID
*/
const getRandomDeviceId = () => {
const min = 0;
const max = 100000000000;
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
return randomNum.toString();
};
/** 随机设备 ID */
const deviceId = getRandomDeviceId();
/**
* 格式化歌曲信息
* @param song 歌曲信息
* @returns 格式化后的歌曲信息
*/
const format = (song: any) => ({
id: song.MUSICRID.split("_").pop(),
name: song.SONGNAME,
duration: song.DURATION * 1000,
album: { id: song.ALBUMID, name: song.ALBUM },
artists: song.ARTIST.split("&").map((name: any, index: any) => ({
id: index ? null : song.ARTISTID,
name,
})),
});
/**
* 生成签名
* @param str 请求字符串
* @returns 包含签名的请求字符串
*/
const generateSign = (str: string) => {
const url = new URL(str);
const currentTime = Date.now();
str += `&timestamp=${currentTime}`;
const filteredChars = str
.substring(str.indexOf("?") + 1)
.replace(/[^a-zA-Z0-9]/g, "")
.split("")
.sort();
const dataToEncrypt = `kuwotest${filteredChars.join("")}${url.pathname}`;
const md5 = createHash("md5").update(dataToEncrypt).digest("hex");
return `${str}&sign=${md5}`;
};
/**
* 搜索歌曲
* @param keyword 搜索关键词
* @returns 歌曲 ID 或 null
*/
const search = async (info: string): Promise<string | null> => {
try {
const keyword = encodeURIComponent(info.replace(" - ", " "));
const url =
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
keyword;
const result = await axios.get(url);
if (
!result.data ||
result.data.content.length < 2 ||
!result.data.content[1].musicpage ||
result.data.content[1].musicpage.abslist.length < 1
) {
return null;
}
// 获取歌曲信息
const list = result.data.content[1].musicpage.abslist.map(format);
if (list[0] && !list[0]?.id) return null;
return list[0].id;
} catch (error) {
serverLog.error("❌ Get BodianSongId Error:", error);
return null;
}
};
/**
* 发送广告免费请求
* @returns 包含广告免费响应的 Promise
*/
const sendAdFreeRequest = () => {
try {
const adurl =
"http://bd-api.kuwo.cn/api/service/advert/watch?uid=-1&token=&timestamp=1724306124436&sign=15a676d66285117ad714e8c8371691da";
const headers = {
"user-agent": "Dart/2.19 (dart:io)",
plat: "ar",
channel: "aliopen",
devid: deviceId,
ver: "3.9.0",
host: "bd-api.kuwo.cn",
qimei36: "1e9970cbcdc20a031dee9f37100017e1840e",
"content-type": "application/json; charset=utf-8",
};
const data = JSON.stringify({
type: 5,
subType: 5,
musicId: 0,
adToken: "",
});
return axios.post(adurl, data, { headers });
} catch (error) {
serverLog.error("❌ Get Bodian Ad Free Error:", error);
return null;
}
};
/**
* 获取波点音乐歌曲 URL
* @param keyword 搜索关键词
* @returns 包含歌曲 URL 的结果对象
*/
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
try {
if (!keyword) return { code: 404, url: null };
const songId = await search(keyword);
if (!songId) return { code: 404, url: null };
// 请求地址
const headers = {
"user-agent": "Dart/2.19 (dart:io)",
plat: "ar",
channel: "aliopen",
devid: deviceId,
ver: "3.9.0",
host: "bd-api.kuwo.cn",
"X-Forwarded-For": "1.0.1.114",
};
let audioUrl = `http://bd-api.kuwo.cn/api/play/music/v2/audioUrl?&br=${"320kmp3"}&musicId=${songId}`;
// 生成签名
audioUrl = generateSign(audioUrl);
// 获取广告
await sendAdFreeRequest();
// 获取歌曲地址
const result = await axios.get(audioUrl, { headers });
if (typeof result.data === "object") {
const urlMatch = result.data.data.audioUrl;
serverLog.log("🔗 BodianSong URL:", urlMatch);
return { code: 200, url: urlMatch };
}
return { code: 404, url: null };
} catch (error) {
serverLog.error("❌ Get BodianSong URL Error:", error);
return { code: 404, url: null };
}
};
export default getBodianSongUrl;

View File

@@ -3,6 +3,7 @@ import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import getKuwoSongUrl from "./kuwo";
import axios from "axios";
import getBodianSongUrl from "./bodian";
/**
* 直接获取 网易云云盘 链接
@@ -61,6 +62,18 @@ export const initUnblockAPI = async (fastify: FastifyInstance) => {
return reply.send(result);
},
);
// bodian
fastify.get(
"/unblock/bodian",
async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
const { keyword } = req.query;
const result = await getBodianSongUrl(keyword);
return reply.send(result);
},
);
serverLog.info("🌐 Register UnblockAPI successfully");
};

View File

@@ -47,7 +47,11 @@ export const songUrl = (
};
// 获取解锁歌曲 URL
export const unlockSongUrl = (id: number, keyword: string, server: "netease" | "kuwo") => {
export const unlockSongUrl = (
id: number,
keyword: string,
server: "netease" | "kuwo" | "bodian",
) => {
const params = server === "netease" ? { id } : { keyword };
return request({
baseURL: "/api/unblock",

View File

@@ -5,11 +5,6 @@ import { parseTTML } from "@applemusic-like-lyrics/lyric";
import { LyricLine } from "@applemusic-like-lyrics/core";
import { LyricType } from "@/types/main";
// 歌词加载并发保护:始终只允许最新一次请求写入
let lyricSessionCounter = 0;
const newLyricSession = () => ++lyricSessionCounter;
const isStale = (sid: number) => sid !== lyricSessionCounter;
/**
* 获取歌词
* @param id 歌曲id
@@ -20,8 +15,6 @@ export const getLyricData = async (id: number) => {
const statusStore = useStatusStore();
// 切歌或重新获取时,先标记为加载中
statusStore.lyricLoading = true;
// 为本次歌词请求创建会话 ID用于防止旧请求覆盖新结果
const currentSessionId = newLyricSession();
if (!id) {
statusStore.usingTTMLLyric = false;
@@ -38,8 +31,6 @@ export const getLyricData = async (id: number) => {
const ttmlPromise = settingStore.enableTTMLLyric ? getLyric("ttml", songLyricTTML) : null;
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
// 如果已经发起了新的歌词请求,直接放弃旧结果
if (isStale(currentSessionId)) return;
parsedLyricsData(lyricRes, lyricLocal && !settingStore.enableExcludeLocalLyrics);
// LRC 到达后即可认为加载完成
statusStore.lyricLoading = false;
@@ -49,15 +40,11 @@ export const getLyricData = async (id: number) => {
statusStore.usingTTMLLyric = false;
void ttmlPromise
.then(({ lyric: ttmlContent, isLocal: ttmlLocal }) => {
// 如果已经发起了新的歌词请求,直接放弃旧结果
if (isStale(currentSessionId)) return;
if (!ttmlContent) {
statusStore.usingTTMLLyric = false;
return;
}
const parsedResult = parseTTML(ttmlContent);
// 如果已经发起了新的歌词请求,直接放弃旧结果
if (isStale(currentSessionId)) return;
if (!parsedResult?.lines?.length) {
statusStore.usingTTMLLyric = false;
return;
@@ -78,7 +65,6 @@ export const getLyricData = async (id: number) => {
updates.yrcData = ttmlYrcLyric;
console.log("✅ TTML Yrc lyrics success");
}
if (isStale(currentSessionId)) return;
if (Object.keys(updates).length) {
musicStore.setSongLyric(updates);
statusStore.usingTTMLLyric = true;
@@ -88,25 +74,18 @@ export const getLyricData = async (id: number) => {
})
.catch((err) => {
console.error("❌ Error loading TTML lyrics:", err);
// 旧请求错误不影响当前状态
if (!isStale(currentSessionId)) {
statusStore.usingTTMLLyric = false;
}
statusStore.usingTTMLLyric = false;
});
} else {
statusStore.usingTTMLLyric = false;
}
// 如果已经发起了新的歌词请求,跳过日志输出以避免混淆
if (!isStale(currentSessionId)) console.log("Lyrics: ", musicStore.songLyric);
console.log("Lyrics: ", musicStore.songLyric);
} catch (error) {
console.error("❌ Error loading lyrics:", error);
// 旧请求错误不影响当前最新状态
if (!isStale(currentSessionId)) {
statusStore.usingTTMLLyric = false;
resetSongLyric();
statusStore.lyricLoading = false;
}
statusStore.usingTTMLLyric = false;
resetSongLyric();
statusStore.lyricLoading = false;
}
};

View File

@@ -86,8 +86,8 @@ export const getUnlockSongUrl = async (songData: SongType): Promise<string | nul
if (!songId || !keyWord) return null;
// 尝试解锁
const results = await Promise.allSettled([
unlockSongUrl(songId, keyWord, "bodian"),
unlockSongUrl(songId, keyWord, "netease"),
unlockSongUrl(songId, keyWord, "kuwo"),
]);
// 解析结果
const [neteaseRes, kuwoRes] = results;