Skip to content

Commit e6e62b0

Browse files
authored
Merge pull request #602 from MoYingJi/prot
✨ feat: 部分支持通过 orpheus 协议唤起 SPlayer
2 parents dc7f011 + 0e4e3f6 commit e6e62b0

7 files changed

Lines changed: 142 additions & 5 deletions

File tree

electron-builder.config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ const config: Configuration = {
4545
arch: ["x64", "arm64"],
4646
},
4747
],
48+
// 注册协议
49+
protocols: [
50+
{
51+
name: "Orpheus Protocol",
52+
schemes: ["orpheus"],
53+
},
54+
],
4855
},
4956
// NSIS 安装器配置
5057
nsis: {
@@ -90,6 +97,13 @@ const config: Configuration = {
9097
"Application requests access to the user's Documents folder.",
9198
NSDownloadsFolderUsageDescription:
9299
"Application requests access to the user's Downloads folder.",
100+
// 注册协议
101+
CFBundleURLTypes: [
102+
{
103+
CFBundleURLName: "Orpheus Protocol",
104+
CFBundleURLSchemes: ["orpheus"],
105+
},
106+
],
93107
},
94108
// 是否启用应用程序的 Notarization(苹果的安全审核)
95109
notarize: false,
@@ -153,6 +167,13 @@ const config: Configuration = {
153167
maintainer: "imsyy.top",
154168
// 应用程序类别
155169
category: "Audio;Music;AudioVideo;",
170+
// 桌面项
171+
desktop: {
172+
entry: {
173+
// 注册协议
174+
MimeType: "x-scheme-handler/orpheus;",
175+
},
176+
},
156177
},
157178
// AppImage 特定配置
158179
appImage: {

electron/main/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import initAppServer from "../server";
1111
import loadWindow from "./windows/load-window";
1212
import mainWindow from "./windows/main-window";
1313
import initIpc from "./ipc";
14+
import { trySendCustomProtocol, registerCustomProtocol } from "./utils/protocol";
1415

1516
// 屏蔽报错
1617
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
@@ -42,11 +43,13 @@ class MainProcess {
4243
// 监听应用事件
4344
this.handleAppEvents();
4445
// Electron 初始化完成后
45-
// 某些API只有在此事件发生后才能使用
46+
// 某些 API 只有在此事件发生后才能使用
4647
app.whenReady().then(async () => {
4748
processLog.info("🚀 Application Process Startup");
4849
// 设置应用程序名称
4950
electronApp.setAppUserModelId("com.imsyy.splayer");
51+
// 注册自定义协议
52+
registerCustomProtocol();
5053
// 启动主服务进程
5154
await initAppServer();
5255
// 启动窗口
@@ -77,7 +80,8 @@ class MainProcess {
7780

7881
// 自定义协议
7982
app.on("open-url", (_, url) => {
80-
processLog.log("Received custom protocol URL:", url);
83+
processLog.log("🔗 Received custom protocol URL:", url);
84+
trySendCustomProtocol(url)
8185
});
8286

8387
// 将要退出

electron/main/utils/protocol.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { app } from "electron";
2+
import { processLog } from "../logger";
3+
import mainWindow from "../windows/main-window";
4+
5+
export const registerCustomProtocol = () => {
6+
app.setAsDefaultProtocolClient("orpheus");
7+
processLog.info("🔗 Registered custom protocol");
8+
};
9+
10+
export const trySendCustomProtocol = (str: string): boolean => {
11+
try {
12+
if (str.startsWith("orpheus://")) {
13+
mainWindow.getWin()!.webContents.send("protocol-url", str);
14+
return true;
15+
}
16+
return false;
17+
} catch (e) {
18+
processLog.error("❌ Failed to send protocol url", e);
19+
return false;
20+
}
21+
}
22+
23+
export const processProtocolFromCommand = (command: string[]): boolean => {
24+
// 这里第一个参数是程序名称 忽略此 仅遍历参数
25+
for (let i = 1; i < command.length; i++) {
26+
const arg = command[i];
27+
if (trySendCustomProtocol(arg)) return true;
28+
}
29+
return false;
30+
}

electron/main/utils/single-lock.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { app } from "electron";
22
import { systemLog } from "../logger";
33
import mainWindow from "../windows/main-window";
4+
import { processProtocolFromCommand } from "./protocol";
45

56
/**
67
* 初始化单实例锁
@@ -16,8 +17,12 @@ export const initSingleLock = (): boolean => {
1617
}
1718
// 当第二个实例启动时触发
1819
else {
19-
app.on("second-instance", () => {
20-
systemLog.warn("❌ 第二个实例将要启动");
20+
app.on("second-instance", (_, commandLine) => {
21+
if (!processProtocolFromCommand(commandLine)) {
22+
systemLog.warn("❌ 第二个实例将要启动");
23+
} else {
24+
systemLog.info("🚀 第二个实例将要启动,通过 Custom Protocol");
25+
}
2126
mainWindow.getWin()?.show();
2227
});
2328
}

scripts/dev.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,16 @@ const startElectronVite = () => {
6161
// 设置 Node.js 选项
6262
env.NODE_OPTIONS = "--max-old-space-size=4096";
6363

64-
const electronVite = spawn("electron-vite", ["dev"], {
64+
// 传递给 electron-vite 的参数
65+
const runArgs = ["dev"];
66+
// 前两个参数分别是 node 和此脚本的路径,丢弃其
67+
const args = process.argv.slice(2);
68+
// 添加参数
69+
if (args.length > 0) {
70+
runArgs.push(...args);
71+
}
72+
73+
const electronVite = spawn("electron-vite", runArgs, {
6574
stdio: "inherit",
6675
shell: true,
6776
env,

src/utils/initIpc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { usePlayer } from "./player";
66
import { cloneDeep } from "lodash-es";
77
import songManager from "./songManager";
88
import { SettingType } from "@/types/main";
9+
import { handleProtocolUrl } from "@/utils/protocal";
910

1011
// 关闭更新状态
1112
const closeUpdateStatus = () => {
@@ -82,6 +83,11 @@ const initIpc = () => {
8283
closeUpdateStatus();
8384
window.$message.error("更新过程出现错误");
8485
});
86+
// 协议数据
87+
window.electron.ipcRenderer.on("protocol-url", (_, url) => {
88+
console.log("📡 Received protocol url:", url);
89+
handleProtocolUrl(url)
90+
});
8591
} catch (error) {
8692
console.log(error);
8793
}

src/utils/protocal.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { songDetail } from "@/api/song";
2+
import { formatSongsList } from "@/utils/format";
3+
import { usePlayer } from "@/utils/player";
4+
5+
class OrpheusData {
6+
constructor(type: string, id: number, cmd: string) {
7+
this.type = type;
8+
this.id = id;
9+
this.cmd = cmd;
10+
}
11+
12+
type: string;
13+
id: number;
14+
cmd: string;
15+
}
16+
17+
export const handleProtocolUrl = (url: string) => {
18+
switch (true) {
19+
case url.startsWith("orpheus://"):
20+
handleOpenOrpheus(url);
21+
break;
22+
default:
23+
break;
24+
}
25+
}
26+
27+
28+
29+
export const handleOpenOrpheus = async (url: string) => {
30+
const data = parseOrpheus(url);
31+
if (!data) return;
32+
console.log("🚀 Open Orpheus:", data);
33+
34+
if (data.cmd === "play" && data.type === "song") {
35+
const player = usePlayer();
36+
const result = await songDetail(data.id);
37+
const song = formatSongsList(result.songs)[0];
38+
player.addNextSong(song, true);
39+
} else {
40+
console.log("❌ Unsupported Command or Type:", data);
41+
}
42+
};
43+
44+
const parseOrpheus = (url: string): OrpheusData | undefined => {
45+
// 这里的协议是从网页端打开官方客户端的协议
46+
// 形如 `orpheus://eyJ0eXBlIjoic29uZyIsImlkIjoiMTgyNjM2MTcxMiIsImNtZCI6InBsYXkifQ==`
47+
// URI 的 Path 部分是 Base64 编码过的,解码后得到 Json
48+
// 形如 `{"type":"song","id":"1826361712","cmd":"play"}`
49+
50+
if (!url.startsWith("orpheus://")) return;
51+
const path = url.replace("orpheus://", "");
52+
const jsonString = atob(path);
53+
let data: OrpheusData;
54+
try {
55+
const json = JSON.parse(jsonString);
56+
data = new OrpheusData(json.type, json.id, json.cmd);
57+
} catch (e) {
58+
console.error("❌ Invalid Data:", e);
59+
return;
60+
}
61+
return data;
62+
}

0 commit comments

Comments
 (0)