diff --git a/forge.config.ts b/forge.config.ts
index 828e61b..0b0b282 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -91,7 +91,7 @@ const config: ForgeConfig = {
js: "./src/webworkers/db-worker.ts",
name: "db",
nodeIntegration: true,
- },
+ }
],
},
}),
diff --git a/res/.service/request-forwarder.js b/res/.service/request-forwarder.js
new file mode 100644
index 0000000..8dff3d1
--- /dev/null
+++ b/res/.service/request-forwarder.js
@@ -0,0 +1,105 @@
+const http = require("http");
+const https = require("https");
+
+
+const defaultPort = 52735;
+const maxRetries = 20;
+
+let retryCount = 0;
+
+function forwardRequest(clientRes, url, method, headers) {
+ const options = {
+ method: method,
+ headers: headers,
+ };
+
+ const protocol = url.startsWith("https") ? https : http;
+
+ const req = protocol.request(url, options, (targetRes) => {
+ // 将目标响应的状态码和头部转发到客户端
+ clientRes.writeHead(targetRes.statusCode, targetRes.headers);
+
+ // 将目标响应的数据流转发到客户端
+ targetRes.pipe(clientRes, {
+ end: true,
+ });
+ });
+
+ req.on("error", (error) => {
+ console.error("Error forwarding request:", error);
+ clientRes.writeHead(500, {"Content-Type": "text/plain"});
+ clientRes.end("Internal Server Error");
+ });
+
+ // 结束目标请求
+ req.end();
+}
+
+
+function safeParse(data) {
+ try {
+ return JSON.parse(data) || {};
+ } catch (e) {
+ return {};
+ }
+}
+
+
+function startServer(port) {
+
+// 创建一个 HTTP 服务器
+ const server = http.createServer((req, res) => {
+ if (req.method !== "GET") {
+ res.writeHead(405, {"Content-Type": "text/plain"});
+ return res.end("Only GET requests are allowed");
+ }
+
+ if (req.url === "/heartbeat") {
+ res.writeHead(200, {"Content-Type": "text/plain"});
+ return res.end("OK");
+ }
+
+ const query = new URLSearchParams(req.url.slice(1));
+
+
+ const url = query.get("url");
+ const method = query.get("method") || "GET"; // 默认使用 GET 方法
+ const headers = safeParse(query.get("headers"));
+
+ res.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有源
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 允许的方法
+
+ if (!url) {
+ res.writeHead(400, {"Content-Type": "text/plain"});
+ return res.end("Bad Request: Missing URL");
+ }
+
+ forwardRequest(res, url, method, {
+ ...(req.headers || {}),
+ ...(headers || {})
+ });
+ });
+
+ server.listen(port, () => {
+ process.send?.({
+ type: "port",
+ port
+ });
+ console.log(`Proxy server is running on http://localhost:${port}`);
+ });
+
+ server.on("error", (err) => {
+ console.error("Server error:", err);
+ if (retryCount < maxRetries) {
+ retryCount++;
+ const newPort = port + 1; // 尝试下一个端口
+ console.log(`Retrying on port: ${newPort} (attempt ${retryCount})`);
+ startServer(newPort);
+ } else {
+ process.send?.({type: "error", error: "Max retries reached"});
+ }
+ })
+}
+
+
+startServer(defaultPort);
diff --git a/src/main/core/global-short-cut/index.ts b/src/main/core/global-short-cut/index.ts
index ffbb526..3e5dea6 100644
--- a/src/main/core/global-short-cut/index.ts
+++ b/src/main/core/global-short-cut/index.ts
@@ -33,7 +33,7 @@ async function registerGlobalShortCut() {
const globalShortCuts = AppConfig.getConfig("shortCut.shortcuts");
for (const shortCutKey of shortCutKeys) {
- const globalShortCutConfig = globalShortCuts[shortCutKey]?.global;
+ const globalShortCutConfig = globalShortCuts?.[shortCutKey]?.global;
if (globalShortCutConfig?.length) {
await registerSingleShortCut(shortCutKey, globalShortCutConfig);
@@ -46,7 +46,7 @@ async function registerSingleShortCut(key: IShortCutKeys, shortCut: string[]) {
if (shortCut.length) {
const prevConfig = AppConfig.getConfig("shortCut.shortcuts");
- if (prevConfig[key].global?.length) {
+ if (prevConfig[key]?.global?.length) {
globalShortcut.unregister(prevConfig[key].global.join("+"));
}
diff --git a/src/main/index.ts b/src/main/index.ts
index a1c1386..b94a8a0 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -19,7 +19,7 @@ import WindowDrag from "@shared/window-drag/main";
import {IAppConfig} from "@/types/app-config";
import axios from "axios";
import {HttpsProxyAgent} from "https-proxy-agent";
-
+import ServiceManager from "@shared/service-manager/main";
// portable
if (process.platform === "win32") {
@@ -162,6 +162,8 @@ app.whenReady().then(async () => {
});
async function bootstrap() {
+ ServiceManager.setup(windowManager);
+
const downloadPath = AppConfig.getConfig("download.path");
if (!downloadPath) {
AppConfig.setConfig({
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 3b0383b..b0b18e0 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -2,4 +2,5 @@
import "./common-preload";
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
-import "@/shared/message-hub/preload/main";
+import "@shared/message-hub/preload/main";
+import "@shared/service-manager/preload";
diff --git a/src/renderer/components/MusicBar/widgets/Controller/index.tsx b/src/renderer/components/MusicBar/widgets/Controller/index.tsx
index d2c1e4d..1503226 100644
--- a/src/renderer/components/MusicBar/widgets/Controller/index.tsx
+++ b/src/renderer/components/MusicBar/widgets/Controller/index.tsx
@@ -2,6 +2,7 @@ import SvgAsset from "@/renderer/components/SvgAsset";
import "./index.scss";
import trackPlayer from "@/renderer/core/track-player";
import { useTranslation } from "react-i18next";
+import { PlayerState } from "@/common/constant";
export default function Controller() {
const playerState = trackPlayer.usePlayerState();
@@ -20,7 +21,7 @@ export default function Controller() {
{
- if(playerState === trackPlayer.PlayerState.Playing) {
+ if(playerState === PlayerState.Playing) {
trackPlayer.pause();
} else {
trackPlayer.resumePlay();
@@ -29,7 +30,7 @@ export default function Controller() {
>
@@ -37,7 +38,7 @@ export default function Controller() {
className="skip controller-btn"
title={t("music_bar.next_music")}
onClick={() => {
-
+
trackPlayer.skipToNext();
}}
>
diff --git a/src/renderer/components/MusicBar/widgets/Extra/index.tsx b/src/renderer/components/MusicBar/widgets/Extra/index.tsx
index dfac2d0..fcc2106 100644
--- a/src/renderer/components/MusicBar/widgets/Extra/index.tsx
+++ b/src/renderer/components/MusicBar/widgets/Extra/index.tsx
@@ -2,7 +2,6 @@ import SvgAsset from "@/renderer/components/SvgAsset";
import "./index.scss";
import SwitchCase from "@/renderer/components/SwitchCase";
import trackPlayer from "@/renderer/core/track-player";
-import {RepeatMode} from "@/renderer/core/track-player/enum";
import {useRef, useState} from "react";
import Condition from "@/renderer/components/Condition";
import Slider from "rc-slider";
@@ -14,6 +13,7 @@ import {useTranslation} from "react-i18next";
import AppConfig from "@shared/app-config/renderer";
import {isCN} from "@/shared/i18n/renderer";
import useAppConfig from "@/hooks/useAppConfig";
+import {RepeatMode} from "@/common/constant";
export default function Extra() {
const repeatMode = trackPlayer.useRepeatMode();
diff --git a/src/renderer/core/track-player/enum.ts b/src/renderer/core/track-player/enum.ts
index a2d81a1..4838a12 100644
--- a/src/renderer/core/track-player/enum.ts
+++ b/src/renderer/core/track-player/enum.ts
@@ -1,26 +1,6 @@
import LyricParser, { IParsedLrcItem } from "@/renderer/utils/lyric-parser";
+import {PlayerState, RepeatMode} from "@/common/constant";
-/** 播放器状态 */
-export enum PlayerState {
- /** 无音频 */
- None,
- /** 播放中 */
- Playing,
- /** 暂停 */
- Paused,
- /** 缓冲中 */
- Buffering,
-}
-
-/** 播放模式 */
-export enum RepeatMode {
- /** 随机 */
- Shuffle = "shuffle",
- /** 播放队列 */
- Queue = "queue-repeat",
- /** 单曲循环 */
- Loop = "loop",
-}
/** 错误信息 */
export enum ErrorReason {
diff --git a/src/renderer/core/track-player/internal.ts b/src/renderer/core/track-player/internal.ts
index 0ce0097..9a97e61 100644
--- a/src/renderer/core/track-player/internal.ts
+++ b/src/renderer/core/track-player/internal.ts
@@ -2,12 +2,14 @@
* 播放音乐
*/
import {encodeUrlHeaders} from "@/common/normalize-util";
-import {ErrorReason, PlayerState, TrackPlayerEvent} from "./enum";
+import {ErrorReason, TrackPlayerEvent} from "./enum";
import trackPlayerEventsEmitter from "./event";
import albumImg from "@/assets/imgs/album-cover.jpg";
import getUrlExt from "@/renderer/utils/get-url-ext";
import Hls from "hls.js";
import {isSameMedia} from "@/common/media-util";
+import {PlayerState} from "@/common/constant";
+import ServiceManager from "@shared/service-manager/renderer";
class TrackPlayerInternal {
private audioContext: AudioContext;
@@ -29,6 +31,8 @@ class TrackPlayerInternal {
});
this.registerEvents();
+ // @ts-ignore
+ window.ad = this.audio;
}
private throwError(reason: ErrorReason) {
@@ -92,15 +96,46 @@ class TrackPlayerInternal {
trackSource: IMusic.IMusicSource,
musicItem: IMusic.IMusicItem
) {
+ // 1. original url
let url = trackSource.url;
- if (trackSource.headers || trackSource.userAgent) {
- const trackSourceHeaders = trackSource.headers ?? {};
- if (trackSource.userAgent) {
- trackSourceHeaders["user-agent"] = trackSource.userAgent;
- }
+ const urlObj = new URL(trackSource.url);
+ let headers: Record | null = null;
- url = encodeUrlHeaders(url, trackSourceHeaders);
+ if (trackSource.headers || trackSource.userAgent) {
+ headers = {...(trackSource.headers ?? {})};
+ if (trackSource.userAgent) {
+ headers["user-agent"] = trackSource.userAgent;
+ }
}
+
+ if (urlObj.username && urlObj.password) {
+ const authHeader = `Basic ${btoa(
+ `${decodeURIComponent(urlObj.username)}:${decodeURIComponent(
+ urlObj.password
+ )}`
+ )}`;
+ urlObj.username = "";
+ urlObj.password = "";
+ headers = {
+ ...(headers || {}),
+ Authorization: authHeader,
+ }
+ url = urlObj.toString();
+ }
+
+ // hack URL
+ if (headers) {
+ const forwardedUrl = ServiceManager.RequestForwarderService.forwardRequest(url, "GET", headers);
+ if (forwardedUrl) {
+ url = forwardedUrl;
+ headers = null;
+ } else if (!headers["Authorization"]) {
+ url = encodeUrlHeaders(url, headers);
+ headers = null;
+ }
+ }
+
+
if (!url) {
this.throwError(ErrorReason.EmptyResource);
return;
@@ -118,36 +153,25 @@ class TrackPlayerInternal {
],
});
// 拓展播放功能
- if (getUrlExt(url) === ".m3u8" && Hls.isSupported()) {
+ if (getUrlExt(trackSource.url) === ".m3u8" && Hls.isSupported()) {
+ // Todo: headers
this.hls.loadSource(url);
+ } else if (headers) {
+ fetch(url, {
+ method: "GET",
+ headers: {
+ ...trackSource.headers,
+ },
+ })
+ .then(async (res) => {
+ console.log("response", res.headers.get("content-type"));
+ const blob = await res.blob();
+ if (isSameMedia(this.currentMusic, musicItem)) {
+ this.audio.src = URL.createObjectURL(blob);
+ }
+ });
} else {
- const urlObj = new URL(trackSource.url);
- if (urlObj.username && urlObj.password) {
- // TODO: 这部分逻辑需要抽离出来 特殊逻辑
- const authHeader = `Basic ${btoa(
- `${decodeURIComponent(urlObj.username)}:${decodeURIComponent(
- urlObj.password
- )}`
- )}`;
- urlObj.username = "";
- urlObj.password = "";
- fetch(urlObj.toString(), {
- method: "GET",
- headers: {
- ...trackSource.headers,
- Authorization: authHeader,
- },
- })
- .then(async (res) => {
- const blob = await res.blob();
- if (isSameMedia(this.currentMusic, musicItem)) {
- this.audio.src = URL.createObjectURL(blob);
- }
- });
-
- } else {
- this.audio.src = url;
- }
+ this.audio.src = url;
}
}
@@ -177,6 +201,7 @@ class TrackPlayerInternal {
seekTo(seconds: number) {
if (this.hasSource() && isFinite(seconds)) {
const duration = this.audio.duration;
+ console.log(duration);
this.audio.currentTime = Math.min(
seconds,
isNaN(duration) ? Infinity : duration
diff --git a/src/renderer/core/track-player/player.ts b/src/renderer/core/track-player/player.ts
index 10b1ef8..65c1507 100644
--- a/src/renderer/core/track-player/player.ts
+++ b/src/renderer/core/track-player/player.ts
@@ -2,8 +2,6 @@ import Store from "@/common/store";
import trackPlayer from "./internal";
import {
ICurrentLyric,
- PlayerState,
- RepeatMode,
TrackPlayerEvent,
} from "./enum";
import trackPlayerEventsEmitter from "./event";
@@ -15,7 +13,7 @@ import {
isSameMedia,
sortByTimestampAndIndex,
} from "@/common/media-util";
-import { timeStampSymbol, sortIndexSymbol } from "@/common/constant";
+import {timeStampSymbol, sortIndexSymbol, PlayerState, RepeatMode} from "@/common/constant";
import { callPluginDelegateMethod } from "../plugin-delegate";
import LyricParser from "@/renderer/utils/lyric-parser";
import {
@@ -571,7 +569,7 @@ async function playIndex(nextIndex: number, options: IPlayOptions = {}) {
if (!mediaSource?.url) {
throw new Error("Empty Source");
}
- console.log("MEDIA SOURCE", mediaSource, musicItem);
+ console.log("MEDIA SOURCE", JSON.stringify(mediaSource), musicItem);
if (isSameMedia(musicItem, musicQueueStore.getValue()[currentIndex])) {
setCurrentQuality(quality);
setCurrentMusic(musicItem);
diff --git a/src/renderer/document/bootstrap.ts b/src/renderer/document/bootstrap.ts
index e664ba2..01421df 100644
--- a/src/renderer/document/bootstrap.ts
+++ b/src/renderer/document/bootstrap.ts
@@ -24,6 +24,7 @@ import {
setupRecentlyPlaylist,
} from "../core/recently-playlist";
import {TrackPlayerEvent} from "../core/track-player/enum";
+import ServiceManager from "@shared/service-manager/renderer";
setAutoFreeze(false);
@@ -46,6 +47,7 @@ export default async function () {
localMusic.setupLocalMusic();
await Downloader.setupDownloader();
setupRecentlyPlaylist();
+ ServiceManager.setup();
// 自动更新插件
if (AppConfig.getConfig("plugin.autoUpdatePlugin")) {
diff --git a/src/shared/service-manager/common.ts b/src/shared/service-manager/common.ts
new file mode 100644
index 0000000..2178793
--- /dev/null
+++ b/src/shared/service-manager/common.ts
@@ -0,0 +1,3 @@
+export enum ServiceName {
+ RequestForwarder = "request-forwarder",
+}
diff --git a/src/shared/service-manager/main.ts b/src/shared/service-manager/main.ts
new file mode 100644
index 0000000..c9e2110
--- /dev/null
+++ b/src/shared/service-manager/main.ts
@@ -0,0 +1,132 @@
+import {ChildProcess, fork} from "child_process";
+import {ipcMain} from "electron";
+import {IWindowManager} from "@/types/main/window-manager";
+import {ServiceName} from "@shared/service-manager/common";
+import getResourcePath from "@/utils/main/get-resource-path";
+
+
+class ServiceInstance {
+ private serviceProcess: ChildProcess = null;
+ private retryTimeOut = 6000;
+ private started = false;
+ private subprocessName: string;
+
+ private hostChangeCallback: (host: string | null) => void;
+
+ public serviceName: string;
+
+ constructor(serviceName: string, subprocessPath: string) {
+ this.serviceName = serviceName;
+ this.subprocessName = subprocessPath;
+ }
+
+
+ onHostChange(callback: (host: string | null) => void) {
+ this.hostChangeCallback = callback;
+ }
+
+
+ start() {
+ if (this.started) {
+ return;
+ }
+ this.started = true;
+ const servicePath = getResourcePath(".service/" + this.subprocessName + ".js");
+ this.serviceProcess = fork(servicePath);
+
+ interface IMessage {
+ type: "port",
+ port: number
+ }
+
+ this.serviceProcess.on("message", (msg: IMessage) => {
+ const host = "http://127.0.0.1:" + msg.port;
+ this.hostChangeCallback(host);
+ })
+
+ this.serviceProcess.on("error", () => {
+ if (this.started) {
+ setTimeout(() => {
+ this.start(); // 自动重启子进程
+ }, this.retryTimeOut);
+
+ this.retryTimeOut = this.retryTimeOut > 300000 ? 300000 : this.retryTimeOut * 2;
+ }
+ })
+
+ this.serviceProcess.on("exit", (code) => {
+ if (this.started) {
+ console.error(`Service exited with code ${code}. Restarting...`);
+ setTimeout(() => {
+ this.start(); // 自动重启子进程
+ }, this.retryTimeOut);
+
+ this.retryTimeOut = this.retryTimeOut > 300000 ? 300000 : this.retryTimeOut * 2;
+ }
+ });
+ }
+
+ stop() {
+ this.started = false;
+ this.serviceProcess.removeAllListeners();
+ this.serviceProcess.kill();
+ this.serviceProcess = null;
+ this.retryTimeOut = 6000;
+ this.hostChangeCallback(null);
+ }
+}
+
+interface IServiceData {
+ instance: ServiceInstance;
+ host: string | null;
+}
+
+class ServiceManager {
+ private windowManager: IWindowManager;
+ private serviceMap = new Map();
+
+
+ private addService(serviceName: ServiceName) {
+ const instance = new ServiceInstance(serviceName, serviceName);
+ this.serviceMap.set(serviceName, {instance, host: null});
+ instance.onHostChange((host) => {
+ const mainWindow = this.windowManager?.mainWindow;
+ if (mainWindow) {
+ mainWindow.webContents.send("@shared/service-manager/host-changed", serviceName, host);
+ }
+ this.serviceMap.get(serviceName).host = host;
+ });
+
+ return instance;
+ }
+
+ startService(serviceName: ServiceName) {
+ this.serviceMap.get(serviceName)?.instance?.start?.();
+ }
+
+ stopService(serviceName: ServiceName) {
+ this.serviceMap.get(serviceName)?.instance?.stop?.();
+ }
+
+ setup(windowManager: IWindowManager) {
+ this.windowManager = windowManager;
+ // put services here
+ this.addService(ServiceName.RequestForwarder).start();
+
+
+ ipcMain.handle("@shared/service-manager/get-service-hosts", () => {
+ const serviceHosts: Record = {};
+ this.serviceMap.forEach((val, key) => {
+ if (val.host) {
+ serviceHosts[key] = val.host;
+ }
+ })
+ return serviceHosts;
+ });
+
+
+ }
+}
+
+
+export default new ServiceManager();
diff --git a/src/shared/service-manager/preload.ts b/src/shared/service-manager/preload.ts
new file mode 100644
index 0000000..03c3b3f
--- /dev/null
+++ b/src/shared/service-manager/preload.ts
@@ -0,0 +1,34 @@
+import {contextBridge, ipcRenderer} from "electron";
+import {ServiceName} from "@shared/service-manager/common";
+
+const serviceHostMap = new Map();
+
+ipcRenderer.on("@shared/service-manager/host-changed", (_evt, serviceName: ServiceName, host: string | null) => {
+ if (host) {
+ serviceHostMap.set(serviceName, host);
+ } else {
+ serviceHostMap.delete(serviceName)
+ }
+});
+
+
+
+async function setup() {
+ const hosts = (await ipcRenderer.invoke("@shared/service-manager/get-service-hosts")) || {};
+ const serviceNames = Object.keys(hosts);
+ for (const serviceName of serviceNames) {
+ serviceHostMap.set(serviceName as any, hosts[serviceName]);
+ }
+}
+
+function getServiceHost(serviceName: ServiceName) {
+ return serviceHostMap.get(serviceName);
+}
+
+const mod = {
+ setup,
+ getServiceHost
+}
+
+contextBridge.exposeInMainWorld("@shared/service-manager", mod);
+
diff --git a/src/shared/service-manager/renderer.ts b/src/shared/service-manager/renderer.ts
new file mode 100644
index 0000000..9fe8d87
--- /dev/null
+++ b/src/shared/service-manager/renderer.ts
@@ -0,0 +1,38 @@
+import {ServiceName} from "@shared/service-manager/common";
+
+interface IMod {
+ setup: () => Promise;
+ getServiceHost: (serviceName: ServiceName) => string | null;
+}
+
+const mod = window["@shared/service-manager" as any] as unknown as IMod;
+
+
+class RequestForwarderService {
+
+ static forwardRequest(url: string, method?: string, headers?: Record): string | null {
+ const host = mod.getServiceHost(ServiceName.RequestForwarder);
+ if (!host) {
+ return null;
+ }
+
+ const fUrl = new URL(host)
+ fUrl.searchParams.set("url", url);
+ if (method) {
+ fUrl.searchParams.set("method", method);
+ }
+ if (headers) {
+ fUrl.searchParams.set("headers", JSON.stringify(headers));
+ }
+ return fUrl.toString();
+ }
+}
+
+
+
+const ServiceManager = {
+ setup: mod.setup,
+ RequestForwarderService
+}
+
+export default ServiceManager;