Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/components/settings/custom/PluginList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ onMounted(() => {
if (!loaded.value) void pluginsStore.load();
});

/** 控制类插件 ready 时声明的设置 schema(配置弹窗用) */
const controlSettings = (info: PluginInfo) => {
/** 插件 ready 时声明的设置 schema(配置弹窗用) */
const pluginSettings = (info: PluginInfo) => {
if (info.status.state !== "ready") return [];
return info.status.settings ?? [];
};
Expand Down Expand Up @@ -132,13 +132,15 @@ const onSettingChange = async (pluginId: string, key: string, value: unknown): P
await pluginsStore.setSetting(pluginId, key, value);
};

/** 当前打开配置弹窗的控制类插件 */
const settingsDialogInfo = computed(
() => controlPlugins.value.find((info) => info.manifest.id === settingsDialogId.value) ?? null,
);
/** 当前打开配置弹窗的插件(支持音源类与控制类) */
const settingsDialogInfo = computed(() => {
if (!settingsDialogId.value) return null;
const allPlugins = [...sourcePlugins.value, ...controlPlugins.value];
return allPlugins.find((info) => info.manifest.id === settingsDialogId.value) ?? null;
});
/** 弹窗内设置表单的 schema 与当前值 */
const settingsDialogSchema = computed(() =>
settingsDialogInfo.value ? controlSettings(settingsDialogInfo.value) : [],
settingsDialogInfo.value ? pluginSettings(settingsDialogInfo.value) : [],
);
const settingsDialogValues = computed(() => settingsDialogInfo.value?.settingsValues ?? {});

Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,10 @@
"label": "Auto-Upgrade Lyric Format",
"description": "Fetch a higher-ranked format from other platforms when the current one is low"
},
"preferPluginLyric": {
"label": "Prefer Plugin Lyrics",
"description": "Request plugin lyrics concurrently and auto-replace if a higher-ranked format is returned"
},
"detectBackgroundLyrics": {
"label": "Auto-Detect Background Lyrics",
"description": "Detect backing vocals from brackets. Disable this if lyrics are misdetected"
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,10 @@
"label": "自动升级歌词格式",
"description": "当前歌词格式较低时,从其他平台获取更高级的格式覆盖"
},
"preferPluginLyric": {
"label": "优先使用插件歌词",
"description": "开启后同时请求插件歌词,若返回更优格式则自动替换"
},
"detectBackgroundLyrics": {
"label": "自动识别背景歌词",
"description": "根据括号识别背景人声,误判时可关闭"
Expand Down
31 changes: 23 additions & 8 deletions src/services/download/lyric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { LyricFormat, LyricInput } from "@shared/types/lyrics";
import { isPlatform } from "@shared/types/platform";
import { buildDownloadLyric } from "@/utils/lyric/serialize";
import {
isBetterFormat,
isPluginLyricPreferred,
resolveLocalRepoLyric,
resolveOnlineByPreference,
resolvePluginLyric,
Expand Down Expand Up @@ -60,20 +62,33 @@ const resolveOnlineDownloadLyric = async (
export const resolveDownloadLyric = async (track: Track): Promise<DownloadLyric | null> => {
const local = toUsableDownloadLyric(await resolveLocalRepoLyric(track));
if (local) return local;

// 流媒体
if (track.source === "streaming") {
return (
toUsableDownloadLyric(await resolveStreamingByPreference(track)) ??
toUsableDownloadLyric(await resolvePluginLyric(track))
);
// 插件优选:请求先行发出,与正常来源并发
const pluginTask = isPluginLyricPreferred() ? resolvePluginLyric(track) : null;
const streaming = toUsableDownloadLyric(await resolveStreamingByPreference(track));
if (pluginTask) {
const plugin = toUsableDownloadLyric(await pluginTask);
if (plugin && isBetterFormat(plugin.format, streaming?.format ?? null)) return plugin;
return streaming ?? plugin ?? null;
}
return streaming ?? toUsableDownloadLyric(await resolvePluginLyric(track));
}

// 在线平台
if (isPlatform(track.source)) {
// 插件优选:请求先行发出,与正常来源并发
const pluginTask = isPluginLyricPreferred() ? resolvePluginLyric(track) : null;
const online = await resolveOnlineByPreference(track, { hasLocal: false, localFormat: null });
return (
(await resolveOnlineDownloadLyric(track, online)) ??
toUsableDownloadLyric(await resolvePluginLyric(track))
);
const onlineLyric = await resolveOnlineDownloadLyric(track, online);
if (pluginTask) {
const plugin = toUsableDownloadLyric(await pluginTask);
if (plugin && isBetterFormat(plugin.format, onlineLyric?.format ?? null)) return plugin;
return onlineLyric ?? plugin ?? null;
}
return onlineLyric ?? toUsableDownloadLyric(await resolvePluginLyric(track));
}

return null;
};
143 changes: 91 additions & 52 deletions src/services/lyric/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { useSettingsStore } from "@/stores/settings";
import { DEFAULT_LYRIC_FORMAT_ORDER } from "@/types/settings";
import {
embeddedLyricFromDetail,
isBetterFormat,
isPluginLyricPreferred,
resolveLocalRepoLyric,
resolveOnlineByPreference,
resolvePluginLyric,
Expand Down Expand Up @@ -131,50 +133,80 @@ const tryLocalRepo = async (token: number, track: Track): Promise<boolean> => {
* @returns 是否已提交有效歌词
*/
const tryPluginFallback = async (token: number, track: Track): Promise<boolean> => {
// 插件优选时不处理
if (isPluginLyricPreferred()) return false;
const resolved = await resolvePluginLyric(track);
if (token !== currentToken) return false;
return resolved ? commitResolvedAndHasParsed(token, resolved) : false;
};

/**
* 流媒体歌词加载:按来源偏好解析,失败后使用插件和内嵌歌词兜底
* 插件优先加载
* 插件请求与正常流程并发发出,正常流程先展示,插件返回更优格式时替换
* @param token - 竞态 token
* @param track - 歌曲信息
* @param detail - 歌曲详细信息
* @param run - 正常加载流程
*/
const loadStreamingLyric = async (
const withPluginPrefer = async (
token: number,
track: Track,
detail: TrackDetail | null,
run: () => Promise<void>,
): Promise<void> => {
const resolved = await resolveStreamingByPreference(track, () => token === currentToken);
if (token !== currentToken) return;
const embeddedFallback = embeddedLyricFromDetail(detail);
if (resolved && commitResolvedAndHasParsed(token, resolved)) return;
if (token !== currentToken) return;
if (await tryPluginFallback(token, track)) return;
if (embeddedFallback) {
commit(token, embeddedFallback.source, { content: embeddedFallback.content });
} else {
commit(token, null, null);
if (!isPluginLyricPreferred()) {
await run();
return;
}
const pluginTask = resolvePluginLyric(track);
await run();
const plugin = await pluginTask;
if (!plugin || token !== currentToken) return;
const currentFormat = useMediaStore().activeLyric?.format ?? null;
if (isBetterFormat(plugin.source.format, currentFormat)) {
commitResolvedAndHasParsed(token, plugin);
}
};

/**
* 流媒体歌词加载:按来源偏好解析,失败后使用插件和内嵌歌词兜底
* @param token - 竞态 token
* @param track - 歌曲信息
* @param detail - 歌曲详细信息
*/
const loadStreamingLyric = (
token: number,
track: Track,
detail: TrackDetail | null,
): Promise<void> =>
withPluginPrefer(token, track, async () => {
const resolved = await resolveStreamingByPreference(track, () => token === currentToken);
if (token !== currentToken) return;
const embeddedFallback = embeddedLyricFromDetail(detail);
if (resolved && commitResolvedAndHasParsed(token, resolved)) return;
if (token !== currentToken) return;
if (await tryPluginFallback(token, track)) return;
if (embeddedFallback) {
commit(token, embeddedFallback.source, { content: embeddedFallback.content });
} else {
commit(token, null, null);
}
});

/**
* 在线平台歌曲歌词加载
* @param token - 竞态 token
* @param track - 歌曲信息
*/
const loadPlatformLyric = async (token: number, track: Track): Promise<void> => {
const online = await resolveOnlineByPreference(track, {
hasLocal: false,
localFormat: null,
shouldContinue: () => token === currentToken,
const loadPlatformLyric = (token: number, track: Track): Promise<void> =>
withPluginPrefer(token, track, async () => {
const online = await resolveOnlineByPreference(track, {
hasLocal: false,
localFormat: null,
shouldContinue: () => token === currentToken,
});
if (token !== currentToken) return;
if (online) await applyOnline(token, track, online, null);
else if (!(await tryPluginFallback(token, track))) commit(token, null, null);
});
if (token !== currentToken) return;
if (online) await applyOnline(token, track, online, null);
else if (!(await tryPluginFallback(token, track))) commit(token, null, null);
};

/** 开启新一轮加载周期 */
export const beginLoad = (): number => {
Expand Down Expand Up @@ -231,21 +263,24 @@ export const loadForTrack = async (detail: TrackDetail | null): Promise<void> =>
// 本地文件存在但解析后空
const hasUsableLocal = !!local && media.parsedLyric.length > 0;
const localFormat = local?.source.format ?? null;
// 按偏好获取歌词
const online = await resolveOnlineByPreference(track, {
hasLocal: hasUsableLocal,
localFormat,
onCandidate: (result) => commit(token, result.source, result.input),
shouldContinue: () => token === currentToken,

await withPluginPrefer(token, track, async () => {
// 按偏好获取歌词
const online = await resolveOnlineByPreference(track, {
hasLocal: hasUsableLocal,
localFormat,
onCandidate: (result) => commit(token, result.source, result.input),
shouldContinue: () => token === currentToken,
});
if (token !== currentToken) return;
// id 回查本地 TTML 库
if (online && (await tryLocalRepo(token, track))) return;
if (online) {
await applyOnline(token, track, online, local);
} else if (!hasUsableLocal && !(await tryPluginFallback(token, track))) {
commit(token, null, null);
}
});
if (token !== currentToken) return;
// id 回查本地 TTML 库
if (online && (await tryLocalRepo(token, track))) return;
if (online) {
await applyOnline(token, track, online, local);
} else if (!hasUsableLocal && !(await tryPluginFallback(token, track))) {
commit(token, null, null);
}
} catch (err) {
console.error("[lyricLoader] loadForTrack failed:", err);
commit(token, null, null);
Expand Down Expand Up @@ -277,22 +312,25 @@ const refreshPreference = async (): Promise<void> => {
if (token !== currentToken) return;
const localFormat = local?.source.format ?? null;
const showingOnline = media.activeLyric?.source === "online";
/** 按偏好获取歌词 */
const online = await resolveOnlineByPreference(track, {
hasLocal: !!local,
localFormat,
onCandidate: (result) => commit(token, result.source, result.input),
shouldContinue: () => token === currentToken,

await withPluginPrefer(token, track, async () => {
/** 按偏好获取歌词 */
const online = await resolveOnlineByPreference(track, {
hasLocal: !!local,
localFormat,
onCandidate: (result) => commit(token, result.source, result.input),
shouldContinue: () => token === currentToken,
});
if (token !== currentToken) return;
if (online) {
await applyOnline(token, track, online, local);
return;
}
// 目标是本地
if (!showingOnline) return;
if (local) commitLocal(token, local);
else commit(token, null, null);
});
if (token !== currentToken) return;
if (online) {
await applyOnline(token, track, online, local);
return;
}
// 目标是本地
if (!showingOnline) return;
if (local) commitLocal(token, local);
else commit(token, null, null);
};

/** 监听歌词偏好变化 */
Expand All @@ -302,6 +340,7 @@ export const watchLyricPreference = (): void => {
() => [
settings.lyric.lyricSourcePreference,
settings.lyric.smartPreferOnline,
settings.lyric.preferPluginLyric,
settings.lyric.detectBackgroundLyrics,
settings.system.lyric.enableOnlineTTMLLyric,
settings.system.localLyric.enableLocalTTMLOverride,
Expand Down
1 change: 1 addition & 0 deletions src/services/lyric/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const buildContextKey = (): string => {
settings.lyric.lyricSourceOrder,
settings.lyric.lyricFormatOrder,
settings.lyric.smartPreferOnline,
settings.lyric.preferPluginLyric,
settings.system.lyric.enableOnlineTTMLLyric,
settings.system.localLyric.enableLocalTTMLOverride,
settings.system.localLyric.repoDir,
Expand Down
Loading