Просмотр исходного кода

refactor(network): 重构远程文件缓存策略实现

- 引入 RemoteCacheManager 统一管理缓存策略
- 为 FTP 和 SMB 实现独立的缓存策略类
- 将 ensureFtpFileCached 和 ensureSmbFileCached 方法内部逻辑封装
- 注册 FTP 和 SMB 缓存策略到统一管理器
- 更新 LocalMusic 视图以使用新的缓存管理机制
- 扩展元数据更新处理以支持多种远程存储类型
- 添加远程存储路径解析方法
- 实现列表视图的延迟刷新机制
- 重命名变量以提高代码可读性
- 优化元数据应用逻辑以减少不必要的更新操作
chendeben 9 месяцев назад
Родитель
Сommit
0d2072c5a3

+ 16 - 1
entry/src/main/ets/common/network/FtpFileCache.ets

@@ -4,6 +4,7 @@ import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import Logger from '../util/Logger';
 import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache';
 import { fileIo } from '@kit.CoreFileKit';
+import { RemoteCacheManager, RemoteCacheRequest, RemoteCacheStrategy } from './RemoteCacheManager';
 
 const TAG = 'FtpFileCache';
 
@@ -53,7 +54,7 @@ async function downloadFtpFile(account: WebDavAccount, remotePath: string, local
   }
 }
 
