Skip to content

Commit ba2393c

Browse files
committed
🐞 fix: 修复数据库结构 & 列表封面处理
1 parent 06cf6c3 commit ba2393c

12 files changed

Lines changed: 213 additions & 67 deletions

File tree

electron/main/database/LocalMusicDB.ts

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,37 @@ import Database from "better-sqlite3";
22
import { existsSync } from "node:fs";
33
import { readFile, rename } from "node:fs/promises";
44

5-
/** 当前本地音乐库 DB 版本,用于控制缓存结构升级 */
6-
const CURRENT_DB_VERSION = 3;
5+
/** 列定义接口 */
6+
interface ColumnDef {
7+
/** 列类型(如 TEXT、INTEGER、REAL) */
8+
type: string;
9+
/** 列约束(如 PRIMARY KEY、NOT NULL、UNIQUE) */
10+
constraints?: string;
11+
/** 默认值(用于 ALTER TABLE 添加 NOT NULL 列) */
12+
default?: string | number | null;
13+
}
14+
15+
/** 声明式表结构定义 */
16+
const TRACKS_SCHEMA: Record<string, ColumnDef> = {
17+
id: { type: "TEXT", constraints: "PRIMARY KEY" },
18+
path: { type: "TEXT", constraints: "NOT NULL UNIQUE" },
19+
title: { type: "TEXT" },
20+
artist: { type: "TEXT" },
21+
album: { type: "TEXT" },
22+
duration: { type: "REAL" },
23+
cover: { type: "TEXT" },
24+
mtime: { type: "REAL" },
25+
size: { type: "INTEGER" },
26+
bitrate: { type: "REAL" },
27+
track_number: { type: "INTEGER" },
28+
};
29+
30+
/** 索引定义 - 自动创建常用查询字段的索引 */
31+
const INDEXES: Record<string, string[]> = {
32+
// path 已有 UNIQUE 约束,无需额外索引
33+
idx_tracks_artist: ["artist"],
34+
idx_tracks_album: ["album"],
35+
};
736

