Sfoglia il codice sorgente

处理Navidrome的封面和歌词问题
Jellyfin和Emby的歌词获取

onecold 7 mesi fa
parent
commit
78f4643247

+ 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> {

+ 99 - 0
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 {
@@ -477,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;

+ 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();

+ 128 - 8
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,6 +90,7 @@ 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';
@@ -9828,6 +9830,14 @@ export struct LocalMusic {
     if (StrUtil.isNotEmpty(neiqianLrc) && !isOnLineAndToast&&!isLocal&&!isLocal) {
       console.log("onecold 找到内嵌歌词 neiqianLrc =" + neiqianLrc)
       //赋值给this.lyricContent,播控中心才可以显示歌词
+      if (this.currentSong && isNavidromeType(this.currentSong.type)) {
+        //这里判断如果是Navidrome的歌词,需要把json格式的歌词转化下lrc标准格式
+        const convertedLyric = LyricUtil.convertNavidromeJsonLyricToLrc(neiqianLrc);
+        if (convertedLyric) {
+          neiqianLrc = convertedLyric;
+          console.log("onecold Navidrome歌词转换成功");
+        }
+      }
       this.lyricContent = neiqianLrc
       // 将文件内容按行分割成字符串数组
       let lines = neiqianLrc.split('\n').map(line => line.trim());
@@ -9842,6 +9852,91 @@ export struct LocalMusic {
     }
 
 
+    // 检查是否为 Jellyfin 类型的歌曲,如果是则从 Jellyfin 服务器获取歌词
+    if (this.currentSong && isJellyfinType(this.currentSong.type)) {
+      console.info('heanup', '检测到Jellyfin类型歌曲,尝试从Jellyfin服务器获取歌词');
+      try {
+        const manager = RemoteDriveManager.getInstance();
+        const accountId = this.currentSong.webdav_account_id;
+        if (!accountId) {
+          throw new Error('Jellyfin账号ID不可用');
+        }
+        const account = await manager.getWebDavAccountById(accountId);
+        if (!account) {
+          throw new Error('Jellyfin账号不可用');
+        }
+        const itemId = this.currentSong.webdav_id || '';
+        if (!itemId) {
+          throw new Error('Jellyfin歌曲ID不可用');
+        }
+        const jellyfinLyric = await jellyfinApi.getLyric(account, itemId);
+
+        if (StrUtil.isNotEmpty(jellyfinLyric)) {
+          console.info('heanup', '成功从Jellyfin服务器获取到歌词');
+          this.lyricContent = jellyfinLyric;
+          let lines = jellyfinLyric.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;
+          // 保存歌词到本地
+          this.saveDataToFile(jellyfinLyric, lyricPath, false);
+          return;
+        } else {
+          console.info('heanup', 'Jellyfin服务器未返回歌词,继续尝试其他方式');
+        }
+      } catch (error) {
+        const err = error as Error;
+        console.info('heanup', `从Jellyfin服务器获取歌词失败: ${err.message}`);
+      }
+    }
+
+    // 检查是否为 Emby 类型的歌曲,如果是则从 Emby 服务器获取歌词
+    if (this.currentSong && isEmbyType(this.currentSong.type)) {
+      console.info('heanup', '检测到Emby类型歌曲,尝试从Emby服务器获取歌词');
+      try {
+        const manager = RemoteDriveManager.getInstance();
+        const accountId = this.currentSong.webdav_account_id;
+        if (!accountId) {
+          throw new Error('Emby账号ID不可用');
+        }
+        const account = await manager.getWebDavAccountById(accountId);
+        if (!account) {
+          throw new Error('Emby账号不可用');
+        }
+        const itemId = this.currentSong.webdav_id || '';
+        const lyricIndex = this.currentSong.lyricIndex; // 从歌曲信息中获取歌词索引
+        if (!itemId) {
+          throw new Error('Emby歌曲ID不可用');
+        }
+        if (lyricIndex === undefined || lyricIndex === null) {
+          console.info('heanup', 'Emby歌曲没有歌词索引,跳过获取歌词');
+        } else {
+          const embyLyric = await embyApi.getLyric(account, itemId, lyricIndex);
+
+          if (StrUtil.isNotEmpty(embyLyric)) {
+            console.info('heanup', '成功从Emby服务器获取到歌词');
+            this.lyricContent = embyLyric;
+            let lines = embyLyric.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;
+            // 保存歌词到本地
+            this.saveDataToFile(embyLyric, lyricPath, false);
+            return;
+          } else {
+            console.info('heanup', 'Emby服务器未返回歌词,继续尝试其他方式');
+          }
+        }
+      } catch (error) {
+        const err = error as Error;
+        console.info('heanup', `从Emby服务器获取歌词失败: ${err.message}`);
+      }
+    }
+
     if (isOnLineAndToast || ((!FileUtil.accessSync(lyricPath) && !FileUtil.accessSync(jiaMilyricPath))
       && !Utility.isVideoByExtension(this.videoUrl))) {
       if (this.currentSong !== undefined) {
@@ -10161,24 +10256,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)
   }
 
   // 定义开始旋转的方法
@@ -11024,6 +11133,11 @@ export struct LocalMusic {
           z: 1,
           angle: this.rotateAngle
         })
+        .animation({
+          duration: 1000,  // 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1
+        })
         .shadow({
           radius: 22,
           type: ShadowType.BLUR,
@@ -11058,8 +11172,9 @@ export struct LocalMusic {
           centerY: "50%",
         })
         .animation({
-          duration: 100,
-          curve: Curve.Linear
+          duration: 1000,  // 改为 1000ms 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1  // 只执行一次,下一次更新会触发新的动画
         })
         .shadow({
           radius: 22,
@@ -11092,6 +11207,11 @@ export struct LocalMusic {
           z: 1,
           angle: this.rotateAngle
         })
+        .animation({
+          duration: 1000,  // 与 setInterval 同步
+          curve: Curve.Linear,
+          iterations: 1
+        })
         .onClick(() => {
           this.isMusicBGCover = !this.isMusicBGCover
         })

+ 116 - 59
entry/src/main/ets/view/NavidromePage.ets

@@ -432,6 +432,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 {
@@ -464,20 +475,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 {
@@ -784,42 +795,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;
   }
 
@@ -830,10 +846,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
@@ -841,31 +859,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;
   }
 
@@ -1120,53 +1140,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;
   }
 
@@ -1277,6 +1339,7 @@ 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) {
@@ -2273,13 +2336,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,