import { http } from '@kit.NetworkKit'; import { WebDavAccount } from '../../viewmodel/WebDavAccount'; import Logger from '../util/Logger'; import FileManager from '../util/FileManager'; import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache'; import { RemoteCacheManager, RemoteCacheRequest, RemoteCacheStrategy } from './RemoteCacheManager'; import { fileIo } from '@kit.CoreFileKit'; const TAG = 'heanup AudioStationFileCache'; const CHUNK_SIZE = 4 * 1024 * 1024; interface AudioStationCacheOptions extends RemoteCacheRequest { account: WebDavAccount; songId: string; streamUrl: string; fileSize?: number; } class AudioStationCacheStrategy implements RemoteCacheStrategy { readonly type: RemoteCacheType = RemoteCacheType.AUDIOSTATION; async ensure(options: RemoteCacheRequest): Promise { const audioOptions = options as AudioStationCacheOptions; return ensureAudioStationFileCachedInternal( audioOptions.account, audioOptions.songId, audioOptions.streamUrl, audioOptions.fileSize ); } } async function ensureAudioStationFileCachedInternal( account: WebDavAccount, songId: string, streamUrl: string, fileSize?: number ): Promise { const normalizedRelative = normalizeCacheRelativePath(songId); const pathInfo = await resolveCacheFilePath( RemoteCacheType.AUDIOSTATION, account.id?.toString(), normalizedRelative ); const cachePath = pathInfo.cachePath; let exists = await FileManager.isExist(cachePath); if (exists) { const size = await FileManager.getFileSize(cachePath); if (size <= 0) { await FileManager.deleteFile(cachePath); exists = false; } else { Logger.info(TAG, `AudioStation 文件已缓存: ${cachePath}, size=${size}`); return cachePath; } } if (!exists) { try { Logger.info(TAG, `开始下载 AudioStation 文件: songId=${songId}, 到 ${cachePath}`); let finalFileSize = fileSize || 0; if (!finalFileSize || finalFileSize <= 0) { try { const headRequest = http.createHttp(); const headResponse = await headRequest.request(streamUrl, { method: http.RequestMethod.HEAD, connectTimeout: 30000, readTimeout: 30000, expectDataType: http.HttpDataType.STRING }); const contentLength = headResponse.header['Content-Length'] as string; if (contentLength) { finalFileSize = parseInt(contentLength, 10); } headRequest.destroy(); } catch (error) { Logger.warn(TAG, `HEAD请求获取文件大小失败,将使用分片下载: ${(error as Error).message}`); } } await downloadAudioStationFileChunked(streamUrl, cachePath, finalFileSize); Logger.info(TAG, `AudioStation 文件下载完成: ${cachePath}`); } catch (error) { await FileManager.deleteFile(cachePath); const err = error as Error; Logger.error(TAG, `AudioStation 文件下载失败: ${err.message}`); throw err; } } return cachePath; } async function downloadAudioStationFileChunked(streamUrl: string, localPath: string, totalSize: number): Promise { const file = fileIo.openSync(localPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC); try { if (totalSize === 0) { await downloadSingleAudioStationChunk(streamUrl, 0, -1, file); } else { let downloadedSize: number = 0; while (downloadedSize < totalSize) { const rangeStart = downloadedSize; const rangeEnd = Math.min(downloadedSize + CHUNK_SIZE - 1, totalSize - 1); await downloadSingleAudioStationChunk(streamUrl, rangeStart, rangeEnd, file); downloadedSize = rangeEnd + 1; } } fileIo.closeSync(file); } finally { // file closed above } } async function downloadSingleAudioStationChunk( streamUrl: string, rangeStart: number, rangeEnd: number, file: fileIo.File ): Promise { const httpRequest = http.createHttp(); try { const options: http.HttpRequestOptions = { method: http.RequestMethod.GET, connectTimeout: 30000, readTimeout: 600000, expectDataType: http.HttpDataType.ARRAY_BUFFER }; if (rangeEnd >= 0) { if (!options.header) { options.header = {}; } options.header['Range'] = `bytes=${rangeStart}-${rangeEnd}`; } const response = await httpRequest.request(streamUrl, options); if (response.responseCode !== 200 && response.responseCode !== 206) { throw new Error(`AudioStation 下载失败 HTTP ${response.responseCode}`); } if (response.result instanceof ArrayBuffer) { const arrayBuffer = response.result as ArrayBuffer; if (!arrayBuffer || arrayBuffer.byteLength === 0) { throw new Error('AudioStation 下载的文件为空'); } await fileIo.write(file.fd, arrayBuffer, { offset: rangeStart, length: arrayBuffer.byteLength }); } } finally { httpRequest.destroy(); } } RemoteCacheManager.registerStrategy(new AudioStationCacheStrategy()); export async function ensureAudioStationFileCached( account: WebDavAccount, songId: string, streamUrl: string, fileSize?: number ): Promise { return RemoteCacheManager.ensureCached(RemoteCacheType.AUDIOSTATION, { account, songId, streamUrl, fileSize } as AudioStationCacheOptions); }