Skip to content

Commit 1a959c8

Browse files
committed
✨ feat: 支持配置 TTML 歌词源
1 parent c96a44b commit 1a959c8

9 files changed

Lines changed: 152 additions & 42 deletions

File tree

electron/main/store/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface StoreType {
2424
config?: LyricConfig;
2525
};
2626
proxy: string;
27+
// amll-db-server
28+
amllDbServer: string;
2729
}
2830

2931
/**
@@ -47,6 +49,7 @@ export const useStore = () => {
4749
config: defaultLyricConfig,
4850
},
4951
proxy: "",
52+
amllDbServer: "https://amll-ttml-db.stevexmh.net",
5053
},
5154
});
5255
};

electron/server/netease/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
22
import { pathCase } from "change-case";
33
import { serverLog } from "../../main/logger";
4+
import { useStore } from "../../main/store";
45
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
56

67
// 获取数据
@@ -68,7 +69,11 @@ export const initNcmAPI = async (fastify: FastifyInstance) => {
6869
if (!id) {
6970
return reply.status(400).send({ error: "id is required" });
7071
}
71-
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
72+
const store = useStore();
73+
const server = store.get("amllDbServer") ?? "https://amll-ttml-db.stevexmh.net";
74+
// 净化网址
75+
const cleanServer = server.replace(/\/$/, "");
76+
const url = `${cleanServer}/ncm/${id}`;
7277
try {
7378
const response = await fetch(url);
7479
if (response.status !== 200) {

src/api/song.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isElectron } from "@/utils/env";
22
import { songLevelData } from "@/utils/meta";
33
import { SongUnlockServer } from "@/utils/songManager";
44
import request from "@/utils/request";
5+
import { useSettingStore } from "@/stores";
56

67
// 获取歌曲详情
78
export const songDetail = (ids: number | number[]) => {
@@ -76,7 +77,9 @@ export const songLyricTTML = async (id: number) => {
7677
if (isElectron) {
7778
return request({ url: "/lyric/ttml", params: { id, noCookie: true } });
7879
} else {
79-
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
80+
const settingStore = useSettingStore();
81+
const server = settingStore.amllDbServer || "https://amll-ttml-db.stevexmh.net";
82+
const url = `${server}/ncm/${id}`;
8083
try {
8184
const response = await fetch(url);
8285
if (response === null || response.status !== 200) {

src/components/Card/SongListCard.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@
5151
import type { SongType } from "@/types/main";
5252
import { coverLoaded } from "@/utils/helper";
5353
import { sampleSize } from "lodash-es";
54+
import { VNodeChild } from "vue";
5455
5556
const props = defineProps<{
5657
size: "normal" | "small";
57-
title: string | VNode;
58+
title: string | VNodeChild;
5859
data?: SongType[];
5960
description?: string;
6061
loading?: boolean;

src/components/Setting/LyricsSetting.vue

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@
260260
</div>
261261
<n-switch v-model:value="settingStore.enableTTMLLyric" class="set" :round="false" />
262262
</n-card>
263+
<n-collapse-transition :show="settingStore.enableTTMLLyric">
264+
<n-card class="set-item">
265+
<div class="label">
266+
<n-text class="name">AMLL TTML DB 地址</n-text>
267+
<n-text class="tip" :depth="3">
268+
AMLL TTML DB 地址,请确保地址正确,否则将导致歌词获取失败
269+
</n-text>
270+
</div>
271+
<n-button type="primary" strong secondary @click="changeAMLLDBServer"> 配置 </n-button>
272+
</n-card>
273+
</n-collapse-transition>
263274
<n-card class="set-item">
264275
<div class="label">
265276
<n-text class="name">启用歌词排除</n-text>
@@ -550,8 +561,10 @@
550561
</template>
551562

552563
<script setup lang="ts">
564+
import { NFlex, NInput, NText } from "naive-ui";
553565
import { useSettingStore, useStatusStore } from "@/stores";
554566
import { cloneDeep, isEqual } from "lodash-es";
567+
import { isValidURL } from "@/utils/validate";
555568
import { isElectron } from "@/utils/env";
556569
import { openLyricExclude } from "@/utils/modal";
557570
import { LyricConfig } from "@/types/desktop-lyric";
@@ -566,6 +579,9 @@ const settingStore = useSettingStore();
566579
// 全部字体
567580
const allFontsData = ref<SelectOption[]>([]);
568581
582+
// AMLL TTML DB 地址
583+
const amllDbServer = ref("https://amll-ttml-db.stevexmh.net");
584+
569585
// 桌面歌词配置
570586
const desktopLyricConfig = reactive<LyricConfig>({ ...defaultDesktopLyricConfig });
571587
@@ -649,10 +665,58 @@ const getAllSystemFonts = async () => {
649665
});
650666
};
651667
652-
onMounted(() => {
668+
// 修改 AMLL DB 服务地址
669+
const changeAMLLDBServer = () => {
670+
window.$modal.create({
671+
preset: "dialog",
672+
title: "修改 AMLL DB 地址",
673+
content: () =>
674+
h(
675+
NFlex,
676+
{ vertical: true },
677+
{
678+
default: () => [
679+
h(
680+
NText,
681+
{ depth: 3, type: "warning" },
682+
{ default: () => "如果你不清楚这里是做什么的,请不要修改" },
683+
),
684+
h(NInput, {
685+
value: amllDbServer.value,
686+
onUpdateValue: (val) => (amllDbServer.value = val),
687+
placeholder: "请输入 AMLL TTML DB 地址",
688+
}),
689+
h(NText, { depth: 3 }, { default: () => "请确保地址正确,否则将导致歌词获取失败" }),
690+
],
691+
},
692+
),
693+
positiveText: "确认",
694+
negativeText: "取消",
695+
onPositiveClick: async () => {
696+
const urlValue = amllDbServer.value.trim();
697+
if (isValidURL(urlValue)) {
698+
await window.api.store.set("amllDbServer", urlValue);
699+
settingStore.amllDbServer = urlValue;
700+
window.$message.success("AMLL TTML DB 地址已更新");
701+
return true;
702+
} else {
703+
window.$message.error("请输入正确的网址格式");
704+
return false;
705+
}
706+
},
707+
});
708+
};
709+
710+
onMounted(async () => {
653711
if (isElectron) {
654712
getDesktopLyricConfig();
655713
getAllSystemFonts();
714+
// 恢复地址
715+
const server = await window.api.store.get("amllDbServer");
716+
if (server) {
717+
amllDbServer.value = server;
718+
settingStore.amllDbServer = server;
719+
}
656720
}
657721
});
658722
</script>

src/main.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import router from "@/router";
99
import { debounceDirective, throttleDirective, visibleDirective } from "@/utils/instruction";
1010
// ipc
1111
import initIpc from "@/utils/initIpc";
12+
// use-store
13+
import { useSettingStore } from "@/stores";
14+
import { sendRegisterProtocol } from "@/utils/protocol";
1215
// 全局样式
1316
import "@/style/main.scss";
1417
import "@/style/animate.scss";
1518
import "github-markdown-css/github-markdown.css";
16-
import { useSettingStore } from "@/stores";
17-
import { sendRegisterProtocol } from "@/utils/protocol";
19+
import { isElectron } from "./utils/env";
1820

1921
// 初始化 ipc
2022
initIpc();
@@ -35,5 +37,7 @@ app.directive("visible", visibleDirective);
3537
app.mount("#app");
3638

3739
// 根据设置判断是否要注册协议
38-
const settings = useSettingStore();
39-
sendRegisterProtocol("orpheus", settings.registryProtocol.orpheus);
40+
if (isElectron) {
41+
const settings = useSettingStore();
42+
sendRegisterProtocol("orpheus", settings.registryProtocol.orpheus);
43+
}

src/stores/setting.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ export interface SettingState {
77
themeMode: "light" | "dark" | "auto";
88
/** 主题类别 */
99
themeColorType:
10-
| "default"
11-
| "orange"
12-
| "blue"
13-
| "pink"
14-
| "brown"
15-
| "indigo"
16-
| "green"
17-
| "purple"
18-
| "yellow"
19-
| "teal"
20-
| "custom";
10+
| "default"
11+
| "orange"
12+
| "blue"
13+
| "pink"
14+
| "brown"
15+
| "indigo"
16+
| "green"
17+
| "purple"
18+
| "yellow"
19+
| "teal"
20+
| "custom";
2121
/** 主题自定义颜色 */
2222
themeCustomColor: string;
2323
/** 全局着色 */
@@ -88,14 +88,14 @@ export interface SettingState {
8888
proxyPort: number;
8989
/** 歌曲音质 */
9090
songLevel:
91-
| "standard"
92-
| "higher"
93-
| "exhigh"
94-
| "lossless"
95-
| "hires"
96-
| "jyeffect"
97-
| "sky"
98-
| "jymaster";
91+
| "standard"
92+
| "higher"
93+
| "exhigh"
94+
| "lossless"
95+
| "hires"
96+
| "jyeffect"
97+
| "sky"
98+
| "jymaster";
9999
/** 播放设备 */
100100
playDevice: "default" | string;
101101
/** 自动播放 */
@@ -142,6 +142,8 @@ export interface SettingState {
142142
useAMSpring: boolean;
143143
/** 是否启用在线 TTML 歌词 */
144144
enableTTMLLyric: boolean;
145+
/** AMLL DB 服务地址 */
146+
amllDbServer: string;
145147
/** 菜单显示封面 */
146148
menuShowCover: boolean;
147149
/** 菜单展开项 */
@@ -280,6 +282,7 @@ export const useSettingStore = defineStore("setting", {
280282
useAMLyrics: false,
281283
useAMSpring: false,
282284
enableTTMLLyric: true,
285+
amllDbServer: "https://amll-ttml-db.stevexmh.net",
283286
showYrc: true,
284287
showYrcAnimation: true,
285288
showYrcLongEffect: true,
@@ -367,11 +370,12 @@ export const useSettingStore = defineStore("setting", {
367370
}
368371
window.$message.info(
369372
`已切换至
370-
${this.themeMode === "auto"
371-
? "跟随系统"
372-
: this.themeMode === "light"
373-
? "浅色模式"
374-
: "深色模式"
373+
${
374+
this.themeMode === "auto"
375+
? "跟随系统"
376+
: this.themeMode === "light"
377+
? "浅色模式"
378+
: "深色模式"
375379
}`,
376380
{
377381
showIcon: false,

src/utils/audioManager.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,22 +324,16 @@ class AudioManager {
324324
* @param deviceId 设备 ID
325325
*/
326326
public async setSinkId(deviceId: string) {
327+
if (deviceId === "default") return;
327328
try {
328-
// 1. 如果 AudioContext 已初始化,优先在 Context 上设置
329-
// 注意:setSinkId 在 AudioContext 上是新特性,需检查是否存在
329+
// 优先在 Context 上设置
330330
if (this.isInitialized && this.audioCtx && typeof this.audioCtx.setSinkId === "function") {
331-
console.log("AudioManager: 使用 AudioContext 切换输出设备", deviceId);
332331
await this.audioCtx.setSinkId(deviceId);
333-
return; // 成功后直接返回,不要再触碰 audioElement
332+
return;
334333
}
335-
336-
// 2. 如果没有初始化 Web Audio,或者 Context 不支持 setSinkId
337-
// 则回退到在 HTMLAudioElement 上设置
334+
// 回退到在 HTMLAudioElement 上设置
338335
if (this.audioElement && typeof this.audioElement.setSinkId === "function") {
339-
console.log("AudioManager: 使用 HTMLAudioElement 切换输出设备", deviceId);
340336
await this.audioElement.setSinkId(deviceId);
341-
} else {
342-
console.warn("AudioManager: 当前浏览器不支持设置输出设备 (setSinkId)");
343337
}
344338
} catch (error) {
345339
console.error("AudioManager: 设置输出设备失败", error);

src/utils/validate.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @file validate.ts
3+
* @description 常用验证函数
4+
* @author imsyy
5+
*/
6+
7+
/**
8+
* 验证字符串是否为有效的 URL。
9+
* 此函数能正确处理包含 localhost、IP 地址和端口号的 URL。
10+
* @param urlString 要验证的字符串。
11+
* @returns 如果是有效 URL,则返回 true;否则返回 false。
12+
*/
13+
export const isValidURL = (urlString: string): boolean => {
14+
const urlValue = urlString.trim();
15+
if (!urlValue) {
16+
return false;
17+
}
18+
19+
// 如果用户未输入协议头,则自动添加 http:// 以便 URL 构造函数进行验证
20+
const urlWithProtocol =
21+
urlValue.startsWith("http://") || urlValue.startsWith("https://")
22+
? urlValue
23+
: `http://${urlValue}`;
24+
25+
try {
26+
// 使用内置的 URL 构造函数进行稳健的验证
27+
new URL(urlWithProtocol);
28+
return true;
29+
} catch (error) {
30+
return false;
31+
}
32+
};

0 commit comments

Comments
 (0)