Skip to content

Commit a421ea5

Browse files
authored
Merge pull request #660 from imsyy/dev-perf
优化下载与修复音质切换BUG
2 parents 101f297 + 6964b45 commit a421ea5

16 files changed

Lines changed: 516 additions & 236 deletions

File tree

electron/main/ipc/ipc-file.ts

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
1+
import { type DownloadItem, app, BrowserWindow, dialog, ipcMain, shell } from "electron";
22
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
33
import { access, mkdir, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
44
import { parseFile } from "music-metadata";
@@ -12,6 +12,9 @@ import { useStore } from "../store";
1212
import FastGlob from "fast-glob";
1313
import pLimit from "p-limit";
1414

15+
// 下载项
16+
const downloadItems = new Map<number, DownloadItem>();
17+
1518
/**
1619
* 文件相关 IPC
1720
*/
@@ -119,10 +122,10 @@ const initFileIpc = (): void => {
119122
"aac",
120123
"webm",
121124
"m4a",
122-
"mp4",
123125
"ogg",
124126
"aiff",
125127
"aif",
128+
"aifc",
126129
];
127130
// 查找指定目录下的所有音乐文件
128131
const musicFiles = await FastGlob(`**/*.{${musicExtensions.join(",")}}`, globOpt(filePath));
@@ -467,7 +470,7 @@ const initFileIpc = (): void => {
467470
fileType: "mp3",
468471
path: app.getPath("downloads"),
469472
},
470-
): Promise<{ status: "success" | "skipped" | "error"; message?: string }> => {
473+
): Promise<{ status: "success" | "skipped" | "error" | "cancelled"; message?: string }> => {
471474
try {
472475
// 获取窗口
473476
const win = BrowserWindow.fromWebContents(event.sender);
@@ -505,16 +508,57 @@ const initFileIpc = (): void => {
505508
}
506509
}
507510

511+
// 尝试删除可能存在的临时文件
512+
const tempPath = join(downloadPath, `${fileName}.${fileType}.tmp`);
513+
try {
514+
await unlink(tempPath);
515+
} catch {
516+
// 忽略错误
517+
}
518+
508519
// 下载文件
509-
const songDownload = await download(win, url, {
510-
directory: downloadPath,
511-
filename: `${fileName}.${fileType}`,
512-
showProgressBar: false,
513-
onProgress: (progress) => {
514-
win.webContents.send("download-progress", { ...progress, id: songData?.id });
515-
},
516-
});
520+
let songDownload: DownloadItem;
521+
try {
522+
songDownload = await download(win, url, {
523+
directory: downloadPath,
524+
filename: `${fileName}.${fileType}`,
525+
showProgressBar: false,
526+
onProgress: (progress) => {
527+
win.webContents.send("download-progress", { ...progress, id: songData?.id });
528+
},
529+
onStarted: (item) => {
530+
if (songData?.id) {
531+
downloadItems.set(songData.id, item);
532+
}
533+
},
534+
});
535+
} catch (error: unknown) {
536+
if (error instanceof Error && error.message === "The download was cancelled") {
537+
return { status: "cancelled", message: "下载已取消" };
538+
}
539+
throw error;
540+
} finally {
541+
if (songData?.id) {
542+
downloadItems.delete(songData.id);
543+
}
544+
}
545+
517546
if (!downloadMeta || !songData?.cover) return { status: "success" };
547+
548+
// 验证文件是否存在
549+
const savedPath = songDownload.getSavePath();
550+
try {
551+
await access(savedPath);
552+
} catch (e) {
553+
// 等待一小段时间再次检查(解决某些情况下文件系统延迟)
554+
await new Promise((resolve) => setTimeout(resolve, 500));
555+
try {
556+
await access(savedPath);
557+
} catch {
558+
throw new Error(`File not found at ${savedPath}`);
559+
}
560+
}
561+
518562
// 下载封面
519563
const coverUrl = songData?.coverSize?.l || songData.cover;
520564
const coverDownload = await download(win, coverUrl, {
@@ -523,7 +567,7 @@ const initFileIpc = (): void => {
523567
showProgressBar: false,
524568
});
525569
// 读取歌曲文件
526-
let songFile = File.createFromPath(songDownload.getSavePath());
570+
let songFile = File.createFromPath(savedPath);
527571
// 清除原有标签,防止脏数据(如模拟播放下载时的乱码歌词)
528572
songFile.removeTags(TagTypes.AllTags);
529573
songFile.save();
@@ -565,6 +609,17 @@ const initFileIpc = (): void => {
565609
},
566610
);
567611

612+
// 取消下载
613+
ipcMain.handle("cancel-download", async (_, songId: number) => {
614+
const item = downloadItems.get(songId);
615+
if (item) {
616+
item.cancel();
617+
downloadItems.delete(songId);
618+
return true;
619+
}
620+
return false;
621+
});
622+
568623
// 检查是否是相同的路径(规范化后比较)
569624
ipcMain.handle("check-if-same-path", (_, localFilesPath: string[], selectedDir: string) => {
570625
const resolvedSelectedDir = resolve(selectedDir);

electron/main/ipc/ipc-tray.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const initTrayIpc = (): void => {
1919

2020
// 音乐名称更改
2121
ipcMain.on("play-song-change", (_, options) => {
22-
let { title } = options;
22+
let title = options?.title;
2323
if (!title) title = appName;
2424
// 更改标题
2525
tray?.setTitle(title);

src/components/Player/FullPlayer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<Teleport to="body">
3-
<Transition name="up" mode="out-in">
3+
<Transition :name="settingStore.playerExpandAnimation" mode="out-in">
44
<div
55
v-if="statusStore.showFullPlayer"
66
:style="{

src/components/Player/PlayerComment.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ onMounted(() => {
243243
}
244244
.comment-list {
245245
margin: 0 auto;
246+
:deep(.comments) {
247+
.text {
248+
&::selection {
249+
background-color: rgba(var(--main-cover-color));
250+
}
251+
}
252+
}
246253
}
247254
.placeholder {
248255
width: 100%;

src/components/Player/PlayerData.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<n-popselect
4444
:value="currentPlayingLevel"
4545
:options="qualityOptions"
46-
:disabled="!!musicStore.playSong.path || statusStore.playUblock"
46+
:disabled="!!musicStore.playSong.path || statusStore.playUblock || !!musicStore.playSong.pc"
4747
class="player"
4848
trigger="click"
4949
placement="top"

src/components/Setting/AboutSetting.vue

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,55 @@
5656
</n-card>
5757
</n-flex>
5858
</div>
59+
<div class="set-list">
60+
<n-h3 prefix="bar"> 开发人员 </n-h3>
61+
<n-flex :size="12" class="link">
62+
<n-card
63+
v-for="(item, index) in developers"
64+
:key="index"
65+
class="link-item"
66+
hoverable
67+
@click="openLink(item.url)"
68+
>
69+
<n-flex align="center">
70+
<n-avatar round :size="40" :src="item.avatar" fallback-src="/images/avatar.jpg?asset" />
71+
<n-flex vertical :gap="4">
72+
<n-text class="name" strong> {{ item.name }} </n-text>
73+
<n-text class="tip" :depth="3" style="font-size: 12px">
74+
{{ item.role }}
75+
</n-text>
76+
</n-flex>
77+
</n-flex>
78+
</n-card>
79+
</n-flex>
80+
</div>
81+
<Transition name="fade" mode="out-in">
82+
<div v-if="allContributors.length > 0" class="set-list">
83+
<n-collapse arrow-placement="right">
84+
<n-collapse-item title="更多贡献者" name="1">
85+
<n-flex :size="12" class="link">
86+
<n-card
87+
v-for="(item, index) in allContributors"
88+
:key="index"
89+
class="link-item"
90+
hoverable
91+
@click="openLink(item.url)"
92+
>
93+
<n-flex align="center">
94+
<n-avatar round :size="40" :src="item.avatar" fallback-src="/images/avatar.jpg?asset" />
95+
<n-flex vertical :gap="4">
96+
<n-text class="name" strong> {{ item.name }} </n-text>
97+
<n-text class="tip" :depth="3" style="font-size: 12px">
98+
{{ item.role }}
99+
</n-text>
100+
</n-flex>
101+
</n-flex>
102+
</n-card>
103+
</n-flex>
104+
</n-collapse-item>
105+
</n-collapse>
106+
</div>
107+
</Transition>
59108
<div class="set-list">
60109
<n-h3 prefix="bar"> 社区与资讯 </n-h3>
61110
<n-flex :size="12" class="link">
@@ -110,6 +159,42 @@ const statusStore = useStatusStore();
110159
// 开发者模式点击次数
111160
const developerModeClickCount = ref(0);
112161
162+
// 开发人员
163+
type DeveloperType = {
164+
name: string;
165+
role: string;
166+
url: string;
167+
avatar: string;
168+
};
169+
170+
const developers = ref<DeveloperType[]>([]);
171+
const allContributors = ref<DeveloperType[]>([]);
172+
173+
// 获取贡献者
174+
const getContributors = async () => {
175+
try {
176+
const response = await fetch(
177+
"https://api.github.com/repos/imsyy/SPlayer/contributors?per_page=100&anon=true",
178+
);
179+
const data = await response.json();
180+
if (Array.isArray(data)) {
181+
const list = data
182+
.filter((item: any) => item.login !== "type-bot" && item.type !== "Bot")
183+
.map((item: any) => ({
184+
name: item.login || item.name,
185+
role: item.login === "imsyy" ? "Owner / Full Stack" : "Contributor",
186+
url: item.html_url || "",
187+
avatar: item.avatar_url || "/images/avatar.jpg?asset",
188+
}));
189+
developers.value = list.slice(0, 6);
190+
allContributors.value = list.slice(6);
191+
}
192+
} catch (error) {
193+
console.error("Failed to fetch contributors:", error);
194+
// Fallback or empty state handling if needed, currently just empty
195+
}
196+
};
197+
113198
// 特别鸣谢
114199
const contributors = [
115200
{
@@ -215,7 +300,10 @@ const openDeveloperMode = useThrottleFn(() => {
215300
}
216301
}, 100);
217302
218-
onMounted(getUpdateData);
303+
onMounted(() => {
304+
getUpdateData();
305+
getContributors();
306+
});
219307
</script>
220308

221309
<style lang="scss" scoped>

src/components/Setting/PlaySetting.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,26 @@
142142
</div>
143143
<div class="set-list">
144144
<n-h3 prefix="bar"> 播放器 </n-h3>
145+
<n-card class="set-item">
146+
<div class="label">
147+
<n-text class="name">播放器展开动画</n-text>
148+
<n-text class="tip" :depth="3">选择播放器展开时的动画效果</n-text>
149+
</div>
150+
<n-select
151+
v-model:value="settingStore.playerExpandAnimation"
152+
:options="[
153+
{
154+
label: '上浮',
155+
value: 'up',
156+
},
157+
{
158+
label: '平滑',
159+
value: 'smooth',
160+
},
161+
]"
162+
class="set"
163+
/>
164+
</n-card>
145165
<n-card class="set-item">
146166
<div class="label">
147167
<n-text class="name">播放器样式</n-text>

src/core/player/PlayerController.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,14 @@ class PlayerController {
491491
this.retryInfo.count = 0;
492492
this.failSkipCount++;
493493

494+
// 连续跳过 3 首直接暂停
495+
if (this.failSkipCount >= 3) {
496+
window.$message.error("播放失败次数过多,已停止播放");
497+
this.pause(true);
498+
this.failSkipCount = 0;
499+
return;
500+
}
501+
494502
// 列表只有一首,或连续跳过所有歌曲
495503
if (dataStore.playList.length <= 1 || this.failSkipCount >= dataStore.playList.length) {
496504
window.$message.error("当前已无可播放歌曲");
@@ -944,11 +952,11 @@ class PlayerController {
944952
? song.album.name
945953
: String(song.album),
946954
artwork: [
947-
{ src: musicStore.getSongCover("s"), sizes: "100x100", type: "image/jpeg" },
948-
{ src: musicStore.getSongCover("m"), sizes: "300x300", type: "image/jpeg" },
949-
{ src: musicStore.getSongCover("cover"), sizes: "512x512", type: "image/jpeg" },
950-
{ src: musicStore.getSongCover("l"), sizes: "1024x1024", type: "image/jpeg" },
951-
{ src: musicStore.getSongCover("xl"), sizes: "1920x1920", type: "image/jpeg" },
955+
{ src: musicStore.getSongCover("s") || musicStore.playSong.cover || "", sizes: "100x100", type: "image/jpeg" },
956+
{ src: musicStore.getSongCover("m") || musicStore.playSong.cover || "", sizes: "300x300", type: "image/jpeg" },
957+
{ src: musicStore.getSongCover("cover") || musicStore.playSong.cover || "", sizes: "512x512", type: "image/jpeg" },
958+
{ src: musicStore.getSongCover("l") || musicStore.playSong.cover || "", sizes: "1024x1024", type: "image/jpeg" },
959+
{ src: musicStore.getSongCover("xl") || musicStore.playSong.cover || "", sizes: "1920x1920", type: "image/jpeg" },
952960
],
953961
});
954962
}

src/core/player/SongManager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ class SongManager {
7979
* @param id 歌曲id
8080
* @returns 在线播放信息
8181
*/
82-
public getOnlineUrl = async (id: number): Promise<AudioSource> => {
82+
public getOnlineUrl = async (id: number, isPc: boolean = false): Promise<AudioSource> => {
8383
const settingStore = useSettingStore();
84-
const res = await songUrl(id, settingStore.songLevel);
84+
const level = isPc ? "exhigh" : settingStore.songLevel;
85+
const res = await songUrl(id, level);
8586
console.log(`🌐 ${id} music data:`, res);
8687
const songData = res.data?.[0];
8788
// 是否有播放地址
@@ -198,7 +199,7 @@ class SongManager {
198199
// 是否可解锁
199200
const canUnlock = isElectron && nextSong.type !== "radio" && settingStore.useSongUnlock;
200201
// 先请求官方地址
201-
const { url: officialUrl, isTrial, quality } = await this.getOnlineUrl(songId);
202+
const { url: officialUrl, isTrial, quality } = await this.getOnlineUrl(songId, false);
202203
if (officialUrl && !isTrial) {
203204
// 官方可播放且非试听
204205
this.nextPrefetch = { id: songId, url: officialUrl, isUnlocked: false, quality };
@@ -273,7 +274,7 @@ class SongManager {
273274
// 是否可解锁
274275
const canUnlock = isElectron && song.type !== "radio" && settingStore.useSongUnlock;
275276
// 尝试获取官方链接
276-
const { url: officialUrl, isTrial, quality } = await this.getOnlineUrl(songId);
277+
const { url: officialUrl, isTrial, quality } = await this.getOnlineUrl(songId, !!song.pc);
277278
// 如果官方链接有效且非试听(或者用户接受试听)
278279
if (officialUrl && (!isTrial || (isTrial && settingStore.playSongDemo))) {
279280
if (isTrial) window.$message.warning("当前歌曲仅可试听");

0 commit comments

Comments
 (0)