Sfoglia il codice sorgente

feat(util): 完善远程播放器元数据提取功能

- 添加缺失上下文检查和提前返回逻辑
- 实现元数据提取任务去重机制,避免重复执行
- 新增封面、歌词、音频信息提取选项控制
- 集成MediaTable数据库存储提取的元数据
- 添加WebDav元数据更新事件发布功能
- 实现远程存储路径解析函数支持多种协议
- 添加字符串和数字类型空值处理辅助函数
- 优化元数据应用逻辑,只在必要时更新字段
- 引入任务队列管理防止并发重复执行
chendeben 7 mesi fa
parent
commit
dece99e582
1 ha cambiato i file con 148 aggiunte e 1 eliminazioni
  1. 148 1
      entry/src/main/ets/common/util/RemotePlayerUtil.ets

+ 148 - 1
entry/src/main/ets/common/util/RemotePlayerUtil.ets

@@ -22,8 +22,13 @@ import { Song } from '../../viewmodel/Song';
 import { repairAudioMetadata } from './MusicTagUtils';
 import { ServerLogUtil } from './ServerLogUtil';
 import { CommonConstants } from '../constants/CommonConstants';
+import { Utility } from './Utility';
+import MediaTable from './MediaTable';
+import emitter from '@ohos.events.emitter';
+import { EventConstants } from '../constants/EventConstants';
 
 const TAG = 'heanup RemotePlayerUtil';
