Просмотр исходного кода

优化Navidrome的卡顿问题,做了缓存机制和taskpool后台加载

onecold 7 месяцев назад
Родитель
Сommit
2ca844388a

+ 306 - 0
entry/src/main/ets/common/util/NavidromeListCache.ets

@@ -0,0 +1,306 @@
+import { PreferencesUtil } from '@pura/harmony-utils';
+import Logger from './Logger';
+import { NavidromeRestArtist, NavidromeRestAlbum, NavidromeRestPlaylist } from '../network/NavidromeRestApi';
+import { VideoItem } from '../../viewmodel/VideoItem';
+
+const TAG = 'heanup NavidromeListCache';
+
+/**
+ * 缓存信息接口
+ */
+export interface CacheInfo {
+  count: number;
+}
+
+/**
+ * 所有缓存数据接口
+ */
+export interface AllCacheData {
+  songs: VideoItem[] | null;
+  artists: NavidromeRestArtist[] | null;
+  albums: NavidromeRestAlbum[] | null;
+  playlists: NavidromeRestPlaylist[] | null;
+}
+
+/**
+ * Navidrome列表缓存工具类
+ * 使用PreferencesUtil持久化缓存前80条数据
+ * 区分不同网盘账号的缓存
+ */
+export class NavidromeListCache {
+  private static instance: NavidromeListCache;
+
+  private constructor() {}
+
+  public static getInstance(): NavidromeListCache {
+    if (!NavidromeListCache.instance) {
+      NavidromeListCache.instance = new NavidromeListCache();
+    }
+    return NavidromeListCache.instance;
+  }
+
+  /**
+   * 生成缓存键名,包含账户ID
+   * @param accountId 账户ID
+   * @param dataType 数据类型
+   * @returns 缓存键名
+   */
+  private getCacheKey(accountId: string, dataType: string): string {
+    return `navidrome_${accountId}_${dataType}`;
+  }
+
+  /**
+   * 保存歌曲列表缓存(前80首)
+   * @param accountId 账户ID
+   * @param songs 歌曲列表
+   */
+  public saveSongsCache(accountId: string, songs: VideoItem[]): void {
+    try {
+      const limitedSongs = songs.slice(0, 80);
+      const cacheKey = this.getCacheKey(accountId, 'songs');
+
+      PreferencesUtil.putSync(cacheKey, JSON.stringify(limitedSongs));
+
+      Logger.info(TAG, `保存歌曲缓存成功: accountId=${accountId}, 数量=${limitedSongs.length}`);
+    } catch (error) {
+      Logger.error(TAG, `保存歌曲缓存失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 读取歌曲列表缓存
+   * @param accountId 账户ID
+   * @returns 歌曲列表或null
+   */
+  public getSongsCache(accountId: string): VideoItem[] | null {
+    try {
+      const cacheKey = this.getCacheKey(accountId, 'songs');
+      const jsonData = PreferencesUtil.getStringSync(cacheKey, '');
+
+      if (!jsonData || jsonData.length === 0) {
+        return null;
+      }
+
+      const songs = JSON.parse(jsonData) as VideoItem[];
+      Logger.info(TAG, `读取歌曲缓存成功: accountId=${accountId}, 数量=${songs.length}`);
+      return songs;
+    } catch (error) {
+      Logger.error(TAG, `读取歌曲缓存失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 保存艺术家列表缓存(前80位)
+   * @param accountId 账户ID
+   * @param artists 艺术家列表
+   */
+  public saveArtistsCache(accountId: string, artists: NavidromeRestArtist[]): void {
+    try {
+      const limitedArtists = artists.slice(0, 80);
+      const cacheKey = this.getCacheKey(accountId, 'artists');
+
+      PreferencesUtil.putSync(cacheKey, JSON.stringify(limitedArtists));
+
+      Logger.info(TAG, `保存艺术家缓存成功: accountId=${accountId}, 数量=${limitedArtists.length}`);
+    } catch (error) {
+      Logger.error(TAG, `保存艺术家缓存失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 读取艺术家列表缓存
+   * @param accountId 账户ID
+   * @returns 艺术家列表或null
+   */
+  public getArtistsCache(accountId: string): NavidromeRestArtist[] | null {
+    try {
+      const cacheKey = this.getCacheKey(accountId, 'artists');
+      const jsonData = PreferencesUtil.getStringSync(cacheKey, '');
+
+      if (!jsonData || jsonData.length === 0) {
+        return null;
+      }
+
+      const artists = JSON.parse(jsonData) as NavidromeRestArtist[];
+      Logger.info(TAG, `读取艺术家缓存成功: accountId=${accountId}, 数量=${artists.length}`);
+      return artists;
+    } catch (error) {
+      Logger.error(TAG, `读取艺术家缓存失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 保存专辑列表缓存(前80张)
+   * @param accountId 账户ID
+   * @param albums 专辑列表
+   */
+  public saveAlbumsCache(accountId: string, albums: NavidromeRestAlbum[]): void {
+    try {
+      const limitedAlbums = albums.slice(0, 80);
+      const cacheKey = this.getCacheKey(accountId, 'albums');
+
+      PreferencesUtil.putSync(cacheKey, JSON.stringify(limitedAlbums));
+
+      Logger.info(TAG, `保存专辑缓存成功: accountId=${accountId}, 数量=${limitedAlbums.length}`);
+    } catch (error) {
+      Logger.error(TAG, `保存专辑缓存失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 读取专辑列表缓存
+   * @param accountId 账户ID
+   * @returns 专辑列表或null
+   */
+  public getAlbumsCache(accountId: string): NavidromeRestAlbum[] | null {
+    try {
+      const cacheKey = this.getCacheKey(accountId, 'albums');
+      const jsonData = PreferencesUtil.getStringSync(cacheKey, '');
+
+      if (!jsonData || jsonData.length === 0) {
+        return null;
+      }
+
+      const albums = JSON.parse(jsonData) as NavidromeRestAlbum[];
+      Logger.info(TAG, `读取专辑缓存成功: accountId=${accountId}, 数量=${albums.length}`);
+      return albums;
+    } catch (error) {
+      Logger.error(TAG, `读取专辑缓存失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 保存歌单列表缓存(前80个)
+   * @param accountId 账户ID
+   * @param playlists 歌单列表
+   */
+  public savePlaylistsCache(accountId: string, playlists: NavidromeRestPlaylist[]): void {
+    try {
+      const limitedPlaylists = playlists.slice(0, 80);
+      const cacheKey = this.getCacheKey(accountId, 'playlists');
+
+      PreferencesUtil.putSync(cacheKey, JSON.stringify(limitedPlaylists));
+
+      Logger.info(TAG, `保存歌单缓存成功: accountId=${accountId}, 数量=${limitedPlaylists.length}`);
+    } catch (error) {
+      Logger.error(TAG, `保存歌单缓存失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 读取歌单列表缓存
+   * @param accountId 账户ID
+   * @returns 歌单列表或null
+   */
+  public getPlaylistsCache(accountId: string): NavidromeRestPlaylist[] | null {
+    try {
+      const cacheKey = this.getCacheKey(accountId, 'playlists');
+      const jsonData = PreferencesUtil.getStringSync(cacheKey, '');
+
+      if (!jsonData || jsonData.length === 0) {
+        return null;
+      }
+
+      const playlists = JSON.parse(jsonData) as NavidromeRestPlaylist[];
+      Logger.info(TAG, `读取歌单缓存成功: accountId=${accountId}, 数量=${playlists.length}`);
+      return playlists;
+    } catch (error) {
+      Logger.error(TAG, `读取歌单缓存失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 删除指定账户的所有缓存
+   * @param accountId 账户ID
+   */
+  public clearAccountCache(accountId: string): void {
+    try {
+      const dataTypes = ['songs', 'artists', 'albums', 'playlists'];
+
+      for (let i = 0; i < dataTypes.length; i++) {
+        const dataType = dataTypes[i];
+        const cacheKey = this.getCacheKey(accountId, dataType);
+
+        PreferencesUtil.deleteSync(cacheKey);
+      }
+
+      Logger.info(TAG, `清空账户缓存成功: accountId=${accountId}`);
+    } catch (error) {
+      Logger.error(TAG, `清空账户缓存失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 获取缓存信息
+   * @param accountId 账户ID
+   * @returns 缓存信息Map
+   */
+  public getCacheInfo(accountId: string): Map<string, CacheInfo> {
+    const infoMap = new Map<string, CacheInfo>();
+    const dataTypes = ['songs', 'artists', 'albums', 'playlists'];
+
+    for (let i = 0; i < dataTypes.length; i++) {
+      const dataType = dataTypes[i];
+      const cacheKey = this.getCacheKey(accountId, dataType);
+
+      try {
+        const jsonData = PreferencesUtil.getStringSync(cacheKey, '');
+
+        if (jsonData && jsonData.length > 0) {
+          const data: Record<string, Object>[] = JSON.parse(jsonData) as Record<string, Object>[];
+          const cacheInfo: CacheInfo = {
+            count: data.length
+          };
+          infoMap.set(dataType, cacheInfo);
+        }
+      } catch (error) {
+        Logger.error(TAG, `获取缓存信息失败: ${dataType}, ${(error as Error).message}`);
+      }
+    }
+
+    return infoMap;
+  }
+
+  /**
+   * 批量保存所有缓存
+   * @param accountId 账户ID
+   * @param songs 歌曲列表
+   * @param artists 艺术家列表
+   * @param albums 专辑列表
+   * @param playlists 歌单列表
+   */
+  public saveAllCache(
+    accountId: string,
+    songs: VideoItem[],
+    artists: NavidromeRestArtist[],
+    albums: NavidromeRestAlbum[],
+    playlists: NavidromeRestPlaylist[]
+  ): void {
+    this.saveSongsCache(accountId, songs);
+    this.saveArtistsCache(accountId, artists);
+    this.saveAlbumsCache(accountId, albums);
+    this.savePlaylistsCache(accountId, playlists);
+
+    Logger.info(TAG, `批量保存缓存完成: accountId=${accountId}`);
+  }
+
+  /**
+   * 批量读取所有缓存
+   * @param accountId 账户ID
+   * @returns 缓存数据对象
+   */
+  public getAllCache(accountId: string): AllCacheData {
+    return {
+      songs: this.getSongsCache(accountId),
+      artists: this.getArtistsCache(accountId),
+      albums: this.getAlbumsCache(accountId),
+      playlists: this.getPlaylistsCache(accountId)
+    };
+  }
+}
+
+export default NavidromeListCache;

+ 367 - 1
entry/src/main/ets/view/NavidromePage.ets

@@ -26,6 +26,8 @@ import { SettingPage } from '../pages/SettingPage';
 import { jellyfinApi, JellyfinAlbum, JellyfinArtist, JellyfinSong } from '../common/network/JellyfinApi';
 import { embyApi, EmbyAlbum, EmbyArtist, EmbySong } from '../common/network/EmbyApi';
 import { getRemoteDriveDisplayLabel } from '../common/util/RemoteDriveLabel';
+import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCache';
+import { taskpool } from '@kit.ArkTS';
 
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 const NAVIDROME_SEARCH_LIMIT = 500;
@@ -56,6 +58,42 @@ interface LibraryInfo {
   scheme: string;
 }
 
+/**
+ * taskpool 任务结果接口
+ * 返回带封面的完整数据
+ */
+interface TaskResult {
+  songs: VideoItem[];
+  artists: NavidromeRestArtist[];
+  albums: NavidromeRestAlbum[];
+  playlists: NavidromeRestPlaylist[];
+}
+
+/**
+ * Navidrome API 分页响应接口
+ */
+interface PagedResponse<T> {
+  data: T[];
+  nextStart: number | null;
+}
+
+/**
+ * Account 序列化数据接口 (用于 taskpool 传递)
+ */
+interface AccountData {
+  id: number;
+  webType: number;
+  host: string;
+  port: number;
+  account: string;
+  password: string;
+  enableHttps: boolean;
+  navidromeBasePath: string;
+  jellyfinBasePath: string;
+  embyBasePath: string;
+  name: string;
+}
+
 @Component
 export struct NavidromePage {
   @State isShowTitleBar: boolean = true //是否显示分类导航条
@@ -113,6 +151,7 @@ export struct NavidromePage {
   private embySongIdSeen: Set<string> = new Set();
   private jellyfinSongKeySeen: Set<string> = new Set();
   private embySongKeySeen: Set<string> = new Set();
+  private listCache = NavidromeListCache.getInstance();
 
   private isNavidromeAccount(account: WebDavAccount): boolean {
     return account.webType === RemoteDriveType.Navidrome;
@@ -326,6 +365,92 @@ export struct NavidromePage {
 
     try {
       await this.logAccountCacheInfo(account);
+
+      // 尝试从缓存读取(前50条)
+      const accountId = account.id?.toString() ?? 'default';
+      const cachedData: AllCacheData = this.listCache.getAllCache(accountId);
+
+      let loadedFromCache = false;
+      if (cachedData.songs && cachedData.songs.length > 0) {
+        this.allVideos = cachedData.songs;
+        loadedFromCache = true;
+        void ServerLogUtil.info('NavidromeLoad', `从缓存加载歌曲: ${cachedData.songs.length} 首`);
+      }
+      if (cachedData.artists && cachedData.artists.length > 0) {
+        this.artists = cachedData.artists;
+        void ServerLogUtil.info('NavidromeLoad', `从缓存加载艺术家: ${cachedData.artists.length} 位`);
+      }
+      if (cachedData.albums && cachedData.albums.length > 0) {
+        this.albums = cachedData.albums;
+        void ServerLogUtil.info('NavidromeLoad', `从缓存加载专辑: ${cachedData.albums.length} 张`);
+      }
+      if (cachedData.playlists && cachedData.playlists.length > 0) {
+        this.playlists = cachedData.playlists;
+        void ServerLogUtil.info('NavidromeLoad', `从缓存加载歌单: ${cachedData.playlists.length} 个`);
+      }
+
+      // 如果有缓存数据,先显示缓存,然后后台加载完整数据
+      if (loadedFromCache || cachedData.artists || cachedData.albums || cachedData.playlists) {
+        this.loading = false;
+        void ServerLogUtil.info('NavidromeLoad', '缓存数据加载完成,开始后台加载完整数据');
+
+        // 使用 taskpool 后台加载完整数据,避免 UI 卡顿
+        // 提取 account 的序列化数据 (taskpool 不支持 Proxy 对象)
+        const accountData: AccountData = {
+          id: account.id ?? 0,
+          webType: account.webType,
+          host: account.host ?? '',
+          port: account.port ?? 80,
+          account: account.account ?? '',
+          password: account.password ?? '',
+          enableHttps: account.enableHttps ?? false,
+          navidromeBasePath: account.navidromeBasePath ?? '/rest',
+          jellyfinBasePath: account.jellyfinBasePath ?? '',
+          embyBasePath: account.embyBasePath ?? '',
+          name: account.name ?? ''
+        };
+
+        const task = new taskpool.Task(loadNavidromeDataTask, accountData, ticket);
+        taskpool.execute(task, taskpool.Priority.MEDIUM).then((result: Object) => {
+          if (ticket !== this.loadTicket) {
+            return;
+          }
+
+          // 类型断言
+          const taskResult = result as TaskResult;
+
+          void ServerLogUtil.info('NavidromeLoad', `后台加载完成: 歌曲 ${taskResult.songs.length} / 艺术家 ${taskResult.artists.length} / 专辑 ${taskResult.albums.length} / 歌单 ${taskResult.playlists.length}`);
+
+          // 直接更新数据(封面已在后台处理)
+          if (taskResult.songs && taskResult.songs.length > 0) {
+            this.allVideos = taskResult.songs;
+          }
+          if (taskResult.artists && taskResult.artists.length > 0) {
+            this.artists = taskResult.artists;
+          }
+          if (taskResult.albums && taskResult.albums.length > 0) {
+            this.albums = taskResult.albums;
+          }
+          if (taskResult.playlists && taskResult.playlists.length > 0) {
+            this.playlists = taskResult.playlists;
+          }
+
+          // 保存新的缓存(前50条)
+          this.listCache.saveSongsCache(accountId, this.allVideos);
+          this.listCache.saveArtistsCache(accountId, this.artists);
+          this.listCache.saveAlbumsCache(accountId, this.albums);
+          this.listCache.savePlaylistsCache(accountId, this.playlists);
+
+          void ServerLogUtil.info('NavidromeLoad', `后台加载完成并更新缓存: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`);
+          void this.logCacheStatistics(account);
+        }).catch((error: Error) => {
+          void ServerLogUtil.error('NavidromeLoad', `后台数据加载失败: ${error.message}`);
+        });
+
+        return;
+      }
+
+      // 没有缓存,正常加载
       await Promise.all([
         this.loadNextSongPage(account, ticket),
         this.loadNextArtistPage(account, ticket),
@@ -335,7 +460,14 @@ export struct NavidromePage {
       if (ticket !== this.loadTicket) {
         return;
       }
-      void ServerLogUtil.info('NavidromeLoad', `首次加载完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`);
+
+      // 保存缓存(前50条)
+      this.listCache.saveSongsCache(accountId, this.allVideos);
+      this.listCache.saveArtistsCache(accountId, this.artists);
+      this.listCache.saveAlbumsCache(accountId, this.albums);
+      this.listCache.savePlaylistsCache(accountId, this.playlists);
+
+      void ServerLogUtil.info('NavidromeLoad', `首次加载完成并已缓存: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`);
       await this.logCacheStatistics(account);
     } catch (error) {
       if (ticket === this.loadTicket) {
@@ -2467,3 +2599,237 @@ export struct NavidromePage {
 
   }
 }
+
+/**
+ * Navidrome 数据加载任务 (使用 taskpool 后台执行)
+ * 该函数在 worker 线程中运行,避免阻塞 UI 线程
+ */
+@Concurrent
+async function loadNavidromeDataTask(accountData: AccountData, ticket: number): Promise<TaskResult> {
+  const result: TaskResult = {
+    songs: [],
+    artists: [],
+    albums: [],
+    playlists: []
+  };
+
+  try {
+    // 从 AccountData 重建 WebDavAccount 对象 (用于 API 调用)
+    const account: WebDavAccount = new WebDavAccount();
+    account.id = accountData.id;
+    account.webType = accountData.webType;
+    account.host = accountData.host;
+    account.port = accountData.port;
+    account.account = accountData.account;
+    account.password = accountData.password;
+    account.enableHttps = accountData.enableHttps;
+    account.navidromeBasePath = accountData.navidromeBasePath;
+    account.jellyfinBasePath = accountData.jellyfinBasePath;
+    account.embyBasePath = accountData.embyBasePath;
+    account.name = accountData.name;
+
+    // 加载歌曲并转换成 VideoItem(包含封面)
+    let songNextStart: number | null = 0;
+    while (songNextStart !== null) {
+      const response: PagedResponse<NavidromeRestSong> = await navidromeRestApi.fetchSongPage(account, songNextStart);
+      const chunk: NavidromeRestSong[] = response.data ?? [];
+      if (chunk.length === 0) {
+        break;
+      }
+
+      // 转换歌曲为 VideoItem 并处理封面
+      for (let i = 0; i < chunk.length; i++) {
+        const song: NavidromeRestSong = chunk[i];
+        const title: string = song.title ?? Constants.UNKNOWN_TITLE;
+
+        // 获取库类型
+        let libraryType = CommonConstants.TYPE_NAVIDROME;
+        let libraryScheme = 'navidrome';
+        if (account.webType === RemoteDriveType.Jellyfin) {
+          libraryType = CommonConstants.TYPE_JELLYFIN;
+          libraryScheme = 'jellyfin';
+        } else if (account.webType === RemoteDriveType.Emby) {
+          libraryType = CommonConstants.TYPE_EMBY;
+          libraryScheme = 'emby';
+        }
+
+        const videoItem = new VideoItem(
+          title,
+          song.id,
+          `${libraryScheme}://${account.id ?? 0}/${song.id}`,
+          libraryType,
+          song.size ?? 0,
+          song.createdAt ?? '',
+          Utility.formatFSize(song.size ?? 0),
+          undefined,
+          song.artist ?? Constants.UNKNOWN_ARTIST,
+          song.album ?? '',
+          `${title}${song.suffix ? '.' + song.suffix : ''}`
+        );
+
+        // 设置时长
+        if (song.duration !== undefined && song.duration !== null && song.duration >= 0) {
+          const totalSeconds = Math.floor(song.duration);
+          const minutes = Math.floor(totalSeconds / 60);
+          const seconds = totalSeconds % 60;
+          const pad = (value: number) => value.toString().padStart(2, '0');
+          videoItem.duration = `${pad(minutes)}:${pad(seconds)}`;
+        }
+
+        videoItem.size = Utility.formatFSize(song.size ?? 0);
+        videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
+        videoItem.genre = song.genre;
+        videoItem.webdav_account_id = account.id?.toString();
+        videoItem.remote_rel_path = song.id;
+        videoItem.navArtistId = song.artistId;
+        videoItem.navAlbumId = song.albumId;
+        videoItem.lyricContent = song.lyrics;
+
+        if (song.track !== undefined && song.track !== null) {
+          videoItem.track = song.track.toString();
+        }
+        if (song.year !== undefined && song.year !== null) {
+          videoItem.year = song.year.toString();
+        }
+        if (song.contentType) {
+          videoItem.mimeType = song.contentType;
+        }
+
+        // 处理封面(在 worker 线程中调用 API)
+        let coverUrl: string | undefined = undefined;
+        try {
+          // 获取封面ID
+          const coverId = song.coverArt ?? song.coverArtId ?? song.id;
+
+          // 判断是否是 Navidrome 账号
+          const isNavidrome = account.webType === RemoteDriveType.Navidrome;
+
+          if (!isNavidrome) {
+            // Jellyfin/Emby 直接使用 albumId 或 id
+            const fallbackId = song.albumId ?? song.id;
+            coverUrl = await navidromeApi.buildCoverArtUrl(account, fallbackId, 300);
+          } else {
+            // Navidrome 优先使用直接封面路径
+            const embedArtPath = song.embedArtPath ?? song.coverArtPath;
+            if (embedArtPath) {
+              // 构建直接封面URL
+              const protocol = account.enableHttps ? 'https' : 'http';
+              const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+              const portPart = account.port && account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
+              const basePath = account.navidromeBasePath ?? '/rest';
+              // 移除 /rest 后缀
+              const apiBase = basePath.endsWith('/rest') ? basePath.slice(0, -5) : basePath;
+              coverUrl = `${protocol}://${host}${portPart}${apiBase}/embed/${embedArtPath}?${account.account}:${account.password}`;
+            } else {
+              // 使用封面API
+              coverUrl = await navidromeApi.buildCoverArtUrl(account, coverId, 300);
+            }
+          }
+        } catch (error) {
+          // 封面获取失败,忽略
+        }
+
+        videoItem.pixelMapPath = coverUrl;
+        result.songs.push(videoItem);
+      }
+
+      songNextStart = response.nextStart;
+    }
+
+    // 加载艺术家并处理封面
+    let artistNextStart: number | null = 0;
+    while (artistNextStart !== null) {
+      const response: PagedResponse<NavidromeRestArtist> = await navidromeRestApi.fetchArtistPage(account, artistNextStart);
+      const chunk: NavidromeRestArtist[] = response.data ?? [];
+      if (chunk.length === 0) {
+        break;
+      }
+
+      // 处理艺术家封面
+      for (let i = 0; i < chunk.length; i++) {
+        const artist = chunk[i];
+
+        // 尝试获取已有图片URL
+        let coverUrl: string | undefined = artist.mediumImageUrl ?? artist.largeImageUrl;
+
+        // 如果没有直接URL,生成封面URL
+        if (!coverUrl) {
+          const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined);
+          try {
+            coverUrl = await navidromeApi.buildCoverArtUrl(account, coverId, 256);
+          } catch (error) {
+            // 封面生成失败,忽略
+          }
+        }
+
+        artist.coverUrl = coverUrl;
+        result.artists.push(artist);
+      }
+
+      artistNextStart = response.nextStart;
+    }
+
+    // 加载专辑并处理封面
+    let albumNextStart: number | null = 0;
+    while (albumNextStart !== null) {
+      const response: PagedResponse<NavidromeRestAlbum> = await navidromeRestApi.fetchAlbumPage(account, albumNextStart);
+      const chunk: NavidromeRestAlbum[] = response.data ?? [];
+      if (chunk.length === 0) {
+        break;
+      }
+
+      // 处理专辑封面
+      for (let i = 0; i < chunk.length; i++) {
+        const album = chunk[i];
+
+        // 专辑没有直接的 imageUrl 属性,需要生成封面URL
+        let coverUrl: string | undefined = undefined;
+
+        // 优先使用 embed 路径
+        if (album.embedArtPath) {
+          const protocol = account.enableHttps ? 'https' : 'http';
+          const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+          const portPart = account.port && account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
+          const basePath = account.navidromeBasePath ?? '/rest';
+          const apiBase = basePath.endsWith('/rest') ? basePath.slice(0, -5) : basePath;
+          coverUrl = `${protocol}://${host}${portPart}${apiBase}/embed/${album.embedArtPath}?${account.account}:${account.password}`;
+        }
+
+        // 如果没有 embed 路径,使用封面API
+        if (!coverUrl) {
+          const coverId = album.coverArt ?? album.coverArtId ?? album.id;
+          try {
+            coverUrl = await navidromeApi.buildCoverArtUrl(account, coverId, 300);
+          } catch (error) {
+            // 封面生成失败,忽略
+          }
+        }
+
+        album.coverUrl = coverUrl;
+        result.albums.push(album);
+      }
+
+      albumNextStart = response.nextStart;
+    }
+
+    // 加载歌单
+    let playlistNextStart: number | null = 0;
+    while (playlistNextStart !== null) {
+      const response: PagedResponse<NavidromeRestPlaylist> = await navidromeRestApi.fetchPlaylistPage(account, playlistNextStart);
+      const chunk: NavidromeRestPlaylist[] = response.data ?? [];
+      if (chunk.length === 0) {
+        break;
+      }
+      // 使用循环代替展开运算符
+      for (let i = 0; i < chunk.length; i++) {
+        result.playlists.push(chunk[i]);
+      }
+      playlistNextStart = response.nextStart;
+    }
+
+  } catch (error) {
+    Logger.error('NavidromeTask', `后台加载数据失败: ${(error as Error).message}`);
+  }
+
+  return result;
+}