fix: 修复音源解析致命性错误

This commit is contained in:
alger
2025-12-13 14:46:15 +08:00
parent 1a0e449e13
commit b9287e1c36
8 changed files with 331 additions and 437 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@ android/app/release
.cursor
.windsurf
.agent
.auto-imports.d.ts

View File

@@ -33,81 +33,81 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@unblockneteasemusic/server": "^0.27.8-patch.1",
"@unblockneteasemusic/server": "^0.27.10",
"cors": "^2.8.5",
"electron-store": "^8.1.0",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3",
"express": "^4.18.2",
"file-type": "^21.0.0",
"express": "^4.22.1",
"file-type": "^21.1.1",
"flac-tagger": "^1.0.7",
"font-list": "^1.5.1",
"font-list": "^1.6.0",
"husky": "^9.1.7",
"music-metadata": "^11.2.3",
"music-metadata": "^11.10.3",
"netease-cloud-music-api-alger": "^4.26.1",
"node-id3": "^0.2.9",
"node-machine-id": "^1.1.12",
"pinia-plugin-persistedstate": "^4.5.0",
"sharp": "^0.34.3",
"vue-i18n": "^11.1.3"
"pinia-plugin-persistedstate": "^4.7.1",
"sharp": "^0.34.5",
"vue-i18n": "^11.2.2"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^2.1.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@eslint/js": "^9.31.0",
"@rushstack/eslint-patch": "^1.10.3",
"@eslint/js": "^9.39.2",
"@rushstack/eslint-patch": "^1.15.0",
"@types/howler": "^2.2.12",
"@types/node": "^20.14.8",
"@types/node": "^20.19.26",
"@types/tinycolor2": "^1.4.6",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/compiler-sfc": "^3.5.0",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.25",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/runtime-core": "^3.5.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/runtime-core": "^3.5.25",
"@vueuse/core": "^11.3.0",
"@vueuse/electron": "^13.8.0",
"@vueuse/electron": "^13.9.0",
"animate.css": "^4.1.1",
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"autoprefixer": "^10.4.22",
"axios": "^1.13.2",
"cross-env": "^7.0.3",
"electron": "^38.1.2",
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"electron-vite": "^4.0.0",
"eslint": "^9.34.0",
"electron-vite": "^4.0.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-vue": "^10.3.0",
"eslint-plugin-vue-scoped-css": "^2.11.0",
"globals": "^16.3.0",
"eslint-plugin-vue": "^10.6.2",
"eslint-plugin-vue-scoped-css": "^2.12.0",
"globals": "^16.5.0",
"howler": "^2.2.4",
"lint-staged": "^15.2.10",
"lint-staged": "^15.5.2",
"lodash": "^4.17.21",
"marked": "^15.0.4",
"naive-ui": "^2.41.0",
"pinia": "^3.0.1",
"pinyin-match": "^1.2.6",
"postcss": "^8.4.47",
"prettier": "^3.6.2",
"remixicon": "^4.6.0",
"sass": "^1.86.0",
"tailwindcss": "^3.4.17",
"marked": "^15.0.12",
"naive-ui": "^2.43.2",
"pinia": "^3.0.4",
"pinyin-match": "^1.2.10",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"remixicon": "^4.7.0",
"sass": "^1.96.0",
"tailwindcss": "^3.4.19",
"tinycolor2": "^1.6.0",
"tunajs": "^1.0.15",
"typescript": "^5.5.2",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"typescript": "^5.9.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.4.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.7.2",
"vue": "^3.5.13",
"vue": "^3.5.25",
"vue-eslint-parser": "^10.2.0",
"vue-router": "^4.5.0",
"vue-tsc": "^2.0.22"
"vue-router": "^4.6.4",
"vue-tsc": "^2.2.12"
},
"build": {
"appId": "com.alger.music",

View File

@@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash';
import { musicDB } from '@/hooks/MusicHook';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { useSettingsStore } from '@/store';
import type { SongResult } from '@/types/music';
import { isElectron } from '@/utils';
@@ -33,13 +34,18 @@ export interface MusicParseResult {
const CACHE_CONFIG = {
// 音乐URL缓存时间30分钟
MUSIC_URL_CACHE_TIME: 30 * 60 * 1000,
// 失败缓存时间:5分钟
FAILED_CACHE_TIME: 5 * 60 * 1000,
// 失败缓存时间:1分钟(减少到 1 分钟以便更快恢复)
FAILED_CACHE_TIME: 1 * 60 * 1000,
// 重试配置
MAX_RETRY_COUNT: 2,
RETRY_DELAY: 1000
};
/**
* 内存失败缓存(替代 IndexedDB更轻量且应用重启后自动失效
*/
const failedCacheMap = new Map<string, number>();
/**
* 缓存管理器
*/
@@ -104,39 +110,46 @@ export class CacheManager {
}
/**
* 检查是否在失败缓存期内
* 检查是否在失败缓存期内(使用内存缓存)
*/
static async isInFailedCache(id: number, strategyName: string): Promise<boolean> {
try {
const cacheKey = `${id}_${strategyName}`;
const cached = await getData('music_failed_cache', cacheKey);
if (cached?.createTime && Date.now() - cached.createTime < CACHE_CONFIG.FAILED_CACHE_TIME) {
console.log(`策略 ${strategyName} 在失败缓存期内,跳过`);
return true;
}
// 清理过期缓存
if (cached) {
await deleteData('music_failed_cache', cacheKey);
}
} catch (error) {
console.warn('检查失败缓存失败:', error);
static isInFailedCache(id: number, strategyName: string): boolean {
const cacheKey = `${id}_${strategyName}`;
const cachedTime = failedCacheMap.get(cacheKey);
if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) {
console.log(`策略 ${strategyName} 在失败缓存期内,跳过`);
return true;
}
// 清理过期缓存
if (cachedTime) {
failedCacheMap.delete(cacheKey);
}
return false;
}
/**
* 添加失败缓存
* 添加失败缓存(使用内存缓存)
*/
static async addFailedCache(id: number, strategyName: string): Promise<void> {
try {
const cacheKey = `${id}_${strategyName}`;
await saveData('music_failed_cache', {
id: cacheKey,
createTime: Date.now()
});
console.log(`添加失败缓存成功: ${strategyName}`);
} catch (error) {
console.error('添加失败缓存失败:', error);
static addFailedCache(id: number, strategyName: string): void {
const cacheKey = `${id}_${strategyName}`;
failedCacheMap.set(cacheKey, Date.now());
console.log(
`添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)`
);
}
/**
* 清除指定歌曲的失败缓存
*/
static clearFailedCache(id: number): void {
const keysToDelete: string[] = [];
failedCacheMap.forEach((_, key) => {
if (key.startsWith(`${id}_`)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => failedCacheMap.delete(key));
if (keysToDelete.length > 0) {
console.log(`清除歌曲 ${id} 的失败缓存: ${keysToDelete.length}`);
}
}
@@ -322,7 +335,7 @@ class CustomApiStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult, quality = 'higher'): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
@@ -339,11 +352,11 @@ class CustomApiStrategy implements MusicSourceStrategy {
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('自定义API解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
@@ -362,7 +375,7 @@ class BilibiliStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
@@ -379,11 +392,11 @@ class BilibiliStrategy implements MusicSourceStrategy {
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('Bilibili解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
@@ -402,7 +415,7 @@ class GDMusicStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
@@ -419,11 +432,11 @@ class GDMusicStrategy implements MusicSourceStrategy {
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('GD音乐台解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
@@ -450,7 +463,7 @@ class UnblockMusicStrategy implements MusicSourceStrategy {
sources?: string[]
): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
@@ -471,11 +484,11 @@ class UnblockMusicStrategy implements MusicSourceStrategy {
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('UnblockMusic解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
@@ -512,23 +525,15 @@ class MusicSourceStrategyFactory {
* @returns 音源列表和音质设置
*/
const getMusicConfig = (id: number, settingsStore?: any) => {
const songId = String(id);
let musicSources: string[] = [];
let quality = 'higher';
try {
// 尝试获取歌曲自定义音源
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
if (savedSourceStr) {
try {
const customSources = JSON.parse(savedSourceStr);
if (Array.isArray(customSources)) {
musicSources = customSources;
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
}
} catch (error) {
console.error('解析自定义音源设置失败:', error);
}
// 尝试获取歌曲自定义音源(使用 SongSourceConfigManager
const songConfig = SongSourceConfigManager.getConfig(id);
if (songConfig && songConfig.sources.length > 0) {
musicSources = songConfig.sources;
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
}
// 如果没有自定义音源,使用全局设置

View File

@@ -81,7 +81,7 @@ import { useI18n } from 'vue-i18n';
import { CacheManager } from '@/api/musicParser';
import { playMusic } from '@/hooks/MusicHook';
import { audioService } from '@/services/audioService';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { usePlayerStore } from '@/store/modules/player';
import type { Platform } from '@/types/music';
@@ -130,16 +130,11 @@ const getSourceIcon = (source: Platform) => {
// 初始化选中的音源
const initSelectedSources = () => {
const songId = String(playMusic.value.id);
const savedSource = localStorage.getItem(`song_source_${songId}`);
const songId = playMusic.value.id;
const config = SongSourceConfigManager.getConfig(songId);
if (savedSource) {
try {
selectedSourcesValue.value = JSON.parse(savedSource);
} catch (e) {
console.error('解析保存的音源设置失败:', e);
selectedSourcesValue.value = [];
}
if (config) {
selectedSourcesValue.value = config.sources;
} else {
selectedSourcesValue.value = [];
}
@@ -147,7 +142,7 @@ const initSelectedSources = () => {
// 清除自定义音源
const clearCustomSource = () => {
localStorage.removeItem(`song_source_${String(playMusic.value.id)}`);
SongSourceConfigManager.clearConfig(playMusic.value.id);
selectedSourcesValue.value = [];
};
@@ -168,11 +163,8 @@ const directReparseMusic = async (source: Platform) => {
// 更新选中的音源值为当前点击的音源
selectedSourcesValue.value = [source];
// 保存到localStorage
localStorage.setItem(
`song_source_${String(songId)}`,
JSON.stringify(selectedSourcesValue.value)
);
// 使用 SongSourceConfigManager 保存配置(手动选择)
SongSourceConfigManager.setConfig(songId, [source], 'manual');
const success = await playerStore.reparseCurrentSong(source, false);
@@ -200,49 +192,6 @@ watch(
},
{ immediate: true }
);
// 监听歌曲变化,检查是否有自定义音源
watch(
() => playMusic.value.id,
async (newId) => {
if (newId) {
const songId = String(newId);
const savedSource = localStorage.getItem(`song_source_${songId}`);
// 如果有保存的音源设置但当前不是使用自定义解析的播放,尝试应用
if (savedSource && playMusic.value.source !== 'bilibili') {
try {
const sources = JSON.parse(savedSource) as Platform[];
console.log(`检测到歌曲ID ${songId} 有自定义音源设置:`, sources);
// 当URL加载失败或过期时自动应用自定义音源重新加载
audioService.on('url_expired', async (trackInfo) => {
if (trackInfo && trackInfo.id === playMusic.value.id) {
console.log('URL已过期自动应用自定义音源重新加载');
try {
isReparsing.value = true;
const songId = String(playMusic.value.id);
const sourceType = localStorage.getItem(`song_source_type_${songId}`);
const isAuto = sourceType === 'auto';
const success = await playerStore.reparseCurrentSong(sources[0], isAuto);
if (!success) {
message.error(t('player.reparse.failed'));
}
} catch (e) {
console.error('自动重新解析失败:', e);
message.error(t('player.reparse.failed'));
} finally {
isReparsing.value = false;
}
}
});
} catch (e) {
console.error('解析保存的音源设置失败:', e);
}
}
}
}
);
</script>
<style lang="scss" scoped>

View File

@@ -5,6 +5,7 @@ import i18n from '@/../i18n/renderer';
import { getBilibiliAudioUrl } from '@/api/bilibili';
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { playbackRequestManager } from '@/services/playbackRequestManager';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
@@ -64,17 +65,8 @@ export const getSongUrl = async (
const globalSources = settingsStore.setData.enabledMusicSources || [];
const useCustomApiGlobally = globalSources.includes('custom');
const songId = String(id);
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
let useCustomApiForSong = false;
if (savedSourceStr) {
try {
const songSources = JSON.parse(savedSourceStr);
useCustomApiForSong = songSources.includes('custom');
} catch (e) {
console.error('解析歌曲音源设置失败:', e);
}
}
const songConfig = SongSourceConfigManager.getConfig(id);
const useCustomApiForSong = songConfig?.sources.includes('custom' as any) ?? false;
// 如果全局或歌曲专属设置中启用了自定义API则最优先尝试
if ((useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) {
@@ -116,9 +108,9 @@ export const getSongUrl = async (
}
// 如果有自定义音源设置直接使用getParsingMusicUrl获取URL
if (savedSourceStr && songData.source !== 'bilibili') {
if (songConfig && songData.source !== 'bilibili') {
try {
console.log(`使用自定义音源解析歌曲 ID: ${songId}`);
console.log(`使用自定义音源解析歌曲 ID: ${id}`);
const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
console.log('res', res);

View File

@@ -0,0 +1,198 @@
/**
* 歌曲音源配置管理器
*
* 职责:
* 1. 统一管理每首歌曲的自定义音源配置
* 2. 提供清晰的读取/写入/清除 API
* 3. 区分"手动"和"自动"设置的音源
* 4. 管理已尝试的音源列表(按歌曲隔离)
*/
import type { Platform } from '@/types/music';
// 歌曲音源配置类型
export type SongSourceConfig = {
sources: Platform[];
type: 'manual' | 'auto';
updatedAt: number;
};
// 内存中缓存已尝试的音源(按歌曲隔离)
const triedSourcesMap = new Map<string, Set<string>>();
const triedSourceDiffsMap = new Map<string, Map<string, number>>();
// localStorage key 前缀
const STORAGE_KEY_PREFIX = 'song_source_';
const STORAGE_TYPE_KEY_PREFIX = 'song_source_type_';
/**
* 歌曲音源配置管理器
*/
export class SongSourceConfigManager {
/**
* 获取歌曲的自定义音源配置
*/
static getConfig(songId: number | string): SongSourceConfig | null {
const id = String(songId);
const sourcesStr = localStorage.getItem(`${STORAGE_KEY_PREFIX}${id}`);
const typeStr = localStorage.getItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);
if (!sourcesStr) {
return null;
}
try {
const sources = JSON.parse(sourcesStr) as Platform[];
if (!Array.isArray(sources) || sources.length === 0) {
return null;
}
return {
sources,
type: typeStr === 'auto' ? 'auto' : 'manual',
updatedAt: Date.now()
};
} catch (error) {
console.error(`[SongSourceConfigManager] 解析歌曲 ${id} 配置失败:`, error);
return null;
}
}
/**
* 设置歌曲的自定义音源配置
*/
static setConfig(
songId: number | string,
sources: Platform[],
type: 'manual' | 'auto' = 'manual'
): void {
const id = String(songId);
if (!sources || sources.length === 0) {
this.clearConfig(songId);
return;
}
try {
localStorage.setItem(`${STORAGE_KEY_PREFIX}${id}`, JSON.stringify(sources));
localStorage.setItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`, type);
console.log(`[SongSourceConfigManager] 设置歌曲 ${id} 音源: ${sources.join(', ')} (${type})`);
} catch (error) {
console.error(`[SongSourceConfigManager] 保存歌曲 ${id} 配置失败:`, error);
}
}
/**
* 清除歌曲的自定义配置
*/
static clearConfig(songId: number | string): void {
const id = String(songId);
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${id}`);
localStorage.removeItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);
// 同时清除内存中的已尝试音源
this.clearTriedSources(songId);
console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 配置`);
}
/**
* 检查歌曲是否有自定义配置
*/
static hasConfig(songId: number | string): boolean {
return this.getConfig(songId) !== null;
}
/**
* 检查配置类型是否为手动设置
*/
static isManualConfig(songId: number | string): boolean {
const config = this.getConfig(songId);
return config?.type === 'manual';
}
// ==================== 已尝试音源管理 ====================
/**
* 获取歌曲已尝试的音源列表
*/
static getTriedSources(songId: number | string): Set<string> {
const id = String(songId);
if (!triedSourcesMap.has(id)) {
triedSourcesMap.set(id, new Set());
}
return triedSourcesMap.get(id)!;
}
/**
* 添加已尝试的音源
*/
static addTriedSource(songId: number | string, source: string): void {
const id = String(songId);
const tried = this.getTriedSources(id);
tried.add(source);
console.log(`[SongSourceConfigManager] 歌曲 ${id} 添加已尝试音源: ${source}`);
}
/**
* 清除歌曲的已尝试音源
*/
static clearTriedSources(songId: number | string): void {
const id = String(songId);
triedSourcesMap.delete(id);
triedSourceDiffsMap.delete(id);
console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 已尝试音源`);
}
/**
* 获取歌曲已尝试音源的时长差异
*/
static getTriedSourceDiffs(songId: number | string): Map<string, number> {
const id = String(songId);
if (!triedSourceDiffsMap.has(id)) {
triedSourceDiffsMap.set(id, new Map());
}
return triedSourceDiffsMap.get(id)!;
}
/**
* 设置音源的时长差异
*/
static setTriedSourceDiff(songId: number | string, source: string, diff: number): void {
const id = String(songId);
const diffs = this.getTriedSourceDiffs(id);
diffs.set(source, diff);
}
/**
* 查找最佳匹配的音源(时长差异最小)
*/
static findBestMatchingSource(songId: number | string): { source: string; diff: number } | null {
const diffs = this.getTriedSourceDiffs(songId);
if (diffs.size === 0) {
return null;
}
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of diffs.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
return bestSource ? { source: bestSource, diff: minDiff } : null;
}
/**
* 清理所有内存缓存(用于测试或重置)
*/
static clearAllMemoryCache(): void {
triedSourcesMap.clear();
triedSourceDiffsMap.clear();
console.log('[SongSourceConfigManager] 清除所有内存缓存');
}
}
// 导出单例实例方便使用
export const songSourceConfig = SongSourceConfigManager;

View File

@@ -1,7 +1,5 @@
import { Howl } from 'howler';
import { cloneDeep } from 'lodash';
import { getParsingMusicUrl } from '@/api/music';
import type { SongResult } from '@/types/music';
class PreloadService {
@@ -60,167 +58,22 @@ class PreloadService {
}
// 创建初始音频实例
let sound = await this._createSound(song.playMusicUrl);
const sound = await this._createSound(song.playMusicUrl);
// 检查时长
const duration = sound.duration();
const expectedDuration = (song.dt || 0) / 1000;
// 如果时长差异超过5秒且不是B站视频且预期时长大于0
// 时长差异只记录警告,不自动触发重新解析
// 用户可以通过 ReparsePopover 手动选择正确的音源
if (
expectedDuration > 0 &&
Math.abs(duration - expectedDuration) > 5 &&
song.source !== 'bilibili'
) {
const songId = String(song.id);
const sourceType = localStorage.getItem(`song_source_type_${songId}`);
// 如果不是用户手动锁定的音源,尝试自动重新解析
if (sourceType !== 'manual') {
console.warn(
`[PreloadService] 时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试智能解析`
);
// 动态导入 store
const { useSettingsStore } = await import('@/store/modules/settings');
const { usePlaylistStore } = await import('@/store/modules/playlist');
const settingsStore = useSettingsStore();
const playlistStore = usePlaylistStore();
const enabledSources = settingsStore.setData.enabledMusicSources || [
'migu',
'kugou',
'pyncmd',
'gdmusic'
];
const availableSources = enabledSources.filter((s: string) => s !== 'bilibili');
const triedSources = new Set<string>();
const triedSourceDiffs = new Map<string, number>();
// 记录当前音源
let currentSource = 'unknown';
const currentSavedSource = localStorage.getItem(`song_source_${songId}`);
if (currentSavedSource) {
try {
const sources = JSON.parse(currentSavedSource);
if (Array.isArray(sources) && sources.length > 0) {
currentSource = sources[0];
}
} catch (e) {
console.log(
`[PreloadService] 时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试智能解析`,
e
);
}
}
triedSources.add(currentSource);
triedSourceDiffs.set(currentSource, Math.abs(duration - expectedDuration));
// 卸载当前不匹配的音频
sound.unload();
// 尝试其他音源
for (const source of availableSources) {
if (triedSources.has(source)) continue;
console.log(`[PreloadService] 尝试音源: ${source}`);
triedSources.add(source);
try {
const songData = cloneDeep(song);
// 临时保存设置以便 getParsingMusicUrl 使用
localStorage.setItem(`song_source_${songId}`, JSON.stringify([source]));
const res = await getParsingMusicUrl(
typeof song.id === 'string' ? parseInt(song.id) : song.id,
songData
);
if (res && res.data && res.data.data && res.data.data.url) {
const newUrl = res.data.data.url;
const tempSound = await this._createSound(newUrl);
const newDuration = tempSound.duration();
const diff = Math.abs(newDuration - expectedDuration);
triedSourceDiffs.set(source, diff);
if (diff <= 5) {
console.log(`[PreloadService] 找到匹配音源: ${source}, 更新歌曲信息`);
// 更新歌曲信息
const updatedSong = {
...song,
playMusicUrl: newUrl,
expiredAt: Date.now() + 1800000
};
// 更新 store
playlistStore.updateSong(updatedSong);
// 记录新的音源设置
localStorage.setItem(`song_source_${songId}`, JSON.stringify([source]));
localStorage.setItem(`song_source_type_${songId}`, 'auto');
return tempSound;
} else {
tempSound.unload();
}
}
} catch (e) {
console.error(`[PreloadService] 尝试音源 ${source} 失败:`, e);
}
}
// 如果没有找到完美匹配,使用最佳匹配
console.warn('[PreloadService] 未找到完美匹配,寻找最佳匹配');
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of triedSourceDiffs.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
if (bestSource && bestSource !== currentSource) {
console.log(`[PreloadService] 使用最佳匹配音源: ${bestSource} (差异: ${minDiff}s)`);
try {
const songData = cloneDeep(song);
localStorage.setItem(`song_source_${songId}`, JSON.stringify([bestSource]));
const res = await getParsingMusicUrl(
typeof song.id === 'string' ? parseInt(song.id) : song.id,
songData
);
if (res && res.data && res.data.data && res.data.data.url) {
const newUrl = res.data.data.url;
const bestSound = await this._createSound(newUrl);
const updatedSong = {
...song,
playMusicUrl: newUrl,
expiredAt: Date.now() + 1800000
};
playlistStore.updateSong(updatedSong);
localStorage.setItem(`song_source_type_${songId}`, 'auto');
return bestSound;
}
} catch (e) {
console.error(`[PreloadService] 获取最佳匹配音源失败:`, e);
}
}
}
}
// 如果不需要修复或修复失败重新加载原始音频因为上面可能unload了
if (sound.state() === 'unloaded') {
sound = await this._createSound(song.playMusicUrl);
console.warn(
`[PreloadService] 时长差异警告:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})`
);
}
return sound;

View File

@@ -11,6 +11,7 @@ import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';
import { audioService } from '@/services/audioService';
import { playbackRequestManager } from '@/services/playbackRequestManager';
import { preloadService } from '@/services/preloadService';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { Platform, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
@@ -30,8 +31,6 @@ export const usePlayerCoreStore = defineStore(
const isPlay = ref(false);
const playMusic = ref<SongResult>({} as SongResult);
const playMusicUrl = ref('');
const triedSources = ref<Set<string>>(new Set());
const triedSourceDiffs = ref<Map<string, number>>(new Map());
const musicFull = ref(false);
const playbackRate = ref(1.0);
const volume = ref(1);
@@ -165,10 +164,9 @@ export const usePlayerCoreStore = defineStore(
* 核心播放处理函数
*/
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
// 如果是新歌曲,重置已尝试的音源
// 如果是新歌曲,重置已尝试的音源(使用 SongSourceConfigManager 按歌曲隔离)
if (music.id !== playMusic.value.id) {
triedSources.value.clear();
triedSourceDiffs.value.clear();
SongSourceConfigManager.clearTriedSources(music.id);
}
// 创建新的播放请求并取消之前的所有请求
@@ -427,110 +425,7 @@ export const usePlayerCoreStore = defineStore(
new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } })
);
// 检查时长是否匹配,如果不匹配则尝试自动重新解析
const duration = newSound.duration();
const expectedDuration = (playMusic.value.dt || 0) / 1000;
// 如果时长差异超过5秒且不是B站视频且预期时长大于0
if (
expectedDuration > 0 &&
Math.abs(duration - expectedDuration) > 5 &&
playMusic.value.source !== 'bilibili' &&
playMusic.value.id
) {
const songId = String(playMusic.value.id);
const sourceType = localStorage.getItem(`song_source_type_${songId}`);
// 如果不是用户手动锁定的音源
if (sourceType !== 'manual') {
console.warn(
`时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试自动切换音源`
);
// 记录当前失败的音源
// 注意:这里假设当前使用的音源是 playMusic.value.source或者是刚刚解析出来的
// 但实际上我们需要知道当前具体是用哪个平台解析成功的,这可能需要从 getSongUrl 的结果中获取
// 暂时简单处理,将当前配置的来源加入已尝试列表
// 获取所有可用音源
const { useSettingsStore } = await import('./settings');
const settingsStore = useSettingsStore();
const enabledSources = settingsStore.setData.enabledMusicSources || [
'migu',
'kugou',
'pyncmd',
'gdmusic'
];
const availableSources: Platform[] = enabledSources.filter(
(s: string) => s !== 'bilibili'
);
// 将当前正在使用的音源加入已尝试列表
let currentSource = 'unknown';
const currentSavedSource = localStorage.getItem(`song_source_${songId}`);
if (currentSavedSource) {
try {
const sources = JSON.parse(currentSavedSource);
if (Array.isArray(sources) && sources.length > 0) {
currentSource = sources[0];
triedSources.value.add(currentSource);
}
} catch {
console.error(`解析当前音源失败: ${currentSource}`);
}
}
// 找到下一个未尝试的音源
const nextSource = availableSources.find((s) => !triedSources.value.has(s));
// 记录当前音源的时间差
if (currentSource !== 'unknown') {
triedSourceDiffs.value.set(currentSource, Math.abs(duration - expectedDuration));
}
if (nextSource) {
console.log(`自动切换到音源: ${nextSource}`);
newSound.stop();
newSound.unload();
// 递归调用 reparseCurrentSong
// 注意:这里是异步调用,不会阻塞当前函数返回,但我们已经停止了播放
const success = await reparseCurrentSong(nextSource, true);
if (success) {
return audioService.getCurrentSound();
}
return null;
} else {
console.warn('所有音源都已尝试,寻找最接近时长的版本');
// 找出时间差最小的音源
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of triedSourceDiffs.value.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
// 如果找到了最佳音源,且不是当前正在播放的音源
if (bestSource && bestSource !== currentSource) {
console.log(`切换到最佳匹配音源: ${bestSource} (差异: ${minDiff}s)`);
newSound.stop();
newSound.unload();
const success = await reparseCurrentSong(bestSource as Platform, true);
if (success) {
return audioService.getCurrentSound();
}
return null;
}
console.log(`当前音源 ${currentSource} 已经是最佳匹配 (差异: ${minDiff}s),保留播放`);
}
}
}
// 时长检查已在 preloadService.ts 中完成
return newSound;
} catch (error) {
@@ -630,11 +525,12 @@ export const usePlayerCoreStore = defineStore(
return false;
}
const songId = String(currentSong.id);
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
// 记录音源设置类型(自动/手动)
localStorage.setItem(`song_source_type_${songId}`, isAuto ? 'auto' : 'manual');
// 使用 SongSourceConfigManager 保存配置
SongSourceConfigManager.setConfig(
currentSong.id,
[sourcePlatform],
isAuto ? 'auto' : 'manual'
);
const currentSound = audioService.getCurrentSound();
if (currentSound) {