Parcourir la source

新增文件缓存

chendeben il y a 9 mois
Parent
commit
82703623ad

+ 122 - 0
entry/src/main/ets/common/network/RemoteSongCache.ets

@@ -0,0 +1,122 @@
+import { MD5 } from '@pura/harmony-utils';
+import { RemoteDriveManager } from '../util/RemoteDriveManager';
+import FileManager, { merge2paths } from '../util/FileManager';
+import Logger from '../util/Logger';
+import { fileIo } from '@kit.CoreFileKit';
+import { common } from '@kit.AbilityKit';
+
+const TAG = 'RemoteSongCache';
+const CACHE_ROOT_DIR = 'remote_cache';
+
+export enum RemoteCacheType {
+  WEBDAV = 'webdav',
+  SMB = 'smb'
+}
+
+export interface CachePathInfo {
+  cacheDir: string;
+  cachePath: string;
+  normalizedRelative: string;
+}
+
+function ensureContext(): common.Context {
+  const manager = RemoteDriveManager.getInstance();
+  if (!manager.context) {
+    throw new Error('应用上下文未初始化,无法访问缓存目录');
+  }
+  return manager.context;
+}
+
+async function ensureCacheRoot(): Promise<string> {
+  const context = ensureContext();
+  const baseDir = context.filesDir ?? context.cacheDir;
+  if (!baseDir) {
+    throw new Error('未找到可用的缓存根目录');
+  }
+  const cacheRoot = merge2paths(baseDir, CACHE_ROOT_DIR);
+  await FileManager.createDir(cacheRoot);
+  return cacheRoot;
+}
+
+async function ensureTypeDir(type: RemoteCacheType): Promise<string> {
+  const root = await ensureCacheRoot();
+  const typeDir = merge2paths(root, type);
+  await FileManager.createDir(typeDir);
+  return typeDir;
+}
+
+async function ensureAccountDir(type: RemoteCacheType, accountId?: string | number): Promise<string> {
+  const typeDir = await ensureTypeDir(type);
+  const accountDir = merge2paths(typeDir, accountId ? accountId.toString() : 'default');
+  await FileManager.createDir(accountDir);
+  return accountDir;
+}
+
+export function normalizeCacheRelativePath(relativePath: string): string {
+  if (!relativePath || relativePath.length === 0) {
+    return '/';
+  }
+  let normalized = relativePath.replace(/\\/g, '/').replace(/\/+/g, '/');
+  if (!normalized.startsWith('/')) {
+    normalized = `/${normalized}`;
+  }
+  if (normalized.length > 1 && normalized.endsWith('/')) {
+    normalized = normalized.slice(0, -1);
+  }
+  return normalized || '/';
+}
+
+async function buildCacheFileName(relativePath: string): Promise<string> {
+  const hash = await MD5.digestSync(relativePath ?? '');
+  const lastSlash = relativePath.lastIndexOf('/');
+  const originalName = lastSlash >= 0 ? relativePath.substring(lastSlash + 1) : relativePath;
+  const safeName = originalName ? originalName.replace(/[^a-zA-Z0-9_.-]/g, '_') : 'remote';
+  return `${hash}_${safeName}`;
+}
+
+export async function resolveCacheFilePath(
+  type: RemoteCacheType,
+  accountId: string | number | undefined,
+  relativePath: string
+): Promise<CachePathInfo> {
+  const accountDir = await ensureAccountDir(type, accountId);
+  const normalizedRelative = normalizeCacheRelativePath(relativePath);
+  const cacheFileName = await buildCacheFileName(normalizedRelative);
+  const cachePath = merge2paths(accountDir, cacheFileName);
+  return {
+    cacheDir: accountDir,
+    cachePath,
+    normalizedRelative
+  };
+}
+
+export async function findExistingCacheFile(
+  type: RemoteCacheType,
+  accountId: string | number | undefined,
+  relativePath: string
+): Promise<string | null> {
+  const pathInfo = await resolveCacheFilePath(type, accountId, relativePath);
+  const cachePath = pathInfo.cachePath;
+  const exists = await FileManager.isExist(cachePath);
+  if (!exists) {
+    return null;
+  }
+  const size = await FileManager.getFileSize(cachePath);
+  if (size <= 0) {
+    await FileManager.deleteFile(cachePath);
+    return null;
+  }
+  return cachePath;
+}
+
+export async function writeBufferToFile(data: ArrayBuffer, filePath: string): Promise<void> {
+  try {
+    const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
+    await fileIo.write(file.fd, data);
+    fileIo.closeSync(file);
+  } catch (error) {
+    const err = error as Error;
+    Logger.error(TAG, `写入缓存文件失败: ${err.message}`);
+    throw err;
+  }
+}

