Răsfoiți Sursa

Merge remote-tracking branch 'origin/master'

chendeben 7 luni în urmă
părinte
comite
d91c2ee138

+ 3 - 2
entry/src/main/ets/common/constants/EventConstants.ets

@@ -28,8 +28,9 @@ export class EventConstants {
    * UI交互事件
    */
   // SwipeBack状态更新事件
-  static readonly EVENT_SWIPE_BACK_UPDATE: number = 888;
-
+  static readonly EVENT_SWIPE_BACK_UPDATE: number = 888;//本地音乐的手势返回通知
+  static readonly EVENT_SWIPE_BACK_NAVID: number = 889;//Navidrome的手势返回通知
+  static readonly EVENT_SWIPE_BACK_DISK: number = 890;//网盘的手势返回通知
   /**
    * 用户相关事件
    */

+ 76 - 0
entry/src/main/ets/common/network/JellyfinApi.ets

@@ -73,6 +73,15 @@ interface JellyfinMediaSource {
   MediaStreams?: Array<JellyfinMediaStream>;
 }
 
+interface JellyfinLyricLine {
+  Text?: string;
+  Start?: number;
+}
+
+interface JellyfinLyricData {
+  Lyrics?: JellyfinLyricLine[];
+}
+
 interface JellyfinAuthRequestBody {
   Username: string;
   Pw: string;
@@ -409,6 +418,73 @@ export class JellyfinApi {
     return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Primary?fillHeight=${height}&fillWidth=${width}&quality=90&api_key=${apiKey}`;
   }
 
+  /**
+   * 获取歌词
+   * GET: /Audio/{id}/Lyrics
+   * @param account WebDavAccount账号信息
+   * @param itemId 歌曲ID
+   * @returns 歌词文本,如果获取失败返回空字符串
+   */
+  async getLyric(account: WebDavAccount, itemId: string): Promise<string> {
+    const httpRequest = http.createHttp();
+    try {
+      const auth = await this.ensureAuth(account);
+      const baseUrl = this.buildBaseUrl(account);
+      const url = `${baseUrl}/Audio/${encodeURIComponent(itemId)}/Lyrics`;
+
+      void ServerLogUtil.info(TAG, `获取Jellyfin歌词: ${url}`);
+
+      const response = await httpRequest.request(url, {
+        method: http.RequestMethod.GET,
+        connectTimeout: 10000,
+        readTimeout: 15000,
+        expectDataType: http.HttpDataType.STRING,
+        header: this.buildAuthHeaderObject(auth)
+      });
+
+      if (response.responseCode < 200 || response.responseCode >= 300) {
+        void ServerLogUtil.error(TAG, `获取Jellyfin歌词失败 code=${response.responseCode}`);
+        return '';
+      }
+
+      void ServerLogUtil.info(TAG, `获取Jellyfin歌词成功 code=${response.responseCode}`);
+      const lyricData = JSON.parse(response.result as string) as JellyfinLyricData;
+      // 转换为标准 LRC 格式
+      return this.convertJellyfinLyricToLrc(lyricData);
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `获取Jellyfin歌词异常: ${err.message}`);
+      return '';
+    } finally {
+      httpRequest.destroy();
+    }
+  }
+
+  /**
+   * 将Jellyfin歌词数据转换为LRC格式
+   * @param lyricData Jellyfin歌词数据
+   * @returns LRC格式歌词字符串
+   */
+  private convertJellyfinLyricToLrc(lyricData: JellyfinLyricData): string {
+    if (!lyricData || !lyricData.Lyrics || !Array.isArray(lyricData.Lyrics)) {
+      return '';
+    }
+    const lines: string[] = [];
+    for (let i = 0; i < lyricData.Lyrics.length; i++) {
+      const lyricLine = lyricData.Lyrics[i];
+      if (lyricLine.Text && lyricLine.Start !== undefined) {
+        // 将纳秒转换为毫秒,再转换为秒
+        const milliseconds = Math.floor(lyricLine.Start / 1000000); // 纳秒转毫秒
+        const seconds = milliseconds / 1000; // 毫秒转秒
+        const minutes = Math.floor(seconds / 60);
+        const remainingSeconds = (seconds % 60).toFixed(2);
+        const timeTag = `[${String(minutes).padStart(2, '0')}:${remainingSeconds.padStart(5, '0')}]`;
+        lines.push(`${timeTag}${lyricLine.Text}`);
+      }
+    }
+    return lines.join('\n');
+  }
+
   async getAuthHeaders(account: WebDavAccount): Promise<Map<string, string>> {
     const auth = await this.ensureAuth(account);
     return this.buildAuthHeaderMap(auth);

+ 36 - 12
entry/src/main/ets/common/network/NavidromeApi.ets

@@ -298,22 +298,46 @@ export class NavidromeApi {
   }
 
   async buildCoverArtUrl(account: WebDavAccount, coverId: string | undefined, size?: number): Promise<string | undefined> {
+    void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 开始 - coverId: ${coverId}, size: ${size}`);
+
     if (!coverId || coverId.trim().length === 0) {
+      void ServerLogUtil.warn(TAG, `[buildCoverArtUrl] coverId为空,返回undefined`);
       return undefined;
     }
-    const baseUrl = this.buildBaseUrl(account);
-    const params: QueryParam[] = [];
-    params.push(new QueryParam('id', coverId));
-    if (size && size > 0) {
-      params.push(new QueryParam('size', size.toString()));
+
+    try {
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 构建baseUrl`);
+      const baseUrl = this.buildBaseUrl(account);
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] baseUrl构建完成: ${baseUrl}`);
+
+      const params: QueryParam[] = [];
+      params.push(new QueryParam('id', coverId));
+
+      if (size && size > 0) {
+        params.push(new QueryParam('size', size.toString()));
+      }
+
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 构建认证参数`);
+      this.appendParams(params, await this.buildAuthParams(account));
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 认证参数构建完成,当前参数数量: ${params.length}`);
+
+      this.appendCommonParams(params);
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 通用参数添加完成,最终参数数量: ${params.length}`);
+
+      const query = this.buildQueryString(params);
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 查询字符串构建完成: ${query}`);
+
+      const url = `${baseUrl}/getCoverArt?${query}`;
+      void ServerLogUtil.info(TAG, `[buildCoverArtUrl] ✅ 成功构建封面URL: ${url}`);
+      void ServerLogUtil.debug(TAG, `[buildCoverArtUrl] 封面参数详情: ${JSON.stringify(params)}`);
+
+      return url;
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `[buildCoverArtUrl] ❌ 构建封面URL异常: ${err.message}`);
+      void ServerLogUtil.error(TAG, `[buildCoverArtUrl] 错误堆栈: ${err.stack || '无'}`);
+      throw err;
     }
