import axios from 'axios'; import NetInfo from '@react-native-community/netinfo'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Track, UserConfig, SortMode } from './types'; const API_BASE_URL = 'https://traesite.umersoft.com:8975'; const PLAYLIST_LIST_PATH = '/api/zen_box/playlists'; const DEFAULT_PLAYLIST_FILE = 'playlist.json'; const PLAYLIST_SNAPSHOT_KEY_PREFIX = '@zen_box_playlist_snapshot:'; const SELECTED_PLAYLIST_KEY = '@zen_box_selected_playlist'; const RESUME_ENABLED_KEY = '@zen_box_resume_enabled'; const PLAYBACK_POSITION_KEY_PREFIX = '@zen_box_playback_position:'; const LOGGED_IN_USER_KEY = '@zen_box_logged_in_user'; const CLOUD_CONFIG_KEY = '@zen_box_cloud_config'; const SORT_PREF_KEY = '@zen_box_sort_pref'; const api = axios.create({ baseURL: API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // 网络状态检测 export const isNetworkConnected = async (): Promise => { try { const state = await NetInfo.fetch(); return state.isConnected ?? false; } catch { return false; } }; export const formatError = (error: any): string => { if (error.response) { return `服务器响应 ${error.response.status}: ${error.response.statusText}`; } else if (error.request) { return `服务器无响应: 无法连接到 ${API_BASE_URL}`; } else if (error.message) { return `请求错误: ${error.message}`; } else { return `未知错误`; } }; const normalizePlaylistFile = (fileName?: string): string => { return String(fileName || DEFAULT_PLAYLIST_FILE).trim() || DEFAULT_PLAYLIST_FILE; }; const buildPlaylistPath = (fileName?: string): string => { return `/tools/web_zen_box/${encodeURIComponent(normalizePlaylistFile(fileName))}`; }; const getPlaylistSnapshotKey = (fileName?: string): string => { return `${PLAYLIST_SNAPSHOT_KEY_PREFIX}${normalizePlaylistFile(fileName)}`; }; const getPlaybackPositionKey = (playlistFile?: string, trackSrc?: string): string => { return `${PLAYBACK_POSITION_KEY_PREFIX}${normalizePlaylistFile(playlistFile)}::${String(trackSrc || '').trim()}`; }; // ========== 登录相关 ========== export const login = async (username: string, password: string): Promise<{ ok: boolean; username?: string; error?: string }> => { try { const response = await api.post('/api/zen_box/auth/login', { username, password }); if (response.data?.ok) { await AsyncStorage.setItem(LOGGED_IN_USER_KEY, String(response.data.username || username)); return { ok: true, username: response.data.username || username }; } return { ok: false, error: '登录失败' }; } catch (error: any) { const msg = error.response?.data?.error || formatError(error); return { ok: false, error: msg }; } }; export const logout = async (): Promise => { try { await AsyncStorage.removeItem(LOGGED_IN_USER_KEY); await AsyncStorage.removeItem(CLOUD_CONFIG_KEY); } catch (error) { console.error('登出失败:', error); } }; export const getLoggedInUser = async (): Promise => { try { return await AsyncStorage.getItem(LOGGED_IN_USER_KEY); } catch { return null; } }; // ========== 云端配置同步 ========== export const pushCloudConfig = async (config: UserConfig): Promise => { try { const user = await getLoggedInUser(); if (!user) return; const connected = await isNetworkConnected(); if (!connected) { await AsyncStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify(config)); return; } await api.post('/api/zen_box/config', { username: user, config }); } catch (error) { console.error('上传云端配置失败:', error); } }; export const pullCloudConfig = async (): Promise => { try { const user = await getLoggedInUser(); if (!user) return null; const connected = await isNetworkConnected(); if (!connected) { const cached = await AsyncStorage.getItem(CLOUD_CONFIG_KEY); return cached ? JSON.parse(cached) : null; } const response = await api.get('/api/zen_box/config', { params: { username: user } }); if (response.data?.ok && response.data?.config) { await AsyncStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify(response.data.config)); return response.data.config as UserConfig; } return null; } catch (error) { console.error('拉取云端配置失败:', error); const cached = await AsyncStorage.getItem(CLOUD_CONFIG_KEY); return cached ? JSON.parse(cached) : null; } }; // ========== 歌单相关 ========== export const getPlaylistFiles = async (): Promise => { try { const connected = await isNetworkConnected(); if (!connected) { return await getLocalPlaylistFiles(); } const response = await api.get<{ ok?: boolean; playlists?: string[] }>(PLAYLIST_LIST_PATH); const raw = Array.isArray(response.data?.playlists) ? response.data.playlists : []; const files = raw.map(item => String(item || '')).filter(Boolean); if (!files.includes(DEFAULT_PLAYLIST_FILE)) { files.unshift(DEFAULT_PLAYLIST_FILE); } return files; } catch (error) { console.error('读取歌单列表失败:', error); return await getLocalPlaylistFiles(); } }; const getLocalPlaylistFiles = async (): Promise => { try { const allKeys = await AsyncStorage.getAllKeys(); const snapshotKeys = allKeys.filter(k => k.startsWith(PLAYLIST_SNAPSHOT_KEY_PREFIX)); const files = snapshotKeys.map(k => k.replace(PLAYLIST_SNAPSHOT_KEY_PREFIX, '')); if (!files.includes(DEFAULT_PLAYLIST_FILE)) { files.unshift(DEFAULT_PLAYLIST_FILE); } return files; } catch { return [DEFAULT_PLAYLIST_FILE]; } }; export const getSavedSelectedPlaylist = async (): Promise => { try { const value = await AsyncStorage.getItem(SELECTED_PLAYLIST_KEY); return normalizePlaylistFile(value || DEFAULT_PLAYLIST_FILE); } catch (error) { console.error('读取当前歌单失败:', error); return DEFAULT_PLAYLIST_FILE; } }; export const saveSelectedPlaylist = async (fileName: string): Promise => { try { await AsyncStorage.setItem(SELECTED_PLAYLIST_KEY, normalizePlaylistFile(fileName)); } catch (error) { console.error('保存当前歌单失败:', error); } }; export const getResumePlaybackEnabled = async (): Promise => { try { const value = await AsyncStorage.getItem(RESUME_ENABLED_KEY); if (value === null) { return true; } return value === '1'; } catch (error) { console.error('读取断点续播设置失败:', error); return true; } }; export const saveResumePlaybackEnabled = async (enabled: boolean): Promise => { try { await AsyncStorage.setItem(RESUME_ENABLED_KEY, enabled ? '1' : '0'); } catch (error) { console.error('保存断点续播设置失败:', error); } }; export const getSavedPlaybackPosition = async (playlistFile?: string, trackSrc?: string): Promise => { try { const key = getPlaybackPositionKey(playlistFile, trackSrc); const value = await AsyncStorage.getItem(key); const seconds = Number(value || 0); return Number.isFinite(seconds) && seconds > 0 ? seconds : 0; } catch (error) { console.error('读取播放进度失败:', error); return 0; } }; export const savePlaybackPosition = async ( playlistFile: string, trackSrc: string, positionSeconds: number ): Promise => { try { const key = getPlaybackPositionKey(playlistFile, trackSrc); const safeSeconds = Math.max(0, Math.floor(positionSeconds || 0)); await AsyncStorage.setItem(key, String(safeSeconds)); } catch (error) { console.error('保存播放进度失败:', error); } }; export const clearPlaybackPosition = async (playlistFile: string, trackSrc: string): Promise => { try { const key = getPlaybackPositionKey(playlistFile, trackSrc); await AsyncStorage.removeItem(key); } catch (error) { console.error('清除播放进度失败:', error); } }; export const getPlaylist = async (fileName?: string): Promise => { try { const connected = await isNetworkConnected(); if (!connected) { throw new Error('网络不可用'); } const response = await api.get(buildPlaylistPath(fileName)); return response.data; } catch (error) { throw new Error(formatError(error)); } }; const isValidPlaylist = (data: unknown): data is Track[] => { return Array.isArray(data) && data.every((item) => { if (!item || typeof item !== 'object') { return false; } const track = item as Track; return typeof track.title === 'string' && typeof track.artist === 'string' && typeof track.src === 'string'; }); }; export const savePlaylistSnapshot = async (tracks: Track[], fileName?: string): Promise => { try { await AsyncStorage.setItem(getPlaylistSnapshotKey(fileName), JSON.stringify(tracks)); } catch (error) { console.error('保存歌单快照失败:', error); } }; export const getPlaylistSnapshot = async (fileName?: string): Promise => { try { const raw = await AsyncStorage.getItem(getPlaylistSnapshotKey(fileName)); if (!raw) { return []; } const parsed: unknown = JSON.parse(raw); return isValidPlaylist(parsed) ? parsed : []; } catch (error) { console.error('读取歌单快照失败:', error); return []; } }; export const getPlaylistWithFallback = async (fileName?: string): Promise<{ tracks: Track[]; source: 'remote' | 'snapshot'; fallbackReason?: string; playlistFile: string; }> => { const normalizedFile = normalizePlaylistFile(fileName); try { const tracks = await getPlaylist(normalizedFile); await savePlaylistSnapshot(tracks, normalizedFile); return { tracks, source: 'remote', playlistFile: normalizedFile }; } catch (error) { const fallbackTracks = await getPlaylistSnapshot(normalizedFile); if (fallbackTracks.length > 0) { return { tracks: fallbackTracks, source: 'snapshot', fallbackReason: error instanceof Error ? error.message : '远程歌单加载失败', playlistFile: normalizedFile, }; } throw error; } }; // ========== 离线歌单反推 ========== export const getOfflineAvailablePlaylists = async ( isCached: (src: string) => boolean ): Promise => { const allKeys = await AsyncStorage.getAllKeys(); const snapshotKeys = allKeys.filter(k => k.startsWith(PLAYLIST_SNAPSHOT_KEY_PREFIX)); const available: string[] = []; for (const key of snapshotKeys) { const fileName = key.replace(PLAYLIST_SNAPSHOT_KEY_PREFIX, ''); const snapshot = await getPlaylistSnapshot(fileName); if (!snapshot.length) continue; const cachedCount = snapshot.filter(track => isCached(track.src)).length; if (cachedCount > 0) { available.push(fileName); } } if (!available.includes(DEFAULT_PLAYLIST_FILE)) { available.unshift(DEFAULT_PLAYLIST_FILE); } return available; }; // ========== 排序偏好 ========== export const getSortPreference = async (): Promise => { try { const saved = await AsyncStorage.getItem(SORT_PREF_KEY); if (saved && ['original', 'asc', 'desc'].includes(saved)) { return saved as SortMode; } return 'original'; } catch { return 'original'; } }; export const saveSortPreference = async (mode: SortMode): Promise => { try { await AsyncStorage.setItem(SORT_PREF_KEY, mode); } catch (error) { console.error('保存排序偏好失败:', error); } };