mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
✨ feat: 更换解灰音源
This commit is contained in:
161
electron/server/unblock/bodian.ts
Normal file
161
electron/server/unblock/bodian.ts
Normal 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 += `×tamp=${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=×tamp=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;
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user