837
/** 音乐数据接口 */
938
export interface MusicTrack {
@@ -53,41 +82,73 @@ export class LocalMusicDB {
5382
try {
5483
this.db = new Database(this.dbPath);
5584
this.db.pragma("journal_mode = WAL");
85+
this.db.pragma("synchronous = NORMAL");
86+
87+
// 使用声明式 schema 创建表
88+
const columnsSQL = Object.entries(TRACKS_SCHEMA)
89+
.map(([name, def]) => {
90+
const parts = [name, def.type];
91+
if (def.constraints) parts.push(def.constraints);
92+
return parts.join(" ");
93+
})
94+
.join(", ");
5695

5796
this.db.exec(`
58-
CREATE TABLE IF NOT EXISTS tracks (
59-
id TEXT PRIMARY KEY,
60-
path TEXT NOT NULL UNIQUE,
61-
title TEXT,
62-
artist TEXT,
63-
album TEXT,
64-
duration REAL,
65-
cover TEXT,
66-
mtime REAL,
67-
size INTEGER,
68-
bitrate REAL,
69-
track_number INTEGER
70-
);
71-
CREATE TABLE IF NOT EXISTS meta (
72-
key TEXT PRIMARY KEY,
73-
value TEXT
74-
);
97+
CREATE TABLE IF NOT EXISTS tracks (${columnsSQL});
98+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
7599
`);
76100

77-
// 检查版本
78-
const versionStmt = this.db.prepare("SELECT value FROM meta WHERE key = ?");
79-
const versionRow = versionStmt.get("version") as { value: string } | undefined;
80-
if (!versionRow) {
81-
this.db
82-
.prepare("INSERT INTO meta (key, value) VALUES (?, ?)")
83-
.run("version", CURRENT_DB_VERSION.toString());
84-
}
101+
// 自动同步缺失的列和索引
102+
this.syncSchema();
103+
this.syncIndexes();
85104
} catch (e) {
86105
console.error("Failed to initialize SQLite DB:", e);
87106
throw e;
88107
}
89108
}
90109

110+
/** 自动同步表结构 - 检测并添加缺失的列 */
111+
private syncSchema() {
112+
if (!this.db) return;
113+
114+
// 获取现有列信息
115+
const columns = this.db.prepare("PRAGMA table_info(tracks)").all() as { name: string }[];
116+
const existingColumns = new Set(columns.map((col) => col.name));
117+
118+
// 检测并添加缺失的列
119+
for (const [columnName, def] of Object.entries(TRACKS_SCHEMA)) {
120+
if (!existingColumns.has(columnName)) {
121+
// NOT NULL 列必须有默认值,否则迁移会失败
122+
const hasNotNull = def.constraints?.includes("NOT NULL");
123+
if (hasNotNull && def.default === undefined) {
124+
throw new Error(
125+
`[LocalMusicDB] Cannot add NOT NULL column '${columnName}' without a default value`,
126+
);
127+
}
128+
129+
// 构建 ALTER TABLE 语句
130+
let sql = `ALTER TABLE tracks ADD COLUMN ${columnName} ${def.type}`;
131+
if (def.default !== undefined) {
132+
const defaultVal = typeof def.default === "string" ? `'${def.default}'` : def.default;
133+
sql += ` DEFAULT ${defaultVal}`;
134+
}
135+
console.log(`[LocalMusicDB] Adding missing column: ${columnName}`);
136+
this.db.exec(sql);
137+
}
138+
}
139+
}
140+
141+
/** 自动同步索引 */
142+
private syncIndexes() {
143+
if (!this.db) return;
144+
145+
for (const [indexName, columns] of Object.entries(INDEXES)) {
146+
// CREATE INDEX IF NOT EXISTS 是安全的
147+
const sql = `CREATE INDEX IF NOT EXISTS ${indexName} ON tracks (${columns.join(", ")})`;
148+
this.db.exec(sql);
149+
}
150+
}
151+
91152
/** 关闭数据库 */
92153
public close() {
93154
if (this.db) {
@@ -136,18 +197,24 @@ export class LocalMusicDB {
136197
public addTracks(tracks: MusicTrack[]) {
137198
if (!this.db || tracks.length === 0) return;
138199

200+
// 动态生成 INSERT 语句
201+
const columns = Object.keys(TRACKS_SCHEMA);
202+
const columnsList = columns.join(", ");
203+
const valuesList = columns.map((c) => `@${c}`).join(", ");
204+
139205
const insertStmt = this.db.prepare(`
140-
INSERT OR REPLACE INTO tracks (id, path, title, artist, album, duration, cover, mtime, size, bitrate, track_number)
141-
VALUES (@id, @path, @title, @artist, @album, @duration, @cover, @mtime, @size, @bitrate, @track_number)
206+
INSERT OR REPLACE INTO tracks (${columnsList})
207+
VALUES (${valuesList})
142208
`);
143209

144210
const transaction = this.db.transaction((tracks: MusicTrack[]) => {
145211
for (const track of tracks) {
146-
insertStmt.run({
147-
...track,
148-
// rust 模块的 cover 是 option,需要特殊处理一下
149-
cover: track.cover ?? null,
150-
});
212+
// 确保所有列都有值,缺失的使用 null
213+
const params: Record<string, unknown> = {};
214+
for (const col of columns) {
215+
params[col] = (track as unknown as Record<string, unknown>)[col] ?? null;
216+
}
217+
insertStmt.run(params);
151218
}
152219
});
153220

@@ -173,6 +240,8 @@ export class LocalMusicDB {
173240
public clearTracks() {
174241
if (!this.db) return;
175242
this.db.prepare("DELETE FROM tracks").run();
243+
// 回收磁盘空间,防止数据库文件膨胀
244+
this.db.exec("VACUUM");
176245
}
177246

178247
/** 获取所有歌曲路径 */

electron/main/ipc/ipc-file.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ const initFileIpc = (): void => {
166166
try {
167167
// 处理元信息 (跳过封面解析以提升速度)
168168
const { common, format } = await parseFile(fullPath, { skipCovers: true });
169-
// 获取文件大小
170-
const { size } = await stat(fullPath);
169+
// 获取文件状态信息(大小和创建时间)
170+
const fileStat = await stat(fullPath);
171171
const ext = extname(fullPath);
172172

173173
return {
@@ -177,9 +177,11 @@ const initFileIpc = (): void => {
177177
album: common.album || "",
178178
alia: common.comment?.[0]?.text || "",
179179
duration: (format?.duration ?? 0) * 1000,
180-
size: (size / (1024 * 1024)).toFixed(2),
180+
size: (fileStat.size / (1024 * 1024)).toFixed(2),
181181
path: fullPath,
182182
quality: format.bitrate ?? 0,
183+
// 文件创建时间(用于排序)
184+
createTime: fileStat.birthtime.getTime(),
183185
replayGain: {
184186
trackGain: common.replaygain_track_gain?.ratio,
185187
trackPeak: common.replaygain_track_peak?.ratio,
@@ -194,8 +196,10 @@ const initFileIpc = (): void => {
194196
}),
195197
);
196198
const metadataResults = await Promise.all(metadataPromises);
197-
// 过滤掉解析失败的文件
198-
return metadataResults.filter((item) => item !== null);
199+
// 过滤掉解析失败的文件,并按创建时间降序排序(最新的在前)
200+
return metadataResults
201+
.filter((item): item is NonNullable<typeof item> => item !== null)
202+
.sort((a, b) => b.createTime - a.createTime);
199203
} catch (error) {
200204
ipcLog.error("❌ Error fetching music metadata:", error);
201205
return [];

src/components/Modal/UpdateApp.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
<n-tag :bordered="false" type="warning">
99
{{ data?.version || "v0.0.0" }}
1010
</n-tag>
11+
<n-tag v-if="isPrerelease" :bordered="false" type="error" size="small"> 测试版 </n-tag>
1112
</n-flex>
13+
<!-- 测试版警告 -->
14+
<n-alert v-if="isPrerelease" type="warning" :bordered="false" class="prerelease-warning">
15+
当前更新为测试版本,可能包含未完成的功能或已知问题,请谨慎更新
16+
</n-alert>
1217
<n-scrollbar style="max-height: 500px">
1318
<div
1419
v-if="data?.releaseNotes"
@@ -32,10 +37,16 @@
3237
import type { UpdateInfoType } from "@/types/main";
3338
import packageJson from "@/../package.json";
3439
35-
defineProps<{ data: UpdateInfoType }>();
40+
const props = defineProps<{ data: UpdateInfoType }>();
3641
3742
const emit = defineEmits<{ close: [] }>();
3843
44+
// 检测是否为预发布版本(alpha/beta/rc 等)
45+
const isPrerelease = computed(() => {
46+
const version = props.data?.version || "";
47+
return /-(alpha|beta|rc|dev|canary|nightly)/i.test(version);
48+
});
49+
3950
// 下载更新数据
4051
const downloadStatus = ref<boolean>(false);
4152
const downloadProgress = ref<number>(0);
@@ -95,6 +106,9 @@ const goDownload = () => {
95106
.menu {
96107
margin-top: 20px;
97108
}
109+
.prerelease-warning {
110+
margin-bottom: 12px;
111+
}
98112
.markdown-body {
99113
margin-top: 0 !important;
100114
}

src/components/Player/PlayerMeta/PlayerCover.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<script setup lang="ts">
5454
import { songDynamicCover } from "@/api/song";
5555
import { useMobile } from "@/composables/useMobile";
56+
import { useBlobURLManager } from "@/core/resource/BlobURLManager";
5657
import { useSettingStore, useStatusStore, useMusicStore } from "@/stores";
5758
import { isLogin } from "@/utils/auth";
5859
import { isElectron } from "@/utils/env";
@@ -102,11 +103,17 @@ const { start: dynamicCoverStart, stop: dynamicCoverStop } = useTimeoutFn(
102103
103104
// 获取本地歌曲高清封面
104105
const getLocalCover = async () => {
105-
// 非本地歌曲或非 Electron 环境,清理并返回
106106
if (!isElectron || !musicStore.playSong.path || musicStore.playSong.type === "streaming") {
107107
cleanupLocalCover();
108108
return;
109109
}
110+
// 先检查blob中是否存在
111+
const blobURLManager = useBlobURLManager();
112+
const blobURL = blobURLManager.getBlobURL(musicStore.playSong.path);
113+
if (blobURL) {
114+
localCoverDataUrl.value = blobURL;
115+
return;
116+
}
110117
try {
111118
const coverData = await window.electron.ipcRenderer.invoke(
112119
"get-music-cover",

src/components/Setting/AboutSetting.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<SvgIcon name="SPlayer" size="26" />
88
<n-text class="logo-name">SPlayer</n-text>
99
<n-tag v-if="statusStore.isDeveloperMode" size="small" type="warning" round> DEV </n-tag>
10-
<n-tag :bordered="false" size="small" type="primary" round @click="openDeveloperMode">
10+
<n-tag size="small" type="primary" round @click="openDeveloperMode">
1111
{{ packageJson.version }}
1212
</n-tag>
1313
</n-flex>
@@ -249,7 +249,6 @@ const getContributors = async () => {
249249
}
250250
} catch (error) {
251251
console.error("Failed to fetch contributors:", error);
252-
// Fallback or empty state handling if needed, currently just empty
253252
}
254253
};
255254
@@ -305,7 +304,7 @@ const specialContributors = [
305304
description: "这个人一点都不神秘,虽然写了一点,但就像什么都没有写",
306305
avatar: "/images/avatar/moyingji.webp",
307306
buttonText: "GitHub",
308-
url: "https://avatars.githubusercontent.com/u/64307394",
307+
url: "https://github.com/MoYingJi",
309308
},
310309
{
311310
name: "apoint123",

src/components/Setting/MainSetting.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,6 @@ import { useStatusStore } from "@/stores";
133133
import { getDisplayVersion, isNightly } from "@/utils/version";
134134
import packageJson from "@/../package.json";
135135
import { usePlaySettings } from "./config/play";
136-
137-
const displayVersion = getDisplayVersion();
138-
139136
import { useGeneralSettings } from "./config/general";
140137
import { useAppearanceSettings } from "./config/appearance";
141138
import { useLyricSettings } from "./config/lyric";
@@ -178,6 +175,7 @@ const allSettingGroups = computed(() => {
178175
});
179176
180177
const statusStore = useStatusStore();
178+
const displayVersion = getDisplayVersion();
181179
const { isSmallScreen } = useMobile();
182180
183181
// 设置内容

src/core/player/LyricManager.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -634,12 +634,16 @@ class LyricManager {
634634
/**
635635
* 处理歌词排除
636636
* @param lyricData 歌词数据
637-
* @param targetSong 目标歌曲(默认为当前播放歌曲)
637+
* @param targetSong 目标歌曲
638+
* @param usingTTMLLyric 是否使用 TTML 歌词
638639
* @returns 处理后的歌词数据
639640
*/
640-
private handleLyricExclude(lyricData: SongLyric, targetSong?: SongType): SongLyric {
641+
private handleLyricExclude(
642+
lyricData: SongLyric,
643+
targetSong?: SongType,
644+
usingTTMLLyric?: boolean,
645+
): SongLyric {
641646
const settingStore = useSettingStore();
642-
const statusStore = useStatusStore();
643647
const musicStore = useMusicStore();
644648

645649
const { enableExcludeLyrics, excludeLyricsUserKeywords, excludeLyricsUserRegexes } =
@@ -683,7 +687,9 @@ class LyricManager {
683687
const lrcData = stripLyricMetadata(lyricData.lrcData || [], options);
684688
let yrcData = lyricData.yrcData || [];
685689

686-
if (!statusStore.usingTTMLLyric || settingStore.enableExcludeLyricsTTML) {
690+
// usingTTMLLyric 未传入时从 lyricData 推断(预加载场景)
691+
const isTTML = usingTTMLLyric ?? false;
692+
if (!isTTML || settingStore.enableExcludeLyricsTTML) {
687693
yrcData = stripLyricMetadata(yrcData, options);
688694
}
689695

@@ -950,7 +956,11 @@ class LyricManager {
950956
}
951957
// 后处理:元数据排除
952958
if (isLocal ? settingStore.enableExcludeLyricsLocal : true) {
953-
fetchResult.data = this.handleLyricExclude(fetchResult.data, song);
959+
fetchResult.data = this.handleLyricExclude(
960+
fetchResult.data,
961+
song,
962+
fetchResult.meta.usingTTMLLyric,
963+
);
954964
}
955965
// 后处理:简繁转换
956966
fetchResult.data = await this.applyChineseVariant(fetchResult.data);

0 commit comments

Comments
 (0)