-export async function ensureFtpFileCached(account: WebDavAccount, remotePath: string): Promise<string> {
+async function ensureFtpFileCachedInternal(account: WebDavAccount, remotePath: string): Promise<string> {
   const normalizedRelative = normalizeCacheRelativePath(remotePath);
   const cacheInfo = await resolveCacheFilePath(
     RemoteCacheType.FTP,
@@ -81,3 +82,17 @@ export async function ensureFtpFileCached(account: WebDavAccount, remotePath: st
   }
   return cachePath;
 }
+
+class FtpCacheStrategy implements RemoteCacheStrategy {
+  readonly type: RemoteCacheType = RemoteCacheType.FTP;
+
+  async ensure(options: RemoteCacheRequest): Promise<string> {
+    return ensureFtpFileCachedInternal(options.account, options.remotePath);
+  }
+}
+
+RemoteCacheManager.registerStrategy(new FtpCacheStrategy());
+
+export async function ensureFtpFileCached(account: WebDavAccount, remotePath: string): Promise<string> {
+  return RemoteCacheManager.ensureCached(RemoteCacheType.FTP, { account, remotePath });
+}

+ 32 - 0
entry/src/main/ets/common/network/RemoteCacheManager.ets

@@ -0,0 +1,32 @@
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import { RemoteCacheType } from './RemoteSongCache';
+
+export interface RemoteCacheRequest {
+  account: WebDavAccount;
+  remotePath: string;
+  fullUrl?: string;
+}
+
+export interface RemoteCacheStrategy {
+  readonly type: RemoteCacheType;
+  ensure(options: RemoteCacheRequest): Promise<string>;
+}
+
+export class RemoteCacheManager {
+  private static strategies: Map<RemoteCacheType, RemoteCacheStrategy> = new Map();
+
+  static registerStrategy(strategy: RemoteCacheStrategy): void {
+    if (!strategy || !strategy.type) {
+      throw new Error('Invalid cache strategy');
+    }
+    RemoteCacheManager.strategies.set(strategy.type, strategy);
+  }
+
+  static async ensureCached(type: RemoteCacheType, options: RemoteCacheRequest): Promise<string> {
+    const strategy = RemoteCacheManager.strategies.get(type);
+    if (!strategy) {
+      throw new Error(`No cache strategy registered for type ${type}`);
+    }
+    return strategy.ensure(options);
+  }
+}

+ 3 - 0
entry/src/main/ets/common/network/RemoteCacheRegistry.ets

@@ -0,0 +1,3 @@
+// 触发各协议策略注册的集中入口
+import './SmbFileCache';
+import './FtpFileCache';

+ 16 - 1
entry/src/main/ets/common/network/SmbFileCache.ets

@@ -4,6 +4,7 @@ import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import Logger from '../util/Logger';
 import nativeBridge from 'libentry.so';
 import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache';
+import { RemoteCacheManager, RemoteCacheRequest, RemoteCacheStrategy } from './RemoteCacheManager';
 
 interface SmbDownloadBinding {
   downloadSmbFile(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, localPath: string): void;
@@ -79,7 +80,7 @@ function shouldRetryRemoteDownload(error: Error): boolean {
     message.indexOf('no such file') >= 0;
 }
 
-export async function ensureSmbFileCached(account: WebDavAccount, relativePath: string): Promise<string> {
+async function ensureSmbFileCachedInternal(account: WebDavAccount, relativePath: string): Promise<string> {
   const manager = RemoteDriveManager.getInstance();
   if (!manager.context) {
     throw new Error('App context is not initialized');
@@ -140,3 +141,17 @@ export async function ensureSmbFileCached(account: WebDavAccount, relativePath:
   }
   return localPath;
 }
+
+class SmbCacheStrategy implements RemoteCacheStrategy {
+  readonly type: RemoteCacheType = RemoteCacheType.SMB;
+
+  async ensure(options: RemoteCacheRequest): Promise<string> {
+    return ensureSmbFileCachedInternal(options.account, options.remotePath);
+  }
+}
+
+RemoteCacheManager.registerStrategy(new SmbCacheStrategy());
+
+export async function ensureSmbFileCached(account: WebDavAccount, relativePath: string): Promise<string> {
+  return RemoteCacheManager.ensureCached(RemoteCacheType.SMB, { account, remotePath: relativePath });
+}

+ 61 - 15
entry/src/main/ets/view/LocalMusic.ets

@@ -80,8 +80,9 @@ import { ringtone } from '@kit.RingtoneKit';
 import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem';
 import { resourceManager } from '@kit.LocalizationKit';
 import { deviceInfo } from '@kit.BasicServicesKit';
-import { ensureSmbFileCached } from '../common/network/SmbFileCache';
-import { ensureFtpFileCached } from '../common/network/FtpFileCache';
+import { RemoteCacheManager } from '../common/network/RemoteCacheManager';
+import '../common/network/RemoteCacheRegistry';
+import { RemoteCacheType } from '../common/network/RemoteSongCache';
 import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
 
@@ -521,7 +522,10 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
         throw new Error('SMB账号不可用');
       }
       const relativePath = extractSmbRelativePath(song, account.smbShare);
-      const cachedPath = await ensureSmbFileCached(account, relativePath);
+      const cachedPath = await RemoteCacheManager.ensureCached(RemoteCacheType.SMB, {
+        account,
+        remotePath: relativePath
+      });
       Logger.info(TAG, `SMB 缓存路径: ${cachedPath}`);
       scheduleMetadataExtractionFromCache(song, cachedPath, metadataOptions);
       return cachedPath;
@@ -540,7 +544,10 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
         throw new Error('FTP账号不可用');
       }
       const relativePath = extractFtpRelativePath(song);
-      const cachedPath = await ensureFtpFileCached(account, relativePath);
+      const cachedPath = await RemoteCacheManager.ensureCached(RemoteCacheType.FTP, {
+        account,
+        remotePath: relativePath
+      });
       Logger.info(TAG, `FTP 缓存路径: ${cachedPath}`);
       scheduleMetadataExtractionFromCache(song, cachedPath, metadataOptions);
       return cachedPath;
@@ -1504,27 +1511,34 @@ export struct LocalMusic {
 
   private applyMetadataToCollections(filePath: string, payload: WorkerMetadataPayload): void {
     const targets: Array<VideoItem> = [];
+    let mainListUpdated = false;
+    let playlistUpdated = false;
     if (this.currentSong && this.currentSong.filePath === filePath) {
       targets.push(this.currentSong);
     }
-    const collectFromList = (list?: Array<VideoItem>) => {
+    const collectFromList = (list?: Array<VideoItem>, markUpdated?: () => void) => {
       if (!list || list.length === 0) {
         return;
       }
       const found = Utility.getItemByFilePath(list, filePath);
       if (found) {
+        markUpdated?.();
         targets.push(found);
       }
     };
-    collectFromList(this.songList);
-    collectFromList(this.videoLocalList);
+    collectFromList(this.videoLocalList, () => {
+      mainListUpdated = true;
+    });
+    collectFromList(this.songList, () => {
+      playlistUpdated = true;
+    });
     collectFromList(this.historyList);
     collectFromList(this.favList);
     const remoteManager = RemoteDriveManager.getInstance();
     collectFromList(remoteManager.webDavSongs);
 
     const updated = new Set<VideoItem>();
-    const webDavUpdates: Map<string, WebDavMetadataUpdatePayload> = new Map();
+    const remoteMetadataUpdates: Map<string, WebDavMetadataUpdatePayload> = new Map();
     for (let i = 0; i < targets.length; i++) {
       const target = targets[i];
       if (!target || updated.has(target)) {
@@ -1532,8 +1546,8 @@ export struct LocalMusic {
       }
       updated.add(target);
       this.mergeVideoItemWithPayload(target, payload);
-      if (target.type === CommonConstants.TYPE_WEBDAV && StrUtil.isNotEmpty(target.filePath)) {
-        webDavUpdates.set(target.filePath, {
+      if (isRemoteCloudType(target.type) && StrUtil.isNotEmpty(target.filePath)) {
+        remoteMetadataUpdates.set(target.filePath, {
           filePath: target.filePath,
           pixelMapPath: target.pixelMapPath,
           name: target.name,
@@ -1567,10 +1581,12 @@ export struct LocalMusic {
       });
     }
 
-    if (webDavUpdates.size > 0) {
+    if (remoteMetadataUpdates.size > 0) {
       const eventUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
-      emitter.emit(eventUpdate, { data: Array.from(webDavUpdates.values()) });
+      emitter.emit(eventUpdate, { data: Array.from(remoteMetadataUpdates.values()) });
     }
+
+    this.refreshLazyListViews(mainListUpdated, playlistUpdated);
   }
 
   private handleEditMusicResult(result: WorkerEditMusicResult): void {
@@ -1779,15 +1795,32 @@ export struct LocalMusic {
     return path.substring(0, lastSlash);
   }
 
+  private resolveRemoteStoragePath(target: VideoItem): string | null {
+    if (!target || !isRemoteCloudType(target.type)) {
+      return null;
+    }
+    if (isWebDavType(target.type)) {
+      const storagePath = target.remote_rel_path || WebDavUrlUtil.toStoragePath(target.filePath);
+      if (storagePath) {
+        target.remote_rel_path = storagePath;
+        return storagePath;
+      }
+      return null;
+    }
+    if (target.remote_rel_path && target.remote_rel_path.length > 0) {
+      return target.remote_rel_path;
+    }
+    return null;
+  }
+
   private async persistRemoteMetadataToDb(target: VideoItem): Promise<void> {
-    if (!this.context || !target || !isWebDavType(target.type)) {
+    if (!this.context || !target || !isRemoteCloudType(target.type)) {
       return;
     }
-    const storagePath = target.remote_rel_path || WebDavUrlUtil.toStoragePath(target.filePath);
+    const storagePath = this.resolveRemoteStoragePath(target);
     if (!storagePath) {
       return;
     }
-    target.remote_rel_path = target.remote_rel_path || storagePath;
     const table = new MediaTable(this.context);
     await new Promise<void>((resolve) => {
       table.getRdbStore(this.context as Context, () => resolve());
@@ -8040,6 +8073,19 @@ export struct LocalMusic {
     this.listRefreshKey++
   }
 
+  private refreshLazyListViews(mainListUpdated: boolean, playlistUpdated: boolean): void {
+    if (!mainListUpdated && !playlistUpdated) {
+      return;
+    }
+    this.listRefreshKey++;
+    if (mainListUpdated) {
+      this.dataSource.notifyDataReload();
+    }
+    if (playlistUpdated) {
+      this.sonDataSource.notifyDataReload();
+    }
+  }
+
   @Builder
   detailSheet(item:VideoItem) {
     Scroll() {