mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-06-22 07:07:28 +08:00
fix: 修复音源解析致命性错误
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user