修复音质切换没有立即生效
This commit is contained in:
+171
-18
@@ -16,6 +16,7 @@ const SPECTRUM_MIN_VOLUME = 5;
|
|||||||
const SPECTRUM_MAX_REFERENCE_VOLUME = 15;
|
const SPECTRUM_MAX_REFERENCE_VOLUME = 15;
|
||||||
|
|
||||||
type MusicSource = 'wy' | 'tx' | 'kw' | 'kg' | 'mg';
|
type MusicSource = 'wy' | 'tx' | 'kw' | 'kg' | 'mg';
|
||||||
|
type MusicQuality = '128k' | '320k' | 'flac' | 'flac24bit';
|
||||||
|
|
||||||
interface Song {
|
interface Song {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -278,7 +279,7 @@ export default function MusicPage() {
|
|||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
const [quality, setQuality] = useState<'128k' | '320k' | 'flac' | 'flac24bit'>('320k');
|
const [quality, setQuality] = useState<MusicQuality>('320k');
|
||||||
const [playMode, setPlayMode] = useState<'loop' | 'single' | 'random'>('loop');
|
const [playMode, setPlayMode] = useState<'loop' | 'single' | 'random'>('loop');
|
||||||
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
||||||
const [showPlayer, setShowPlayer] = useState(false);
|
const [showPlayer, setShowPlayer] = useState(false);
|
||||||
@@ -362,6 +363,9 @@ export default function MusicPage() {
|
|||||||
const currentTimeRef = useRef(0);
|
const currentTimeRef = useRef(0);
|
||||||
const volumeRef = useRef(volume);
|
const volumeRef = useRef(volume);
|
||||||
const spectrumSeedRef = useRef(Math.random() * Math.PI * 2);
|
const spectrumSeedRef = useRef(Math.random() * Math.PI * 2);
|
||||||
|
const qualitySwitchRequestRef = useRef(0);
|
||||||
|
const currentSongRef = useRef<Song | null>(null);
|
||||||
|
const currentSourceRef = useRef(currentSource);
|
||||||
|
|
||||||
const mapSong = (song: any): Song => ({
|
const mapSong = (song: any): Song => ({
|
||||||
id: song.songId || song.id,
|
id: song.songId || song.id,
|
||||||
@@ -399,7 +403,7 @@ export default function MusicPage() {
|
|||||||
{ key: 'mg', label: '咪咕' },
|
{ key: 'mg', label: '咪咕' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const buildStreamUrl = (song: Song, source: MusicSource, songQuality: '128k' | '320k' | 'flac' | 'flac24bit') => {
|
const buildStreamUrl = (song: Song, source: MusicSource, songQuality: MusicQuality) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
songId: song.id,
|
songId: song.id,
|
||||||
source,
|
source,
|
||||||
@@ -422,7 +426,7 @@ export default function MusicPage() {
|
|||||||
const fetchPlayData = async (
|
const fetchPlayData = async (
|
||||||
song: Song,
|
song: Song,
|
||||||
source: MusicSource,
|
source: MusicSource,
|
||||||
songQuality: '128k' | '320k' | 'flac' | 'flac24bit',
|
songQuality: MusicQuality,
|
||||||
includeUrl = false
|
includeUrl = false
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch('/api/music/v2/play', {
|
const response = await fetch('/api/music/v2/play', {
|
||||||
@@ -461,7 +465,8 @@ export default function MusicPage() {
|
|||||||
song: Song,
|
song: Song,
|
||||||
playTime: number,
|
playTime: number,
|
||||||
totalDuration: number,
|
totalDuration: number,
|
||||||
lastPlayedAt = Date.now()
|
lastPlayedAt = Date.now(),
|
||||||
|
recordQuality: MusicQuality = quality
|
||||||
) => {
|
) => {
|
||||||
await fetch('/api/music/v2/history', {
|
await fetch('/api/music/v2/history', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -480,7 +485,7 @@ export default function MusicPage() {
|
|||||||
},
|
},
|
||||||
playProgressSec: playTime,
|
playProgressSec: playTime,
|
||||||
lastPlayedAt,
|
lastPlayedAt,
|
||||||
lastQuality: quality,
|
lastQuality: recordQuality,
|
||||||
createdAt: record.timestamp,
|
createdAt: record.timestamp,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -491,9 +496,10 @@ export default function MusicPage() {
|
|||||||
song: Song,
|
song: Song,
|
||||||
playTime = 0,
|
playTime = 0,
|
||||||
totalDuration = 0,
|
totalDuration = 0,
|
||||||
lastPlayedAt?: number
|
lastPlayedAt?: number,
|
||||||
|
recordQuality?: MusicQuality
|
||||||
) => {
|
) => {
|
||||||
saveHistoryRecord(record, song, playTime, totalDuration, lastPlayedAt).catch(err => {
|
saveHistoryRecord(record, song, playTime, totalDuration, lastPlayedAt, recordQuality).catch(err => {
|
||||||
console.error('保存播放记录到数据库失败:', err);
|
console.error('保存播放记录到数据库失败:', err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -720,6 +726,14 @@ export default function MusicPage() {
|
|||||||
}
|
}
|
||||||
}, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
|
}, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
currentSongRef.current = currentSong;
|
||||||
|
}, [currentSong]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
currentSourceRef.current = currentSource;
|
||||||
|
}, [currentSource]);
|
||||||
|
|
||||||
// 监听 playRecords 变化,更新 playlistIndex
|
// 监听 playRecords 变化,更新 playlistIndex
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingSongToPlay) {
|
if (pendingSongToPlay) {
|
||||||
@@ -1469,11 +1483,154 @@ export default function MusicPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 切换音质
|
// 切换音质
|
||||||
|
const handleQualityChange = async (nextQuality: MusicQuality) => {
|
||||||
|
setShowQualityMenu(false);
|
||||||
|
|
||||||
|
if (nextQuality === quality) return;
|
||||||
|
|
||||||
|
const targetSong = currentSong;
|
||||||
|
const audio = audioRef.current;
|
||||||
|
const targetPlatform = targetSong?.platform || currentSource;
|
||||||
|
|
||||||
|
currentSongRef.current = targetSong;
|
||||||
|
currentSourceRef.current = currentSource;
|
||||||
|
|
||||||
|
setQuality(nextQuality);
|
||||||
|
|
||||||
|
// 没有正在播放的歌曲时,仅保存偏好;下次播放会使用新音质。
|
||||||
|
if (!targetSong || !audio) return;
|
||||||
|
|
||||||
|
const requestId = ++qualitySwitchRequestRef.current;
|
||||||
|
const targetSongKey = `${targetPlatform}:${targetSong.id}`;
|
||||||
|
const resumeTime = Number.isFinite(audio.currentTime) ? audio.currentTime : currentTimeRef.current;
|
||||||
|
const shouldResume = isPlaying || (!audio.paused && !audio.ended);
|
||||||
|
|
||||||
|
const isStillTargetSong = () => {
|
||||||
|
const activeSong = currentSongRef.current;
|
||||||
|
if (!activeSong) return false;
|
||||||
|
|
||||||
|
const activePlatform = activeSong.platform || currentSourceRef.current;
|
||||||
|
return (
|
||||||
|
requestId === qualitySwitchRequestRef.current &&
|
||||||
|
`${activePlatform}:${activeSong.id}` === targetSongKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beginResolving();
|
||||||
|
try {
|
||||||
|
const proxyEnabled = getMusicProxyEnabled();
|
||||||
|
setMusicProxyEnabled(proxyEnabled);
|
||||||
|
|
||||||
|
let nextSongUrl = '';
|
||||||
|
|
||||||
|
if (proxyEnabled) {
|
||||||
|
nextSongUrl = buildStreamUrl(targetSong, targetPlatform, nextQuality);
|
||||||
|
} else {
|
||||||
|
const data = await fetchPlayData(targetSong, targetPlatform, nextQuality, true);
|
||||||
|
if (!isStillTargetSong()) return;
|
||||||
|
|
||||||
|
if (!data.success || !data.data?.play?.directUrl) {
|
||||||
|
throw new Error(data.error?.message || '获取播放地址失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSongUrl = data.data.play.directUrl;
|
||||||
|
|
||||||
|
if (data.data.song?.cover) {
|
||||||
|
setCurrentSong({
|
||||||
|
...targetSong,
|
||||||
|
pic: data.data.song.cover,
|
||||||
|
platform: targetPlatform,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data.lyric?.lyric) {
|
||||||
|
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
||||||
|
setLyrics(parsedLyrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStillTargetSong()) return;
|
||||||
|
|
||||||
|
const activeRecord =
|
||||||
|
playRecords[playlistIndex]?.platform === targetPlatform && playRecords[playlistIndex]?.id === targetSong.id
|
||||||
|
? playRecords[playlistIndex]
|
||||||
|
: playRecords.find((record) => record.platform === targetPlatform && record.id === targetSong.id);
|
||||||
|
const totalDuration =
|
||||||
|
Number.isFinite(audio.duration) && audio.duration > 0
|
||||||
|
? audio.duration
|
||||||
|
: duration || targetSong.duration || 0;
|
||||||
|
|
||||||
|
if (activeRecord) {
|
||||||
|
saveHistoryRecordSafely(
|
||||||
|
activeRecord,
|
||||||
|
{ ...targetSong, platform: targetPlatform },
|
||||||
|
resumeTime,
|
||||||
|
totalDuration,
|
||||||
|
Date.now(),
|
||||||
|
nextQuality
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSongUrl(nextSongUrl);
|
||||||
|
setCurrentTime(resumeTime);
|
||||||
|
songStartTimeRef.current = Date.now();
|
||||||
|
restoredTimeRef.current = resumeTime;
|
||||||
|
|
||||||
|
const resumeAfterMetadata = () => {
|
||||||
|
if (!isStillTargetSong()) return;
|
||||||
|
|
||||||
|
if (resumeTime > 0) {
|
||||||
|
try {
|
||||||
|
const maxSeekTime =
|
||||||
|
Number.isFinite(audio.duration) && audio.duration > 0
|
||||||
|
? Math.max(0, audio.duration - 0.25)
|
||||||
|
: resumeTime;
|
||||||
|
const seekTime = Math.min(resumeTime, maxSeekTime);
|
||||||
|
|
||||||
|
if (Math.abs(audio.currentTime - seekTime) > 1) {
|
||||||
|
audio.currentTime = seekTime;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('切换音质后恢复播放进度失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTime(audio.currentTime || resumeTime);
|
||||||
|
|
||||||
|
if (shouldResume) {
|
||||||
|
audio.play()
|
||||||
|
.then(() => setIsPlaying(true))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('切换音质后播放失败:', error);
|
||||||
|
setIsPlaying(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.pause();
|
||||||
|
audio.src = nextSongUrl;
|
||||||
|
audio.addEventListener('loadedmetadata', resumeAfterMetadata, { once: true });
|
||||||
|
audio.load();
|
||||||
|
setIsPlaying(shouldResume);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换音质失败:', error);
|
||||||
|
setToast({
|
||||||
|
message: (error as Error).message || '切换音质失败',
|
||||||
|
type: 'error',
|
||||||
|
onClose: () => setToast(null),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
endResolving();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cycleQuality = () => {
|
const cycleQuality = () => {
|
||||||
const qualities: Array<'128k' | '320k' | 'flac' | 'flac24bit'> = ['128k', '320k', 'flac', 'flac24bit'];
|
const qualities: MusicQuality[] = ['128k', '320k', 'flac', 'flac24bit'];
|
||||||
const currentIndex = qualities.indexOf(quality);
|
const currentIndex = qualities.indexOf(quality);
|
||||||
const nextIndex = (currentIndex + 1) % qualities.length;
|
const nextIndex = (currentIndex + 1) % qualities.length;
|
||||||
setQuality(qualities[nextIndex]);
|
void handleQualityChange(qualities[nextIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空播放记录
|
// 清空播放记录
|
||||||
@@ -1693,7 +1850,7 @@ export default function MusicPage() {
|
|||||||
audio.removeEventListener('durationchange', handleDurationChange);
|
audio.removeEventListener('durationchange', handleDurationChange);
|
||||||
audio.removeEventListener('ended', handleEnded);
|
audio.removeEventListener('ended', handleEnded);
|
||||||
};
|
};
|
||||||
}, [playMode, songs, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords]);
|
}, [playMode, songs, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords, quality]);
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -3187,8 +3344,7 @@ export default function MusicPage() {
|
|||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuality('128k');
|
void handleQualityChange('128k');
|
||||||
setShowQualityMenu(false);
|
|
||||||
}}
|
}}
|
||||||
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
||||||
quality === '128k'
|
quality === '128k'
|
||||||
@@ -3212,8 +3368,7 @@ export default function MusicPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuality('320k');
|
void handleQualityChange('320k');
|
||||||
setShowQualityMenu(false);
|
|
||||||
}}
|
}}
|
||||||
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
||||||
quality === '320k'
|
quality === '320k'
|
||||||
@@ -3237,8 +3392,7 @@ export default function MusicPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuality('flac');
|
void handleQualityChange('flac');
|
||||||
setShowQualityMenu(false);
|
|
||||||
}}
|
}}
|
||||||
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
||||||
quality === 'flac'
|
quality === 'flac'
|
||||||
@@ -3262,8 +3416,7 @@ export default function MusicPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuality('flac24bit');
|
void handleQualityChange('flac24bit');
|
||||||
setShowQualityMenu(false);
|
|
||||||
}}
|
}}
|
||||||
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
|
||||||
quality === 'flac24bit'
|
quality === 'flac24bit'
|
||||||
|
|||||||
Reference in New Issue
Block a user