feat: 曲库索引 —— 曲库里已有的同名歌曲在列表中标记「曲库」

不是通过声匣下载的文件(其他工具 / 手动放入)以前一律显示未下载。
现在启动时扫描音乐目录,按「歌名 + 歌手文件夹 / 专辑文件夹」匹配,
下载完成增量更新,每 30 分钟或删除 / 补全标签后重建;设置页可手动重建。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jocay
2026-10-10 22:53:04 +08:00
parent d5152080bb
commit 85b2d36b3d
8 changed files with 181 additions and 10 deletions
+20 -6
View File
@@ -35,7 +35,8 @@ watch(
);
const allSelected = computed(() => props.tracks.length > 0 && selected.value.size === props.tracks.length);
const pendingCount = computed(() => props.tracks.filter((t) => !isDownloaded(t)).length);
const owned = (t: TrackSummary) => isDownloaded(t) || !!t.inLibrary;
const pendingCount = computed(() => props.tracks.filter((t) => !owned(t)).length);
function toggleRow(index: number, e: MouseEvent) {
const next = new Set(selected.value);
@@ -56,7 +57,7 @@ function toggleAll() {
}
function selectPending() {
selected.value = new Set(props.tracks.filter((t) => !isDownloaded(t)).map((t) => t.id));
selected.value = new Set(props.tracks.filter((t) => !owned(t)).map((t) => t.id));
}
async function downloadSelected() {
@@ -69,7 +70,14 @@ function downloadAll() {
download(props.tracks, quality.value, props.group);
}
function rowState(t: TrackSummary) {
interface RowState {
kind: string;
text: string;
pct: number;
title?: string;
}
function rowState(t: TrackSummary): RowState | null {
const task = activeByTrack.value.get(trackKey(t.source, t.id));
if (task) {
if (task.status === 'running') {
@@ -80,6 +88,7 @@ function rowState(t: TrackSummary) {
return { kind: task.status, text: task.status === 'paused' ? '已暂停' : '排队中', pct: 0 };
}
if (isDownloaded(t)) return { kind: 'done', text: '已下载', pct: 1 };
if (t.inLibrary) return { kind: 'library', text: '曲库中有', pct: 1, title: `曲库中有同名歌曲:${t.inLibrary}` };
return null;
}
@@ -158,9 +167,9 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
</span>
<span class="state">
<template v-if="rowState(t)">
<span class="st" :class="rowState(t)!.kind">
<Icon v-if="rowState(t)!.kind === 'done'" name="check" :size="14" />
{{ rowState(t)!.kind === 'done' ? '' : rowState(t)!.text }}
<span class="st" :class="rowState(t)!.kind" :title="rowState(t)!.title ?? rowState(t)!.text">
<Icon v-if="rowState(t)!.kind === 'done' || rowState(t)!.kind === 'library'" name="check" :size="14" />
{{ rowState(t)!.kind === 'done' ? '' : rowState(t)!.kind === 'library' ? '曲库' : rowState(t)!.text }}
</span>
<span v-if="rowState(t)!.kind === 'running'" class="bar"><i :style="{ width: `${rowState(t)!.pct * 100}%` }" /></span>
</template>
@@ -392,6 +401,11 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
font-family: var(--font-mono);
}
.st.library {
color: var(--ink-3);
cursor: help;
}
.st.done {
color: var(--ok);
}