Преглед изворни кода

feat(navidrome): 实现封面图片加载功能

chendeben пре 9 месеци
родитељ
комит
71fbac4662

+ 16 - 0
entry/src/main/ets/common/network/NavidromeApi.ets

@@ -227,6 +227,22 @@ export class NavidromeApi {
     return `${baseUrl}/stream?${query}`;
   }
 
+  async buildCoverArtUrl(account: WebDavAccount, coverId: string | undefined, size?: number): Promise<string | undefined> {
+    if (!coverId || coverId.trim().length === 0) {
+      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()));
+    }
+    this.appendParams(params, await this.buildAuthParams(account));
+    this.appendCommonParams(params);
+    const query = this.buildQueryString(params);
+    return `${baseUrl}/getCoverArt?${query}`;
+  }
+
   private async request(account: WebDavAccount, endpoint: string, extraParams: Array<QueryParam>): Promise<SubsonicBody> {
     const baseUrl = this.buildBaseUrl(account);
     const params: QueryParam[] = [];

+ 26 - 0
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -42,6 +42,10 @@ export interface NavidromeRestSong {
   size?: number;
   createdAt?: string;
   genre?: string;
+  coverArt?: string;
+  coverArtId?: string;
+  coverArtPath?: string;
+  embedArtPath?: string;
 }
 
 export interface NavidromeRestArtist {
@@ -52,6 +56,10 @@ export interface NavidromeRestArtist {
   playCount?: number;
   mediumImageUrl?: string;
   largeImageUrl?: string;
+  coverArt?: string;
+  coverArtId?: string;
+  coverArtPath?: string;
+  coverUrl?: string;
 }
 
 export interface NavidromeRestAlbum {
@@ -65,6 +73,10 @@ export interface NavidromeRestAlbum {
   maxYear?: number;
   createdAt?: string;
   embedArtPath?: string;
+  coverArt?: string;
+  coverArtId?: string;
+  coverArtPath?: string;
+  coverUrl?: string;
 }
 
 export class NavidromeRestApi {
@@ -250,6 +262,20 @@ export class NavidromeRestApi {
     return normalized === '/' ? '' : normalized;
   }
 
+  resolveResourceUrl(account: WebDavAccount, path?: string): string | undefined {
+    if (!path || path.trim().length === 0) {
+      return undefined;
+    }
+    let normalized = path.trim();
+    if (normalized.startsWith('http')) {
+      return normalized;
+    }
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    return `${this.buildRootBase(account)}${normalized}`;
+  }
+
   private getAccountKey(account: WebDavAccount): string {
     if (account.id && account.id > 0) {
       return account.id.toString();

+ 5 - 2
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -1099,7 +1099,9 @@ export class RemoteDriveManager {
       const detail: NavidromeAlbumDetail = await this.navidromeApi.getAlbum(account, albumId);
       this.registerPathLabel(`/album/${albumId}`, detail.name);
       this.webDavFiles = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
-      this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail));
+      const albumCoverUrl = await this.navidromeApi.buildCoverArtUrl(account,
+        detail.coverArt ?? (detail.id ? `al-${detail.id}` : undefined));
+      this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
       await this.enrichSongsWithDatabase(this.webDavSongs);
       Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
       return;
@@ -1393,7 +1395,7 @@ export class RemoteDriveManager {
     return info;
   }
 
-  private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail): VideoItem {
+  private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail, coverUrl?: string): VideoItem {
     const title = song.title ?? '未知曲目';
     const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
     const videoItem = new VideoItem(
@@ -1414,6 +1416,7 @@ export class RemoteDriveManager {
     videoItem.remote_rel_path = song.id;
     videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
     videoItem.mimeType = song.contentType;
+    videoItem.pixelMapPath = coverUrl;
     return videoItem;
   }
 

+ 143 - 9
entry/src/main/ets/view/NavidromePage.ets

@@ -19,6 +19,7 @@ import { Constants } from '../Constants';
 import { EventConstants } from '../common/constants/EventConstants';
 import { emitter } from '@kit.BasicServicesKit';
 import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
+import { navidromeApi } from '../common/network/NavidromeApi';
 
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 
@@ -61,7 +62,8 @@ export struct NavidromePage {
   @State filterType: NavFilterType = NavFilterType.None;
   @State filterLabel: string = '';
   @State filterId: string = '';
-  
+  private coverUrlCache: Map<string, string> = new Map();
+
   // 搜索和排序相关状态
   @State isSearchMode: boolean = false;
   @State searchText: string = ''; // 用户输入内容
@@ -132,11 +134,13 @@ export struct NavidromePage {
     this.artists = [];
     this.albums = [];
     this.clearFilter();
+    this.coverUrlCache.clear();
   }
 
   private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
     const ticket = ++this.loadTicket;
     this.loading = true;
+    this.coverUrlCache.clear();
     try {
       const requestTasks: Promise<object>[] = [
         navidromeRestApi.fetchAllSongs(account),
@@ -150,9 +154,27 @@ export struct NavidromePage {
       if (ticket !== this.loadTicket) {
         return;
       }
-      this.allVideos = songs.map(song => this.convertSongToVideoItem(song, account));
-      this.artists = artistList;
-      this.albums = albumList;
+      const albumCoverMap = await this.buildAlbumCoverMap(albumList, account);
+      if (ticket !== this.loadTicket) {
+        return;
+      }
+      const artistCoverMap = await this.buildArtistCoverMap(artistList, account);
+      if (ticket !== this.loadTicket) {
+        return;
+      }
+      const videos = await this.convertSongsToVideoItems(songs, account, albumCoverMap);
+      if (ticket !== this.loadTicket) {
+        return;
+      }
+      this.allVideos = videos;
+      this.artists = artistList.map(artist => {
+        artist.coverUrl = artistCoverMap.get(artist.id);
+        return artist;
+      });
+      this.albums = albumList.map(album => {
+        album.coverUrl = albumCoverMap.get(album.id);
+        return album;
+      });
       Logger.info('heanup', `Navidrome 已加载: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
     } catch (error) {
       if (ticket === this.loadTicket) {
@@ -166,7 +188,114 @@ export struct NavidromePage {
     }
   }
 
-  private convertSongToVideoItem(song: NavidromeRestSong, account: WebDavAccount): VideoItem {
+  private async buildAlbumCoverMap(albums: NavidromeRestAlbum[], account: WebDavAccount): Promise<Map<string, string>> {
+    const map = new Map<string, string>();
+    const tasks: Promise<void>[] = [];
+    for (let i = 0; i < albums.length; i++) {
+      const album = albums[i];
+      tasks.push((async () => {
+        try {
+          const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath);
+          if (directUrl) {
+            map.set(album.id, directUrl);
+            return;
+          }
+          const coverId = album.coverArt ?? album.coverArtId ?? (album.id ? `al-${album.id}` : undefined);
+          const url = await this.buildCoverUrl(account, coverId);
+          if (url) {
+            map.set(album.id, url);
+          }
+        } catch (error) {
+          Logger.warn('heanup', `Navidrome 专辑封面解析失败: ${(error as Error).message}`);
+        }
+      })());
+    }
+    await Promise.all(tasks);
+    return map;
+  }
+
+  private async buildArtistCoverMap(artists: NavidromeRestArtist[], account: WebDavAccount): Promise<Map<string, string>> {
+    const map = new Map<string, string>();
+    const tasks: Promise<void>[] = [];
+    for (let i = 0; i < artists.length; i++) {
+      const artist = artists[i];
+      tasks.push((async () => {
+        try {
+          const directUrl = artist.mediumImageUrl ?? artist.largeImageUrl ?? this.resolveEmbedCover(account, artist.coverArtPath);
+          if (directUrl) {
+            map.set(artist.id, directUrl);
+            return;
+          }
+          const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined);
+          const url = await this.buildCoverUrl(account, coverId, 256);
+          if (url) {
+            map.set(artist.id, url);
+          }
+        } catch (error) {
+          Logger.warn('heanup', `Navidrome 艺术家封面解析失败: ${(error as Error).message}`);
+        }
+      })());
+    }
+    await Promise.all(tasks);
+    return map;
+  }
+
+  private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount,
+    albumCoverMap: Map<string, string>): Promise<VideoItem[]> {
+    const tasks: Promise<VideoItem>[] = [];
+    for (let i = 0; i < songs.length; i++) {
+      const song = songs[i];
+      tasks.push((async () => {
+        let coverUrl: string | undefined;
+        try {
+          coverUrl = await this.resolveSongCover(song, account, albumCoverMap);
+        } catch (error) {
+          Logger.warn('heanup', `Navidrome 单曲封面解析失败: ${(error as Error).message}`);
+        }
+        return this.convertSongToVideoItem(song, account, coverUrl);
+      })());
+    }
+    return Promise.all(tasks);
+  }
+
+  private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount,
+    albumCoverMap: Map<string, string>): Promise<string | undefined> {
+    if (song.albumId) {
+      const albumCover = albumCoverMap.get(song.albumId);
+      if (albumCover) {
+        return albumCover;
+      }
+    }
+    const directUrl = this.resolveEmbedCover(account, song.embedArtPath ?? song.coverArtPath);
+    if (directUrl) {
+      return directUrl;
+    }
+    const coverId = song.coverArt ?? song.coverArtId ?? song.id;
+    return this.buildCoverUrl(account, coverId);
+  }
+
+  private async buildCoverUrl(account: WebDavAccount, coverId?: string, size: number = 300): Promise<string | undefined> {
+    if (!coverId || coverId.trim().length === 0) {
+      return undefined;
+    }
+    const normalizedId = coverId.trim();
+    const cacheKey = `${normalizedId}_${size}`;
+    const cached = this.coverUrlCache.get(cacheKey);
+    if (cached) {
+      return cached;
+    }
+    const url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
+    if (url) {
+      this.coverUrlCache.set(cacheKey, url);
+    }
+    return url;
+  }
+
+  private resolveEmbedCover(account: WebDavAccount, path?: string): string | undefined {
+    return navidromeRestApi.resolveResourceUrl(account, path);
+  }
+
+  private convertSongToVideoItem(song: NavidromeRestSong, account: WebDavAccount, coverUrl?: string): VideoItem {
     const title = song.title ?? Constants.UNKNOWN_TITLE;
     const videoItem = new VideoItem(
       title,
@@ -192,6 +321,7 @@ export struct NavidromePage {
     videoItem.remote_rel_path = song.id;
     videoItem.navArtistId = song.artistId;
     videoItem.navAlbumId = song.albumId;
+    videoItem.pixelMapPath = coverUrl;
     return videoItem;
   }
 
@@ -397,7 +527,7 @@ export struct NavidromePage {
   doSortType(index: number) {
     this.sortType = index;
     const songs = this.isSearchMode ? this.filteredList :this.allVideos;
-    
+
     // 对歌曲列表进行排序
     switch (index) {
       case 0: // 名称升序
@@ -509,7 +639,7 @@ export struct NavidromePage {
           .borderRadius(10)
           .sourceSize({ width: 38, height: 38 })
           .alt($r('app.media.music_red'))
-          .fillColor(this.themeColor)
+          .fillColor(song.pixelMapPath ? undefined : this.themeColor)
           .objectFit(ImageFit.Cover)
           .margin({ left: 8 })
           .onClick(() => {
@@ -573,10 +703,12 @@ export struct NavidromePage {
   buildArtistItem(artist: NavidromeRestArtist) {
     Button({ type: ButtonType.Normal, stateEffect: false }) {
       Row({ space: 12 }) {
-        Image($r('app.media.music_red'))
+        Image(artist.coverUrl ?? $r('app.media.music_red'))
           .width(48)
           .height(48)
           .borderRadius(10)
+          .fillColor(artist.coverUrl ? undefined : this.themeColor)
+          .objectFit(ImageFit.Cover)
           .margin({ left: 8 })
 
         Column({ space: 4 }) {
@@ -613,10 +745,12 @@ export struct NavidromePage {
   buildAlbumItem(album: NavidromeRestAlbum) {
     Button({ type: ButtonType.Normal, stateEffect: false }) {
       Row({ space: 12 }) {
-        Image($r('app.media.music_red'))
+        Image(album.coverUrl ?? $r('app.media.music_red'))
           .width(48)
           .height(48)
           .borderRadius(10)
+          .fillColor(album.coverUrl ? undefined : this.themeColor)
+          .objectFit(ImageFit.Cover)
           .margin({ left: 8 })
 
         Column({ space: 4 }) {