import * as FileSystem from 'expo-file-system/legacy'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { CacheInfo, Track } from '../types'; const CACHE_DIR = `${FileSystem.documentDirectory}audio_cache/`; const CACHE_INFO_KEY = '@zen_box_cache_info'; const CACHE_ONLY_KEY = '@zen_box_cache_only'; let cacheInfo: CacheInfo = {}; export const initCache = async (): Promise => { try { // 确保缓存目录存在 const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR); if (!dirInfo.exists) { await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true }); } // 加载缓存信息 const cachedInfo = await AsyncStorage.getItem(CACHE_INFO_KEY); if (cachedInfo) { cacheInfo = JSON.parse(cachedInfo); } } catch (error) { console.error('初始化缓存失败:', error); } }; export const getCacheInfo = (): CacheInfo => { return { ...cacheInfo }; }; export interface DownloadAllResult { successCount: number; skippedCount: number; failedTracks: string[]; total: number; } const generateCacheKey = (url: string): string => { // 简单的 URL 哈希 let hash = 0; for (let i = 0; i < url.length; i++) { const char = url.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36) + '.mp3'; }; export const isCached = (url: string): boolean => { return !!cacheInfo[url]; }; export const getCachedPath = (url: string): string | null => { if (cacheInfo[url]) { return cacheInfo[url].path; } return null; }; const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // 安全地编码 URL 中的路径部分,保留协议、域名等 const encodeUrlPath = (url: string): string => { try { const parsedUrl = new URL(url); // 只编码路径部分,不要编码整个 URL const pathParts = parsedUrl.pathname.split('/'); const encodedPathParts = pathParts.map((part, index) => { // 不要编码空字符串(开头的斜杠) if (part === '') return part; // 使用 encodeURIComponent 编码每个路径段 return encodeURIComponent(decodeURIComponent(part)); }); parsedUrl.pathname = encodedPathParts.join('/'); const finalUrl = parsedUrl.toString(); console.log(`[URL] 原始: ${url}`); console.log(`[URL] 编码后: ${finalUrl}`); return finalUrl; } catch (error) { console.error('[URL] 编码失败:', error); // 如果解析失败,尝试简单编码 try { const encoded = encodeURI(url); console.log(`[URL] 简单编码: ${encoded}`); return encoded; } catch { return url; } } }; export const downloadAndCache = async ( url: string, onProgress?: (progress: number) => void ): Promise => { const maxRetries = 3; let retryCount = 0; // 使用原始 URL 作为缓存键,但下载时使用编码后的 URL const cacheKey = generateCacheKey(url); const cachePath = `${CACHE_DIR}${cacheKey}`; // 直接使用原始 URL,不做额外编码 // OSS URL 通常已经正确编码,二次编码会破坏签名验证 const downloadUrl = url; while (retryCount < maxRetries) { try { console.log(`[下载] 开始下载: ${downloadUrl}`); // 添加更多下载选项 const downloadResumable = FileSystem.createDownloadResumable( downloadUrl, cachePath, { // 设置一些常用的请求头,兼容 OSS headers: { 'User-Agent': 'ZenBox/1.0', 'Accept': 'audio/mpeg,audio/*;q=0.9,*/*;q=0.8', }, }, (downloadProgress) => { if (onProgress && downloadProgress.totalBytesWritten > 0 && downloadProgress.totalBytesExpectedToWrite > 0) { const progress = downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite; onProgress(progress); } } ); const downloadResult = await downloadResumable.downloadAsync(); if (downloadResult && downloadResult.status === 200) { console.log(`[下载] 成功: ${downloadUrl}`); const fileInfo = await FileSystem.getInfoAsync(cachePath); const size = fileInfo.exists ? (fileInfo as any).size || 0 : 0; cacheInfo[url] = { path: cachePath, size, downloadedAt: Date.now(), }; await AsyncStorage.setItem(CACHE_INFO_KEY, JSON.stringify(cacheInfo)); return cachePath; } console.error(`[下载] 失败,状态码: ${downloadResult?.status}`); throw new Error(`下载失败,状态码: ${downloadResult?.status}`); } catch (error) { retryCount++; console.error(`[下载] 尝试 ${retryCount}/${maxRetries} 失败:`, error); if (retryCount >= maxRetries) { console.error(`[下载] 达到最大重试次数,放弃: ${downloadUrl}`); throw error; } // 指数退避重试 const backoffTime = Math.min(1000 * Math.pow(2, retryCount - 1), 5000); console.log(`[下载] ${backoffTime}ms 后重试...`); await sleep(backoffTime); } } throw new Error('下载失败,已达到最大重试次数'); }; export const deleteCache = async (url: string): Promise => { try { if (cacheInfo[url]) { const path = cacheInfo[url].path; await FileSystem.deleteAsync(path, { idempotent: true }); delete cacheInfo[url]; await AsyncStorage.setItem(CACHE_INFO_KEY, JSON.stringify(cacheInfo)); } } catch (error) { console.error('删除缓存失败:', error); } }; export const clearAllCache = async (): Promise => { try { // 删除所有缓存文件 await FileSystem.deleteAsync(CACHE_DIR, { idempotent: true }); await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true }); // 清空缓存信息 cacheInfo = {}; await AsyncStorage.setItem(CACHE_INFO_KEY, JSON.stringify(cacheInfo)); } catch (error) { console.error('清除缓存失败:', error); } }; export const getCacheSize = async (): Promise => { try { let totalSize = 0; for (const url in cacheInfo) { totalSize += cacheInfo[url].size; } return totalSize; } catch (error) { console.error('获取缓存大小失败:', error); return 0; } }; export const formatSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; export const downloadAllTracks = async ( tracks: Track[], onProgress?: (completed: number, total: number, trackTitle: string) => void ): Promise => { let successCount = 0; let skippedCount = 0; const failedTracks: string[] = []; let completedCount = 0; for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; if (onProgress) { onProgress(completedCount, tracks.length, track.title); } if (!isCached(track.src)) { try { await downloadAndCache(track.src); successCount += 1; } catch (error) { console.error(`Failed to download ${track.title}:`, error); failedTracks.push(track.title); } } else { skippedCount += 1; } completedCount += 1; } if (onProgress) { onProgress(tracks.length, tracks.length, '完成'); } return { successCount, skippedCount, failedTracks, total: tracks.length, }; }; export const getCacheOnlySetting = async (): Promise => { try { const value = await AsyncStorage.getItem(CACHE_ONLY_KEY); // 默认返回 true return value !== 'false'; } catch (error) { console.error('Failed to get cache only setting:', error); return true; } }; export const setCacheOnlySetting = async (value: boolean): Promise => { try { await AsyncStorage.setItem(CACHE_ONLY_KEY, value.toString()); } catch (error) { console.error('Failed to save cache only setting:', error); throw error; } };