feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
import * as FileSystem from 'expo-file-system';
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<void> => {
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<string> => {
const maxRetries = 3;
let retryCount = 0;
// 使用原始 URL 作为缓存键,但下载时使用编码后的 URL
const cacheKey = generateCacheKey(url);
const cachePath = `${CACHE_DIR}${cacheKey}`;
// 对 URL 进行编码,特别处理中文路径
const encodedUrl = encodeUrlPath(url);
while (retryCount < maxRetries) {
try {
console.log(`[下载] 开始下载: ${encodedUrl}`);
// 添加更多下载选项
const downloadResumable = FileSystem.createDownloadResumable(
encodedUrl,
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(`[下载] 成功: ${encodedUrl}`);
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(`[下载] 达到最大重试次数,放弃: ${encodedUrl}`);
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<void> => {
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<void> => {
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<number> => {
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<DownloadAllResult> => {
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<boolean> => {
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<void> => {
try {
await AsyncStorage.setItem(CACHE_ONLY_KEY, value.toString());
} catch (error) {
console.error('Failed to save cache only setting:', error);
throw error;
}
};