|
|
@@ -21,7 +21,7 @@ import { emitter } from '@kit.BasicServicesKit';
|
|
|
import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
|
|
|
import { navidromeApi } from '../common/network/NavidromeApi';
|
|
|
import { ServerLogUtil } from '../common/util/ServerLogUtil';
|
|
|
-import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
|
|
|
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
|
|
|
const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
|
|
|
|
|
|
interface PlaylistEventData {
|
|
|
@@ -39,6 +39,11 @@ enum NavFilterType {
|
|
|
Album = 2
|
|
|
}
|
|
|
|
|
|
+interface CachePreviewInfo {
|
|
|
+ cacheRoot: string;
|
|
|
+ examplePath: string;
|
|
|
+}
|
|
|
+
|
|
|
@Component
|
|
|
export struct NavidromePage {
|
|
|
@Link mType: number;
|
|
|
@@ -52,6 +57,12 @@ export struct NavidromePage {
|
|
|
@State artists: NavidromeRestArtist[] = [];
|
|
|
@State albums: NavidromeRestAlbum[] = [];
|
|
|
@State loading: boolean = false;
|
|
|
+ @State songNextStart: number | null = 0;
|
|
|
+ @State artistNextStart: number | null = 0;
|
|
|
+ @State albumNextStart: number | null = 0;
|
|
|
+ @State isSongPageLoading: boolean = false;
|
|
|
+ @State isArtistPageLoading: boolean = false;
|
|
|
+ @State isAlbumPageLoading: boolean = false;
|
|
|
@StorageProp('isDarkMode') isDarkMode: boolean = false;
|
|
|
@StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
|
|
|
@StorageProp('topRectHeight') topRectHeight: number = 0;
|
|
|
@@ -64,6 +75,49 @@ export struct NavidromePage {
|
|
|
@State filterLabel: string = '';
|
|
|
@State filterId: string = '';
|
|
|
private coverUrlCache: Map<string, string> = new Map();
|
|
|
+ private albumCoverLookup: Map<string, string> = new Map();
|
|
|
+
|
|
|
+ private createTabOptions(): SegmentButtonOptions {
|
|
|
+ const buttons = [
|
|
|
+ { text: '全部' },
|
|
|
+ { text: '艺术家' },
|
|
|
+ { text: '专辑' }
|
|
|
+ ] as SegmentButtonItemTuple;
|
|
|
+ return SegmentButtonOptions.capsule({
|
|
|
+ buttons,
|
|
|
+ direction: Direction.Ltr,
|
|
|
+ buttonPadding: { top: 10, bottom: 10 },
|
|
|
+ backgroundColor: $r('app.color.index_background'),
|
|
|
+ selectedBackgroundColor: $r('app.color.start_window_background'),
|
|
|
+ selectedFontColor: $r('app.color.text_color'),
|
|
|
+ fontSize: 14,
|
|
|
+ selectedFontSize: 15,
|
|
|
+ localizedTextPadding: {
|
|
|
+ end: LengthMetrics.vp(20),
|
|
|
+ start: LengthMetrics.vp(20)
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ private getCachePreviewInfo(account: WebDavAccount): CachePreviewInfo | null {
|
|
|
+ try {
|
|
|
+ const manager = RemoteDriveManager.getInstance();
|
|
|
+ const context = manager.context;
|
|
|
+ if (!context) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const baseDir = context.filesDir ?? context.cacheDir;
|
|
|
+ if (!baseDir) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const accountId = account.id?.toString() || 'default';
|
|
|
+ const cacheRoot = `${baseDir}/remote_cache/webdav/${accountId}`;
|
|
|
+ const examplePath = `${cacheRoot}/navidrome_preview.cache`;
|
|
|
+ return { cacheRoot, examplePath };
|
|
|
+ } catch (error) {
|
|
|
+ Logger.error('NavidromePage', `获取缓存预览路径失败: ${(error as Error).message}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 搜索和排序相关状态
|
|
|
@State isSearchMode: boolean = false;
|
|
|
@@ -72,20 +126,7 @@ export struct NavidromePage {
|
|
|
@State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序
|
|
|
|
|
|
// SegmentButton选项
|
|
|
- @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
|
|
|
- buttons: [{ text: '全部' }, { text: '艺术家' }, { text: '专辑' }] as SegmentButtonItemTuple,
|
|
|
- direction: Direction.Ltr,
|
|
|
- buttonPadding: { top: 10, bottom: 10 },
|
|
|
- backgroundColor: $r('app.color.index_background'),
|
|
|
- selectedBackgroundColor: $r('app.color.start_window_background'),
|
|
|
- selectedFontColor: $r('app.color.text_color'),
|
|
|
- fontSize: 14,
|
|
|
- selectedFontSize: 15,
|
|
|
- localizedTextPadding: {
|
|
|
- end: LengthMetrics.vp(20),
|
|
|
- start: LengthMetrics.vp(20)
|
|
|
- }
|
|
|
- });
|
|
|
+ @State tabOptions: SegmentButtonOptions = this.createTabOptions();
|
|
|
|
|
|
//当胶囊按钮的选择发生变化时调用此函数
|
|
|
onTabSelectedIndexesChanged() {
|
|
|
@@ -154,90 +195,169 @@ export struct NavidromePage {
|
|
|
this.albums = [];
|
|
|
this.clearFilter();
|
|
|
this.coverUrlCache.clear();
|
|
|
+ this.albumCoverLookup.clear();
|
|
|
+ this.songNextStart = 0;
|
|
|
+ this.artistNextStart = 0;
|
|
|
+ this.albumNextStart = 0;
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ this.isArtistPageLoading = false;
|
|
|
+ this.isAlbumPageLoading = false;
|
|
|
}
|
|
|
|
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
|
const ticket = ++this.loadTicket;
|
|
|
this.loading = true;
|
|
|
- this.coverUrlCache.clear();
|
|
|
-
|
|
|
- // 记录开始加载的信息
|
|
|
+ this.resetData();
|
|
|
void ServerLogUtil.info('NavidromeLoad', '开始加载 Navidrome 数据库');
|
|
|
void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`);
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清理,当前缓存数量: ${this.coverUrlCache.size}`);
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清空,当前缓存数量: ${this.coverUrlCache.size}`);
|
|
|
|
|
|
try {
|
|
|
- const requestTasks: Promise<object>[] = [
|
|
|
- navidromeRestApi.fetchAllSongs(account),
|
|
|
- navidromeRestApi.fetchArtists(account),
|
|
|
- navidromeRestApi.fetchAlbums(account)
|
|
|
- ];
|
|
|
-
|
|
|
- void ServerLogUtil.debug('NavidromeLoad', '并发请求歌曲、艺术家、专辑数据');
|
|
|
- const responses = await Promise.all(requestTasks);
|
|
|
- const songs = responses[0] as NavidromeRestSong[];
|
|
|
- const artistList = responses[1] as NavidromeRestArtist[];
|
|
|
- const albumList = responses[2] as NavidromeRestAlbum[];
|
|
|
-
|
|
|
+ await this.logAccountCacheInfo(account);
|
|
|
+ await Promise.all([
|
|
|
+ this.loadNextSongPage(account, ticket),
|
|
|
+ this.loadNextArtistPage(account, ticket),
|
|
|
+ this.loadNextAlbumPage(account, ticket)
|
|
|
+ ]);
|
|
|
if (ticket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', `首次加载完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`);
|
|
|
+ await this.logCacheStatistics(account);
|
|
|
+ } catch (error) {
|
|
|
+ if (ticket === this.loadTicket) {
|
|
|
+ void ServerLogUtil.error('NavidromeLoad', `数据加载失败: ${(error as Error).message}`);
|
|
|
+ void ServerLogUtil.error('NavidromeLoad', `失败时服务器信息: ${ServerLogUtil.sanitizeAccount(account)}`);
|
|
|
+ ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (ticket === this.loadTicket) {
|
|
|
+ this.loading = false;
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', '加载流程结束');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 记录原始数据加载结果
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `原始数据加载完成: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
|
|
|
-
|
|
|
- // 处理专辑封面
|
|
|
- void ServerLogUtil.debug('NavidromeLoad', '开始构建专辑封面映射');
|
|
|
- const albumCoverMap = await this.buildAlbumCoverMap(albumList, account);
|
|
|
- if (ticket !== this.loadTicket) {
|
|
|
+ private async loadNextSongPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isSongPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await navidromeRestApi.fetchSongPage(account, this.songNextStart);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `专辑封面映射完成: ${albumCoverMap.size}/${albumList.length} 张专辑有封面`);
|
|
|
-
|
|
|
- // 处理艺术家封面
|
|
|
- void ServerLogUtil.debug('NavidromeLoad', '开始构建艺术家封面映射');
|
|
|
- const artistCoverMap = await this.buildArtistCoverMap(artistList, account);
|
|
|
- if (ticket !== this.loadTicket) {
|
|
|
+ const chunk = response.data ?? [];
|
|
|
+ if (chunk.length === 0) {
|
|
|
+ this.songNextStart = null;
|
|
|
return;
|
|
|
}
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `艺术家封面映射完成: ${artistCoverMap.size}/${artistList.length} 位艺术家有封面`);
|
|
|
-
|
|
|
- // 转换歌曲数据
|
|
|
- void ServerLogUtil.debug('NavidromeLoad', '开始转换歌曲为VideoItem对象');
|
|
|
- const videos = await this.convertSongsToVideoItems(songs, account, albumCoverMap);
|
|
|
- if (ticket !== this.loadTicket) {
|
|
|
+ const videos = await this.convertSongsToVideoItems(chunk, account);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
+ this.allVideos = [...this.allVideos, ...videos];
|
|
|
+ if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
+ this.onSearchInput(this.searchText);
|
|
|
+ }
|
|
|
+ this.songNextStart = response.nextStart;
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', `歌曲列表追加: 本次 ${videos.length} 首, 总数 ${this.allVideos.length}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- this.allVideos = videos;
|
|
|
- this.artists = artistList.map(artist => {
|
|
|
- artist.coverUrl = artistCoverMap.get(artist.id);
|
|
|
+ private async loadNextArtistPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.artistNextStart === null || this.isArtistPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isArtistPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await navidromeRestApi.fetchArtistPage(account, this.artistNextStart);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const chunk = response.data ?? [];
|
|
|
+ if (chunk.length === 0) {
|
|
|
+ this.artistNextStart = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const coverMap = await this.buildArtistCoverMap(chunk, account);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const processed = chunk.map(artist => {
|
|
|
+ artist.coverUrl = coverMap.get(artist.id);
|
|
|
return artist;
|
|
|
});
|
|
|
- this.albums = albumList.map(album => {
|
|
|
- album.coverUrl = albumCoverMap.get(album.id);
|
|
|
- return album;
|
|
|
- });
|
|
|
-
|
|
|
- // 记录最终加载结果
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `数据转换完成: 歌曲 ${songs.length} -> VideoItem ${videos.length}`);
|
|
|
- void ServerLogUtil.info('NavidromePage', `加载完成: 歌曲 ${songs.length} / 艺术家 ${artistList.length} / 专辑 ${albumList.length}`);
|
|
|
- void ServerLogUtil.debug('NavidromePage', `歌曲ID列表: ${songs.slice(0, 50).map(s => s.id).join(',')}${songs.length > 50 ? '...' : ''}`);
|
|
|
-
|
|
|
- // 记录缓存统计信息
|
|
|
- await this.logCacheStatistics(account);
|
|
|
+ this.artists = [...this.artists, ...processed];
|
|
|
+ this.artistNextStart = response.nextStart;
|
|
|
+ void ServerLogUtil.info('ArtistCover', `艺术家列表追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`);
|
|
|
+ } finally {
|
|
|
+ this.isArtistPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- } catch (error) {
|
|
|
- if (ticket === this.loadTicket) {
|
|
|
- void ServerLogUtil.error('NavidromeLoad', `数据加载失败: ${(error as Error).message}`);
|
|
|
- void ServerLogUtil.error('NavidromeLoad', `失败时服务器信息: ${ServerLogUtil.sanitizeAccount(account)}`);
|
|
|
- ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败');
|
|
|
+ private async loadNextAlbumPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.albumNextStart === null || this.isAlbumPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isAlbumPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await navidromeRestApi.fetchAlbumPage(account, this.albumNextStart);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
}
|
|
|
- } finally {
|
|
|
- if (ticket === this.loadTicket) {
|
|
|
- this.loading = false;
|
|
|
- void ServerLogUtil.info('NavidromeLoad', '加载流程结束');
|
|
|
+ const chunk = response.data ?? [];
|
|
|
+ if (chunk.length === 0) {
|
|
|
+ this.albumNextStart = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const coverMap = await this.buildAlbumCoverMap(chunk, account);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ coverMap.forEach((value, key) => {
|
|
|
+ if (value) {
|
|
|
+ this.albumCoverLookup.set(key, value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const processed = chunk.map(album => {
|
|
|
+ album.coverUrl = coverMap.get(album.id);
|
|
|
+ return album;
|
|
|
+ });
|
|
|
+ this.albums = [...this.albums, ...processed];
|
|
|
+ this.albumNextStart = response.nextStart;
|
|
|
+ void ServerLogUtil.info('AlbumCover', `专辑列表追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`);
|
|
|
+ } finally {
|
|
|
+ this.isAlbumPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async handleReachEnd(): Promise<void> {
|
|
|
+ if (this.loading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const account = this.selectedAccount;
|
|
|
+ if (!account) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ switch (this.selectedTab) {
|
|
|
+ case 0:
|
|
|
+ await this.loadNextSongPage(account);
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ await this.loadNextArtistPage(account);
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ await this.loadNextAlbumPage(account);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -333,8 +453,7 @@ export struct NavidromePage {
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
- private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount,
|
|
|
- albumCoverMap: Map<string, string>): Promise<VideoItem[]> {
|
|
|
+ private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount): Promise<VideoItem[]> {
|
|
|
const tasks: Promise<VideoItem>[] = [];
|
|
|
let successCount = 0;
|
|
|
let failedCount = 0;
|
|
|
@@ -347,7 +466,7 @@ export struct NavidromePage {
|
|
|
try {
|
|
|
let coverUrl: string | undefined;
|
|
|
try {
|
|
|
- coverUrl = await this.resolveSongCover(song, account, albumCoverMap);
|
|
|
+ coverUrl = await this.resolveSongCover(song, account);
|
|
|
} catch (error) {
|
|
|
void ServerLogUtil.warn('SongConvert', `歌曲 ${song.title} 封面解析失败: ${(error as Error).message}`);
|
|
|
}
|
|
|
@@ -374,10 +493,9 @@ export struct NavidromePage {
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
- private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount,
|
|
|
- albumCoverMap: Map<string, string>): Promise<string | undefined> {
|
|
|
+ private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount): Promise<string | undefined> {
|
|
|
if (song.albumId) {
|
|
|
- const albumCover = albumCoverMap.get(song.albumId);
|
|
|
+ const albumCover = song.albumId ? this.albumCoverLookup.get(song.albumId) : undefined;
|
|
|
if (albumCover) {
|
|
|
return albumCover;
|
|
|
}
|
|
|
@@ -421,16 +539,13 @@ export struct NavidromePage {
|
|
|
void ServerLogUtil.info('AccountInfo', `账号信息: ${ServerLogUtil.sanitizeAccount(account)}`);
|
|
|
|
|
|
// 记录缓存路径信息
|
|
|
- const accountId = account.id?.toString() || 'unknown';
|
|
|
-
|
|
|
- // 尝试获取Navidrome缓存路径信息
|
|
|
- try {
|
|
|
- const cachePathInfo = await resolveCacheFilePath(RemoteCacheType.WEBDAV, accountId, 'navidrome_test_path');
|
|
|
+ const cachePreview = this.getCachePreviewInfo(account);
|
|
|
+ if (cachePreview) {
|
|
|
void ServerLogUtil.info('AccountInfo', `Navidrome缓存目录结构:`);
|
|
|
- void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePathInfo.cacheDir}`);
|
|
|
- void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePathInfo.cachePath}`);
|
|
|
- } catch (error) {
|
|
|
- void ServerLogUtil.warn('AccountInfo', `缓存路径解析失败: ${(error as Error).message}`);
|
|
|
+ void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePreview.cacheRoot}`);
|
|
|
+ void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePreview.examplePath}`);
|
|
|
+ } else {
|
|
|
+ void ServerLogUtil.warn('AccountInfo', '无法预览缓存目录:上下文或根目录不可用');
|
|
|
}
|
|
|
|
|
|
// 记录封面缓存状态
|
|
|
@@ -1269,6 +1384,9 @@ export struct NavidromePage {
|
|
|
.listDirection(Axis.Vertical)
|
|
|
.scrollBar(BarState.Auto)
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
|
|
+ .onReachEnd(() => {
|
|
|
+ void this.handleReachEnd();
|
|
|
+ })
|
|
|
|
|
|
// 空状态
|
|
|
if (this.getCurrentCount() === 0) {
|