diff --git a/electron/server/unblock/bodian.ts b/electron/server/unblock/bodian.ts new file mode 100644 index 0000000..b31605b --- /dev/null +++ b/electron/server/unblock/bodian.ts @@ -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 => { + 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 => { + 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; diff --git a/electron/server/unblock/index.ts b/electron/server/unblock/index.ts index d04b17f..fd5a72e 100644 --- a/electron/server/unblock/index.ts +++ b/electron/server/unblock/index.ts @@ -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"); }; diff --git a/src/api/song.ts b/src/api/song.ts index 9b5bbcd..b27fa40 100644 --- a/src/api/song.ts +++ b/src/api/song.ts @@ -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", diff --git a/src/utils/player-utils/lyric.ts b/src/utils/player-utils/lyric.ts index 680e593..945ef45 100644 --- a/src/utils/player-utils/lyric.ts +++ b/src/utils/player-utils/lyric.ts @@ -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; } }; diff --git a/src/utils/player-utils/song.ts b/src/utils/player-utils/song.ts index b0eb4c5..2b6d9b8 100644 --- a/src/utils/player-utils/song.ts +++ b/src/utils/player-utils/song.ts @@ -86,8 +86,8 @@ export const getUnlockSongUrl = async (songData: SongType): Promise