feat: 添加网易云音乐同步到Navidrome的功能
新增NetEase-sync模块,实现将网易云音乐歌单同步到Navidrome的功能 修复iOS设备自动播放问题,优化播放器体验
This commit is contained in:
89
index.html
89
index.html
@@ -148,6 +148,14 @@
|
||||
{ id: 'qq', name: 'QQ音乐' },
|
||||
{ id: 'kugou', name: '酷狗' }
|
||||
];
|
||||
const IS_IOS = (() => {
|
||||
if (typeof navigator === 'undefined') return false;
|
||||
const ua = navigator.userAgent || '';
|
||||
const platform = navigator.platform || '';
|
||||
const iOSUA = /iPad|iPhone|iPod/.test(ua);
|
||||
const iPadOS = platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||||
return iOSUA || iPadOS;
|
||||
})();
|
||||
|
||||
// --- Utility Functions ---
|
||||
const formatTime = (seconds) => {
|
||||
@@ -945,6 +953,7 @@
|
||||
|
||||
const audioRef = useRef(null);
|
||||
const autoAdvanceLockRef = useRef(false);
|
||||
const autoNextPendingRef = useRef(false);
|
||||
const currentSongRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -955,6 +964,33 @@
|
||||
autoAdvanceLockRef.current = false;
|
||||
}, [currentSong]);
|
||||
|
||||
const playAudioWithFallback = (audio, options = {}) => {
|
||||
if (!audio) return;
|
||||
const { deferOnIOS = false } = options;
|
||||
const doPlay = () => {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(e => {
|
||||
console.warn("Auto-play prevented:", e);
|
||||
if (e && e.name === 'AbortError') return;
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (deferOnIOS && IS_IOS) {
|
||||
const onCanPlay = () => {
|
||||
audio.removeEventListener('canplay', onCanPlay);
|
||||
doPlay();
|
||||
};
|
||||
audio.addEventListener('canplay', onCanPlay);
|
||||
try { audio.load(); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
|
||||
doPlay();
|
||||
};
|
||||
|
||||
// Media Session Refs
|
||||
const playNextRef = useRef(null);
|
||||
const playPrevRef = useRef(null);
|
||||
@@ -1185,30 +1221,39 @@
|
||||
const triggerAutoNext = () => {
|
||||
if (autoAdvanceLockRef.current) return;
|
||||
autoAdvanceLockRef.current = true;
|
||||
playNext(true, { immediate: true });
|
||||
playNext(true, { immediate: !IS_IOS, deferOnIOS: IS_IOS });
|
||||
};
|
||||
|
||||
const isNearEnd = () => {
|
||||
const durationSeconds = resolveDurationSeconds(audio, currentSongRef.current);
|
||||
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
|
||||
const threshold = IS_IOS ? 0.15 : 0.35;
|
||||
return audio.currentTime >= durationSeconds - threshold;
|
||||
};
|
||||
|
||||
const updateTime = () => {
|
||||
setCurrentTime(audio.currentTime);
|
||||
if (autoAdvanceLockRef.current) return;
|
||||
const durationSeconds = resolveDurationSeconds(audio, currentSongRef.current);
|
||||
if (Number.isFinite(durationSeconds) && durationSeconds > 0) {
|
||||
if (audio.currentTime >= durationSeconds - 0.35) {
|
||||
triggerAutoNext();
|
||||
}
|
||||
}
|
||||
if (!IS_IOS && isNearEnd()) triggerAutoNext();
|
||||
};
|
||||
const updateDuration = () => setDuration(resolveDurationSeconds(audio, currentSongRef.current));
|
||||
const onEnded = () => triggerAutoNext();
|
||||
const onPause = () => {
|
||||
if (!IS_IOS) return;
|
||||
if (autoAdvanceLockRef.current) return;
|
||||
if (isNearEnd()) triggerAutoNext();
|
||||
};
|
||||
|
||||
audio.addEventListener('timeupdate', updateTime);
|
||||
audio.addEventListener('loadedmetadata', updateDuration);
|
||||
audio.addEventListener('ended', onEnded);
|
||||
audio.addEventListener('pause', onPause);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', updateTime);
|
||||
audio.removeEventListener('loadedmetadata', updateDuration);
|
||||
audio.removeEventListener('ended', onEnded);
|
||||
audio.removeEventListener('pause', onPause);
|
||||
};
|
||||
}, [playlist, currentSong, mode, volume, quality]);
|
||||
|
||||
@@ -1222,20 +1267,21 @@
|
||||
// Only update src if it's different to avoid reloading same song on re-render (unless quality changed)
|
||||
// Note: audioRef.current.src returns full absolute URL
|
||||
const currentSrc = audio.src;
|
||||
const wasPlaying = isPlaying;
|
||||
const deferOnIOS = autoNextPendingRef.current;
|
||||
autoNextPendingRef.current = false;
|
||||
|
||||
// Simple check if src changed significantly (avoiding minor encoding diffs if possible, but exact match is safer)
|
||||
if (currentSrc !== url) {
|
||||
const wasPlaying = isPlaying;
|
||||
audio.src = url;
|
||||
if (wasPlaying) {
|
||||
audio.play()
|
||||
.then(() => setIsPlaying(true))
|
||||
.catch(e => {
|
||||
console.warn("Auto-play prevented:", e);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
playAudioWithFallback(audio, { deferOnIOS });
|
||||
}
|
||||
} else if (deferOnIOS && wasPlaying) {
|
||||
playAudioWithFallback(audio, { deferOnIOS: true });
|
||||
}
|
||||
} else {
|
||||
autoNextPendingRef.current = false;
|
||||
}
|
||||
}, [currentSong, quality]); // Re-run when quality changes
|
||||
|
||||
@@ -1243,7 +1289,7 @@
|
||||
if (currentSong) {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
if (isPlaying) audio.play().catch(() => setIsPlaying(false));
|
||||
if (isPlaying) playAudioWithFallback(audio);
|
||||
else audio.pause();
|
||||
}
|
||||
}, [isPlaying]);
|
||||
@@ -1325,11 +1371,15 @@
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
audio.currentTime = 0;
|
||||
audio.play().catch(() => setIsPlaying(false));
|
||||
playAudioWithFallback(audio);
|
||||
autoAdvanceLockRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto) {
|
||||
autoNextPendingRef.current = true;
|
||||
}
|
||||
|
||||
let nextIdx;
|
||||
const currIdx = playlist.findIndex(s => s.id === currentSong?.id);
|
||||
|
||||
@@ -1350,12 +1400,7 @@
|
||||
if (audio.src !== url) {
|
||||
audio.src = url;
|
||||
}
|
||||
audio.play()
|
||||
.then(() => setIsPlaying(true))
|
||||
.catch(e => {
|
||||
console.warn("Auto-play prevented:", e);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
playAudioWithFallback(audio, { deferOnIOS: options.deferOnIOS });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user