From d1dc08bcee280159e859592edbfd3197c9a7dafb Mon Sep 17 00:00:00 2001 From: maotoumao Date: Thu, 7 Nov 2024 22:15:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=B7=E6=B1=82=E8=BD=AC=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- forge.config.ts | 2 +- res/.service/request-forwarder.js | 105 ++++++++++++++ src/main/core/global-short-cut/index.ts | 4 +- src/main/index.ts | 4 +- src/preload/index.ts | 3 +- .../MusicBar/widgets/Controller/index.tsx | 7 +- .../MusicBar/widgets/Extra/index.tsx | 2 +- src/renderer/core/track-player/enum.ts | 22 +-- src/renderer/core/track-player/internal.ts | 95 ++++++++----- src/renderer/core/track-player/player.ts | 6 +- src/renderer/document/bootstrap.ts | 2 + src/shared/service-manager/common.ts | 3 + src/shared/service-manager/main.ts | 132 ++++++++++++++++++ src/shared/service-manager/preload.ts | 34 +++++ src/shared/service-manager/renderer.ts | 38 +++++ 15 files changed, 390 insertions(+), 69 deletions(-) create mode 100644 res/.service/request-forwarder.js create mode 100644 src/shared/service-manager/common.ts create mode 100644 src/shared/service-manager/main.ts create mode 100644 src/shared/service-manager/preload.ts create mode 100644 src/shared/service-manager/renderer.ts 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;