AudioStationFileCache.ets 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { http } from '@kit.NetworkKit';
  2. import { WebDavAccount } from '../../viewmodel/WebDavAccount';
  3. import Logger from '../util/Logger';
  4. import FileManager from '../util/FileManager';
  5. import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache';
  6. import { RemoteCacheManager, RemoteCacheRequest, RemoteCacheStrategy } from './RemoteCacheManager';
  7. import { fileIo } from '@kit.CoreFileKit';
  8. const TAG = 'heanup AudioStationFileCache';
  9. const CHUNK_SIZE = 4 * 1024 * 1024;
  10. interface AudioStationCacheOptions extends RemoteCacheRequest {
  11. account: WebDavAccount;
  12. songId: string;
  13. streamUrl: string;
  14. fileSize?: number;
  15. }
  16. class AudioStationCacheStrategy implements RemoteCacheStrategy {
  17. readonly type: RemoteCacheType = RemoteCacheType.AUDIOSTATION;
  18. async ensure(options: RemoteCacheRequest): Promise<string> {
  19. const audioOptions = options as AudioStationCacheOptions;
  20. return ensureAudioStationFileCachedInternal(
  21. audioOptions.account,
  22. audioOptions.songId,
  23. audioOptions.streamUrl,
  24. audioOptions.fileSize
  25. );
  26. }
  27. }
  28. async function ensureAudioStationFileCachedInternal(
  29. account: WebDavAccount,
  30. songId: string,
  31. streamUrl: string,
  32. fileSize?: number
  33. ): Promise<string> {
  34. const normalizedRelative = normalizeCacheRelativePath(songId);
  35. const pathInfo = await resolveCacheFilePath(
  36. RemoteCacheType.AUDIOSTATION,
  37. account.id?.toString(),
  38. normalizedRelative
  39. );
  40. const cachePath = pathInfo.cachePath;
  41. let exists = await FileManager.isExist(cachePath);
  42. if (exists) {
  43. const size = await FileManager.getFileSize(cachePath);
  44. if (size <= 0) {
  45. await FileManager.deleteFile(cachePath);
  46. exists = false;
  47. } else {
  48. Logger.info(TAG, `AudioStation 文件已缓存: ${cachePath}, size=${size}`);
  49. return cachePath;
  50. }
  51. }
  52. if (!exists) {
  53. try {
  54. Logger.info(TAG, `开始下载 AudioStation 文件: songId=${songId}, 到 ${cachePath}`);
  55. let finalFileSize = fileSize || 0;
  56. if (!finalFileSize || finalFileSize <= 0) {
  57. try {
  58. const headRequest = http.createHttp();
  59. const headResponse = await headRequest.request(streamUrl, {
  60. method: http.RequestMethod.HEAD,
  61. connectTimeout: 30000,
  62. readTimeout: 30000,
  63. expectDataType: http.HttpDataType.STRING
  64. });
  65. const contentLength = headResponse.header['Content-Length'] as string;
  66. if (contentLength) {
  67. finalFileSize = parseInt(contentLength, 10);
  68. }
  69. headRequest.destroy();
  70. } catch (error) {
  71. Logger.warn(TAG, `HEAD请求获取文件大小失败,将使用分片下载: ${(error as Error).message}`);
  72. }
  73. }
  74. await downloadAudioStationFileChunked(streamUrl, cachePath, finalFileSize);
  75. Logger.info(TAG, `AudioStation 文件下载完成: ${cachePath}`);
  76. } catch (error) {
  77. await FileManager.deleteFile(cachePath);
  78. const err = error as Error;
  79. Logger.error(TAG, `AudioStation 文件下载失败: ${err.message}`);
  80. throw err;
  81. }
  82. }
  83. return cachePath;
  84. }
  85. async function downloadAudioStationFileChunked(streamUrl: string, localPath: string, totalSize: number): Promise<void> {
  86. const file = fileIo.openSync(localPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC);
  87. try {
  88. if (totalSize === 0) {
  89. await downloadSingleAudioStationChunk(streamUrl, 0, -1, file);
  90. } else {
  91. let downloadedSize: number = 0;
  92. while (downloadedSize < totalSize) {
  93. const rangeStart = downloadedSize;
  94. const rangeEnd = Math.min(downloadedSize + CHUNK_SIZE - 1, totalSize - 1);
  95. await downloadSingleAudioStationChunk(streamUrl, rangeStart, rangeEnd, file);
  96. downloadedSize = rangeEnd + 1;
  97. }
  98. }
  99. fileIo.closeSync(file);
  100. } finally {
  101. // file closed above
  102. }
  103. }
  104. async function downloadSingleAudioStationChunk(
  105. streamUrl: string,
  106. rangeStart: number,
  107. rangeEnd: number,
  108. file: fileIo.File
  109. ): Promise<void> {
  110. const httpRequest = http.createHttp();
  111. try {
  112. const options: http.HttpRequestOptions = {
  113. method: http.RequestMethod.GET,
  114. connectTimeout: 30000,
  115. readTimeout: 600000,
  116. expectDataType: http.HttpDataType.ARRAY_BUFFER
  117. };
  118. if (rangeEnd >= 0) {
  119. if (!options.header) {
  120. options.header = {};
  121. }
  122. options.header['Range'] = `bytes=${rangeStart}-${rangeEnd}`;
  123. }
  124. const response = await httpRequest.request(streamUrl, options);
  125. if (response.responseCode !== 200 && response.responseCode !== 206) {
  126. throw new Error(`AudioStation 下载失败 HTTP ${response.responseCode}`);
  127. }
  128. if (response.result instanceof ArrayBuffer) {
  129. const arrayBuffer = response.result as ArrayBuffer;
  130. if (!arrayBuffer || arrayBuffer.byteLength === 0) {
  131. throw new Error('AudioStation 下载的文件为空');
  132. }
  133. await fileIo.write(file.fd, arrayBuffer, {
  134. offset: rangeStart,
  135. length: arrayBuffer.byteLength
  136. });
  137. }
  138. } finally {
  139. httpRequest.destroy();
  140. }
  141. }
  142. RemoteCacheManager.registerStrategy(new AudioStationCacheStrategy());
  143. export async function ensureAudioStationFileCached(
  144. account: WebDavAccount,
  145. songId: string,
  146. streamUrl: string,
  147. fileSize?: number
  148. ): Promise<string> {
  149. return RemoteCacheManager.ensureCached(RemoteCacheType.AUDIOSTATION, {
  150. account,
  151. songId,
  152. streamUrl,
  153. fileSize
  154. } as AudioStationCacheOptions);
  155. }