恢复单一SyncToken模式,合并网易云ID到SyncToken,实现"导入即替换"逻辑
调整同步策略:网易云模式下,自动执行"拉取网易云 -> 合并本地(非覆盖) -> 推送至私有云KV"
This commit is contained in:
110
index.html
110
index.html
@@ -431,7 +431,7 @@
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SideDrawer = ({ isOpen, onClose, view, setView, quality, setQuality, onClearCache, syncToken, setSyncToken, onSyncNow, onImportNetease, neteasePlaylistId, onStopNeteaseSync }) => {
|
const SideDrawer = ({ isOpen, onClose, view, setView, quality, setQuality, onClearCache, syncToken, setSyncToken, onSyncNow, onImportNetease, syncMode }) => {
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [syncMsg, setSyncMsg] = useState('');
|
const [syncMsg, setSyncMsg] = useState('');
|
||||||
|
|
||||||
@@ -510,7 +510,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<label className="block text-sm text-gray-300 mb-2">云同步密钥</label>
|
<label className="block text-sm text-gray-300 mb-2">
|
||||||
|
{syncMode === 'netease_playlist' ? '网易云歌单ID (自动同步)' : '云同步密钥'}
|
||||||
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -530,41 +532,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center mt-1">
|
<div className="flex justify-between items-center mt-1">
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
私有云多端同步收藏列表
|
{syncMode === 'netease_playlist'
|
||||||
|
? '已关联歌单,将合并新歌并自动备份至云端'
|
||||||
|
: '使用相同的密钥在多端同步收藏列表'}
|
||||||
</p>
|
</p>
|
||||||
{syncMsg && <span className="text-xs text-primary font-bold animate-[fadeIn_0.3s]">{syncMsg}</span>}
|
{syncMsg && <span className="text-xs text-primary font-bold animate-[fadeIn_0.3s]">{syncMsg}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-white/10 my-2 mx-4"></div>
|
|
||||||
|
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<label className="block text-sm text-gray-300 mb-2">关联网易云歌单</label>
|
<button
|
||||||
{neteasePlaylistId ? (
|
onClick={handleNeteaseImportClick}
|
||||||
<div className="bg-white/5 rounded-lg p-3 flex flex-col gap-2 border border-white/10">
|
className="w-full py-2 px-4 rounded-lg bg-red-600/20 border border-red-500/50 text-red-500 hover:bg-red-600/30 transition-colors text-sm flex items-center justify-center gap-2"
|
||||||
<div className="flex justify-between items-center">
|
>
|
||||||
<div className="text-xs text-primary font-mono truncate mr-2">ID: {neteasePlaylistId}</div>
|
<Icon name="cloud-arrow-down" />
|
||||||
<div className="text-[10px] text-green-400 bg-green-400/10 px-1.5 py-0.5 rounded">自动更新中</div>
|
导入网易云歌单
|
||||||
</div>
|
</button>
|
||||||
<button
|
<p className="text-xs text-gray-500 mt-1 px-1">导入后将设置ID为同步密钥,并定期获取新歌</p>
|
||||||
onClick={onStopNeteaseSync}
|
|
||||||
className="w-full py-1.5 rounded bg-red-500/20 text-red-500 text-xs hover:bg-red-500/30 transition-colors border border-red-500/20"
|
|
||||||
>
|
|
||||||
取消关联
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleNeteaseImportClick}
|
|
||||||
className="w-full py-2 px-4 rounded-lg bg-white/5 border border-white/10 text-gray-300 hover:bg-white/10 hover:text-white transition-colors text-sm flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<Icon name="cloud-arrow-down" />
|
|
||||||
导入并关联歌单
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<p className="text-[10px] text-gray-500 mt-2 px-1">
|
|
||||||
{neteasePlaylistId ? '每15分钟自动检测新歌并加入收藏,随后触发云同步。' : '导入后将自动合并新歌到收藏列表。'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2 mt-2">
|
<div className="px-4 py-2 mt-2">
|
||||||
@@ -937,7 +921,7 @@
|
|||||||
const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k');
|
const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k');
|
||||||
const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || '');
|
const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || '');
|
||||||
const [lastSuccessToken, setLastSuccessToken] = useState(() => localStorage.getItem('th_last_success_token') || '');
|
const [lastSuccessToken, setLastSuccessToken] = useState(() => localStorage.getItem('th_last_success_token') || '');
|
||||||
const [neteasePlaylistId, setNeteasePlaylistId] = useState(() => localStorage.getItem('th_netease_playlist_id') || '');
|
const [syncMode, setSyncMode] = useState(() => localStorage.getItem('th_sync_mode') || 'server'); // 'server' | 'netease_playlist'
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
const [showFullPlayer, setShowFullPlayer] = useState(false);
|
const [showFullPlayer, setShowFullPlayer] = useState(false);
|
||||||
@@ -971,7 +955,7 @@
|
|||||||
useEffect(() => { localStorage.setItem('th_quality', quality); }, [quality]);
|
useEffect(() => { localStorage.setItem('th_quality', quality); }, [quality]);
|
||||||
useEffect(() => { localStorage.setItem('th_sync_token', syncToken); }, [syncToken]);
|
useEffect(() => { localStorage.setItem('th_sync_token', syncToken); }, [syncToken]);
|
||||||
useEffect(() => { localStorage.setItem('th_last_success_token', lastSuccessToken); }, [lastSuccessToken]);
|
useEffect(() => { localStorage.setItem('th_last_success_token', lastSuccessToken); }, [lastSuccessToken]);
|
||||||
useEffect(() => { localStorage.setItem('th_netease_playlist_id', neteasePlaylistId); }, [neteasePlaylistId]);
|
useEffect(() => { localStorage.setItem('th_sync_mode', syncMode); }, [syncMode]);
|
||||||
|
|
||||||
// Auto Sync Logic for Private Server
|
// Auto Sync Logic for Private Server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -987,36 +971,40 @@
|
|||||||
|
|
||||||
// Auto Sync Logic for Netease Playlist (Interval based)
|
// Auto Sync Logic for Netease Playlist (Interval based)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (neteasePlaylistId) {
|
if (syncMode === 'netease_playlist' && syncToken) {
|
||||||
const doSync = async () => {
|
const doSync = async () => {
|
||||||
console.log("Auto syncing netease playlist...");
|
console.log("Auto syncing netease playlist...");
|
||||||
const songs = await api.getPlaylist(neteasePlaylistId);
|
try {
|
||||||
if (songs && songs.length > 0) {
|
const songs = await api.getPlaylist(syncToken);
|
||||||
setFavorites(prev => {
|
if (songs && songs.length > 0) {
|
||||||
const newFavs = [...prev];
|
let hasChanges = false;
|
||||||
let changed = false;
|
setFavorites(prev => {
|
||||||
// Add new songs (reverse to keep latest at top if we prepend)
|
const newFavs = [...prev];
|
||||||
[...songs].reverse().forEach(song => {
|
// Add new songs (reverse to keep latest at top if we prepend)
|
||||||
if (!newFavs.find(s => s.id === song.id)) {
|
[...songs].reverse().forEach(song => {
|
||||||
newFavs.unshift({ ...song, source: 'netease' });
|
if (!newFavs.find(s => s.id === song.id)) {
|
||||||
changed = true;
|
newFavs.unshift({ ...song, source: 'netease' });
|
||||||
}
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hasChanges ? newFavs : prev;
|
||||||
});
|
});
|
||||||
return changed ? newFavs : prev;
|
}
|
||||||
});
|
} catch (e) {
|
||||||
|
console.error("Auto sync netease playlist failed", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial sync on mount/mode change (if not already handled by import)
|
// Initial sync on mount is skipped because import handles it,
|
||||||
// We can skip initial immediate sync if we assume import did it, but it doesn't hurt to check.
|
// or if page refresh, server sync will pull first.
|
||||||
// Actually, if we just refreshed page, we want to check.
|
// But if we want to ensure we get new songs from netease on page load:
|
||||||
doSync();
|
doSync();
|
||||||
|
|
||||||
// Interval sync (15 minutes)
|
// Interval sync (15 minutes)
|
||||||
const interval = setInterval(doSync, 15 * 60 * 1000);
|
const interval = setInterval(doSync, 15 * 60 * 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}
|
}
|
||||||
}, [neteasePlaylistId]);
|
}, [syncMode, syncToken]);
|
||||||
|
|
||||||
// 1. Auto Sync: Incremental update based on diff (Server Mode)
|
// 1. Auto Sync: Incremental update based on diff (Server Mode)
|
||||||
const autoSyncFavorites = async () => {
|
const autoSyncFavorites = async () => {
|
||||||
@@ -1136,7 +1124,13 @@
|
|||||||
return newFavs;
|
return newFavs;
|
||||||
});
|
});
|
||||||
|
|
||||||
setNeteasePlaylistId(id);
|
setSyncToken(id);
|
||||||
|
setLastSuccessToken(id);
|
||||||
|
setSyncMode('netease_playlist');
|
||||||
|
|
||||||
|
// Trigger cloud sync immediately after state update
|
||||||
|
// Note: state update is async, so we might need to rely on the useEffect auto-sync
|
||||||
|
// which watches 'favorites' change.
|
||||||
|
|
||||||
// Switch view to favorites to see result
|
// Switch view to favorites to see result
|
||||||
setView('favorites');
|
setView('favorites');
|
||||||
@@ -1151,11 +1145,12 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopNeteaseSync = () => {
|
// Detect manual token change to reset mode
|
||||||
if(confirm("确定要停止自动同步网易云歌单吗?已导入的歌曲不会被删除。")) {
|
useEffect(() => {
|
||||||
setNeteasePlaylistId('');
|
if (!syncToken) {
|
||||||
|
setSyncMode('server');
|
||||||
}
|
}
|
||||||
};
|
}, [syncToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSong) {
|
if (currentSong) {
|
||||||
@@ -1550,8 +1545,7 @@
|
|||||||
setSyncToken={setSyncToken}
|
setSyncToken={setSyncToken}
|
||||||
onSyncNow={manualSyncFavorites}
|
onSyncNow={manualSyncFavorites}
|
||||||
onImportNetease={importNeteasePlaylist}
|
onImportNetease={importNeteasePlaylist}
|
||||||
neteasePlaylistId={neteasePlaylistId}
|
syncMode={syncMode}
|
||||||
onStopNeteaseSync={stopNeteaseSync}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Top Navigation / Search Bar */}
|
{/* Top Navigation / Search Bar */}
|
||||||
|
|||||||
Reference in New Issue
Block a user