|
@@ -19,6 +19,7 @@ import { Constants } from '../Constants';
|
|
|
import { EventConstants } from '../common/constants/EventConstants';
|
|
import { EventConstants } from '../common/constants/EventConstants';
|
|
|
import { emitter } from '@kit.BasicServicesKit';
|
|
import { emitter } from '@kit.BasicServicesKit';
|
|
|
import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
|
|
import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
|
|
|
|
|
+import { navidromeApi } from '../common/network/NavidromeApi';
|
|
|
|
|
|
|
|
const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
|
|
const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
|
|
|
|
|
|
|
@@ -61,7 +62,8 @@ export struct NavidromePage {
|
|
|
@State filterType: NavFilterType = NavFilterType.None;
|
|
@State filterType: NavFilterType = NavFilterType.None;
|
|
|
@State filterLabel: string = '';
|
|
@State filterLabel: string = '';
|
|
|
@State filterId: string = '';
|
|
@State filterId: string = '';
|
|
|
-
|
|
|
|
|
|
|
+ private coverUrlCache: Map<string, string> = new Map();
|
|
|
|
|
+
|
|
|
// 搜索和排序相关状态
|
|
// 搜索和排序相关状态
|
|
|
@State isSearchMode: boolean = false;
|
|
@State isSearchMode: boolean = false;
|
|
|
@State searchText: string = ''; // 用户输入内容
|
|
@State searchText: string = ''; // 用户输入内容
|
|
@@ -132,11 +134,13 @@ export struct NavidromePage {
|
|
|
this.artists = [];
|
|
this.artists = [];
|
|
|
this.albums = [];
|
|
this.albums = [];
|
|
|
this.clearFilter();
|
|
this.clearFilter();
|
|
|
|
|
+ this.coverUrlCache.clear();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
|
const ticket = ++this.loadTicket;
|
|
const ticket = ++this.loadTicket;
|
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
|
|
+ this.coverUrlCache.clear();
|
|
|
try {
|
|
try {
|
|
|
const requestTasks: Promise<object>[] = [
|
|
const requestTasks: Promise<object>[] = [
|
|
|
navidromeRestApi.fetchAllSongs(account),
|
|
navidromeRestApi.fetchAllSongs(account),
|
|
@@ -150,9 +154,27 @@ export struct NavidromePage {
|
|
|
if (ticket !== this.loadTicket) {
|
|
if (ticket !== this.loadTicket) {
|
|
|
return;
|
|
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} 张`);
|
|
Logger.info('heanup', `Navidrome 已加载: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
if (ticket === this.loadTicket) {
|
|
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 title = song.title ?? Constants.UNKNOWN_TITLE;
|
|
|
const videoItem = new VideoItem(
|
|
const videoItem = new VideoItem(
|
|
|
title,
|
|
title,
|
|
@@ -192,6 +321,7 @@ export struct NavidromePage {
|
|
|
videoItem.remote_rel_path = song.id;
|
|
videoItem.remote_rel_path = song.id;
|
|
|
videoItem.navArtistId = song.artistId;
|
|
videoItem.navArtistId = song.artistId;
|
|
|
videoItem.navAlbumId = song.albumId;
|
|
videoItem.navAlbumId = song.albumId;
|
|
|
|
|
+ videoItem.pixelMapPath = coverUrl;
|
|
|
return videoItem;
|
|
return videoItem;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -397,7 +527,7 @@ export struct NavidromePage {
|
|
|
doSortType(index: number) {
|
|
doSortType(index: number) {
|
|
|
this.sortType = index;
|
|
this.sortType = index;
|
|
|
const songs = this.isSearchMode ? this.filteredList :this.allVideos;
|
|
const songs = this.isSearchMode ? this.filteredList :this.allVideos;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 对歌曲列表进行排序
|
|
// 对歌曲列表进行排序
|
|
|
switch (index) {
|
|
switch (index) {
|
|
|
case 0: // 名称升序
|
|
case 0: // 名称升序
|
|
@@ -509,7 +639,7 @@ export struct NavidromePage {
|
|
|
.borderRadius(10)
|
|
.borderRadius(10)
|
|
|
.sourceSize({ width: 38, height: 38 })
|
|
.sourceSize({ width: 38, height: 38 })
|
|
|
.alt($r('app.media.music_red'))
|
|
.alt($r('app.media.music_red'))
|
|
|
- .fillColor(this.themeColor)
|
|
|
|
|
|
|
+ .fillColor(song.pixelMapPath ? undefined : this.themeColor)
|
|
|
.objectFit(ImageFit.Cover)
|
|
.objectFit(ImageFit.Cover)
|
|
|
.margin({ left: 8 })
|
|
.margin({ left: 8 })
|
|
|
.onClick(() => {
|
|
.onClick(() => {
|
|
@@ -573,10 +703,12 @@ export struct NavidromePage {
|
|
|
buildArtistItem(artist: NavidromeRestArtist) {
|
|
buildArtistItem(artist: NavidromeRestArtist) {
|
|
|
Button({ type: ButtonType.Normal, stateEffect: false }) {
|
|
Button({ type: ButtonType.Normal, stateEffect: false }) {
|
|
|
Row({ space: 12 }) {
|
|
Row({ space: 12 }) {
|
|
|
- Image($r('app.media.music_red'))
|
|
|
|
|
|
|
+ Image(artist.coverUrl ?? $r('app.media.music_red'))
|
|
|
.width(48)
|
|
.width(48)
|
|
|
.height(48)
|
|
.height(48)
|
|
|
.borderRadius(10)
|
|
.borderRadius(10)
|
|
|
|
|
+ .fillColor(artist.coverUrl ? undefined : this.themeColor)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
.margin({ left: 8 })
|
|
.margin({ left: 8 })
|
|
|
|
|
|
|
|
Column({ space: 4 }) {
|
|
Column({ space: 4 }) {
|
|
@@ -613,10 +745,12 @@ export struct NavidromePage {
|
|
|
buildAlbumItem(album: NavidromeRestAlbum) {
|
|
buildAlbumItem(album: NavidromeRestAlbum) {
|
|
|
Button({ type: ButtonType.Normal, stateEffect: false }) {
|
|
Button({ type: ButtonType.Normal, stateEffect: false }) {
|
|
|
Row({ space: 12 }) {
|
|
Row({ space: 12 }) {
|
|
|
- Image($r('app.media.music_red'))
|
|
|
|
|
|
|
+ Image(album.coverUrl ?? $r('app.media.music_red'))
|
|
|
.width(48)
|
|
.width(48)
|
|
|
.height(48)
|
|
.height(48)
|
|
|
.borderRadius(10)
|
|
.borderRadius(10)
|
|
|
|
|
+ .fillColor(album.coverUrl ? undefined : this.themeColor)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
.margin({ left: 8 })
|
|
.margin({ left: 8 })
|
|
|
|
|
|
|
|
Column({ space: 4 }) {
|
|
Column({ space: 4 }) {
|