+ 11 - 37
entry/src/main/ets/common/network/SmbFileCache.ets

@@ -1,9 +1,9 @@
-import { MD5 } from '@pura/harmony-utils';
 import { RemoteDriveManager } from '../util/RemoteDriveManager';
-import FileManager, { merge2paths } from '../util/FileManager';
+import FileManager from '../util/FileManager';
 import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import Logger from '../util/Logger';
 import nativeBridge from 'libentry.so';
+import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache';
 
 interface SmbDownloadBinding {
   downloadSmbFile(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, localPath: string): void;
@@ -11,20 +11,6 @@ interface SmbDownloadBinding {
 
 const smbBinding: SmbDownloadBinding = nativeBridge as SmbDownloadBinding;
 
-function normalizeRelativePath(relativePath: string): string {
-  if (!relativePath || relativePath.length === 0) {
-    return '/';
-  }
-  let normalized = relativePath.replace(/\\/g, '/').replace(/\/+/g, '/');
-  if (!normalized.startsWith('/')) {
-    normalized = `/${normalized}`;
-  }
-  if (normalized.length > 1 && normalized.endsWith('/')) {
-    normalized = normalized.slice(0, -1);
-  }
-  return normalized || '/';
-}
-
 function cleanShareName(name?: string): string {
   return name ? name.replace(/^\/+|\/+$/g, '') : '';
 }
@@ -36,14 +22,14 @@ function buildRemotePathCandidates(relativePath: string, account: WebDavAccount)
     if (!value || value.length === 0) {
       return;
     }
-    const normalized = normalizeRelativePath(value);
+    const normalized = normalizeCacheRelativePath(value);
     if (!seen.has(normalized)) {
       seen.add(normalized);
       ordered.push(normalized);
     }
   };
 
-  const base = normalizeRelativePath(relativePath);
+  const base = normalizeCacheRelativePath(relativePath);
   pushCandidate(base);
 
   const trimmed = base.replace(/^\/+/, '');
@@ -93,14 +79,6 @@ function shouldRetryRemoteDownload(error: Error): boolean {
     message.indexOf('no such file') >= 0;
 }
 
-async function buildCacheFileName(relativePath: string): Promise<string> {
-  const hash = await MD5.digestSync(relativePath ?? '');
-  const lastSlash = relativePath.lastIndexOf('/');
-  const originalName = lastSlash >= 0 ? relativePath.substring(lastSlash + 1) : relativePath;
-  const safeName = originalName ? originalName.replace(/[^a-zA-Z0-9_.-]/g, '_') : 'remote';
-  return `${hash}_${safeName}`;
-}
-
 export async function ensureSmbFileCached(account: WebDavAccount, relativePath: string): Promise<string> {
   const manager = RemoteDriveManager.getInstance();
   if (!manager.context) {
@@ -109,17 +87,13 @@ export async function ensureSmbFileCached(account: WebDavAccount, relativePath:
   if (!account.smbShare) {
     throw new Error('SMB account missing share name');
   }
-  const baseDir = manager.context.filesDir ?? manager.context.cacheDir;
-  if (!baseDir) {
-    throw new Error('Cache directory unavailable');
-  }
-  const cacheRoot = merge2paths(baseDir, 'smb_cache');
-  await FileManager.createDir(cacheRoot);
-  const accountDir = merge2paths(cacheRoot, account.id?.toString() ?? 'default');
-  await FileManager.createDir(accountDir);
-  const normalizedRelative = normalizeRelativePath(relativePath);
-  const cacheFileName = await buildCacheFileName(normalizedRelative);
-  const localPath = merge2paths(accountDir, cacheFileName);
+  const normalizedRelative = normalizeCacheRelativePath(relativePath);
+  const pathInfo = await resolveCacheFilePath(
+    RemoteCacheType.SMB,
+    account.id?.toString(),
+    normalizedRelative
+  );
+  const localPath = pathInfo.cachePath;
   let exists = await FileManager.isExist(localPath);
   if (exists) {
     const currentSize = await FileManager.getFileSize(localPath);

+ 151 - 0
entry/src/main/ets/common/network/WebDavFileCache.ets

@@ -0,0 +1,151 @@
+import Logger from '../util/Logger';
+import FileManager from '../util/FileManager';
+import { buffer } from '@kit.ArkTS';
+import { fileIo } from '@kit.CoreFileKit';
+import { rcp } from '@kit.RemoteCommunicationKit';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import {
+  RemoteCacheType,
+  resolveCacheFilePath,
+  findExistingCacheFile,
+  normalizeCacheRelativePath
+} from './RemoteSongCache';
+
+const TAG = 'WebDavFileCache';
+const ongoingDownloads: Map<string, Promise<string | null>> = new Map();
+
+function normalizeRelativePath(path: string): string {
+  if (!path) {
+    return '/';
+  }
+  try {
+    return normalizeCacheRelativePath(decodeURIComponent(path));
+  } catch (error) {
+    Logger.warn(TAG, `WebDAV 相对路径解码失败,继续使用原始值: ${(error as Error).message}`);
+    return normalizeCacheRelativePath(path);
+  }
+}
+
+function buildAuthHeader(account: WebDavAccount): string | undefined {
+  if (!account.account || !account.password) {
+    return undefined;
+  }
+  const credentials = `${account.account}:${account.password}`;
+  return `Basic ${buffer.from(credentials).toString('base64')}`;
+}
+
+export async function findWebDavCacheIfExists(account: WebDavAccount, relativePath: string): Promise<string | null> {
+  const normalizedRelative = normalizeRelativePath(relativePath);
+  return findExistingCacheFile(RemoteCacheType.WEBDAV, account.id?.toString(), normalizedRelative);
+}
+
+export async function triggerWebDavCacheDownload(
+  account: WebDavAccount,
+  relativePath: string,
+  fullUrl: string
+): Promise<string | null> {
+  const pathInfo = await resolveCacheFilePath(
+    RemoteCacheType.WEBDAV,
+    account.id?.toString(),
+    normalizeRelativePath(relativePath)
+  );
+  const cachePath = pathInfo.cachePath;
+  const normalizedRelative = pathInfo.normalizedRelative;
+
+  const exists = await FileManager.isExist(cachePath);
+  if (exists) {
+    const validSize = await FileManager.getFileSize(cachePath);
+    if (validSize > 0) {
+      Logger.info(TAG, `WebDAV 缓存已存在,跳过下载: ${normalizedRelative}`);
+      return null;
+    }
+    await FileManager.deleteFile(cachePath);
+  }
+
+  if (ongoingDownloads.has(cachePath)) {
+    Logger.info(TAG, `WebDAV 缓存下载进行中,跳过重复任务: ${normalizedRelative}`);
+    const existingTask = ongoingDownloads.get(cachePath);
+    if (existingTask) {
+      return existingTask;
+    }
+    return Promise.resolve(null);
+  }
+
+  const downloadTask = (async () => {
+    const headers: Record<string, string> = {
+      'User-Agent': 'TTMusic-WebDAV/1.0',
+      'Accept': '*/*'
+    };
+    const authHeader = buildAuthHeader(account);
+    if (authHeader) {
+      headers['Authorization'] = authHeader;
+    }
+    try {
+      await downloadWebDavToFile(fullUrl, authHeader, cachePath);
+      Logger.info(TAG, `WebDAV 缓存完成: ${normalizedRelative}`);
+      return cachePath;
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `WebDAV 缓存异常: ${err.message}`);
+      await FileManager.deleteFile(cachePath);
+      throw err;
+    } finally {
+      ongoingDownloads.delete(cachePath);
+    }
+  })();
+
+  ongoingDownloads.set(cachePath, downloadTask);
+  return downloadTask;
+}
+
+async function downloadWebDavToFile(
+  url: string,
+  authHeader: string | undefined,
+  cachePath: string
+): Promise<void> {
+  const fileHandle: fileIo.File = await fileIo.open(cachePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
+  const headers: rcp.RequestHeaders = {
+    'User-Agent': 'TTMusic-WebDAV/1.0',
+    'Accept': '*/*',
+    'Method': 'GET'
+  };
+  if (authHeader) {
+    headers['Authorization'] = authHeader;
+  }
+  const tracingConfig: rcp.TracingConfiguration = {
+    verbose: false,
+    infoToCollect: {
+      textual: false,
+      incomingHeader: false,
+      outgoingHeader: false,
+      incomingData: false,
+      outgoingData: false,
+    },
+    collectTimeInfo: false
+  };
+  const session = rcp.createSession({
+    requestConfiguration: { tracing: tracingConfig },
+    headers: headers
+  });
+  const streamData: rcp.WriteStream = {
+    async write(buffer: ArrayBuffer): Promise<number> {
+      await fileIo.write(fileHandle.fd, buffer);
+      return buffer.byteLength;
+    }
+  };
+  const downloadToStream: rcp.DownloadToStream = {
+    kind: 'stream',
+    stream: streamData
+  };
+  return new Promise<void>((resolve, reject) => {
+    session.downloadToStream(url, downloadToStream).then(() => {
+      fileIo.closeSync(fileHandle);
+      session.close();
+      resolve();
+    }).catch((error: Error) => {
+      fileIo.closeSync(fileHandle);
+      session.close();
+      reject(error);
+    });
+  });
+}

+ 286 - 33
entry/src/main/ets/view/LocalMusic.ets

@@ -80,6 +80,7 @@ import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSys
 import { resourceManager } from '@kit.LocalizationKit';
 import { deviceInfo } from '@kit.BasicServicesKit';
 import { ensureSmbFileCached } from '../common/network/SmbFileCache';
+import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
 
 import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker'
@@ -232,6 +233,63 @@ function extractSmbRelativePath(song: VideoItem, shareNameOverride?: string): st
   return remainder;
 }
 
+const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
+
+interface MetadataExtractionOptions {
+  context?: common.Context;
+  autoParseMusicName?: boolean;
+}
+
+interface WorkerMetadataPayload {
+  originFilePath: string;
+  originId?: string;
+  bit_rate?: string;
+  duration?: string;
+  sampleRate?: string;
+  mimeType?: string;
+  md5Str?: string;
+  videoSize?: number;
+  size?: string;
+  pixelMapPath?: string;
+  artist?: string;
+  album?: string;
+  name?: string;
+  error?: string;
+}
+
+const metadataExtractionInFlight: Set<string> = new Set();
+
+function scheduleMetadataExtractionFromCache(
+  song: VideoItem,
+  cachePath: string,
+  options?: MetadataExtractionOptions
+): void {
+  if (!song || !cachePath || !song.filePath || !options || !options.context) {
+    return;
+  }
+  if (!isRemoteCloudType(song.type ?? -1)) {
+    return;
+  }
+  if (metadataExtractionInFlight.has(song.filePath)) {
+    return;
+  }
+  if (song.bit_rate && song.sampleRate && song.duration) {
+    return;
+  }
+  metadataExtractionInFlight.add(song.filePath);
+  workerInstance.postMessage({
+    code: 5,
+    data: options.context,
+    data2: cachePath,
+    data3: song.type,
+    data4: {
+      originFilePath: song.filePath,
+      originId: song.id,
+      autoParseMusicName: options.autoParseMusicName ?? false
+    }
+  });
+}
+
 function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
   if (!rawUrl) {
     return '';
@@ -248,6 +306,60 @@ function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
   }
 }
 
+function cloneVideoItem(item: VideoItem): VideoItem {
+  const copy = new VideoItem(
+    item.name,
+    item.id,
+    item.filePath,
+    item.type,
+    item.videoSize,
+    item.cTime,
+    item.pixelMap,
+    item.size,
+    item.pixelMapPath,
+    item.artist,
+    item.album,
+    item.fileName,
+    item.lastPlayed
+  );
+  copy.parentPath = item.parentPath;
+  copy.isFav = item.isFav;
+  copy.pixelMap = item.pixelMap;
+  copy.size = item.size;
+  copy.pixelMapPath = item.pixelMapPath;
+  copy.artist = item.artist;
+  copy.album = item.album;
+  copy.duration = item.duration;
+  copy.mimeType = item.mimeType;
+  copy.sampleRate = item.sampleRate;
+  copy.trackCount = item.trackCount;
+  copy.lastPlayedStr = item.lastPlayedStr;
+  copy.playCount = item.playCount;
+  copy.lyricContent = item.lyricContent;
+  copy.md5Str = item.md5Str;
+  copy.extra_json = item.extra_json;
+  copy.pyStr = item.pyStr;
+  copy.bit_rate = item.bit_rate;
+  copy.probe_score = item.probe_score;
+  copy.year = item.year;
+  copy.nb_streams = item.nb_streams;
+  copy.nb_programs = item.nb_programs;
+  copy.genre = item.genre;
+  copy.track = item.track;
+  copy.bits_per_raw_sample = item.bits_per_raw_sample;
+  copy.channels = item.channels;
+  copy.channel_layout = item.channel_layout;
+  copy.start_time = item.start_time;
+  copy.ALBUMARTIST = item.ALBUMARTIST;
+  copy.COMPOSER = item.COMPOSER;
+  copy.LYRICIST = item.LYRICIST;
+  copy.COMMENT = item.COMMENT;
+  copy.disc = item.disc;
+  copy.webdav_account_id = item.webdav_account_id;
+  copy.remote_rel_path = item.remote_rel_path;
+  return copy;
+}
+
 /**
  * 异步设置videoUrl的辅助方法,处理WebDAV URL的构建
  * @param song 歌曲对象
@@ -259,17 +371,48 @@ function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
  * @param song 歌曲对象
  * @returns Promise<string> 完整的URL
  */
-async function setVideoUrlForSong(song: VideoItem): Promise<string> {
+async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionOptions): Promise<string> {
+  const metadataOptions = options ?? {};
   if (isWebDavType(song.type) && song.webdav_account_id) {
     try {
-      Logger.info(TAG, `WebDAV完整URL构建成功remote_rel_path: ${song.remote_rel_path}`);
-      Logger.info(TAG, `WebDAV完整URL构建成功song.filePath: ${song.filePath}`);
+      Logger.info(TAG, `WebDAV 路径信息 remote_rel_path: ${song.remote_rel_path}`);
+      Logger.info(TAG, `WebDAV 路径信息 filePath: ${song.filePath}`);
       const relativePath = song.remote_rel_path || song.filePath;
-      const fullUrl = await WebDavUrlUtil.buildFullUrlByAccountId(song.webdav_account_id, relativePath);
-      if (fullUrl) {
-        Logger.info(TAG, `WebDAV完整URL构建成功: ${fullUrl}`);
-        return sanitizePlaybackUrl(fullUrl);
+      const manager = RemoteDriveManager.getInstance();
+      const account = await manager.getWebDavAccountById(song.webdav_account_id);
+
+      if (account) {
+        const cachePath = await findWebDavCacheIfExists(account, relativePath);
+        if (cachePath) {
+          Logger.info(TAG, `WebDAV 缓存命中,直接播放: ${cachePath}`);
+          scheduleMetadataExtractionFromCache(song, cachePath, metadataOptions);
+          return cachePath;
+        }
+
+        const fullUrlFromAccount = WebDavUrlUtil.buildFullUrl(account, relativePath);
+        if (fullUrlFromAccount) {
+          const sanitizedUrl = sanitizePlaybackUrl(fullUrlFromAccount);
+          const downloadPromise = triggerWebDavCacheDownload(account, relativePath, sanitizedUrl);
+          if (downloadPromise) {
+            downloadPromise.then((completedPath?: string | null) => {
+              if (completedPath) {
+                scheduleMetadataExtractionFromCache(song, completedPath, metadataOptions);
+              }
+            }).catch((error: Error) => {
+              Logger.error(TAG, `WebDAV 缓存后台下载失败: ${error.message}`);
+            });
+          }
+          Logger.info(TAG, `WebDAV 启动异步缓存: ${relativePath}`);
+          return sanitizedUrl;
+        }
       }
+
+      const fallbackUrl = await WebDavUrlUtil.buildFullUrlByAccountId(song.webdav_account_id, relativePath);
+      if (fallbackUrl) {
+        Logger.warn(TAG, `WebDAV 使用全局查询到的URL: ${fallbackUrl}`);
+        return sanitizePlaybackUrl(fallbackUrl);
+      }
+
       Logger.error(TAG, `WebDAV URL构建失败,使用原始路径: ${relativePath}`);
       return sanitizePlaybackUrl(relativePath);
     } catch (error) {
@@ -307,6 +450,7 @@ async function setVideoUrlForSong(song: VideoItem): Promise<string> {
       const relativePath = extractSmbRelativePath(song, account.smbShare);
       const cachedPath = await ensureSmbFileCached(account, relativePath);
       Logger.info(TAG, `SMB 缓存路径: ${cachedPath}`);
+      scheduleMetadataExtractionFromCache(song, cachedPath, metadataOptions);
       return cachedPath;
     } catch (error) {
       const err = error as Error;
@@ -329,7 +473,6 @@ async function setVideoUrlForSong(song: VideoItem): Promise<string> {
   return song.filePath;
 }
 
-const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
 // 定义接口
 interface HiCarAspectRatio {
   ratio: number;
@@ -1220,31 +1363,111 @@ export struct LocalMusic {
             .slice(0, 60);
           PreferencesUtil.putSync('albumMap',  JSON.stringify(mapArrayAlbum));
           break;
+        case 105: // 缓存完成后返回的元数据
+          this.handleWorkerMetadataPayload(e.data.data as WorkerMetadataPayload);
+          break;
 
       }
     };
 
     // 在调用terminate后,执行onexit
     workerInstance.onexit = (code) => {
-      console.log("main thread terminate");
+      metadataExtractionInFlight.clear();
+      Logger.info(TAG, 'workerInstance 线程已退出,code: ' + code);
     }
-
-    //若Worker处于已销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的错误。
-    // 使用Worker模块时,需要在主线程中注册onerror接口,否则当worker线程出现异常时会发生jscrash问题。
     workerInstance.onerror = (err: ErrorEvent) => {
-
-      console.log("onerror" + err.message);
-
+      metadataExtractionInFlight.clear();
+      console.error('workerInstance 线程执行发生异常: ' + err.message)
     }
+  }
 
-    // let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
-    //碰一碰分享的监听
-    this.knockController = KnockController.getInstance(this.context);
-    this.knockController?.immersiveListening();
-
+  private handleWorkerMetadataPayload(payload: WorkerMetadataPayload | undefined): void {
+    if (!payload || !payload.originFilePath) {
+      return;
+    }
+    metadataExtractionInFlight.delete(payload.originFilePath);
+    if (payload.error) {
+      Logger.warn(TAG, `缓存文件元数据解析失败: ${payload.error}`);
+      return;
+    }
+    this.applyMetadataToCollections(payload.originFilePath, payload);
   }
 
+  private applyMetadataToCollections(filePath: string, payload: WorkerMetadataPayload): void {
+    const targets: Array<VideoItem> = [];
+    if (this.currentSong && this.currentSong.filePath === filePath) {
+      targets.push(this.currentSong);
+    }
+    const collectFromList = (list?: Array<VideoItem>) => {
+      if (!list || list.length === 0) {
+        return;
+      }
+      const found = Utility.getItemByFilePath(list, filePath);
+      if (found) {
+        targets.push(found);
+      }
+    };
+    collectFromList(this.songList);
+    collectFromList(this.videoLocalList);
+    collectFromList(this.historyList);
+    collectFromList(this.favList);
+
+    const updated = new Set<VideoItem>();
+    for (let i = 0; i < targets.length; i++) {
+      const target = targets[i];
+      if (!target || updated.has(target)) {
+        continue;
+      }
+      updated.add(target);
+      if (payload.name) {
+        target.name = payload.name;
+      }
+      if (payload.artist) {
+        target.artist = payload.artist;
+      }
+      if (payload.album) {
+        target.album = payload.album;
+      }
+      if (payload.bit_rate) {
+        target.bit_rate = payload.bit_rate;
+      }
+      if (payload.duration) {
+        target.duration = payload.duration;
+      }
+      if (payload.sampleRate) {
+        target.sampleRate = payload.sampleRate;
+      }
+      if (payload.mimeType) {
+        target.mimeType = payload.mimeType;
+      }
+      if (payload.md5Str) {
+        target.md5Str = payload.md5Str;
+      }
+      if (payload.videoSize !== undefined) {
+        target.videoSize = payload.videoSize;
+      }
+      if (payload.size) {
+        target.size = payload.size;
+      }
+      if (payload.pixelMapPath) {
+        target.pixelMapPath = payload.pixelMapPath;
+      }
+    }
 
+    if (this.currentSong && this.currentSong.filePath === filePath) {
+      if (payload.name) {
+        this.name = payload.name;
+      }
+      if (payload.artist) {
+        this.artist = payload.artist;
+      }
+      if (payload.pixelMapPath) {
+        this.cover = payload.pixelMapPath;
+      }
+      this.currentSong = cloneVideoItem(this.currentSong);
+      AppStorage.setOrCreate('currentSong', this.currentSong);
+    }
+  }
 
   onPageHide(): void {
     this.knockController?.immersiveDisableListening();
@@ -1299,7 +1522,10 @@ export struct LocalMusic {
           this.currentSong = this.songList[0]
         }
         // 使用辅助方法设置videoUrl,等待WebDAV URL构建完成
-        this.videoUrl = await setVideoUrlForSong(this.currentSong);
+        this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+          context: this.context,
+          autoParseMusicName: this.autoParseMusicName
+        });
         Logger.info(TAG, `WebDAV URL同步构建完成: ${this.videoUrl}`);
         this.name = this.currentSong.name
         this.cover = this.currentSong.pixelMapPath
@@ -6159,7 +6385,10 @@ export struct LocalMusic {
         AppStorage.setOrCreate('currentSong', this.currentSong);
 
         // 使用辅助方法设置videoUrl,等待WebDAV URL构建完成
-        this.videoUrl = await setVideoUrlForSong(this.currentSong);
+        this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+          context: this.context,
+          autoParseMusicName: this.autoParseMusicName
+        });
         Logger.info(TAG, `WebDAV URL同步构建完成(分支2): ${this.videoUrl}`);
 
         this.name = this.currentSong.name
@@ -6211,7 +6440,7 @@ export struct LocalMusic {
       onCancel:()=>{
         this.getUIContext().getPromptAction().closeCustomDialog(this.cueComponentId)
       },
-      onDoItemClick:(item: CueTrack, index: number)=>{
+      onDoItemClick: async (item: CueTrack, index: number)=>{
         //获取cueinfo对应的filePath
         let globalVideoList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL) as VideoItem[];
         this.curIndex = Utility.getCurIndexFromGlobalList(globalVideoList, cueinfo.filePath)
@@ -6224,10 +6453,16 @@ export struct LocalMusic {
           }
           this.songList = globalVideoList
           this.sonDataSource.pushArrayData(this.songList)
-          if(this.currentSong && isRemoteCloudType(this.currentSong.type!)){
-            this.videoUrl = sanitizePlaybackUrl(this.currentSong.filePath);
-          }else{
-            this.videoUrl =  this.currentSong.filePath
+          if (this.currentSong) {
+            try {
+              this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+                context: this.context,
+                autoParseMusicName: this.autoParseMusicName
+              });
+            } catch (error) {
+              Logger.error(TAG, `CUE 点播构建播放地址失败: ${(error as Error).message}`);
+              this.videoUrl = this.currentSong.filePath;
+            }
           }
           this.name = item.title||this.currentSong.name
           this.cover = this.currentSong.pixelMapPath
@@ -13425,7 +13660,10 @@ export struct LocalMusic {
       Logger.info('heanup playNext', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
 
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-      this.videoUrl = await setVideoUrlForSong(this.currentSong);
+      this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+        context: this.context,
+        autoParseMusicName: this.autoParseMusicName
+      });
 
       this.artist = this.songList[this.curIndex].artist
       this.name = this.songList[this.curIndex].name
@@ -13516,7 +13754,10 @@ export struct LocalMusic {
       this.currentSong = this.songList[this.curIndex];
 
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-      this.videoUrl = await setVideoUrlForSong(this.currentSong);
+      this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+        context: this.context,
+        autoParseMusicName: this.autoParseMusicName
+      });
       this.name = this.songList[this.curIndex].name;
       this.artist = this.songList[this.curIndex].artist
       // this.cover = this.songList[this.curIndex].pixelMapPath
@@ -13536,7 +13777,10 @@ export struct LocalMusic {
       this.currentSong = this.songList[index];
 
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-      this.videoUrl = await setVideoUrlForSong(this.currentSong);
+      this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+        context: this.context,
+        autoParseMusicName: this.autoParseMusicName
+      });
 
       this.name = this.currentSong.name
       this.artist = this.currentSong.artist
@@ -13569,7 +13813,10 @@ export struct LocalMusic {
     Logger.info('heanup playPrevious', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
 
     // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-    this.videoUrl = await setVideoUrlForSong(this.currentSong);
+    this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+      context: this.context,
+      autoParseMusicName: this.autoParseMusicName
+    });
     this.name = this.songList[this.curIndex].name
     this.artist = this.songList[this.curIndex].artist
     this.changeImageAnimation()
@@ -13589,7 +13836,10 @@ export struct LocalMusic {
       Logger.info('heanup randomModePlayFromHistory', `直接使用历史歌曲信息: ${prevSong.name}, type: ${prevSong.type}`);
 
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-      this.videoUrl = await setVideoUrlForSong(this.currentSong);
+      this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+        context: this.context,
+        autoParseMusicName: this.autoParseMusicName
+      });
       this.cover = prevSong.pixelMapPath
       this.artist = prevSong.artist
       this.name = prevSong.name;
@@ -13610,7 +13860,10 @@ export struct LocalMusic {
       Logger.info('heanup randomModePlayFromHistory', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
 
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
-      this.videoUrl = await setVideoUrlForSong(this.currentSong);
+      this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+        context: this.context,
+        autoParseMusicName: this.autoParseMusicName
+      });
       this.artist = this.songList[this.curIndex].artist
       this.name = this.songList[this.curIndex].name;
       this.changeImageAnimation()

+ 61 - 1
entry/src/main/ets/workers/Worker.ets

@@ -8,6 +8,12 @@ import { VideoItem } from '../viewmodel/VideoItem';
 
 const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
 
+interface MetadataRequestPayload {
+  originFilePath?: string;
+  originId?: string;
+  autoParseMusicName?: boolean;
+}
+
 /**
  * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
  * The event handler is executed in the worker thread.
@@ -35,6 +41,14 @@ workerPort.onmessage = async (e: MessageEvents) => {
     case 4://查询专辑列表
       queryAlbumTask(e.data.data)
       break;
+    case 5://解析缓存音频元数据
+      await analyzeCachedMediaMetadata(
+        e.data.data,
+        e.data.data2,
+        e.data.data3,
+        e.data.data4 as MetadataRequestPayload
+      );
+      break;
 
   }
 
@@ -261,6 +275,53 @@ async function queryAlbumTask(context:Context) {
   }
 }
 
+async function analyzeCachedMediaMetadata(
+  context: Context,
+  cachePath: string,
+  songType: number,
+  extra?: MetadataRequestPayload
+): Promise<void> {
+  if (!context || !cachePath) {
+    return;
+  }
+  try {
+    const videoItem = await Utility.uriGetMusicAssetsFromFile(
+      context,
+      cachePath,
+      songType ?? CommonConstants.TYPE_LOCAL,
+      extra?.autoParseMusicName ?? false
+    );
+    workerPort.postMessage({
+      code: 105,
+      data: {
+        originFilePath: extra?.originFilePath ?? cachePath,
+        originId: extra?.originId,
+        bit_rate: videoItem.bit_rate,
+        duration: videoItem.duration,
+        sampleRate: videoItem.sampleRate,
+        mimeType: videoItem.mimeType,
+        md5Str: videoItem.md5Str,
+        videoSize: videoItem.videoSize,
+        size: videoItem.size,
+        pixelMapPath: videoItem.pixelMapPath,
+        artist: videoItem.artist,
+        album: videoItem.album,
+        name: videoItem.name
+      }
+    });
+  } catch (error) {
+    const err = error as Error;
+    workerPort.postMessage({
+      code: 105,
+      data: {
+        originFilePath: extra?.originFilePath ?? cachePath,
+        originId: extra?.originId,
+        error: err.message
+      }
+    });
+  }
+}
+
 
 /**
  * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
@@ -279,4 +340,3 @@ workerPort.onmessageerror = (event: MessageEvents) => {
  */
 workerPort.onerror = (event: ErrorEvent) => {
 };
-