+const metadataExtractionInFlight: Map<string, Promise<void>> = new Map();
 
 
 export interface MetadataExtractionOptions {
@@ -253,12 +258,133 @@ export async function setVideoUrlForSong(
     expectedSize?: number
   ): Promise<void> => {
     try {
+      if (!opts.context) {
+        Logger.info(TAG, '缺少上下文,跳过缓存元数据解析');
+        return;
+      }
+      const shouldExtractCover = opts.extractCover !== false;
+      const shouldExtractLyric = opts.extractLyric !== false;
+      const shouldExtractAudioInfo = opts.extractAudioInfo !== false;
+      if (!shouldExtractCover && !shouldExtractLyric && !shouldExtractAudioInfo && !opts.autoParseMusicName) {
+        return;
+      }
       const size = await FileManager.getFileSize(cachePath);
       if (expectedSize && size < expectedSize * 0.95) {
         Logger.warn(TAG, `缓存文件大小不匹配,跳过元数据提取: ${cachePath}`);
         return;
       }
-      Logger.info(TAG, `从缓存提取元数据: ${cachePath}`);
+      const taskKey = `${cachePath}::${targetSong.filePath ?? targetSong.remote_rel_path ?? ''}`;
+      if (metadataExtractionInFlight.has(taskKey)) {
+        return;
+      }
+      const task = (async () => {
+        Logger.info(TAG, `从缓存提取元数据: ${cachePath}`);
+        const originName = targetSong.name || targetSong.fileName;
+        const metadataItem = await Utility.uriGetMusicAssetsFromFile(
+          opts.context as Context,
+          cachePath,
+          targetSong.type ?? CommonConstants.TYPE_LOCAL,
+          opts.autoParseMusicName ?? false,
+          originName
+        );
+
+        const applyStringIfEmpty = (value?: string, fallback?: string): string | undefined => {
+          if (value && value.length > 0) {
+            return value;
+          }
+          if (fallback && fallback.length > 0) {
+            return fallback;
+          }
+          return value;
+        };
+
+        const applyNumberIfEmpty = (value?: number, fallback?: number): number | undefined => {
+          if (value !== undefined && value !== null && value !== 0) {
+            return value;
+          }
+          if (fallback !== undefined && fallback !== null && fallback !== 0) {
+            return fallback;
+          }
+          return value;
+        };
+
+        if (shouldExtractCover && metadataItem.pixelMapPath) {
+          targetSong.pixelMapPath = metadataItem.pixelMapPath;
+        }
+        if (shouldExtractLyric && metadataItem.lyricContent) {
+          targetSong.lyricContent = metadataItem.lyricContent;
+        }
+        if (shouldExtractAudioInfo) {
+          targetSong.duration = applyStringIfEmpty(targetSong.duration, metadataItem.duration) ?? targetSong.duration;
+          targetSong.bit_rate = applyStringIfEmpty(targetSong.bit_rate, metadataItem.bit_rate) ?? targetSong.bit_rate;
+          targetSong.sampleRate = applyStringIfEmpty(targetSong.sampleRate, metadataItem.sampleRate) ?? targetSong.sampleRate;
+          targetSong.trackCount = applyStringIfEmpty(targetSong.trackCount, metadataItem.trackCount) ?? targetSong.trackCount;
+          targetSong.mimeType = applyStringIfEmpty(targetSong.mimeType, metadataItem.mimeType) ?? targetSong.mimeType;
+          targetSong.md5Str = applyStringIfEmpty(targetSong.md5Str, metadataItem.md5Str) ?? targetSong.md5Str;
+          targetSong.size = applyStringIfEmpty(targetSong.size, metadataItem.size) ?? targetSong.size;
+          targetSong.videoSize = applyNumberIfEmpty(targetSong.videoSize, metadataItem.videoSize) ?? targetSong.videoSize;
+          targetSong.bits_per_raw_sample = applyStringIfEmpty(targetSong.bits_per_raw_sample, metadataItem.bits_per_raw_sample)
+            ?? targetSong.bits_per_raw_sample;
+          targetSong.channels = applyStringIfEmpty(targetSong.channels, metadataItem.channels) ?? targetSong.channels;
+          targetSong.channel_layout = applyStringIfEmpty(targetSong.channel_layout, metadataItem.channel_layout) ?? targetSong.channel_layout;
+          targetSong.start_time = applyStringIfEmpty(targetSong.start_time, metadataItem.start_time) ?? targetSong.start_time;
+          targetSong.genre = applyStringIfEmpty(targetSong.genre, metadataItem.genre) ?? targetSong.genre;
+          targetSong.year = applyStringIfEmpty(targetSong.year, metadataItem.year) ?? targetSong.year;
+          targetSong.probe_score = applyNumberIfEmpty(targetSong.probe_score, metadataItem.probe_score) ?? targetSong.probe_score;
+          targetSong.track = applyStringIfEmpty(targetSong.track, metadataItem.track) ?? targetSong.track;
+          targetSong.disc = applyStringIfEmpty(targetSong.disc, metadataItem.disc) ?? targetSong.disc;
+          targetSong.ALBUMARTIST = applyStringIfEmpty(targetSong.ALBUMARTIST, metadataItem.ALBUMARTIST) ?? targetSong.ALBUMARTIST;
+          targetSong.COMPOSER = applyStringIfEmpty(targetSong.COMPOSER, metadataItem.COMPOSER) ?? targetSong.COMPOSER;
+          targetSong.COMMENT = applyStringIfEmpty(targetSong.COMMENT, metadataItem.COMMENT) ?? targetSong.COMMENT;
+          targetSong.LYRICIST = applyStringIfEmpty(targetSong.LYRICIST, metadataItem.LYRICIST) ?? targetSong.LYRICIST;
+          targetSong.nb_streams = applyNumberIfEmpty(targetSong.nb_streams, metadataItem.nb_streams) ?? targetSong.nb_streams;
+          targetSong.nb_programs = applyNumberIfEmpty(targetSong.nb_programs, metadataItem.nb_programs) ?? targetSong.nb_programs;
+
+          if (!targetSong.name || targetSong.name.length === 0) {
+            targetSong.name = metadataItem.name || targetSong.name;
+          }
+          if (!targetSong.artist || targetSong.artist.length === 0) {
+            targetSong.artist = metadataItem.artist || targetSong.artist;
+          }
+          if (!targetSong.album || targetSong.album.length === 0) {
+            targetSong.album = metadataItem.album || targetSong.album;
+          }
+        }
+
+        const storagePath = resolveRemoteStoragePathForMetadata(targetSong);
+        if (storagePath) {
+          const dbItem = cloneVideoItem(targetSong);
+          dbItem.filePath = storagePath;
+          dbItem.remote_rel_path = storagePath;
+          if (!dbItem.parentPath && storagePath.includes('/')) {
+            dbItem.parentPath = storagePath.substring(0, storagePath.lastIndexOf('/'));
+          }
+          const table = new MediaTable(opts.context as Context);
+          await new Promise<void>((resolve, reject) => {
+            table.getRdbStore(opts.context as Context, (err: Error) => {
+              err ? reject(err) : resolve();
+            });
+          });
+          await table.saveOrUpdateWebDavItem(dbItem);
+        }
+
+        if (targetSong.filePath && (targetSong.pixelMapPath || targetSong.name || targetSong.artist)) {
+          const payload: WebDavMetadataUpdatePayload = {
+            filePath: targetSong.filePath,
+            pixelMapPath: targetSong.pixelMapPath,
+            name: targetSong.name,
+            artist: targetSong.artist
+          };
+          const eventUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
+          emitter.emit(eventUpdate, { data: [payload] });
+        }
+      })();
+      metadataExtractionInFlight.set(taskKey, task);
+      try {
+        await task;
+      } finally {
+        metadataExtractionInFlight.delete(taskKey);
+      }
     } catch (error) {
       const err = error as Error;
       Logger.warn(TAG, `从缓存提取元数据失败: ${err.message}`);
@@ -710,6 +836,27 @@ export function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
   }
 }
 
+function resolveRemoteStoragePathForMetadata(item: VideoItem): string | null {
+  if (!item) {
+    return null;
+  }
+  if (isWebDavType(item.type)) {
+    return item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath) || null;
+  }
+  if (isSmbType(item.type)) {
+    const relative = item.remote_rel_path || extractSmbRelativePath(item);
+    return relative && relative.length > 0 ? relative : null;
+  }
+  if (isFtpType(item.type)) {
+    const relative = item.remote_rel_path || extractFtpRelativePath(item);
+    return relative && relative.length > 0 ? relative : null;
+  }
+  if (isNavidromeType(item.type) || isJellyfinType(item.type) || isEmbyType(item.type) || isBaiduType(item.type)) {
+    return item.remote_rel_path || item.id || item.filePath || null;
+  }
+  return item.filePath || null;
+}
+
 
 //排序类型
 export function getTypeOrder(type: number) {