-    this.appendParams(params, await this.buildAuthParams(account));
-    this.appendCommonParams(params);
-    const query = this.buildQueryString(params);
-    const url = `${baseUrl}/getCoverArt?${query}`;
-    void ServerLogUtil.debug(TAG, `构建封面: ${url}`);
-    void ServerLogUtil.debug(TAG, `cover params: ${JSON.stringify(params)}`);
-    return url;
   }
 
   private async request(account: WebDavAccount, endpoint: string, extraParams: Array<QueryParam>): Promise<SubsonicBody> {

+ 146 - 3
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -50,6 +50,7 @@ export interface NavidromeRestSong {
   coverArtId?: string;
   coverArtPath?: string;
   embedArtPath?: string;
+  lyrics?: string;
 }
 
 export interface NavidromeRestArtist {
@@ -101,6 +102,11 @@ export interface NavidromeRestPlaylist {
   evaluatedAt?: string;
 }
 
+// 歌单API返回的歌曲数据,包含mediaFileId字段
+interface NavidromePlaylistSong extends NavidromeRestSong {
+  mediaFileId?: string;
+}
+
 export interface NavidromePagedResponse<T> {
   data: T[];
   nextStart: number | null;
@@ -187,12 +193,51 @@ export class NavidromeRestApi {
         new QueryParam('_order', 'DESC')
       ];
       const path = `/api/playlist/${playlistId}/tracks`;
-      const chunk = await this.get<NavidromeRestSong[]>(account, path, params);
+
+      // 获取原始响应数据,使用NavidromePlaylistSong类型(包含mediaFileId字段)
+      const chunk = await this.get<NavidromePlaylistSong[]>(account, path, params);
       if (!chunk || chunk.length === 0) {
         break;
       }
-      results.push(...chunk);
-      if (chunk.length < this.PAGE_SIZE) {
+
+      // 处理歌单API返回的数据,将mediaFileId映射到id字段
+      const processed: NavidromeRestSong[] = [];
+      for (let i = 0; i < chunk.length; i++) {
+        const item = chunk[i];
+
+        // 如果存在mediaFileId,使用它作为id;否则使用原id
+        const songId = item.mediaFileId && item.mediaFileId.length > 0 ? item.mediaFileId : item.id;
+
+        if (item.mediaFileId && item.mediaFileId.length > 0) {
+          void ServerLogUtil.debug('NavidromePlaylist', `歌单歌曲使用mediaFileId作为id: ${item.mediaFileId}`);
+        }
+
+        const processedItem: NavidromeRestSong = {
+          id: songId,
+          title: item.title,
+          album: item.album,
+          albumId: item.albumId,
+          artist: item.artist,
+          artistId: item.artistId,
+          duration: item.duration,
+          bitRate: item.bitRate,
+          suffix: item.suffix,
+          size: item.size,
+          createdAt: item.createdAt,
+          genre: item.genre,
+          track: item.track,
+          year: item.year,
+          contentType: item.contentType,
+          coverArt: item.coverArt,
+          coverArtId: item.coverArtId,
+          coverArtPath: item.coverArtPath,
+          embedArtPath: item.embedArtPath
+        };
+        processed.push(processedItem);
+      }
+
+      results.push(...processed);
+      if (processed.length < this.PAGE_SIZE) {
         break;
       }
       start = end;
@@ -433,6 +478,104 @@ export class NavidromeRestApi {
     return normalized === '/' ? '' : normalized;
   }
 
+  /**
+   * 获取歌词
+   * 使用 Subsonic API: /rest/getLyrics
+   * @param account Navidrome 账号信息
+   * @param artist 歌手名(可选)
+   * @param title 歌曲名(可选)
+   * @returns 歌词文本,如果获取失败返回空字符串
+   */
+  async getLyrics(account: WebDavAccount, artist?: string, title?: string): Promise<string> {
+    const httpRequest = http.createHttp();
+    try {
+      // 构建Subsonic API参数
+      const params: Array<QueryParam> = [
+        new QueryParam('u', account.account ?? ''),
+        new QueryParam('p', account.password ?? ''),
+        new QueryParam('v', '1.16.1'),
+        new QueryParam('c', 'TTMusic'),
+        new QueryParam('f', 'xml')
+      ];
+
+      // 添加可选参数
+      if (artist && artist.trim().length > 0) {
+        params.push(new QueryParam('artist', artist.trim()));
+      }
+      if (title && title.trim().length > 0) {
+        params.push(new QueryParam('title', title.trim()));
+      }
+
+      const query = this.buildQueryString(params);
+      const url = `${this.buildRootBase(account)}/rest/getLyrics${query}`;
+
+      void ServerLogUtil.info(TAG, `获取歌词 GET ${url}`);
+
+      const response = await httpRequest.request(url, {
+        method: http.RequestMethod.GET,
+        connectTimeout: 10000,
+        readTimeout: 15000,
+        expectDataType: http.HttpDataType.STRING,
+        header: {
+          'Accept': 'application/xml; charset=utf-8',
+          'Accept-Charset': 'utf-8'
+        }
+      });
+
+      if (response.responseCode !== 200) {
+        void ServerLogUtil.error(TAG, `获取歌词失败 code=${response.responseCode}`);
+        return '';
+      }
+
+      void ServerLogUtil.info(TAG, `获取歌词成功 code=${response.responseCode}`);
+      const xmlText = response.result as string;
+      void ServerLogUtil.info(TAG, `获取歌词成功 xmlText=${xmlText}`);
+      // 解析XML响应,提取lyrics标签内容
+      return this.parseLyricsFromXml(xmlText);
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `获取歌词异常: ${err.message}`);
+      return '';
+    } finally {
+      httpRequest.destroy();
+    }
+  }
+
+  /**
+   * 从Subsonic API的XML响应中解析歌词文本
+   * @param xmlText XML响应文本
+   * @returns 歌词文本,如果解析失败返回空字符串
+   */
+  private parseLyricsFromXml(xmlText: string): string {
+    try {
+      // 查找 <lyrics> 标签
+      const lyricsMatch = xmlText.match(/<lyrics[^>]*>([\s\S]*?)<\/lyrics>/);
+      if (!lyricsMatch || lyricsMatch.length < 2) {
+        void ServerLogUtil.warn(TAG, 'XML响应中未找到lyrics标签');
+        return '';
+      }
+
+      // 提取歌词内容并处理XML转义字符
+      let lyrics = lyricsMatch[1];
+
+      // 处理XML转义字符
+      lyrics = lyrics
+        .replace(/&amp;/g, '&')
+        .replace(/&lt;/g, '<')
+        .replace(/&gt;/g, '>')
+        .replace(/&quot;/g, '"')
+        .replace(/&apos;/g, "'")
+        .trim();
+
+      void ServerLogUtil.info(TAG, `成功解析歌词 lyrics=${lyrics}`);
+      return lyrics;
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `解析歌词XML失败: ${err.message}`);
+      return '';
+    }
+  }
+
   resolveResourceUrl(account: WebDavAccount, path?: string): string | undefined {
     if (!path || path.trim().length === 0) {
       return undefined;

+ 164 - 0
entry/src/main/ets/common/service/LyricService.ets

@@ -0,0 +1,164 @@
+import { navidromeRestApi } from '../network/NavidromeRestApi';
+import { jellyfinApi } from '../network/JellyfinApi';
+import { embyApi } from '../network/EmbyApi';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import { ServerLogUtil } from '../util/ServerLogUtil';
+import { StrUtil } from '@pura/harmony-utils';
+
+const TAG = 'heanup LyricService';
+
+/**
+ * 歌曲数据接口
+ */
+export interface SongData {
+  webdav_account_id?: string;
+  artist?: string;
+  name?: string;
+  webdav_id?: string;
+  lyricIndex?: number;
+  type?: number;
+}
+
+/**
+ * 账号获取回调类型
+ */
+export type AccountGetter = (accountId: string) => Promise<WebDavAccount | null>;
+
+/**
+ * 歌词服务类
+ * 负责从各种媒体服务器获取歌词
+ */
+class LyricService {
+
+  /**这个方法不能用,返回的歌词没时间戳
+   * 从Navidrome服务器获取歌词
+   * @param currentSong 当前歌曲对象
+   * @param getAccountById 根据账号ID获取账号信息的回调
+   * @returns 歌词文本,如果获取失败返回空字符串
+   */
+  async fetchNavidromeLyric(
+    currentSong: SongData,
+    getAccountById: AccountGetter
+  ): Promise<string> {
+    if (!currentSong || !currentSong.webdav_account_id) {
+      return '';
+    }
+
+    try {
+      ServerLogUtil.info(TAG, '尝试从Navidrome服务器获取歌词');
+      const account = await getAccountById(currentSong.webdav_account_id);
+      if (!account) {
+        throw new Error('Navidrome账号不可用');
+      }
+
+      const artist = currentSong.artist || '';
+      const title = currentSong.name || '';
+      const navidromeLyric = await navidromeRestApi.getLyrics(account, artist, title);
+
+      if (StrUtil.isNotEmpty(navidromeLyric)) {
+        ServerLogUtil.info(TAG, '成功从Navidrome服务器获取到歌词');
+        return navidromeLyric;
+      } else {
+        ServerLogUtil.info(TAG, 'Navidrome服务器未返回歌词');
+        return '';
+      }
+    } catch (error) {
+      const err = error as Error;
+      ServerLogUtil.error(TAG, `从Navidrome服务器获取歌词失败: ${err.message}`);
+      return '';
+    }
+  }
+
+  /**
+   * 从Jellyfin服务器获取歌词
+   * @param currentSong 当前歌曲对象
+   * @param getAccountById 根据账号ID获取账号信息的回调
+   * @returns 歌词文本,如果获取失败返回空字符串
+   */
+  async fetchJellyfinLyric(
+    currentSong: SongData,
+    getAccountById: AccountGetter
+  ): Promise<string> {
+    if (!currentSong || !currentSong.webdav_account_id) {
+      return '';
+    }
+
+    try {
+      ServerLogUtil.info(TAG, '尝试从Jellyfin服务器获取歌词');
+      const account = await getAccountById(currentSong.webdav_account_id);
+      if (!account) {
+        throw new Error('Jellyfin账号不可用');
+      }
+
+      const itemId = currentSong.webdav_id || '';
+      if (!itemId) {
+        throw new Error('Jellyfin歌曲ID不可用');
+      }
+
+      const jellyfinLyric = await jellyfinApi.getLyric(account, itemId);
+
+      if (StrUtil.isNotEmpty(jellyfinLyric)) {
+        ServerLogUtil.info(TAG, '成功从Jellyfin服务器获取到歌词');
+        return jellyfinLyric;
+      } else {
+        ServerLogUtil.info(TAG, 'Jellyfin服务器未返回歌词');
+        return '';
+      }
+    } catch (error) {
+      const err = error as Error;
+      ServerLogUtil.error(TAG, `从Jellyfin服务器获取歌词失败: ${err.message}`);
+      return '';
+    }
+  }
+
+  /**
+   * 从Emby服务器获取歌词
+   * @param currentSong 当前歌曲对象
+   * @param getAccountById 根据账号ID获取账号信息的回调
+   * @returns 歌词文本,如果获取失败返回空字符串
+   */
+  async fetchEmbyLyric(
+    currentSong: SongData,
+    getAccountById: AccountGetter
+  ): Promise<string> {
+    if (!currentSong || !currentSong.webdav_account_id) {
+      return '';
+    }
+
+    try {
+      ServerLogUtil.info(TAG, '尝试从Emby服务器获取歌词');
+      const account = await getAccountById(currentSong.webdav_account_id);
+      if (!account) {
+        throw new Error('Emby账号不可用');
+      }
+
+      const itemId = currentSong.webdav_id || '';
+      const lyricIndex = currentSong.lyricIndex;
+
+      if (!itemId) {
+        throw new Error('Emby歌曲ID不可用');
+      }
+
+      if (lyricIndex === undefined || lyricIndex === null) {
+        ServerLogUtil.info(TAG, 'Emby歌曲没有歌词索引,跳过获取歌词');
+        return '';
+      }
+
+      const embyLyric = await embyApi.getLyric(account, itemId, lyricIndex);
+
+      if (StrUtil.isNotEmpty(embyLyric)) {
+        ServerLogUtil.info(TAG, '成功从Emby服务器获取到歌词');
+        return embyLyric;
+      } else {
+        ServerLogUtil.info(TAG, 'Emby服务器未返回歌词');
+        return '';
+      }
+    } catch (error) {
+      const err = error as Error;
+      ServerLogUtil.error(TAG, `从Emby服务器获取歌词失败: ${err.message}`);
+      return '';
+    }
+  }
+}
+
+export const lyricService = new LyricService();

+ 99 - 0
entry/src/main/ets/common/util/LyricUtil.ets

@@ -9,6 +9,18 @@ interface ParseResult {
   words: LyricWord[];
 }
 
+// 定义Navidrome歌词行的接口
+interface NavidromeLyricLine {
+  start: number;
+  value: string;
+}
+
+// 定义Navidrome语言数据的接口
+interface NavidromeLangData {
+  lang?: string;
+  line: NavidromeLyricLine[];
+}
+
 class LyricUtil {
 
   // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
@@ -136,6 +148,93 @@ class LyricUtil {
     // 将结果数组连接成单一字符串
     return result.join('\n');
   }
+
+  /**
+   * 将Navidrome的JSON格式歌词转换为LRC标准格式
+   * JSON格式: [{"lang":"xxx","line":[{"start":0,"value":"歌词内容"},...]}]
+   * LRC格式: [00:00.00]歌词内容
+   * @param jsonLyric Navidrome返回的JSON格式歌词
+   * @returns 转换后的LRC格式歌词,如果转换失败返回undefined
+   */
+  public convertNavidromeJsonLyricToLrc(jsonLyric: string): string | undefined {
+    if (!jsonLyric || jsonLyric.trim().length === 0) {
+      return undefined;
+    }
+
+    try {
+      // 尝试解析JSON
+      const trimmed = jsonLyric.trim();
+
+      // 检查是否是JSON格式(以[开头)
+      if (!trimmed.startsWith('[')) {
+        console.log("onecold LyricUtil: 歌词不是JSON格式,直接返回原歌词");
+        return undefined;
+      }
+
+      console.log("onecold LyricUtil: 开始解析Navidrome JSON歌词");
+      const jsonData = JSON.parse(trimmed) as NavidromeLangData[];
+
+      if (!jsonData || jsonData.length === 0) {
+        console.log("onecold LyricUtil: JSON歌词解析失败:数据为空");
+        return undefined;
+      }
+
+      // 提取所有语言的歌词行,合并去重
+      const allLyricLines: Map<number, string> = new Map();
+
+      for (let i = 0; i < jsonData.length; i++) {
+        const langData = jsonData[i];
+        if (!langData.line || langData.line.length === 0) {
+          continue;
+        }
+
+        // 遍历该语言的所有歌词行
+        for (let j = 0; j < langData.line.length; j++) {
+          const line = langData.line[j];
+          if (line.start !== undefined && line.value) {
+            // 如果该时间点还没有歌词,或者当前语言的歌词不为空,则添加
+            const existing = allLyricLines.get(line.start);
+            if (!existing || line.value.trim().length > 0) {
+              allLyricLines.set(line.start, line.value);
+            }
+          }
+        }
+      }
+
+      if (allLyricLines.size === 0) {
+        console.log("onecold LyricUtil: 没有有效的歌词行");
+        return undefined;
+      }
+
+      // 按时间戳排序
+      const sortedTimes = Array.from(allLyricLines.keys()).sort((a, b) => a - b);
+
+      // 转换为LRC格式
+      const lrcLines: string[] = [];
+      for (let k = 0; k < sortedTimes.length; k++) {
+        const timeMs = sortedTimes[k];
+        const text = allLyricLines.get(timeMs) ?? '';
+
+        // 转换时间戳: 毫秒 -> [mm:ss.xx]
+        const minutes = Math.floor(timeMs / 60000);
+        const seconds = Math.floor((timeMs % 60000) / 1000);
+        const centiseconds = Math.floor((timeMs % 1000) / 10);
+
+        const timeTag = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}]`;
+        lrcLines.push(`${timeTag}${text}`);
+      }
+
+      const result = lrcLines.join('\n');
+      console.log("onecold LyricUtil: Navidrome歌词转换成功,共" + lrcLines.length + "行");
+      console.log("onecold LyricUtil: 转换后歌词预览:" + result.substring(0, 200));
+
+      return result;
+    } catch (error) {
+      const err = error as Error;
+      console.log("onecold LyricUtil: Navidrome歌词转换失败: " + err.message);
+      return undefined;
+    }
+  }
 }
 
 export default new LyricUtil();

+ 210 - 381
entry/src/main/ets/common/util/Utility.ets

@@ -120,6 +120,15 @@ export interface FFprobeMetadata {
   format: FFprobeFormat;
 }
 
+// 音频流信息接口
+interface AudioStreamInfo {
+  sampleRate: string;
+  bits_per_raw_sample: string;
+  channels: string;
+  channel_layout: string;
+  start_time: string;
+}
+
 export class Utility {
 
   private constructor() {}
@@ -705,20 +714,6 @@ export class Utility {
   static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number
     ,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
     await ReqPermissionUtil.persistPermission(uri);
-    //这里加个判断,如果文件的大小是0K,这个文件是空的,直接返回
-    try {
-      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
-      const fileStat = await fs.stat(file.fd);
-      if (fileStat.size < 1024) {// 检查是否为有效文件(>1KB)
-        console.warn(`File ${uri} is empty (0KB). Returning default VideoItem.`);
-        // Return a default or empty VideoItem object
-        return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
-      }
-    } catch (error) {
-      console.error(`Failed to get stats for file ${uri}:`, error);
-      // If we can't read the file, still return a default item
-      return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
-    }
 
     if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
       const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
@@ -726,393 +721,233 @@ export class Utility {
       return  cueItem
     }
     return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName,originFileName)
+  }
 
-    //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav  不支持内嵌封面dsf,aif,aiff
-    if(StrUtil.isNotEmpty(uri)){
-      if(uri.toLowerCase().endsWith('.dsf')
-        ||uri.toLowerCase().endsWith('.aif')
-        // ||uri.toLowerCase().endsWith('.wav')
-        ||uri.toLowerCase().endsWith('.aiff')){
-        return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
-      }
-    }
-
-
-    let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
-    try {
-      console.info('asset file.uri: ', uri);
-
-
-      let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
-      console.info("file.fd " + file.fd);
-      let fdfd = 'fd://' + file.fd
-      //3、通过fs.stat方法获取stat对象
-      console.info('asset file.name: ', file.name);
-      console.info('asset file.uri: ', uri);
-      console.info('asset file.fd: ', file.fd);
-      console.info('asset file.path: ', file.path);
-      item = new VideoItem(file.name,uri,uri,type,0,'')
-      await fs.stat(file.fd).then(async (stat: fs.Stat) => {
-        console.info("get file info succeed, the size of file is " + stat.size);
-        let videoSize =  stat.size
-        // let videoTime = stat.ctime
-
-        let fileSize = Utility.formatFSize(videoSize)
-        //按照添加时间
-        let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
-        // console.info('onecold asset addTime: ', addTime);
-        // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
-
-        // console.info('asset stat.ctime: ', stat.ctime);
-
-        let musicName:string | undefined = file.name
-        let artist:string | undefined = ''
-        let album:string | undefined = ''
-        let pixelMap:image.PixelMap|undefined|null = undefined
-        let imagePath = ''
-
-        let duration:string | undefined = ''
-        let mimeType:string | undefined = ''
-        let trackCount:string | undefined = ''//轨道数量
-        let sampleRate:string | undefined = ''//音频的采样率单位为Hz
-
-        if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
-          try {
-            // 创建AVMetadataExtractor对象
-            const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
-            // 设置fdSrc
-            const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
-            avMetadataExtractor.fdSrc = fd;
-
-            // 获取元数据(promise模式)
-            const metadata = await avMetadataExtractor.fetchMetadata();
-            if(StrUtil.isNotEmpty(metadata.title)){
-              musicName = metadata.title
-            }else{
-              //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
-              console.log(`onecold musicName为空:${file.name}`);
-              const musicData = parseMusicFileName(file.name);
-              if (musicData.isValid)  {
-                // console.log(`onecold 艺术家:${musicData.artist}`);   // 输出:周杰伦
-                // console.log(`onecold 歌曲名:${musicData.title}`);   // 输出:七里香
-                musicName = musicData.title
-                if(artist==''||artist==undefined)
-                  artist = musicData.artist
-              } else {
-                musicName = file.name
-                // console.log("onecold 文件名格式不符合要求");
-              }
-            }
-
-
-            if(StrUtil.isNotEmpty(metadata.artist)){
-              artist = metadata.artist
-            }
-            if(artist==undefined)
-              artist = ''
-
-            if(StrUtil.isNotEmpty(metadata.album)){
-              album = metadata.album
-            }else{
-              album = ''
-            }
+  static async readMetaInfoFFmpeg(context: Context, inputPath: string, type: number,
+    autoParseMusicName?: boolean, originFileName?: string): Promise<VideoItem> {
+    // 辅助函数:创建默认的VideoItem
+    const createDefaultItem = (filePath: string): VideoItem => {
+      return new VideoItem(FileUtil.getFileName(filePath), filePath, filePath, type, 0, '');
+    };
 
-            if(StrUtil.isNotEmpty(metadata.duration)){
-              if(metadata.duration)
-                duration = convertSecondsToTime(metadata.duration.toString())
-            }
-            if(StrUtil.isNotEmpty(metadata.mimeType)){
-              mimeType = metadata.mimeType
+    // 辅助函数:提取歌词(处理标准字段和lyrics-开头的自定义字段)
+    const extractLyrics = (tags: FFMpegTags): string => {
+      // 1. 尝试标准字段
+      let lyrics = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
+
+      // 2. 如果标准字段为空,尝试lyrics-开头的自定义字段
+      if (StrUtil.isEmpty(lyrics)) {
+        const tagsRecord = tags as Record<string, string>;
+        const possibleKeys = Object.keys(tagsRecord);
+        for (let i = 0; i < possibleKeys.length; i++) {
+          const key = possibleKeys[i];
+          if (key && key.toLowerCase().startsWith('lyrics-')) {
+            lyrics = tagsRecord[key];
+            if (lyrics) {
+              break;
             }
-            if(StrUtil.isNotEmpty(metadata.trackCount)){
-              trackCount = metadata.trackCount
-            }
-            if(StrUtil.isNotEmpty(metadata.sampleRate)){
-              sampleRate = metadata.sampleRate
-            }
-
-            let name = await MD5.digestSync(uri)
-
-            // if(isLoadPixelMap){
-              // 获取专辑封面(promise模式)
-              // pixelMap = await avMetadataExtractor.fetchAlbumCover();
-              // // 释放资源(promise模式)
-              // await avMetadataExtractor.release();
-
-              pixelMap = await fetchAlbumCover(avMetadataExtractor)
-
-              // console.info('onecold release success. name= '+musicName);
-              if(pixelMap!==undefined&&pixelMap!==null){
-                // console.info('onecold pixelMap is not empty= '+musicName);
-                imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
-                imagePath = fileUri.getUriFromPath(imagePath)
-              }else{
-                // console.info('onecold pixelMap is  empty= '+musicName);
-                imagePath = ''
-                // if(StrUtil.isNotEmpty(artist))
-                //   imagePath =  await NetAxiosUtil.getLyricCover(musicName,artist)
-              }
-
+          }
+        }
+      }
 
-            // }else{
-            //   imagePath = context.filesDir + FileUtil.separator + name
-            //   imagePath = fileUri.getUriFromPath(imagePath)
-            // }
-            console.info('onecold release success. imagePath= '+imagePath);
+      return lyrics;
+    };
 
+    // 辅助函数:处理封面提取
+    const extractCover = async (hasCover: boolean, filePath: string, name: string): Promise<string> => {
+      const md5Name = await MD5.digestSync(filePath);
+      let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
 
-          } catch (error) {
-            console.error('Error during metadata extraction:', error);
+      try {
+        if (hasCover) {
+          const isSuccess = await getFFmpegCover(filePath, imagePath);
+          if (isSuccess) {
+            return fileUri.getUriFromPath(imagePath);
+          } else {
+            Logger.warn('readMetaInfoFFmpeg', 'FFmpeg提取封面失败,使用华为接口');
+            return await extractCoverWithHwInterface(filePath, context, name);
+          }
+        } else if (Utility.isVideoByExtension(filePath)) {
+          const pixelMap = await Utility.getFetchFrameByTime(filePath);
+          if (pixelMap) {
+            imagePath = await ImageUtil.savePixelMap(pixelMap, context.filesDir, md5Name);
+            return fileUri.getUriFromPath(imagePath);
           }
-        } else {
-          console.warn('AVMetadataExtractor capability is not supported.');
         }
+      } catch (error) {
+        Logger.error('readMetaInfoFFmpeg', `提取封面失败: ${error instanceof Error ? error.message : String(error)}`);
+      }
 
+      return '';
+    };
 
-        if(musicName==undefined)
-          musicName = file.name
-
-        item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,fileSize,
-          imagePath,artist,album,file.name)
-        item.duration = duration+'';
-        item.mimeType = mimeType;
-        item.trackCount = trackCount;
-        item.sampleRate = sampleRate;
-        item.isFav = 0;
-        item.playCount = 0;
-        item.pyStr = pinyin4js.getShortPinyin(musicName)
-
-        // item.lyricContent = await extractLyricsContent(uri)
-        let metaItem = await parseAudioMetadata(uri)
-        if(metaItem){
-          item.lyricContent = metaItem.lyricContent
-          item.bit_rate = formatBitrateToKbps(metaItem.bit_rate  || "0");
-          item.year = metaItem.year ||'unknown'
-          item.probe_score = metaItem.probe_score
-          item.nb_streams = metaItem.nb_streams
-          item.nb_programs = metaItem.nb_programs
+    // 辅助函数:解析音频流信息
+    const parseAudioStream = (metadata: FFprobeMetadata): AudioStreamInfo => {
+      let sampleRate = '';
+      let bits_per_raw_sample = '';
+      let channels = '';
+      let channel_layout = '';
+      let start_time = '';
+
+      if (metadata.streams && metadata.streams.length > 0) {
+        const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
+        if (audioStream) {
+          sampleRate = audioStream.sample_rate || '';
+          bits_per_raw_sample = audioStream.bits_per_raw_sample || '';
+          channel_layout = audioStream.channel_layout || '';
+          channels = audioStream.channels || '';
+          start_time = audioStream.start_time || '';
         }
+      }
 
-      })
-    } catch (error) {
-      console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
-    }
-
-    return item
-
-  }
+      return { sampleRate, bits_per_raw_sample, channels, channel_layout, start_time };
+    };
 
+    try {
+      inputPath = FileUtil.getFilePath(inputPath);
+      Logger.info('readMetaInfoFFmpeg', `正在获取元数据: ${inputPath}`)
 
-  static async  readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,
-    autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
-    return new Promise((resolve, reject) => {
-      inputPath = FileUtil.getFilePath(inputPath)
-      let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
+      const commands = ["ffprobe", "-v", "quiet", "-print_format",
+        "json", "-show_format", "-show_streams", inputPath];
       let outputJson = "";
 
+      // 使用Promise方式执行FFprobe命令
+      await new Promise<void>((resolve, reject) => {
+        FFmpeg.execute(commands, {
+          outputCallback: (message: string) => {
+            outputJson += message;
+          },
+        })
+          .then(() => {
+            Logger.info('readMetaInfoFFmpeg', `获取元数据成功, JSON长度: ${outputJson.length}`);
+            resolve();
+          })
+          .catch((error: Error) => {
+            Logger.error('readMetaInfoFFmpeg', `FFprobe执行失败: ${error.message}`);
+            reject(error);
+          });
+      });
+      // 解析元数据
+      const metadata: FFprobeMetadata = JSON.parse(outputJson);
+      const format = metadata.format;
+      const tags = format.tags || {};
 
-      FFmpeg.execute(commands,  {
-        logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
-        outputCallback: (message: string) => {
-          outputJson += message;
-        },
-      }).then(async () => {
-        try {
-          const metadata: FFprobeMetadata = JSON.parse(outputJson);
-          const format = metadata.format;
-          console.log(`onecold outputJson:${outputJson}`);
-          let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
-          console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
-          let videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
-          await fs.stat(file.fd).then(async (stat: fs.Stat) => {
-
-            // 获取音频流的采样率
-            let sampleRate:string|undefined = '';
-            let bits_per_raw_sample:string|undefined = '';
-            let channels:string|undefined = '';
-            let channel_layout:string|undefined = '';
-            let start_time:string|undefined = '';
-            if (metadata.streams  && metadata.streams.length  > 0) {
-              // 查找第一个音频流
-              const audioStream = metadata.streams.find(stream  => StrUtil.isNotEmpty(stream.sample_rate));
-              if (audioStream)  {
-                sampleRate = audioStream.sample_rate;
-                bits_per_raw_sample = audioStream.bits_per_raw_sample
-                channel_layout = audioStream.channel_layout
-                channels = audioStream.channels
-                start_time = audioStream.start_time
-              }
-            }
-
-            let videoSize =  stat.size
-            const modifyTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
-            let fileSize = Utility.formatFSize(videoSize)
-            //按照添加时间
-            let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
-            // Extract artist and title from tags
-            const tags = format.tags  || {};
-            let artist = tags.artist ||tags.ARTIST || '';
-            let title = tags.title ||tags.TITLE || '';
-            let album = tags.album ||tags.ALBUM|| '';
-            Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
-            console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
-            // 检查是否有乱码
-            if (hasGarbledText(tags))  {
-              // 第二次尝试用华为的接口提取元数据
-              let  hwTags = await extractHwMediaMetadata(inputPath)
-              artist = hwTags?.artist|| '';
-              title = hwTags?.title|| '';
-              album = hwTags?.album|| '';
-              console.info('onecold 乱码修正 artist='+artist);
-              console.info('onecold 乱码修正 title='+title);
-              console.info('onecold 乱码修正 album='+album);
-            }
-            Logger.info('readMetaInfoFFmpeg', `最终标签 title="${title}", artist="${artist}", album="${album}", autoParse=${autoParseMusicName}`);
-            if(StrUtil.isEmpty(title)){
-              //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
-              console.log(`onecold musicName为空:${file.name}`);
-              // let autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName',false)
-              if(autoParseMusicName){
-                const musicData = parseMusicFileName(file.name);
-                if (musicData.isValid)  {
-                  console.log(`onecold 艺术家:${musicData.artist}`);   // 输出:周杰伦
-                  console.log(`onecold 歌曲名:${musicData.title}`);   // 输出:七里香
-                  title = musicData.title
-                  if(artist==''||artist==undefined)
-                    artist = musicData.artist
-                } else {
-
-                  title = originFileName||file.name
-                }
-              }else{
-                title =  originFileName||file.name
-              }
+      // 获取文件信息
+      let file: fs.File;
+      try {
+        file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY);
+      } catch (error) {
+        Logger.error('readMetaInfoFFmpeg', `打开文件失败: ${inputPath}`);
+        return createDefaultItem(inputPath);
+      }
 
-            }
+      try {
+        const stat = await fs.stat(file.fd);
+        const videoSize = stat.size;
+        const addTime = Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm');
+        const fileSize = Utility.formatFSize(videoSize);
+
+        // 解析音频流信息
+        const audioStreamInfo = parseAudioStream(metadata);
+
+        // 提取基础标签信息
+        let artist = tags.artist || tags.ARTIST || '';
+        let title = tags.title || tags.TITLE || '';
+        let album = tags.album || tags.ALBUM || '';
+
+        Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
+
+        // 检查并修正乱码
+        if (hasGarbledText(tags)) {
+          const hwTags = await extractHwMediaMetadata(inputPath);
+          artist = hwTags?.artist || artist;
+          title = hwTags?.title || title;
+          album = hwTags?.album || album;
+          Logger.info('readMetaInfoFFmpeg', `乱码修正后的标签 title="${title}", artist="${artist}"`);
+        }
 
-            let name: string = title;
-            if (!name) {
-              name = getFileNameWithoutExtension(inputPath);
-            }
-            let pixelMap:image.PixelMap|undefined|null = undefined
-            // Create VideoItem
-            videoItem = new VideoItem(
-              name,
-              inputPath, // id can be generated or left empty
-              inputPath,
-              type, // assuming it's local
-              videoSize,
-              addTime // convert to ISO string
-            );
-
-            // Set additional properties from format metadata
-            videoItem.artist  = artist;
-            videoItem.album  = album;
-            videoItem.sampleRate = sampleRate
-            videoItem.pyStr = pinyin4js.getShortPinyin(name)
-            videoItem.fileName  = FileUtil.getFileName(inputPath);
-            if(format.duration)
-              videoItem.duration = formatDuration(format.duration.toString()||'00:00')
-            videoItem.size  = fileSize;
-            videoItem.bit_rate  =formatBitrateToKbps(format.bit_rate  || "0");
-            videoItem.probe_score  = format.probe_score;
-            videoItem.nb_streams  = format.nb_streams;
-            videoItem.nb_programs  = format.nb_programs;
-            videoItem.year  = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
-            videoItem.lyricContent  = tags.LYRICS || tags.lyrics  ||  tags.USLT || tags.UNSYNCEDLYRICS || '';
-            //console.info('readMetaInfoFFmpeg   videoItem.lyricContent: ',  videoItem.lyricContent);
-            // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
-            // tags 必须是 Record<string, string | undefined>(或 any)
-            // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
-            if(StrUtil.isEmpty( videoItem.lyricContent)){
-              const tagsRecord = tags as Record<string, string>;
-              const possibleKeys = Object.keys(tagsRecord);
-              for (let i = 0; i < possibleKeys.length;  i++) {
-                const key = possibleKeys[i];
-                if (key && key.toLowerCase().startsWith('lyrics-'))  {
-                  //console.info('readMetaInfoFFmpeg  videoItem.key:  ', key);
-                  // 通过转换后的 Record 类型安全访问属性
-                  videoItem.lyricContent  = tagsRecord[key];
-                  //console.info('readMetaInfoFFmpeg  videoItem.lyricContent2:  ', videoItem.lyricContent);
-                  if (videoItem.lyricContent)  {
-                    break;
-                  }
-                }
+        // 处理空标题的情况
+        if (StrUtil.isEmpty(title)) {
+          if (autoParseMusicName) {
+            const musicData = parseMusicFileName(file.name);
+            if (musicData.isValid) {
+              title = musicData.title;
+              if (StrUtil.isEmpty(artist)) {
+                artist = musicData.artist;
               }
+            } else {
+              title = originFileName || file.name;
             }
+          } else {
+            title = originFileName || file.name;
+          }
+        }
 
+        // 确保有有效的名称
+        let name = title || getFileNameWithoutExtension(inputPath);
+
+        // 构建VideoItem
+        const videoItem = new VideoItem(name, inputPath, inputPath, type, videoSize, addTime);
+        videoItem.artist = artist;
+        videoItem.album = album;
+        videoItem.sampleRate = audioStreamInfo.sampleRate;
+        videoItem.pyStr = pinyin4js.getShortPinyin(name);
+        videoItem.fileName = FileUtil.getFileName(inputPath);
+        videoItem.duration = format.duration ? formatDuration(format.duration.toString()) : '00:00';
+        videoItem.size = fileSize;
+        videoItem.bit_rate = formatBitrateToKbps(format.bit_rate || "0");
+        videoItem.probe_score = format.probe_score;
+        videoItem.nb_streams = format.nb_streams;
+        videoItem.nb_programs = format.nb_programs;
+        videoItem.year = tags.TYER || tags.date || tags.DATE || '';
+        videoItem.lyricContent = extractLyrics(tags);
+        videoItem.genre = tags.genre || tags.GENRE || '';
+        videoItem.track = tags.track || tags.TRACK || '';
+        videoItem.ALBUMARTIST = tags.ALBUMARTIST || tags.album_artist || tags.albumartist || tags.TPE2 || '';
+        videoItem.COMPOSER = tags.COMPOSER || tags.composer || '';
+        videoItem.LYRICIST = tags.LYRICIST || tags.lyricist || tags.TEXT || '';
+        videoItem.COMMENT = tags.COMMENT || tags.comment || tags.COMM || '';
+        videoItem.disc = tags.disc || '';
+        videoItem.bits_per_raw_sample = audioStreamInfo.bits_per_raw_sample || '1';
+        videoItem.channel_layout = audioStreamInfo.channel_layout || '';
+        videoItem.channels = audioStreamInfo.channels || '0';
+        videoItem.start_time = audioStreamInfo.start_time || '00:00';
+        videoItem.mimeType = format.format_name;
+
+        // 检查并提取封面
+        const hasCover = metadata.streams.some(stream => stream.disposition?.attached_pic === 1);
+        videoItem.pixelMapPath = await extractCover(hasCover, inputPath, name);
+
+        // 确定音质等级
+        if (audioStreamInfo.sampleRate && format.bit_rate) {
+          videoItem.md5Str = determineAudioQuality(
+            videoItem.mimeType,
+            Number(format.bit_rate),
+            Number(audioStreamInfo.sampleRate)
+          );
+        }
 
-            videoItem.genre  = tags.genre||tags.GENRE|| '';
-            videoItem.track  = tags.track||tags.TRACK|| '';
-
-            videoItem.ALBUMARTIST  = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
-            videoItem.COMPOSER  = tags.COMPOSER||tags.composer|| '';
-            videoItem.LYRICIST  = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
-            videoItem.COMMENT  = tags.COMMENT||tags.comment||tags.COMM|| '';
-            videoItem.disc  = tags.disc|| '';
-
-            videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
-            videoItem.channel_layout = channel_layout || ''
-            videoItem.channels = channels ||'0'
-            videoItem.start_time = start_time || '00:00'
-
-            videoItem.mimeType = format.format_name
-            // 检查是否有封面图片流
-            const hasCover = metadata.streams.some(stream  =>
-            stream.disposition?.attached_pic  === 1
-            );
-            console.info('onecold readMetaInfoFFmpeg asset hasCover: ', hasCover);
-            let md5Name = await MD5.digestSync(inputPath)
-            let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
-            console.info('onecold readMetaInfoFFmpeg asset imagePath: ', imagePath);
-            // 如果有封面图片,则提取
-            try {
-              if (hasCover) {
-                //提取封面
-                let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
-
-                if(isSuccess){
-                  imagePath = fileUri.getUriFromPath(imagePath)
-                }else{
-                  // 使用华为接口提取封面(备用方案)
-                  console.warn('onecold 提取封面图片失败,使用华为接口提取封面');
-                  imagePath = await extractCoverWithHwInterface(inputPath, context, name);
-                }
-                videoItem.pixelMapPath  = imagePath;
-              }else if(Utility.isVideoByExtension(inputPath)){
-                pixelMap = await Utility.getFetchFrameByTime(inputPath)
-                if(pixelMap!==undefined&&pixelMap!==null){
-                  imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,md5Name)
-                  imagePath = fileUri.getUriFromPath(imagePath)
-                }else{
-                  imagePath = ''
-                }
-                imagePath = fileUri.getUriFromPath(imagePath)
-                videoItem.pixelMapPath  = imagePath;
-              }
-            } catch (error) {
-              console.warn('onecold 提取封面图片失败:', error.message);
-            }
-            if(sampleRate&&format.bit_rate){
-              videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
-            }
-            console.info('onecold Successfully  parsed metadata:', videoItem);
-            resolve(videoItem);
-          })
-
+        Logger.info('readMetaInfoFFmpeg', `成功解析元数据: ${name}`);
+        return videoItem;
 
+      } finally {
+        // 确保文件句柄被关闭
+        try {
+          fs.closeSync(file);
         } catch (error) {
-          console.error('onecold Failed  to parse metadata:', error);
-          reject(new Error('onecold Failed to parse metadata: ' + error.message));
+          Logger.error('readMetaInfoFFmpeg', `关闭文件失败: ${error instanceof Error ? error.message : String(error)}`);
         }
-      }).catch((error: Error) => {
-        console.error(`onecold Execution  failed with error: ${error.message}`);
-        reject(error);
-      });
-    });
+      }
+
+    } catch (error) {
+      Logger.error('onecold readMetaInfoFFmpeg', `解析失败: ${JSON.stringify(error)})`);
+      return createDefaultItem(inputPath);
+    }
   }
 
 
+
+
   private completionNum(num: number): string | number {
     if (num < 10) {
       return '0' + num;
@@ -1949,9 +1784,7 @@ async function getFFmpegCover(inputPath: string, outputPath: string): Promise<bo
 
   try {
     await FFmpeg.execute(commands,  {
-      logCallback: (logLevel: number, logMessage: string) => {
-        console.info(`[FFmpeg  LOG] [${logLevel}] ${logMessage}`);
-      },
+      logCallback: (message) => console.info(`[log] ${message}`),
       progressCallback: (message: string) => {
         console.info(`[FFmpeg  progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
       },
@@ -1969,9 +1802,7 @@ async function getFFmpegCover(inputPath: string, outputPath: string): Promise<bo
         outputPath
       ];
       await FFmpeg.execute(primaryCommands,  {
-        logCallback: (logLevel: number, logMessage: string) => {
-          console.info(`[FFmpeg  LOG] [${logLevel}] ${logMessage}`);
-        },
+        logCallback: (message) => console.info(`[log] ${message}`),
         progressCallback: (message: string) => {
           console.info(`[FFmpeg  progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
         },
@@ -2003,9 +1834,7 @@ async function getVideoFFmpegCover(inputPath: string, outputPath: string) {
     outputPath
   ];
   FFmpeg.execute(commands, {
-    logCallback: (logLevel: number, logMessage: string) => {
-      console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`)
-    },
+    logCallback: (message) => console.info(`[log]${message}`),
     progressCallback: (message: string) => {
       console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
     },

+ 2 - 2
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -1513,9 +1513,9 @@ export function getCloudDiskIcon(type: number): ResourceStr {
     case RemoteDriveType.Navidrome:
       return $r('app.media.navidrome');
     case RemoteDriveType.Jellyfin:
-      return $r('app.media.cloudDisk');
+      return $r('app.media.jellyfin');
     case RemoteDriveType.Emby:
-      return $r('app.media.cloudDisk');
+      return $r('app.media.emby');
     case RemoteDriveType.Baidu:
       return $r('app.media.baiduwp');
     case RemoteDriveType.ALi:

+ 23 - 9
entry/src/main/ets/pages/NewIndex.ets

@@ -70,7 +70,7 @@ const TAG = 'NewIndex'; // 日志标签
 @Entry
 @Component
 struct NewIndex {
-
+  @State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
   @Provide isShowPlay: boolean = false;
   @Provide CONTROL_PlayStatus: number = PlayStatus.INIT;
   @Provide progressValue: number = 0;
@@ -207,12 +207,23 @@ struct NewIndex {
    * - 如果是根目录,双击返回键退出应用,否则提示
    */
   onBackPress(): boolean | void {
+    console.info('onecold onBackPress isDetailView = '+ this.isDetailView);
+    console.info('onecold onBackPress mType = '+  this.mType);
     if (this.currentPath !== this.rootPath || this.isHistory || this.isFavMusic ||
       (this.modeType !== 0 && this.isCanBack)) {
       console.info('onecold 返回键处理逻辑:发送音频广播通知更新列表');
       const eventData: emitter.EventData = {};
       emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_UPDATE }, eventData); // 发送音频广播通知更新doSwipBack
-    }else if(this.mType > 0){
+    } else if(this.mType === 7&&this.isDetailView) {
+      // NavidromePage/Jellyfin/Emby 页面,发送手势返回事件
+      console.info('onecold NavidromePage 返回键处理:发送手势返回事件');
+      const eventData: emitter.EventData = {};
+      emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_NAVID}, eventData);
+    } else if(this.mType === 6) {
+      console.info('onecold 网盘 返回键处理:发送手势返回事件');
+      const eventData: emitter.EventData = {};
+      emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_DISK }, eventData);
+    }  else if(this.mType > 0){
       this.getUIContext()?.animateTo({ duration: 555 }, () => {
         // 动画闭包内控制Image组件的出现和消失
         // this.isShowDrawer = !this.isShowDrawer
@@ -417,17 +428,20 @@ struct NewIndex {
         .visibility(this.mType === 4 ? Visibility.Visible : Visibility.None)
       AboutPage()
         .visibility(this.mType === 5 ? Visibility.Visible : Visibility.None)
+      if(this.mType === 6){
+        WebDavMainPage({
+          offsetX:this.offsetX,
+          isShowDrawer:this.isShowDrawer,
+          mType:this.mType,
+          selectedAccount:this.selectedAccount
+        })
+          .visibility(this.mType === 6 ? Visibility.Visible : Visibility.None)
+      }
 
-      WebDavMainPage({
-        offsetX:this.offsetX,
-        isShowDrawer:this.isShowDrawer,
-        mType:this.mType,
-        selectedAccount:this.selectedAccount
-      })
-        .visibility(this.mType === 6 ? Visibility.Visible : Visibility.None)
       if(this.mType === 7){
         NavidromePage({
           offsetX:this.offsetX,
+          isDetailView:this.isDetailView,
           isShowDrawer:this.isShowDrawer,
           mType:this.mType,
           selectedAccount:this.selectedAccount

+ 16 - 2
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -384,6 +384,15 @@ export struct WebDavMainPage {
     // 订阅WebDAV状态变化
     this.webdavManager.subscribe(this.eventHandler);
     this.subscribeWebDavMetadataUpdates();
+
+
+    // 监听手势返回事件
+    let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_DISK }
+    emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => {
+      console.info('onecold', ' 收到 EVENT_SWIPE_BACK_DISK 事件');
+      // 如果在详情视图模式,退出详情视图
+      this.goBack()
+    });
   }
 
 
@@ -621,8 +630,15 @@ export struct WebDavMainPage {
           this.isLoading = false;
         });
       this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+    }else{
+      this.getUIContext().animateTo({ duration: 555 }, () => {
+        // 动画闭包内控制Image组件的出现和消失
+        this.isShowDrawer = !this.isShowDrawer
+        this.offsetX = 0
+      })
     }
 
+
   }
 
   // 切换账户
@@ -1346,10 +1362,8 @@ export struct WebDavMainPage {
             const currentOffsetY = this.listScroller.currentOffset().yOffset;
             // 判断滚动方向
             if (currentOffsetY > this.prevOffsetY) {
-              console.log("onecold 向上滚动");
               this.isShowTitleBar = false
             } else if (currentOffsetY < this.prevOffsetY) {
-              console.log("onecold 向下滚动");
               this.isShowTitleBar = true
             }
             // 更新前一次偏移量

+ 216 - 225
entry/src/main/ets/view/LocalMusic.ets

@@ -78,6 +78,7 @@ import { hilog } from '@kit.PerformanceAnalysisKit';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import  MediaTable  from '../common/util/MediaTable';
 import FileDeletionWatcher from '../common/util/FileDeletionWatcher';
+import LyricUtil from '../common/util/LyricUtil';
 import NetAxiosUtil from '../common/util/NetAxiosUtil';
 import ImageUtils from '../common/util/ImageUtils';
 import { ringtone } from '@kit.RingtoneKit';
@@ -89,12 +90,14 @@ import '../common/network/RemoteCacheRegistry';
 import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
 import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
+import { navidromeRestApi } from '../common/network/NavidromeRestApi';
 import { jellyfinApi } from '../common/network/JellyfinApi';
 import { embyApi } from '../common/network/EmbyApi';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { ensureSmbFileStreaming, SmbStreamingMeta } from '../common/network/SmbFileCache';
 import { ensureBaiduFileCached } from '../common/network/BaiduFileCache';
 import FileManager from '../common/util/FileManager';
+import { lyricService, SongData } from '../common/service/LyricService';
 
 import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker'
 import app from '@system.app';
@@ -192,8 +195,8 @@ function isSmbType(type: number): boolean {
   return type === CommonConstants.TYPE_SMB;
 }
 
-function isNavidromeType(type: number): boolean {
-  return type === CommonConstants.TYPE_NAVIDROME;
+function isNavidromeType(type: number | undefined): boolean {
+  return type !== undefined && type === CommonConstants.TYPE_NAVIDROME;
 }
 
 function isFtpType(type: number): boolean {
@@ -204,12 +207,12 @@ function isBaiduType(type: number): boolean {
   return type === CommonConstants.TYPE_BAIDU;
 }
 
-function isJellyfinType(type: number): boolean {
-  return type === CommonConstants.TYPE_JELLYFIN;
+function isJellyfinType(type: number | undefined): boolean {
+  return type !== undefined && type === CommonConstants.TYPE_JELLYFIN;
 }
 
-function isEmbyType(type: number): boolean {
-  return type === CommonConstants.TYPE_EMBY;
+function isEmbyType(type: number | undefined): boolean {
+  return type !== undefined && type === CommonConstants.TYPE_EMBY;
 }
 
 function isRemoteCloudType(type: number): boolean {
@@ -814,6 +817,8 @@ const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 @Preview
 @Component
 export struct LocalMusic {
+  @State isPaging: boolean = true;//是否分页加载机制
+  @State isCoverRectangle: boolean = true;
   @State showZMIndex: boolean = false //是否右侧显示字母索引
   private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量
   @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
@@ -1286,7 +1291,7 @@ export struct LocalMusic {
 
     //侧滑广播接收时间
     let eventUpdateSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_UPDATE }
-    // 监听广播事件(打开其他应用处理)
+    // 监听广播事件(手势返回处理)
     emitter.on(eventUpdateSwipeBack, (eventData: emitter.EventData) => {
 
       animateTo({ duration: 555 }, () => {
@@ -1880,22 +1885,11 @@ export struct LocalMusic {
     this.refreshLazyListViews(mainListUpdated, playlistUpdated);
   }
 
-  private closeLoadingDialog(): void {
-    if (!this.loadingDialogId) {
-      return;
-    }
-    DialogHelper.closeDialog(this.loadingDialogId);
-    this.loadingDialogId = '';
-  }
 
-  private closeLoadingProgressDialog(): void {
-    DialogHelper.closeLoading();
-    this.loadingProgressDialogId = '';
-  }
 
   private handleEditMusicResult(result: WorkerEditMusicResult): void {
     // 关闭进度条
-    this.closeLoadingDialog();
+    DialogHelper.closeDialog(this.loadingDialogId)
 
     if (result.success) {
       console.log("heanup 编辑信息成功,数据库已同步");
@@ -2961,14 +2955,13 @@ export struct LocalMusic {
 
     // 初始化进度条
     this.progress  = 0;
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId =  DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
       loadColor: $r('app.color.title_bar_bg'),
       fontColor: $r('app.color.title_bar_bg')
     });
-    this.loadingProgressDialogId = '';
 
     // 计算处理总数用于进度计算
     const totalItems = uris.length;
@@ -2996,18 +2989,18 @@ export struct LocalMusic {
         // 更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(` 正在处理 ${this.progress}%`, this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId,` 正在处理 ${this.progress}%`, this.progress);
 
       } catch (error) {
         Logger.error(TAG,  'saveVideoDatas failed with err: ' + JSON.stringify(error));
         // 即使出错也更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(` 正在处理 ${this.progress}%`, this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId,` 正在处理 ${this.progress}%`, this.progress);
       }
     }
     // 关闭进度条
-    this.closeLoadingProgressDialog();
+    DialogHelper.closeDialog(this.loadingProgressDialogId)
     this.isZero = false
     // 删除目标路径缓存
     setTimeout(() => {
@@ -3024,14 +3017,13 @@ export struct LocalMusic {
 
     let newUris: string[] = [];
     this.progress = 0
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
       loadColor: this.themeColor,
       fontColor: this.themeColor
     });
-    this.loadingProgressDialogId = '';
     // 计算所有文件的总大小
     let totalSize = 0;
     for (let uri of uris) {
@@ -3063,7 +3055,7 @@ export struct LocalMusic {
 
           // 计算总进度
           this.progress = Math.floor((totalRead / totalSize) * 100);
-          DialogHelper.updateLoading(`正在导入 ${this.progress}%`, this.progress);
+          DialogHelper.updateLoading( this.loadingProgressDialogId,`正在导入 ${this.progress}%`, this.progress);
 
           len = await fileIo.read(sourceFile.fd, buffer);
         }
@@ -3093,16 +3085,10 @@ export struct LocalMusic {
 
 
     }
-
-    this.closeLoadingProgressDialog()
+    DialogHelper.closeDialog(this.loadingProgressDialogId);
     this.isZero = false
-    this.loadingProgressDialogId = ''
 
-    // setTimeout(() => {
-    //   // 删除目标路径缓存
-    //   this.cache.delete(this.currentPath);
-    //   this.getSortedFiles(this.currentPath,true,destPath,isOpen)
-    // }, 500);
+
 
 
     Logger.info(TAG, 'scan select video result:' + newUris)
@@ -3456,8 +3442,7 @@ export struct LocalMusic {
           })
       }
       .bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), {
-        modalTransition: this.isCoverRectangle?ModalTransition.DEFAULT:ModalTransition.NONE,
-        transition: this.isCoverRectangle?null:AnimationHelper.transitionInDown(500),
+        modalTransition:ModalTransition.DEFAULT,
         onWillDisappear: () => {
           this.setShowPlayFalse()
         },
@@ -6738,7 +6723,7 @@ export struct LocalMusic {
         ) //拖动List关键代码结束
 
       }, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey)
-      // }
+
     }
     .onScrollStart(() => {
       this.isScrolling = true
@@ -7424,8 +7409,7 @@ export struct LocalMusic {
     }
     //内嵌音乐标签不能开启监听
     // this.deletionWatcher?.stop();
-    DialogHelper.showLoadingDialog()
-    this.loadingDialogId = 'loading_dialog'
+    this.loadingDialogId  = DialogHelper.showLoadingDialog()
     await PermissionUtil.activatePermission(item.filePath)
     let tempOutPath = ''
     // 如果不是 packName 包下的文件,则直接路径用this.currentPath
@@ -7536,13 +7520,12 @@ export struct LocalMusic {
           console.error(" onecold  编辑信息数据库失败 ");
         }
         // 关闭进度条
-        this.closeLoadingDialog();
-
+        DialogHelper.closeDialog(this.loadingDialogId)
 
 
       }).catch((error: Error) => {
         // 关闭进度条
-        this.closeLoadingDialog();
+        DialogHelper.closeDialog(this.loadingDialogId)
         console.error('onecold Subtitle sync failed:', error);
       });
 
@@ -7551,7 +7534,7 @@ export struct LocalMusic {
 
     } else {
       // 关闭进度条
-      this.closeLoadingDialog();
+      DialogHelper.closeDialog(this.loadingDialogId)
       ToastUtil.showToast('内嵌音乐标签失败')
       console.log('onecold 内嵌音乐标签失败');
     }
@@ -7586,7 +7569,7 @@ export struct LocalMusic {
       }
 
     }
-    this.closeLoadingDialog();
+    DialogHelper.closeDialog(this.loadingDialogId)
   }
 
   @Builder
@@ -9812,7 +9795,7 @@ export struct LocalMusic {
       .setTextSize(15)
       .setCacheSize(4)
       .setTextColor("#FFFFFF")
-      .setHighlightColor("#FFFFFF")
+      .setHighlightColor(this.currentHighLightLyricColor)
       .setHighlightScale(1.2)
       .setEmptyHint("")
       .setAlignMode('center')
@@ -9846,183 +9829,137 @@ export struct LocalMusic {
     this.lyricControllerSingle.setLyric(null)
     let jiaMilyricPath = lyricPath.substring(0, lyricPath.lastIndexOf('.')) + '.lrcc'
     console.log("onecold lyricPath =" + lyricPath)
+
+    // 1. 检查内嵌歌词
     let neiqianLrc = ''
     let lyContent = this.currentSong?.lyricContent
-    if(lyContent&&StrUtil.isNotEmpty(lyContent)){//取数据库里面的歌词lyContent
+    if(lyContent && StrUtil.isNotEmpty(lyContent)){
       neiqianLrc = lyContent
     }
-    if (StrUtil.isNotEmpty(neiqianLrc) && !isOnLineAndToast&&!isLocal&&!isLocal) {
+
+    if (StrUtil.isNotEmpty(neiqianLrc) && !isOnLineAndToast && !isLocal) {
       console.log("onecold 找到内嵌歌词 neiqianLrc =" + neiqianLrc)
-      //赋值给this.lyricContent,播控中心才可以显示歌词
-      this.lyricContent = neiqianLrc
-      // 将文件内容按行分割成字符串数组
-      let lines = neiqianLrc.split('\n').map(line => line.trim());
-      // 3.解析歌词
-      let lyric = this.parser.parse(lines);
-      // 4.设置歌词
-      this.lyricController.setLyric(lyric);
-      this.lyricControllerXF.setLyric(lyric)
-      this.lyricControllerSingle.setLyric(lyric)
+      // Navidrome JSON歌词转换
+      if (this.currentSong && isNavidromeType(this.currentSong.type)) {
+        const convertedLyric = LyricUtil.convertNavidromeJsonLyricToLrc(neiqianLrc);
+        if (convertedLyric) {
+          neiqianLrc = convertedLyric;
+          console.log("onecold Navidrome歌词转换成功");
+        }
+      }
+      this.setLyricToControllers(neiqianLrc);
       return
-
     }
 
+    // 2. 尝试从服务器获取歌词 (Navidrome/Jellyfin/Emby)
+    if (this.currentSong && (isJellyfinType(this.currentSong.type)|| isEmbyType(this.currentSong.type))) {
+      const manager = RemoteDriveManager.getInstance();
+      const getAccount = (accountId: string) => manager.getWebDavAccountById(accountId);
+
+      // 将currentSong转换为SongData接口
+      const songData: SongData = {
+        webdav_account_id: this.currentSong.webdav_account_id,
+        artist: this.currentSong.artist,
+        name: this.currentSong.name,
+        webdav_id: this.currentSong.webdav_id,
+        lyricIndex: this.currentSong.lyricIndex,
+        type: this.currentSong.type
+      };
 
-    if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath) && !FileUtil.accessSync(jiaMilyricPath))
-      && !Utility.isVideoByExtension(this.videoUrl))) {
-      if (this.currentSong !== undefined) {
-        // ToastUtil.showShort('正在为你搜索在线歌词!')
-        //判断是不是赞助会员,是会员的话才开启歌词功能
-        // if (!this.isDebug) {
-        // if (!Utility.isNoble()) {
-        //     LogUtil.debug("onecold 不是赞助会员")
-        //     return
-        //   }
-        // if(!Utility.isPassInstallTime(33)){
-        //     LogUtil.debug("onecold not pass time")
-        //     // LogUtil.debug("onecold 用户安装app没超过12天" )
-        //     return
-        // }
-        // }
-
-        let artist = this.currentSong?.artist
-        if (artist === undefined) {
-          artist = ''
-        }
-        if (isApi2 === undefined) {
-          isApi2 = false
-        }
-        NetAxiosUtil.getLyric(this.name, artist, isApi2).then((res) => {
-
-          // LogUtil.debug("onecold res =" + res)
-          if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') {
-
-            if (StrUtil.isNotEmpty(res)) {
-              this.lyricContent = res
-              let lines = res.split('\n').map(line => line.trim());
-
-              // 3.解析歌词
-              let lyric = this.parser.parse(lines);
-              // 4.设置歌词
-              this.lyricController.setLyric(lyric);
-              this.lyricControllerXF.setLyric(lyric)
-              this.lyricControllerSingle.setLyric(lyric)
-              this.showSingleLyric = true
-              // 4.保存歌词文件lyric到本地
-              //2025年8月28日歌词不再加密下载
-              this.saveDataToFile(res, lyricPath, false)
-
-
-            } else {
-              this.lyricController.setLyric(null)
-              this.lyricControllerXF.setLyric(null)
-              this.lyricControllerSingle.setLyric(null)
-              if (isOnLineAndToast) {
-                ToastUtil.showShort('未获取到歌词!')
-              }
-            }
-
-
-          } else {
-            this.lyricController.setLyric(null)
-            this.lyricControllerXF.setLyric(null)
-            this.lyricControllerSingle.setLyric(null)
-            if (isOnLineAndToast) {
-              ToastUtil.showShort('未获取到歌词!')
-            }
-          }
-
+      let serverLyric = '';
+      if (!serverLyric && isJellyfinType(songData.type)) {
+        serverLyric = await lyricService.fetchJellyfinLyric(songData, getAccount);
+      }
+      if (!serverLyric && isEmbyType(songData.type)) {
+        serverLyric = await lyricService.fetchEmbyLyric(songData, getAccount);
+      }
 
-        })
-      } else {
-        this.lyricController.setLyric(null)
-        this.lyricControllerXF.setLyric(null)
-        this.lyricControllerSingle.setLyric(null)
-        if (isOnLineAndToast) {
-          ToastUtil.showShort('未获取到歌词!')
-        }
+      if (serverLyric) {
+        this.setLyricToControllers(serverLyric, lyricPath);
+        return;
       }
+    }
 
-      return
+    // 3. 尝试在线歌词API
+    if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath) && !FileUtil.accessSync(jiaMilyricPath))
+      && !Utility.isVideoByExtension(this.videoUrl))) {
+      this.fetchOnlineLyric(lyricPath, isOnLineAndToast || false, isApi2);
+      return;
     }
 
+    // 4. 从本地文件读取歌词
+    await this.loadLyricFromFile(lyricPath, jiaMilyricPath, isLocal || false);
+  }
+
+  /**
+   * 从本地文件加载歌词
+   */
+  private async loadLyricFromFile(lyricPath: string, jiaMilyricPath: string, isLocal: boolean): Promise<void> {
     try {
       let isJiaMi = false;
-      let realLyricPath = lyricPath
+      let realLyricPath = lyricPath;
+
       if (FileUtil.accessSync(jiaMilyricPath)) {
-        isJiaMi = true
-        realLyricPath = jiaMilyricPath
+        isJiaMi = true;
+        realLyricPath = jiaMilyricPath;
         console.info("onecold 找到加密本地歌词 ");
       } else {
-        isJiaMi = false
-        realLyricPath = lyricPath
-
+        isJiaMi = false;
+        realLyricPath = lyricPath;
         console.info("onecold 找到本地歌词 ");
       }
-      // 3.读取文件内容并指定编码为 UTF-8
-      let file = fs.openSync(realLyricPath,  fs.OpenMode.READ_ONLY);
+
+      // 读取文件内容
+      let file = fs.openSync(realLyricPath, fs.OpenMode.READ_ONLY);
       const stat = await fileIo.stat(file.fd);
       const arrayBuffer = new ArrayBuffer(stat.size);
+
       fs.read(file.fd, arrayBuffer)
         .then((readLen: number) => {
           console.info("read file data succeed");
-          // let buf = buffer.from(arrayBuffer, 0, readLen);
-
           let buf = new Uint8Array(arrayBuffer, 0, readLen);
-          // 检查 buf 是否为空
+
           if (buf.length === 0) {
             throw new Error("Buffer is empty after reading the file");
           }
+
           try {
             // 检测文件编码
-            // let detectedEncoding = chardet.detect(buf);
-            // console.info("onecoldT Detected encoding: " + detectedEncoding);
-            let detectedEncoding = this.detect(arrayBuffer)
+            let detectedEncoding = this.detect(arrayBuffer);
             console.info("onecoldT Detected encoding: " + detectedEncoding);
+
             // 使用检测到的编码解码,解决中文乱码问题
             let textDecoder = new util.TextDecoder(detectedEncoding || 'utf-8');
             console.info("onecoldT Detected textDecoder: " + textDecoder);
             let fileContent = textDecoder.decode(buf);
             console.info(`onecoldT The content of file1: ${fileContent}`);
-            //解密
+
+            // 解密
             if (isJiaMi) {
-              fileContent = StrUtil.unit8ArrayToStr(Base64Util.decodeSync(fileContent))
+              fileContent = StrUtil.unit8ArrayToStr(Base64Util.decodeSync(fileContent));
             }
+
             console.info(`onecold The content of file2: ${fileContent}`);
-            this.lyricContent = fileContent
-            if(isLocal){//编辑标签选择本地歌词需要赋值给this.lyricConStr
-              this.lyricConStr = fileContent
+            this.lyricContent = fileContent;
+
+            if(isLocal){
+              // 编辑标签选择本地歌词需要赋值给this.lyricConStr
+              this.lyricConStr = fileContent;
             }
-            // 将文件内容按行分割成字符串数组
-            let lines = fileContent.split('\n').map(line => line.trim());
-            console.info(`onecold The content of file: ${fileContent}`);
-            // 3.解析歌词
-            let lyric = this.parser.parse(lines);
-
-            // 4.设置歌词
-            this.lyricController.setLyric(lyric);
-            this.lyricControllerXF.setLyric(lyric)
-            this.lyricControllerSingle.setLyric(lyric)
-
-            this.showSingleLyric = true
+
+            this.setLyricToControllers(fileContent);
             console.info(`The content of file: ${fileContent}`);
           } catch (chardetError) {
             console.error("chardet.detect failed with error: " + chardetError.message);
-            this.lyricController.setLyric(null);
-            this.lyricControllerXF.setLyric(null)
-            this.lyricControllerSingle.setLyric(null)
+            this.clearLyricControllers();
           }
         })
         .catch((err: BusinessError) => {
-          this.lyricController.setLyric(null)
-          this.lyricControllerXF.setLyric(null)
-          this.lyricControllerSingle.setLyric(null)
+          this.clearLyricControllers();
           console.error("read file data failed with error message: " + err.message + ", error code: " + err.code);
         })
         .catch((err: Error) => {
-          this.lyricController.setLyric(null)
-          this.lyricControllerXF.setLyric(null)
-          this.lyricControllerSingle.setLyric(null)
+          this.clearLyricControllers();
           console.error("read file 2 data failed with error message: " + err.message);
         })
         .finally(() => {
@@ -10030,12 +9967,9 @@ export struct LocalMusic {
         });
 
     } catch (error) {
-      this.lyricController.setLyric(null)
-      this.lyricControllerSingle.setLyric(null)
-      this.lyricControllerXF.setLyric(null)
+      this.clearLyricControllers();
       Logger.error(TAG, 'init Lyric failed with err: ' + JSON.stringify(error));
     }
-
   }
 
   detect(data: ArrayBuffer): string {
@@ -10053,6 +9987,70 @@ export struct LocalMusic {
     return detected;
   }
 
+  /**
+   * 设置歌词到所有控制器
+   * @param lyricText 歌词文本
+   * @param savePath 可选,如果提供则保存到本地文件
+   */
+  private setLyricToControllers(lyricText: string, savePath?: string): void {
+    if (StrUtil.isEmpty(lyricText)) {
+      this.clearLyricControllers();
+      return;
+    }
+
+    this.lyricContent = lyricText;
+    let lines = lyricText.split('\n').map(line => line.trim());
+    let lyric = this.parser.parse(lines);
+
+    this.lyricController.setLyric(lyric);
+    this.lyricControllerXF.setLyric(lyric);
+    this.lyricControllerSingle.setLyric(lyric);
+    this.showSingleLyric = true;
+
+    // 如果提供了保存路径,保存歌词到本地
+    if (savePath) {
+      this.saveDataToFile(lyricText, savePath, false);
+    }
+  }
+
+  /**
+   * 清空所有歌词控制器
+   */
+  private clearLyricControllers(): void {
+    this.lyricController.setLyric(null);
+    this.lyricControllerXF.setLyric(null);
+    this.lyricControllerSingle.setLyric(null);
+  }
+
+  /**
+   * 从在线API获取歌词
+   */
+  private fetchOnlineLyric(lyricPath: string, isOnLineAndToast: boolean, isApi2?: boolean): void {
+    if (!this.currentSong) {
+      this.clearLyricControllers();
+      if (isOnLineAndToast) {
+        ToastUtil.showShort('未获取到歌词!');
+      }
+      return;
+    }
+
+    let artist = this.currentSong.artist || '';
+    if (isApi2 === undefined) {
+      isApi2 = false;
+    }
+
+    NetAxiosUtil.getLyric(this.name, artist, isApi2).then((res) => {
+      if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') {
+        this.setLyricToControllers(res, lyricPath);
+      } else {
+        this.clearLyricControllers();
+        if (isOnLineAndToast) {
+          ToastUtil.showShort('未获取到歌词!');
+        }
+      }
+    });
+  }
+
   //初始化悬浮歌词的自定义设置
   initPipLyricSetting() {
     this.lyricControllerXF
@@ -10187,24 +10185,38 @@ export struct LocalMusic {
 
 
   // 定义开始旋转的方法
-  // 定时器用于一百毫秒执行一次旋转角度
+  // 使用动画系统优化旋转性能,避免 setInterval 频繁更新导致卡顿
   @State timer: number = 0
   @State rotateAngle: number = 0
+  @State isRotationRunning: boolean = false // 旋转动画是否正在运行
 
   //封面旋转动画
   animationRoFun() {
-
     if (this.isPlaying && !this.isCoverRectangle) {
-      this.timer = setInterval(() => {
-        this.rotateAngle += 1
-      }, 100)
+      // 启动旋转动画
+      if (!this.isRotationRunning) {
+        this.isRotationRunning = true
+        this.startContinuousRotation()
+      }
       this.startRotation()
     } else {
+      // 停止旋转动画
+      this.isRotationRunning = false
       clearInterval(this.timer)
       this.stopRotation()
     }
+  }
 
-
+  // 使用持续的动画进行旋转,避免频繁状态更新
+  private startContinuousRotation() {
+    clearInterval(this.timer)
+    // 改为每 1000ms 更新 36 度(10秒一圈),减少更新频率
+    // 不使用 % 360 取模,让角度持续累加,避免反转
+    this.timer = setInterval(() => {
+      if (this.isRotationRunning) {
+        this.rotateAngle = this.rotateAngle + 36
+      }
+    }, 1000)
   }
 
   // 定义开始旋转的方法
@@ -10941,7 +10953,7 @@ export struct LocalMusic {
   }
 
   @State is_auto_hide_progress: boolean = false //手机横屏自动隐藏播放进退条,点击屏幕可以显示,倒计时4秒后又自动隐藏
-  @State isCoverRectangle: boolean = false
+
   // 添加控制缩放的状态变量
   @State scaleValueImage: number = 1
   @State scaleValueText: number = 1
@@ -11050,6 +11062,11 @@ export struct LocalMusic {
           z: 1,
           angle: this.rotateAngle
         })
+        .animation({
+          duration: 1000,  // 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1
+        })
         .shadow({
           radius: 22,
           type: ShadowType.BLUR,
@@ -11084,8 +11101,9 @@ export struct LocalMusic {
           centerY: "50%",
         })
         .animation({
-          duration: 100,
-          curve: Curve.Linear
+          duration: 1000,  // 改为 1000ms 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1  // 只执行一次,下一次更新会触发新的动画
         })
         .shadow({
           radius: 22,
@@ -11118,6 +11136,11 @@ export struct LocalMusic {
           z: 1,
           angle: this.rotateAngle
         })
+        .animation({
+          duration: 1000,  // 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1
+        })
         .onClick(() => {
           this.isMusicBGCover = !this.isMusicBGCover
         })
@@ -11864,38 +11887,6 @@ export struct LocalMusic {
       })
 
 
-      if (!isPip) {
-        Row() {
-          Text(`歌词模糊:`)
-            .fontSize(14)
-            .fontColor(Color.White)
-          Slider({
-            value: isPip ? this.blurDegreePip : this.blurDegree,
-            min: 0,
-            max: 5,
-            step: 0.1,
-            style: SliderStyle.OutSet
-          })
-            .blockColor(this.themeColor)
-            .trackColor($r('app.color.speed_text_color'))
-            .selectedColor(Color.White)
-            .trackThickness(6)
-            .onChange((value: number) => {
-              this.setBlurDegree(value, isPip)
-
-            })
-            .width('76%')
-
-
-        }
-        .margin({
-          left: 20,
-          right: 15,
-          top: 5,
-          bottom: 15
-        })
-      }
-
       Row() {
         this.pushLyricButton($r('app.media.cut_current'), 0, '本地歌词')
         this.pushLyricButton($r('app.media.white_search'), 1, '获取歌词')
@@ -13948,8 +13939,8 @@ export struct LocalMusic {
     let mOnErrorListener: OnErrorListener = {
       onError: (what: number, extra: number) => {
         this.stopProgressTask();
-        LogUtils.getInstance().LOGI("heanup OnErrorListener-->go:" + what + "===" + extra + " this.videoUrl=" + this.videoUrl)
-        LogUtils.getInstance().LOGI('heanup 播放错误,歌曲详情:' + JSON.stringify(this.currentSong))
+        console.info("heanup OnErrorListener-->go:" + what + "===" + extra + " this.videoUrl=" + this.videoUrl)
+        console.info('heanup 播放错误,歌曲详情:' + JSON.stringify(this.currentSong))
         // 检查是否为WebDAV播放错误
         let isWebDavError = false;
         if (this.currentSong && isRemoteCloudType(this.currentSong.type) && this.currentSong.filePath) {
@@ -13965,11 +13956,11 @@ export struct LocalMusic {
         }
 
         if (isWebDavError) {
-          Logger.error(`heanup WebDAV播放错误 - what: ${what}, extra: ${extra}, URL: ${this.videoUrl}`);
+          console.info(`heanup WebDAV播放错误 - what: ${what}, extra: ${extra}, URL: ${this.videoUrl}`);
 
           // 播放失败时清空实例变量中的WebDAV认证信息,防止错误账户持续使用
           // this.currentWebDavAuthInfo = null;
-          Logger.info('heanup WebDAV播放失败,保留认证信息用于后续歌曲切换');
+          console.info('heanup WebDAV播放失败,保留认证信息用于后续歌曲切换');
 
           // 根据错误代码提供更具体的错误信息
           let errorMessage = "WebDAV播放失败";
@@ -13986,14 +13977,14 @@ export struct LocalMusic {
           ToastUtil.showToast(errorMessage);
         } else {
           if (StrUtil.isNotEmpty(this.videoUrl) &&
-            !this.videoUrl.startsWith('http://') &&
-            !this.videoUrl.startsWith('https://') &&
+            !this.videoUrl.startsWith('http') &&
+            this.currentSong &&this.currentSong.type==CommonConstants.TYPE_LOCAL&&
             !FileUtil.accessSync(this.videoUrl)) {
-            ToastUtil.showToast(Utility.resourceToString(getContext(), $r('app.string.file_not_exist')))
+            console.info('heanup 播放.文件不存在');
+            ToastUtil.showToast(Utility.resourceToString(this.context, $r('app.string.file_not_exist')))
           }
 
         }
-
         that.hideLoadIng();
       }
     }

+ 151 - 69
entry/src/main/ets/view/NavidromePage.ets

@@ -90,7 +90,7 @@ export struct NavidromePage {
   @State isPlaylistPageLoading: boolean = false;
 
   // 新增:详情视图状态
-  @State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
+  @Link isDetailView: boolean; // 是否在艺术家/专辑详情视图
   @State previousTab: number = 0; // 进入详情视图前的标签页索引
   @StorageProp('isDarkMode') isDarkMode: boolean = false;
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
@@ -229,6 +229,19 @@ export struct NavidromePage {
       this.initSetting()
     });
 
+    // 监听手势返回事件
+    let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_NAVID }
+    emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => {
+      console.info('heanup', 'NavidromePage 收到 EVENT_SWIPE_BACK_NAVID 事件');
+      // 如果在详情视图模式,退出详情视图
+      if (this.isDetailView) {
+        this.getUIContext().animateTo({ duration: 555 }, () => {
+          this.isDetailView = false;
+          this.clearFilter();
+        })
+      }
+    });
+
     this.refreshNavidromeData();
   }
 
@@ -421,6 +434,17 @@ export struct NavidromePage {
         return;
       }
       this.allVideos = [...this.allVideos, ...videos];
+      // 打印前20条数据
+      // const previewCount = Math.min(20, this.allVideos.length);
+      // console.info(`onecold allVideos: ${this.allVideos.length}, 打印前${previewCount}条:`);
+      // for (let i = 0; i < previewCount; i++) {
+      //   const all = this.allVideos[i];
+      //   console.info(`onecold allVideos[${i}]:`, JSON.stringify({
+      //     id: all.id,
+      //     name: all.name,
+      //     pixelMapPath: all.pixelMapPath
+      //   }));
+      // }
       this.songNextStart = response.nextStart;
       void ServerLogUtil.info('NavidromeLoad', `歌曲列表追加: 本次 ${videos.length} 首, 总数 ${this.allVideos.length}`);
     } finally {
@@ -453,20 +477,20 @@ export struct NavidromePage {
         return artist;
       });
       this.artists = [...this.artists, ...processed];
-      console.info('onecold  帮我打印这个artists的前20条数据');
+      // console.info('onecold  帮我打印这个artists的前20条数据');
       // 打印前20条艺术家数据
-      const previewCount = Math.min(20, this.artists.length);
-      console.info(`onecold artists总数: ${this.artists.length}, 打印前${previewCount}条:`);
-      for (let i = 0; i < previewCount; i++) {
-        const artist = this.artists[i];
-        console.info(`onecold artist[${i}]:`, JSON.stringify({
-          id: artist.id,
-          name: artist.name,
-          albumCount: artist.albumCount,
-          songCount: artist.songCount,
-          coverUrl: artist.coverUrl
-        }));
-      }
+      // const previewCount = Math.min(20, this.artists.length);
+      // console.info(`onecold artists总数: ${this.artists.length}, 打印前${previewCount}条:`);
+      // for (let i = 0; i < previewCount; i++) {
+      //   const artist = this.artists[i];
+      //   console.info(`onecold artist[${i}]:`, JSON.stringify({
+      //     id: artist.id,
+      //     name: artist.name,
+      //     albumCount: artist.albumCount,
+      //     songCount: artist.songCount,
+      //     coverUrl: artist.coverUrl
+      //   }));
+      // }
       this.artistNextStart = response.nextStart;
       void ServerLogUtil.info('ArtistCover', `艺术家列表追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`);
     } finally {
@@ -773,42 +797,47 @@ export struct NavidromePage {
     let generatedCoverCount = 0;
     let failedCoverCount = 0;
 
-    void ServerLogUtil.info('AlbumCover', `开始处理 ${albums.length} 张专辑的封面`);
+    void ServerLogUtil.info('NavidromePageCover', `📀 开始处理 ${albums.length} 张专辑的封面 - 账号: ${ServerLogUtil.sanitizeAccount(account)}`);
 
     for (let i = 0; i < albums.length; i++) {
       const album = albums[i];
+      void ServerLogUtil.debug('NavidromePageCover', `处理专辑 [${i + 1}/${albums.length}] - id: ${album.id}, name: ${album.name}`);
+
       tasks.push((async () => {
         try {
-          // 尝试获取直接嵌入的封面路径
-          const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath);
-          if (directUrl) {
-            map.set(album.id, directUrl);
-            directCoverCount++;
-            void ServerLogUtil.debug('AlbumCover', `专辑 ${album.name} 使用直接封面: ${directUrl}`);
-            return;
-          }
+          // 注意: embedArtPath 和 coverArtPath 可能指向音频文件而非封面图片
+          // 所以这里不能直接使用,需要通过 buildCoverUrl 生成正确的封面URL
+          // const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath);
+          // if (directUrl) {
+          //   map.set(album.id, directUrl);
+          //   directCoverCount++;
+          //   void ServerLogUtil.info('NavidromePageCover', `✅ 专辑使用直接封面 - name: ${album.name}, url: ${directUrl}`);
+          //   return;
+          // }
 
           // 生成封面URL
           const coverId = album.coverArt ?? album.coverArtId ?? (album.id ? `al-${album.id}` : undefined);
+          void ServerLogUtil.debug('NavidromePageCover', `专辑封面ID - name: ${album.name}, coverArt: ${album.coverArt}, coverArtId: ${album.coverArtId}, 最终coverId: ${coverId}`);
+
           const url = await this.buildCoverUrl(account, coverId);
           if (url) {
             map.set(album.id, url);
             generatedCoverCount++;
-            void ServerLogUtil.debug('AlbumCover', `专辑 ${album.name} 生成封面URL: ${coverId} -> ${url}`);
+            void ServerLogUtil.info('NavidromePageCover', `✅ 专辑生成封面成功 - name: ${album.name}, coverId: ${coverId}, url: ${url}`);
           } else {
             failedCoverCount++;
-            void ServerLogUtil.warn('AlbumCover', `专辑 ${album.name} 无可用封面: ${JSON.stringify({coverArt: album.coverArt, coverArtId: album.coverArtId, id: album.id})}`);
+            void ServerLogUtil.error('NavidromePageCover', `❌ 专辑封面失败 - name: ${album.name}, coverId: ${coverId}, 原始数据: ${JSON.stringify({coverArt: album.coverArt, coverArtId: album.coverArtId, id: album.id})}`);
           }
         } catch (error) {
           failedCoverCount++;
-          void ServerLogUtil.warn('AlbumCover', `专辑 ${album.name} 封面解析失败: ${(error as Error).message}`);
+          void ServerLogUtil.error('NavidromePageCover', `❌ 专辑封面解析异常 - name: ${album.name}, error: ${(error as Error).message}`);
         }
       })());
     }
 
     await Promise.all(tasks);
 
-    void ServerLogUtil.info('AlbumCover', `专辑封面处理完成: 直接嵌入 ${directCoverCount}, 生成URL ${generatedCoverCount}, 失败 ${failedCoverCount}`);
+    void ServerLogUtil.info('NavidromePageCover', `📊 专辑封面处理完成 - 总数: ${albums.length}, 直接嵌入: ${directCoverCount}, 生成URL: ${generatedCoverCount}, 失败: ${failedCoverCount}`);
     return map;
   }
 
@@ -819,10 +848,12 @@ export struct NavidromePage {
     let generatedCoverCount = 0;
     let failedCoverCount = 0;
 
-    void ServerLogUtil.info('ArtistCover', `开始处理 ${artists.length} 位艺术家的封面`);
+    void ServerLogUtil.info('NavidromePageCover', `🎤 开始处理 ${artists.length} 位艺术家的封面 - 账号: ${ServerLogUtil.sanitizeAccount(account)}`);
 
     for (let i = 0; i < artists.length; i++) {
       const artist = artists[i];
+      void ServerLogUtil.debug('NavidromePageCover', `处理艺术家 [${i + 1}/${artists.length}] - id: ${artist.id}, name: ${artist.name}`);
+
       tasks.push((async () => {
         try {
           // 尝试获取已有图片URL
@@ -830,31 +861,33 @@ export struct NavidromePage {
           if (directUrl) {
             map.set(artist.id, directUrl);
             directCoverCount++;
-            void ServerLogUtil.debug('ArtistCover', `艺术家 ${artist.name} 使用已有图片: ${directUrl}`);
+            void ServerLogUtil.info('NavidromePageCover', `✅ 艺术家使用已有图片 - name: ${artist.name}, url: ${directUrl}`);
             return;
           }
 
           // 生成封面URL
           const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined);
+          void ServerLogUtil.debug('NavidromePageCover', `艺术家封面ID - name: ${artist.name}, coverArt: ${artist.coverArt}, coverArtId: ${artist.coverArtId}, 最终coverId: ${coverId}`);
+
           const url = await this.buildCoverUrl(account, coverId, 256);
           if (url) {
             map.set(artist.id, url);
             generatedCoverCount++;
-            void ServerLogUtil.debug('ArtistCover', `艺术家 ${artist.name} 生成封面URL: ${coverId} -> ${url}`);
+            void ServerLogUtil.info('NavidromePageCover', `✅ 艺术家生成封面成功 - name: ${artist.name}, coverId: ${coverId}, url: ${url}`);
           } else {
             failedCoverCount++;
-            void ServerLogUtil.warn('ArtistCover', `艺术家 ${artist.name} 无可用封面: ${JSON.stringify({coverArt: artist.coverArt, coverArtId: artist.coverArtId, id: artist.id})}`);
+            void ServerLogUtil.error('NavidromePageCover', `❌ 艺术家封面失败 - name: ${artist.name}, coverId: ${coverId}, 原始数据: ${JSON.stringify({coverArt: artist.coverArt, coverArtId: artist.coverArtId, id: artist.id})}`);
           }
         } catch (error) {
           failedCoverCount++;
-          void ServerLogUtil.warn('ArtistCover', `艺术家 ${artist.name} 封面解析失败: ${(error as Error).message}`);
+          void ServerLogUtil.error('NavidromePageCover', `❌ 艺术家封面解析异常 - name: ${artist.name}, error: ${(error as Error).message}`);
         }
       })());
     }
 
     await Promise.all(tasks);
 
-    void ServerLogUtil.info('ArtistCover', `艺术家封面处理完成: 直接链接 ${directCoverCount}, 生成URL ${generatedCoverCount}, 失败 ${failedCoverCount}`);
+    void ServerLogUtil.info('NavidromePageCover', `📊 艺术家封面处理完成 - 总数: ${artists.length}, 直接链接: ${directCoverCount}, 生成URL: ${generatedCoverCount}, 失败: ${failedCoverCount}`);
     return map;
   }
 
@@ -1109,53 +1142,95 @@ export struct NavidromePage {
   }
 
   private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount): Promise<string | undefined> {
-    if (this.isNavidromeAccount(account) && song.albumId) {
-      const albumCover = song.albumId ? this.albumCoverLookup.get(song.albumId) : undefined;
-      if (albumCover) {
-        return albumCover;
-      }
-    }
+    void ServerLogUtil.debug('NavidromePageCover', `🎵 解析歌曲封面 - title: ${song.title}, albumId: ${song.albumId}, id: ${song.id}`);
+
+    // 注释掉这个代码可以解决NavidRome部分账号没有封面问题
+    // if (this.isNavidromeAccount(account) && song.albumId) {
+    //   const albumCover = song.albumId ? this.albumCoverLookup.get(song.albumId) : undefined;
+    //   if (albumCover) {
+    //     void ServerLogUtil.debug('NavidromePageCover', `✅ 歌曲使用专辑封面缓存 - title: ${song.title}, albumCover: ${albumCover}`);
+    //     return albumCover;
+    //   }
+    // }
+
     if (!this.isNavidromeAccount(account)) {
       const fallbackId = song.albumId ?? song.id;
+      void ServerLogUtil.debug('NavidromePageCover', `歌曲使用非Navidrome账号封面 - title: ${song.title}, fallbackId: ${fallbackId}`);
       return this.buildCoverUrl(account, fallbackId);
     }
+
     const directUrl = this.resolveEmbedCover(account, song.embedArtPath ?? song.coverArtPath);
     if (directUrl) {
+      void ServerLogUtil.info('NavidromePageCover', `✅ 歌曲使用直接封面 - title: ${song.title}, url: ${directUrl}`);
       return directUrl;
     }
+
     const coverId = song.coverArt ?? song.coverArtId ?? song.id;
+    void ServerLogUtil.debug('NavidromePageCover', `歌曲生成封面URL - title: ${song.title}, coverArt: ${song.coverArt}, coverArtId: ${song.coverArtId}, 最终coverId: ${coverId}`);
     return this.buildCoverUrl(account, coverId);
   }
 
   private async buildCoverUrl(account: WebDavAccount, coverId?: string, size: number = 300): Promise<string | undefined> {
+    void ServerLogUtil.info('NavidromePageCover', `开始构建封面URL - coverId: ${coverId}, size: ${size}, 账号: ${ServerLogUtil.sanitizeAccount(account)}`);
+
     if (!coverId || coverId.trim().length === 0) {
+      void ServerLogUtil.warn('NavidromePageCover', `封面ID为空,跳过构建 - coverId: "${coverId}"`);
       return undefined;
     }
+
     const normalizedId = coverId.trim();
     const cacheKey = `${normalizedId}_${size}`;
+
     if (this.isNavidromeAccount(account)) {
       const cached = this.coverUrlCache.get(cacheKey);
       if (cached) {
-        void ServerLogUtil.debug('CoverCache', `封面URL缓存命中: ${cacheKey} -> ${cached}`);
+        void ServerLogUtil.info('NavidromePageCover', `✅ 缓存命中 - coverId: ${coverId}, size: ${size}, url: ${cached}`);
         return cached;
       }
     }
 
-    void ServerLogUtil.debug('CoverCache', `生成封面URL: ${coverId} (尺寸: ${size})`);
+    void ServerLogUtil.info('NavidromePageCover', `⚡ 调用API生成封面URL - coverId: ${coverId}, size: ${size}`);
     let url: string | undefined = undefined;
-    if (this.isNavidromeAccount(account)) {
-      url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
-    } else if (this.isJellyfinAccount(account)) {
-      url = await jellyfinApi.buildPrimaryImageUrl(account, normalizedId, size, size);
-    } else if (this.isEmbyAccount(account)) {
-      url = await embyApi.buildPrimaryImageUrl(account, normalizedId, size, size);
-    }
-    if (url && this.isNavidromeAccount(account)) {
-      this.coverUrlCache.set(cacheKey, url);
-      void ServerLogUtil.debug('CoverCache', `封面URL已缓存: ${cacheKey} -> ${url}`);
-    } else {
-      void ServerLogUtil.warn('CoverCache', `封面URL生成失败: ${coverId} (尺寸: ${size})`);
+
+    try {
+      if (this.isNavidromeAccount(account)) {
+        void ServerLogUtil.debug('NavidromePageCover', `[开始] 调用 navidromeApi.buildCoverArtUrl - coverId: ${normalizedId}, size: ${size}`);
+        void ServerLogUtil.debug('NavidromePageCover', `[账号信息] host=${account.host}, port=${account.port}, enableHttps=${account.enableHttps}`);
+        void ServerLogUtil.debug('NavidromePageCover', `[账号信息] basePath=${account.navidromeBasePath}`);
+
+        url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
+
+        void ServerLogUtil.debug('NavidromePageCover', `[完成] navidromeApi.buildCoverArtUrl 返回 - coverId: ${normalizedId}, 返回值: "${url}"`);
+
+        if (url) {
+          void ServerLogUtil.info('NavidromePageCover', `✅ API返回URL成功 - coverId: ${coverId}, url: ${url}`);
+        } else {
+          void ServerLogUtil.warn('NavidromePageCover', `⚠️ API返回空值 - coverId: ${coverId}`);
+        }
+      } else if (this.isJellyfinAccount(account)) {
+        void ServerLogUtil.debug('NavidromePageCover', `调用 jellyfinApi.buildPrimaryImageUrl - coverId: ${normalizedId}, size: ${size}`);
+        url = await jellyfinApi.buildPrimaryImageUrl(account, normalizedId, size, size);
+      } else if (this.isEmbyAccount(account)) {
+        void ServerLogUtil.debug('NavidromePageCover', `调用 embyApi.buildPrimaryImageUrl - coverId: ${normalizedId}, size: ${size}`);
+        url = await embyApi.buildPrimaryImageUrl(account, normalizedId, size, size);
+      }
+
+      if (url && this.isNavidromeAccount(account)) {
+        this.coverUrlCache.set(cacheKey, url);
+        void ServerLogUtil.info('NavidromePageCover', `✅ 封面URL生成成功并已缓存 - coverId: ${coverId}, url: ${url}`);
+        void ServerLogUtil.debug('NavidromePageCover', `缓存键值 - cacheKey: ${cacheKey}`);
+      } else if (!url) {
+        void ServerLogUtil.error('NavidromePageCover', `❌ 封面URL生成失败(返回空) - coverId: ${coverId}, size: ${size}`);
+      } else {
+        void ServerLogUtil.info('NavidromePageCover', `✅ 封面URL生成成功(非Navidrome账号) - coverId: ${coverId}, url: ${url}`);
+      }
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error('NavidromePageCover', `❌ 封面URL生成异常 - coverId: ${coverId}, error: ${err.message}`);
+      void ServerLogUtil.error('NavidromePageCover', `错误类型: ${err.name || 'Unknown'}`);
+      void ServerLogUtil.error('NavidromePageCover', `错误堆栈: ${err.stack || '无'}`);
     }
+
     return url;
   }
 
@@ -1234,6 +1309,13 @@ export struct NavidromePage {
   private convertSongToVideoItem(song: NavidromeRestSong, account: WebDavAccount, coverUrl?: string): VideoItem {
     const title = song.title ?? Constants.UNKNOWN_TITLE;
     const libraryInfo = this.resolveLibraryInfo(account);
+
+    // 调试日志:记录从playlist或其他API获取的歌曲的id字段
+    void ServerLogUtil.debug('SongConvert', `转换歌曲: ${title}`);
+    void ServerLogUtil.debug('SongConvert', `- song.id: ${song.id}`);
+    void ServerLogUtil.debug('SongConvert', `- song.artistId: ${song.artistId}`);
+    void ServerLogUtil.debug('SongConvert', `- song.albumId: ${song.albumId}`);
+
     const videoItem = new VideoItem(
       title,
       song.id,
@@ -1259,7 +1341,8 @@ export struct NavidromePage {
     videoItem.navArtistId = song.artistId;
     videoItem.navAlbumId = song.albumId;
     videoItem.pixelMapPath = coverUrl;
-    
+    videoItem.lyricContent = song.lyrics;
+
     // 调试日志:检查歌曲的 albumId 和 artistId
     if (this.allVideos.length < 3) {
       Logger.info('heanup', `歌曲 ${title}: artistId=${song.artistId}, albumId=${song.albumId}, artist=${song.artist}, album=${song.album}`);
@@ -1273,6 +1356,11 @@ export struct NavidromePage {
     if (song.contentType) {
       videoItem.mimeType = song.contentType;
     }
+
+    // 记录最终生成的VideoItem路径
+    void ServerLogUtil.debug('SongConvert', `- VideoItem.filePath: ${videoItem.filePath}`);
+    void ServerLogUtil.debug('SongConvert', `- VideoItem.remote_rel_path: ${videoItem.remote_rel_path}`);
+
     return videoItem;
   }
 
@@ -1915,14 +2003,14 @@ export struct NavidromePage {
             .maxLines(1)
             .textOverflow({ overflow: TextOverflow.Ellipsis })
 
-          if (playlist.ownerName) {
-            Text(playlist.ownerName)
-              .fontSize(12)
-              .fontColor($r('app.color.index_tab_font_color'))
-              .opacity(0.5)
-              .maxLines(1)
-              .textOverflow({ overflow: TextOverflow.Ellipsis })
-          }
+          // if (playlist.ownerName) {
+          //   Text(playlist.ownerName)
+          //     .fontSize(12)
+          //     .fontColor($r('app.color.index_tab_font_color'))
+          //     .opacity(0.5)
+          //     .maxLines(1)
+          //     .textOverflow({ overflow: TextOverflow.Ellipsis })
+          // }
         }
         .alignItems(HorizontalAlign.Start)
         .padding({ right: 20 })
@@ -2256,13 +2344,7 @@ export struct NavidromePage {
       } else {
         List({scroller:this.scroller, space: 8 }) {
           // 详情视图模式下显示筛选后的歌曲,否则根据标签页显示对应内容
-          if (this.isDetailView) {
-            ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
-              ListItem() {
-                this.buildSongItem(item, index)
-              }
-            }, (item: VideoItem) => item.id)
-          } else if (this.selectedTab === 0) {
+          if (this.selectedTab === 0||this.isDetailView) {
             ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
               ListItem() {
                 this.buildSongItem(item, index)

+ 1 - 0
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -71,6 +71,7 @@ export class VideoItem  {
   navArtistId?: string;
   navAlbumId?: string;
   baiduFsId?: string // 百度网盘 fs_id
+  webdav_id?: string // Jellyfin/Emby 歌曲/媒体ID
   lyricIndex?: number // Emby 歌词流索引
 
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,

BIN
entry/src/main/resources/base/media/emby.png


BIN
entry/src/main/resources/base/media/jellyfin.png


+ 48 - 2
lib/src/main/ets/bean/LyricLine.ts

@@ -21,8 +21,54 @@ export class LyricLine {
         this.translation = translation || ''
     }
 
-    // 新增方法:判断是否有逐字歌词
+    /**
+     * 判断该行是否包含有效的逐字歌词数据
+     * 判断标准:
+     * 1. words 数组不为空
+     * 2. 每个 word 对象都有有效的开始时间(>= 0)
+     * 3. 每个 word 对象都有有效的时长(> 0)
+     * 4. 每个 word只能包含一个中文字或者一个英文单词
+     * @returns true 表示是逐字歌词格式,false 表示是普通歌词格式
+     */
     hasWords(): boolean {
-        return this.words.length > 0
+        // 如果 words 数组为空,肯定不是逐字歌词
+        if (!this.words || this.words.length === 0) {
+            return false;
+        }
+
+        // 检查是否所有 word 对象都符合逐字歌词的条件
+        for (let i = 0; i < this.words.length; i++) {
+            const word = this.words[i];
+
+            // word 必须存在
+            if (!word || !word.word) {
+                return false;
+            }
+            if(word.word.length==1){
+                return true
+            }
+            // 必须有有效的时间信息
+            if (word.startTime < 0 || word.duration <= 0) {
+                return false;
+            }
+
+
+            // 检查 word 是否只包含一个中文字或一个英文单词
+            const wordText = word.word.trim();
+
+            // 判断是否是单个中文字符
+            const isSingleChineseChar = /^[\u4e00-\u9fa5]$/.test(wordText);
+
+            // 判断是否是单个英文单词(不含空格)
+            const isSingleEnglishWord = /^[a-zA-Z]+$/.test(wordText) && !wordText.includes(' ');
+
+            // 如果既不是单个中文字,也不是单个英文单词,则不是逐字歌词
+            if (!isSingleChineseChar && !isSingleEnglishWord) {
+                return false;
+            }
+        }
+
+        // 所有 word 都符合条件,是逐字歌词
+        return true;
     }
 }

+ 212 - 24
lib/src/main/ets/view/LyricView2.ets

@@ -5,7 +5,7 @@ import { ListAdapter } from '../extensions/ListAdapter';
 import { LyricLine } from '../bean/LyricLine';
 import { LyricWord } from '../bean/LyricWord';
 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"
-import { LengthMetrics } from '@kit.ArkUI';
+import { curves, LengthMetrics } from '@kit.ArkUI';
 
 /**
  * A component to display the lyric with scroll animation.
@@ -190,25 +190,33 @@ export struct LyricView2 {
                             this.NormalLyricLine(item, index)
                         }
 
+                        if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
+                            && this.enableSeek && this.isUserTouching){
+                            this.JumpProgress()
+                        }
 
-                            Text(this.scrollDurationText)
-                                .fontSize(this.textSize)
-                                .fontColor(this.seekUIColor)
-                                .textAlign( TextAlign.End)
-                                .width(100)
-                                .visibility(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
-                                      && this.enableSeek && this.isUserTouching?Visibility.Visible:Visibility.Hidden)
 
                     }
                     .align(Alignment.End)
 
                 }
                 .padding(8)
-                .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
-                    TransitionEffect.scale({ x: 0, y: 0 })  ))
+                // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
+                //     TransitionEffect.scale({ x: 0, y: 0 })  ))
+                .transition(
+                    TransitionEffect
+                        .scale({ x: 0.9, y: 0.9 })
+                        .combine(TransitionEffect
+                            .opacity(0.1)
+                        )
+                        .animation({
+                            duration: 150,
+                            curve: Curve.EaseInOut,
+                        })
+                )
                 .border({ radius: 12 })
-                .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
-                    && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
+                // .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
+                //     && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
                 .onClick(() => {
                     if (this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0) {
                         if (this.seekIndex != -1 && this.seekIndex != this.currentIndex) {
@@ -223,13 +231,21 @@ export struct LyricView2 {
         }
         .width('100%')
         .height('100%')
+        .layoutWeight(1)
         .scrollBar(BarState.Off)
         .fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(20)})
+        .edgeEffect(EdgeEffect.Spring)
+        .contentEndOffset(this.h / 3)
+        .contentStartOffset(this.isUserTouching?this.h / 3:0)
         .cachedCount(this.cacheSize)
-        .transition(TransitionEffect.asymmetric(
-             this.isSingleLine? TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }):
-             TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500 }),
-            TransitionEffect.scale({ x: 0, y: 0 })  ))
+        .animation({
+            curve: curves.springCurve(100, 10, 80, 10),
+            duration: 500
+        })
+        // .transition(TransitionEffect.asymmetric(
+        //      this.isSingleLine? TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }):
+        //      TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500 }),
+        //     TransitionEffect.scale({ x: 0, y: 0 })  ))
         .visibility(this.isLoadingData ? Visibility.Hidden : Visibility.Visible)
         .onScrollIndex((_, __, center) => {
             const now = Date.now()
@@ -283,7 +299,14 @@ export struct LyricView2 {
                 })
                 .fontColor(index == this.currentIndex ? this.textHighlightColor : this.textColor)
                 .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
-                .animation({ duration: this.animDuration, curve: Curve.FastOutSlowIn })
+                .animation({
+                    duration: 150,
+                    curve: Curve.Linear
+                })
+                .blendMode(
+                    index == this.currentIndex ? BlendMode.DST_IN : undefined,
+                    index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+                )
                 .visibility(this.isSingleLine?
                     (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                     :Visibility.Visible)
@@ -298,14 +321,130 @@ export struct LyricView2 {
                             index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
                         .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
                         .margin({ top: 4 })
+                        .animation({
+                            duration: 150,
+                            curve: Curve.Linear
+                        })
+                        .blendMode(
+                            index == this.currentIndex ? BlendMode.DST_IN : undefined,
+                            index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+                        )
                 }
                 .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
             }
         }
+        // 在 Row 上应用渐变
+        .linearGradient(index == this.currentIndex ? {
+            direction: GradientDirection.Right,
+            colors: this.getLyricItemLinearGradient(item, index)
+        } : undefined)
+        .blendMode(
+            index == this.currentIndex ? BlendMode.SRC_OVER : undefined,
+            index == this.currentIndex ? BlendApplyType.OFFSCREEN : undefined
+        )
 
     }
 
+
+
+    /**
+     * 计算卡拉OK渐变 - 同色系从浅到深的平滑过渡
+     */
+    getLyricItemLinearGradient(item: LyricLine, index: number): [ResourceColor, number][] {
+        // 只对当前播放行且包含逐字数据的行应用卡拉OK效果
+        if (index !== this.currentIndex  || item.words.length === 0) {
+            //console.info('heanup', `getLyricItemLinearGradient - 非高亮行或无逐字数据: index=${index}, currentIndex=${this.currentIndex}, hasWords=${item.hasWords()}, wordsCount=${item.words.length}`)
+            return [[Color.White, 0.0], [Color.White, 1.0]]
+        }
+
+        // 计算该行歌词的总时长
+        let lyricDuration: number
+        if (index < this.listAdapter.totalCount() - 1) {
+            const nextLine = this.listAdapter.getData(index + 1)
+            lyricDuration = nextLine.beginTime - item.beginTime
+        } else {
+            // 最后一行,使用 nextTime(如果有)或者估计时长
+            lyricDuration = item.nextTime > item.beginTime ? item.nextTime - item.beginTime : 5000
+        }
+
+        //console.info('heanup', `getLyricItemLinearGradient - index=${index}, lyricDuration=${lyricDuration}, currentMediaPosition=${this.currentMediaPosition}, itemBeginTime=${item.beginTime}`)
+
+        if (lyricDuration <= 0) {
+            console.info('heanup', `getLyricItemLinearGradient - 歌词时长<=0, 返回透明`)
+            return [[Color.Transparent, 0.0], [Color.Transparent, 1.0]]
+        }
+
+        // 计算当前播放进度(0-1之间)
+        let diff = this.currentMediaPosition - item.beginTime
+        let value = diff / lyricDuration
+
+        value = Math.max(0, Math.min(1, value))
+
+        return [[this.textHighlightColor, 0.0],
+            [this.textHighlightColor, value],
+            [this.textColor, value],
+            [this.textColor, 1.0]]
+    }
+
+    /**
+     * 计算逐字歌词的卡拉OK渐变效果
+     * 该方法针对逐字歌词格式,根据当前播放进度和每个字的时间信息计算渐变
+     * @param item 当前歌词行
+     * @param word 当前字的信息
+     * @param index 当前行索引
+     * @returns 渐变颜色数组
+     */
+    getWordByWordLyricLyricItemLinearGradient(item: LyricLine, word: LyricWord, index: number): [ResourceColor, number][] {
+        // 非高亮行或无效数据,返回透明
+        if (index !== this.currentIndex || !word || !word.word) {
+            //console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - 非高亮行或无效word: index=${index}, currentIndex=${this.currentIndex}`)
+            return [[Color.White, 0.0], [Color.White, 1.0]]
+        }
+
+        // 计算该字的播放进度
+        const wordEndTime = word.startTime + word.duration
+        const wordDuration = word.duration
+
+        // 异常情况处理
+        if (wordDuration <= 0) {
+            //console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - word时长<=0: startTime=${word.startTime}, duration=${word.duration}`)
+            // 如果时长无效,检查当前播放位置是否已到达开始时间
+            if (this.currentMediaPosition >= word.startTime) {
+                // 已开始播放,全部高亮
+                return [[this.textHighlightColor, 0.0], [this.textHighlightColor, 1.0]]
+            } else {
+                // 未开始播放,全部普通颜色
+                return [[this.textColor, 0.0], [this.textColor, 1.0]]
+            }
+        }
+
+        // 计算当前在该字内的播放进度(0-1之间)
+        let progress = 0
+        if (this.currentMediaPosition < word.startTime) {
+            // 还没播放到这个字
+            progress = 0
+        } else if (this.currentMediaPosition >= wordEndTime) {
+            // 这个字已经播放完
+            progress = 1
+        } else {
+            // 正在播放这个字,计算进度
+            const diff = this.currentMediaPosition - word.startTime
+            progress = diff / wordDuration
+            progress = Math.max(0, Math.min(1, progress))
+        }
+
+        console.info('heanup', `getWordByWordLyricLyricItemLinearGradient - word=${word.word}, progress=${progress}, currentPos=${this.currentMediaPosition}, wordStart=${word.startTime}, wordEnd=${wordEndTime}`)
+
+        // 返回卡拉OK渐变效果
+        // 0.0 到 progress:高亮色(已播放部分)
+        // progress 到 1.0:普通色(未播放部分)
+        return [[this.textHighlightColor, 0.0],
+            [this.textHighlightColor, progress],
+            [this.textColor, progress],
+            [this.textColor, 1.0]]
+    }
+
     @Builder
     WordByWordLyric(item: LyricLine, index: number) {
         Column() {
@@ -317,12 +456,15 @@ export struct LyricView2 {
                         .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
                         .fontColor(this.currentMediaPosition >= word.startTime ?
                             index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
-                        .fontWeight(this.currentMediaPosition >= word.startTime && this.isHighlightBold ?
-                            index == this.currentIndex? FontWeight.Bold : this.textWeight: this.textWeight)
+                        .fontWeight(index == this.currentIndex? FontWeight.Bold : this.textWeight)
                         .margin(isEnglish(word.word) ?{ right:4 }:{})
                         .visibility(this.isSingleLine?
                             (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                             :Visibility.Visible)
+                        .shaderStyle(index == this.currentIndex ?{
+                            direction: GradientDirection.Right,
+                            colors: this.getWordByWordLyricLyricItemLinearGradient(item, word, index)
+                        }:undefined)
                         .animation({
                             // 动画播放速度
                             tempo: 0.8,
@@ -343,7 +485,7 @@ export struct LyricView2 {
                         .fontSize(index == this.currentIndex ?this.textSize*this.controller.getHighlightScale():this.textSize)
                         .fontColor(this.currentMediaPosition >= item.beginTime ?
                             index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
-                        .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
+                        .fontWeight(index == this.currentIndex? FontWeight.Bold : this.textWeight)
                         .margin({ top: 4 })
                 }
                 .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
@@ -355,10 +497,6 @@ export struct LyricView2 {
     }
 
 
-
-    isTopBottomLine(index:number){
-        return  index === 0 || index === this.listAdapter.totalCount()  - 1;
-    }
     //修复get Property index out of bounds
     private handleSeekAction() {
         clearTimeout(this.seekUiHideTimeout);
@@ -399,6 +537,56 @@ export struct LyricView2 {
 
     }
 
+    @Builder
+    JumpProgress(){
+        Row(){
+            Row(){
+                Divider()
+                    .width(10)
+                    .strokeWidth(2)
+                    .color(Color.Transparent)
+                    .foregroundBlurStyle(BlurStyle.BACKGROUND_REGULAR,
+                        { colorMode: ThemeColorMode.LIGHT, adaptiveColor: AdaptiveColor.DEFAULT, scale: 1.0 })
+
+                Row({space: 10}){
+                    Text(this.scrollDurationText)
+                        .fontSize(12)
+                        .fontWeight(FontWeight.Medium)
+                        .fontColor(Color.White)
+
+                    SymbolGlyph($r('sys.symbol.play'))
+                        .fontSize(13)
+                        .fontColor([Color.White])
+                        .alignSelf(ItemAlign.Center)
+                }
+                .borderRadius(10)
+                .alignItems(VerticalAlign.Center)
+                .height(32)
+                .padding(10)
+                .backgroundColor(Color.Transparent)
+                .backgroundBlurStyle(BlurStyle.BACKGROUND_REGULAR,
+                    { colorMode: ThemeColorMode.LIGHT, adaptiveColor: AdaptiveColor.DEFAULT, scale: 1.0 })
+            }
+            .transition(
+                TransitionEffect
+                    .scale({ x: 0.7, y: 0.7 })
+                    .combine(TransitionEffect
+                        .opacity(0.1)
+                    )
+                    .animation({
+                        duration: 150,
+                        curve: Curve.EaseInOut,
+                    })
+            )
+        }
+        .hitTestBehavior(HitTestMode.Transparent)
+        .width(110)
+        .justifyContent(FlexAlign.End)
+        .backgroundColor(Color.Transparent)
+
+    }
+
+
     @Builder
     SeekLine() {
         Row() {