Explorar el Código

对应网盘的webdav和smb和百度网盘 歌词可以获取内嵌歌词和同名lrc进行处理

onecold hace 5 meses
padre
commit
347468d390

+ 89 - 0
entry/src/main/ets/common/util/RemoteMediaProbeUtil.ets

@@ -0,0 +1,89 @@
+import { StrUtil } from '@pura/harmony-utils';
+
+export interface RemoteFfprobeResult {
+  format?: RemoteFfprobeFormat;
+}
+
+export interface RemoteFfprobeFormat {
+  tags?: Record<string, string>;
+}
+
+export class RemoteMediaProbeUtil {
+  public static parseHeadersJson(headersJson: string): Record<string, string> {
+    const headersRecord: Record<string, string> = {};
+    if (StrUtil.isEmpty(headersJson)) {
+      return headersRecord;
+    }
+    try {
+      const parsed: Record<string, string> = JSON.parse(headersJson) as Record<string, string>;
+      const headerKeys: string[] = Object.keys(parsed);
+      for (let index: number = 0; index < headerKeys.length; index += 1) {
+        const key: string = headerKeys[index];
+        const value: string = parsed[key];
+        if (StrUtil.isEmpty(key) || StrUtil.isEmpty(value)) {
+          continue;
+        }
+        headersRecord[key] = value;
+      }
+    } catch (_error) {
+      return {};
+    }
+    return headersRecord;
+  }
+
+  public static buildFfprobeHeaderBlock(headersJson: string): string {
+    const headersRecord: Record<string, string> = RemoteMediaProbeUtil.parseHeadersJson(headersJson);
+    const headerKeys: string[] = Object.keys(headersRecord);
+    if (headerKeys.length <= 0) {
+      return '';
+    }
+    const lines: string[] = [];
+    for (let index: number = 0; index < headerKeys.length; index += 1) {
+      const key: string = headerKeys[index];
+      const value: string = headersRecord[key];
+      lines.push(`${key}: ${value}`);
+    }
+    return `${lines.join('\r\n')}\r\n`;
+  }
+
+  public static extractLyricFromTags(tags: Record<string, string> | undefined): string {
+    if (tags === undefined) {
+      return '';
+    }
+    const directKeys: string[] = ['LYRICS', 'lyrics', 'USLT', 'UNSYNCEDLYRICS', 'unsyncedlyrics', 'LYRIC', 'lyric'];
+    for (let index: number = 0; index < directKeys.length; index += 1) {
+      const value: string = tags[directKeys[index]] ?? '';
+      if (StrUtil.isNotEmpty(value)) {
+        return value.trim();
+      }
+    }
+    const tagKeys: string[] = Object.keys(tags);
+    for (let index: number = 0; index < tagKeys.length; index += 1) {
+      const key: string = tagKeys[index];
+      if (StrUtil.isEmpty(key)) {
+        continue;
+      }
+      if (key.toLowerCase().indexOf('lyric') < 0) {
+        continue;
+      }
+      const value: string = tags[key] ?? '';
+      if (StrUtil.isNotEmpty(value)) {
+        return value.trim();
+      }
+    }
+    return '';
+  }
+
+  public static extractLyricFromFfprobeJson(ffprobeJson: string): string {
+    if (StrUtil.isEmpty(ffprobeJson)) {
+      return '';
+    }
+    try {
+      const ffprobeResult: RemoteFfprobeResult = JSON.parse(ffprobeJson) as RemoteFfprobeResult;
+      return RemoteMediaProbeUtil.extractLyricFromTags(ffprobeResult.format?.tags);
+    } catch (_error) {
+      return '';
+    }
+  }
+}
+

+ 21 - 3
entry/src/main/ets/view/LocalMusic.ets

@@ -113,6 +113,7 @@ import { deviceInfo } from '@kit.BasicServicesKit';
 import '../common/network/RemoteCacheRegistry';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { lyricService, SongData } from '../common/service/LyricService';
+import { shouldResolveRemoteLyricOnPlay, tryResolveRemotePlaybackLyric } from '../common/util/RemotePlaybackLyricUtil';
 import {
   cloneVideoItem,
   preloadNextSongIfNeeded,
@@ -11116,6 +11117,7 @@ export struct LocalMusic {
   @State lyricContent: string = ''
   @State isDebug: boolean = false
   @State isHightLightCenter: boolean = true
+
   /**
    * 初始化歌词加载与展示逻辑
    * @param lyricPath 歌词文件路径(支持普通.lrc和加密.lrcc)
@@ -11202,7 +11204,23 @@ export struct LocalMusic {
       return
     }
 
-    // 2. 尝试从服务器获取歌词 (Navidrome/Jellyfin/Emby/AudioStation/Plex)
+    // 2. 远程播放实时探测歌词: 优先探测内嵌歌词,失败后查找同路径同名 .lrc
+    if (this.currentSong && shouldResolveRemoteLyricOnPlay(this.currentSong) && !isOnLineAndToast && !isLocal) {
+      const remoteLyric: string = await tryResolveRemotePlaybackLyric({
+        song: this.currentSong,
+        playbackUrl: this.videoUrl,
+        manager: RemoteDriveManager.getInstance(),
+        detectEncoding: (data: ArrayBuffer): string => this.detect(data)
+      });
+      if (StrUtil.isNotEmpty(remoteLyric)) {
+        this.currentSong.lyricContent = remoteLyric;
+        this.setLyricToControllers(remoteLyric);
+        void this.persistRemoteMetadataToDb(this.currentSong);
+        return;
+      }
+    }
+
+    // 3. 尝试从服务器获取歌词 (Navidrome/Jellyfin/Emby/AudioStation/Plex)
     if (this.currentSong && (isJellyfinType(this.currentSong.type)||isNavidromeType(this.currentSong.type)
       || isEmbyType(this.currentSong.type) || isAudioStationType(this.currentSong.type)
       || isPlexType(this.currentSong.type))) {
@@ -11242,14 +11260,14 @@ export struct LocalMusic {
       }
     }
 
-    // 3. 尝试在线歌词API
+    // 4. 尝试在线歌词API
     if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath))
       && !Utility.isVideoByExtension(this.videoUrl))) {
       this.fetchOnlineLyric(lyricPath, isOnLineAndToast || false, isApi2);
       return;
     }
 
-    // 4. 从本地文件读取歌词
+    // 5. 从本地文件读取歌词
     await this.loadLyricFromFile(lyricPath, isLocal || false);
   }
 

+ 8 - 28
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -3591,8 +3591,6 @@ export struct RemoteMusicPage {
     .width('94%')
     .height('100%')
     .editMode(true)
-    .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
-      .animation({ duration: 500, curve: Curve.Ease }))
     .padding({bottom:this.bottomSafeHeight+this.bottomSafeHeight+52})
     .reuseId('grid_item')
     .layoutWeight(1)
@@ -3753,9 +3751,7 @@ export struct RemoteMusicPage {
         }
         .width(this.getGridWight())
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(item.pixelMapPath)?$r('app.media.alt'):item.pixelMapPath)
-        .backgroundImageSize({ height: '100%', width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
 
@@ -3891,9 +3887,7 @@ export struct RemoteMusicPage {
         }
         .width(this.getGridWight())
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(artist.coverUrl) ? $r('app.media.nocover') : artist.coverUrl)
-        .backgroundImageSize({ height: '100%', width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
       }
@@ -3977,9 +3971,7 @@ export struct RemoteMusicPage {
         }
         .width(this.getGridWight())
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(album.coverUrl) ? $r('app.media.nocover') : album.coverUrl)
-        .backgroundImageSize({ height: '100%', width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
       }
@@ -4063,9 +4055,7 @@ export struct RemoteMusicPage {
         }
         .width(this.getGridWight())
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(playlist.coverUrl) ? $r('app.media.nocover') : playlist.coverUrl)
-        .backgroundImageSize({ height: '100%', width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
       }
@@ -4273,8 +4263,6 @@ export struct RemoteMusicPage {
         .rowsGap(16) // 行间距
         .cachedCount(6)
         .padding({bottom:this.bottomSafeHeight+52})
-        .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
-          .animation({ duration: 500, curve: Curve.Ease }))
         .scrollBar(BarState.Off)
         .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
         .width('90%')
@@ -4470,9 +4458,7 @@ export struct RemoteMusicPage {
         }
         .width('100%')
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.alt') : item.pixelMapPath)
-        .backgroundImageSize({ height: '100%', width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
       }
     }
@@ -4539,9 +4525,7 @@ export struct RemoteMusicPage {
         }
         .width('100%')
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(artist.coverUrl) ? $r('app.media.dir_alt') : artist.coverUrl)
-        .backgroundImageSize({  width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
       }
@@ -4608,9 +4592,7 @@ export struct RemoteMusicPage {
         .width('100%')
         .height('auto')
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(album.coverUrl) ? $r('app.media.dir_alt') : album.coverUrl)
-        .backgroundImageSize({  width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
 
       }
@@ -4676,9 +4658,7 @@ export struct RemoteMusicPage {
         }
         .width('100%')
         .borderRadius(12)
-        .backgroundImage(StrUtil.isEmpty(playlist.coverUrl) ? $r('app.media.dir_alt') : playlist.coverUrl)
-        .backgroundImageSize({  width: '100%' })
-        .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+        .backgroundColor(this.isDarkMode ? '#141414' : '#F7F7F7')
         .animation({ curve: Curve.Sharp, duration: 300 })
       }
     }